diff --git a/.changelog.yml b/.changelog.yml index bfdee0c0ca2..a7df8779de1 100644 --- a/.changelog.yml +++ b/.changelog.yml @@ -22,20 +22,25 @@ groups: name: FEATURES labels: - type/feature - - - name: API - labels: - - modifies/api - name: ENHANCEMENTS labels: - type/enhancement - - type/refactoring - - topic/ui + - + name: PERFORMANCE + labels: + - performance/memory + - performance/speed + - performance/bigrepo + - performance/cpu - name: BUGFIXES labels: - type/bug + - + name: API + labels: + - modifies/api - name: TESTING labels: diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 9f9f6f27d17..0bf0edc45ee 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,13 +1,17 @@ { "name": "Gitea DevContainer", - "image": "mcr.microsoft.com/devcontainers/go:1.23-bookworm", + "image": "mcr.microsoft.com/devcontainers/go:1.24-bookworm", + "containerEnv": { + // override "local" from packaged version + "GOTOOLCHAIN": "auto" + }, "features": { // installs nodejs into container "ghcr.io/devcontainers/features/node:1": { - "version": "20" + "version": "lts" }, "ghcr.io/devcontainers/features/git-lfs:1.2.2": {}, - "ghcr.io/devcontainers-contrib/features/poetry:2": {}, + "ghcr.io/jsburckhardt/devcontainer-features/uv:1": {}, "ghcr.io/devcontainers/features/python:1": { "version": "3.12" }, diff --git a/.dockerignore b/.dockerignore index b696e1603cc..843f12a7be8 100644 --- a/.dockerignore +++ b/.dockerignore @@ -36,15 +36,6 @@ _testmain.go coverage.all cpu.out -/modules/migration/bindata.go -/modules/migration/bindata.go.hash -/modules/options/bindata.go -/modules/options/bindata.go.hash -/modules/public/bindata.go -/modules/public/bindata.go.hash -/modules/templates/bindata.go -/modules/templates/bindata.go.hash - *.db *.log @@ -79,18 +70,6 @@ cpu.out /public/assets/fonts /public/assets/img/avatar /vendor -/web_src/fomantic/node_modules -/web_src/fomantic/build/* -!/web_src/fomantic/build/semantic.js -!/web_src/fomantic/build/semantic.css -!/web_src/fomantic/build/themes -/web_src/fomantic/build/themes/* -!/web_src/fomantic/build/themes/default -/web_src/fomantic/build/themes/default/assets/* -!/web_src/fomantic/build/themes/default/assets/fonts -/web_src/fomantic/build/themes/default/assets/fonts/* -!/web_src/fomantic/build/themes/default/assets/fonts/icons.woff2 -!/web_src/fomantic/build/themes/default/assets/fonts/outline-icons.woff2 /VERSION /.air /.go-licenses diff --git a/.editorconfig b/.editorconfig index c0946ac9975..13aa8d50f01 100644 --- a/.editorconfig +++ b/.editorconfig @@ -12,11 +12,15 @@ insert_final_newline = true [*.{go,tmpl,html}] indent_style = tab +[go.*] +indent_style = tab + [templates/custom/*.tmpl] insert_final_newline = false [templates/swagger/v1_json.tmpl] indent_style = space +insert_final_newline = false [templates/user/auth/oidc_wellknown.tmpl] indent_style = space diff --git a/.eslintrc.cjs b/.eslintrc.cjs new file mode 100644 index 00000000000..e2609922da1 --- /dev/null +++ b/.eslintrc.cjs @@ -0,0 +1,1006 @@ +const vitestPlugin = require('@vitest/eslint-plugin'); +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-wc', + ], + env: { + es2024: true, + node: true, + }, + overrides: [ + { + files: ['**/*.cjs'], + rules: { + 'import-x/no-commonjs': [0], + '@typescript-eslint/no-require-imports': [0], + }, + }, + { + 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'], + plugins: ['@vitest/eslint-plugin'], + globals: vitestPlugin.environments.env.globals, + rules: { + 'github/unescaped-html-literal': [0], + '@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': [2], + '@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/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-unnecessary-type-conversion': [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': [2, {functions: false, classes: true, variables: true, allowNamedExports: true, typedefs: false, enums: false, ignoreTypeReferences: true}], + '@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': [0], // https://github.com/github/eslint-plugin-github/issues/599 + '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': [2], + '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': [0], + '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': [0], // handled by @typescript-eslint/no-implied-eval + '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': [2], // it is still needed by eslint & IDE to prompt undefined names in real time + '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': [0], // handled by @typescript-eslint/no-use-before-define + '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'], + }, +}; diff --git a/.eslintrc.yaml b/.eslintrc.yaml deleted file mode 100644 index 88ae98db8df..00000000000 --- a/.eslintrc.yaml +++ /dev/null @@ -1,965 +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/extensions: [".js", ".ts"] - import/parsers: - "@typescript-eslint/parser": [".js", ".ts"] - import/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-i - - 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: - i/no-unused-modules: [0] - - files: ["**/*.d.ts"] - rules: - i/no-unused-modules: [0] - "@typescript-eslint/consistent-type-definitions": [0] - "@typescript-eslint/consistent-type-imports": [0] - - files: ["web_src/js/types.ts"] - rules: - i/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] - i/consistent-type-specifier-style: [0] - i/default: [0] - i/dynamic-import-chunkname: [0] - i/export: [2] - i/exports-last: [0] - i/extensions: [2, always, {ignorePackages: true}] - i/first: [2] - i/group-exports: [0] - i/max-dependencies: [0] - i/named: [2] - i/namespace: [0] - i/newline-after-import: [0] - i/no-absolute-path: [0] - i/no-amd: [2] - i/no-anonymous-default-export: [0] - i/no-commonjs: [2] - i/no-cycle: [2, {ignoreExternal: true, maxDepth: 1}] - i/no-default-export: [0] - i/no-deprecated: [0] - i/no-dynamic-require: [0] - i/no-empty-named-blocks: [2] - i/no-extraneous-dependencies: [2] - i/no-import-module-exports: [0] - i/no-internal-modules: [0] - i/no-mutable-exports: [0] - i/no-named-as-default-member: [0] - i/no-named-as-default: [0] - i/no-named-default: [0] - i/no-named-export: [0] - i/no-namespace: [0] - i/no-nodejs-modules: [0] - i/no-relative-packages: [0] - i/no-relative-parent-imports: [0] - i/no-restricted-paths: [0] - i/no-self-import: [2] - i/no-unassigned-import: [0] - i/no-unresolved: [2, {commonjs: true, ignore: ["\\?.+$"]}] - i/no-unused-modules: [2, {unusedExports: true}] - i/no-useless-path-segments: [2, {commonjs: true}] - i/no-webpack-loader-syntax: [2] - i/order: [0] - i/prefer-default-export: [0] - i/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-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-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: [2] - 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] diff --git a/.gitattributes b/.gitattributes index 9fb4a4e83dd..e218bbe25df 100644 --- a/.gitattributes +++ b/.gitattributes @@ -4,8 +4,7 @@ /assets/*.json linguist-generated /public/assets/img/svg/*.svg linguist-generated /templates/swagger/v1_json.tmpl linguist-generated +/options/fileicon/** linguist-generated /vendor/** -text -eol linguist-vendored -/web_src/fomantic/build/** linguist-generated -/web_src/fomantic/_site/globals/site.variables linguist-language=Less /web_src/js/vendor/** -text -eol linguist-vendored Dockerfile.* linguist-language=Dockerfile diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index d37ce219c39..d409f18cd91 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -13,5 +13,5 @@ contact_links: url: https://docs.gitea.com/help/faq about: Please check if your question isn't mentioned here. - name: Crowdin Translations - url: https://crowdin.com/project/gitea + url: https://translate.gitea.com about: Translations are managed here. diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml index 023fb05a296..2adea23aa40 100644 --- a/.github/actionlint.yaml +++ b/.github/actionlint.yaml @@ -3,3 +3,5 @@ self-hosted-runner: - actuated-4cpu-8gb - actuated-4cpu-16gb - nscloud + - namespace-profile-gitea-release-docker + - namespace-profile-gitea-release-binary diff --git a/.github/labeler.yml b/.github/labeler.yml index 46efbcb1949..a82bf0c2b21 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -41,7 +41,7 @@ modifies/internal: - ".dockerignore" - "docker/**" - ".editorconfig" - - ".eslintrc.yaml" + - ".eslintrc.cjs" - ".golangci.yml" - ".gitpod.yml" - ".markdownlint.yaml" @@ -49,7 +49,7 @@ modifies/internal: - "stylelint.config.js" - ".yamllint.yaml" - ".github/**" - - ".gitea/" + - ".gitea/**" - ".devcontainer/**" - "build.go" - "build/**" @@ -61,7 +61,7 @@ modifies/dependencies: - "package.json" - "package-lock.json" - "pyproject.toml" - - "poetry.lock" + - "uv.lock" - "go.mod" - "go.sum" @@ -73,11 +73,21 @@ modifies/go: modifies/frontend: - changed-files: - any-glob-to-any-file: - - "**/*.js" - - "**/*.ts" - - "**/*.vue" + - "*.js" + - "*.ts" + - "web_src/**" docs-update-needed: - changed-files: - any-glob-to-any-file: - "custom/conf/app.example.ini" + +topic/code-linting: + - changed-files: + - any-glob-to-any-file: + - ".eslintrc.cjs" + - ".golangci.yml" + - ".markdownlint.yaml" + - ".spectral.yaml" + - ".yamllint.yaml" + - "stylelint.config.js" diff --git a/.github/workflows/cron-licenses.yml b/.github/workflows/cron-licenses.yml index cd8386ecc52..c34066d3187 100644 --- a/.github/workflows/cron-licenses.yml +++ b/.github/workflows/cron-licenses.yml @@ -1,8 +1,8 @@ name: cron-licenses on: - schedule: - - cron: "7 0 * * 1" # every Monday at 00:07 UTC + # schedule: + # - cron: "7 0 * * 1" # every Monday at 00:07 UTC workflow_dispatch: jobs: @@ -15,7 +15,7 @@ jobs: with: go-version-file: go.mod check-latest: true - - run: make generate-license generate-gitignore + - run: make generate-gitignore timeout-minutes: 40 - name: push translations to repo uses: appleboy/git-push-action@v0.0.3 diff --git a/.github/workflows/files-changed.yml b/.github/workflows/files-changed.yml index 7c1fb024421..21d3ea73939 100644 --- a/.github/workflows/files-changed.yml +++ b/.github/workflows/files-changed.yml @@ -51,14 +51,16 @@ jobs: - "options/locale/locale_en-US.ini" frontend: - - "**/*.js" + - "*.js" + - "*.ts" - "web_src/**" + - "tools/*.js" + - "tools/*.ts" - "assets/emoji.json" - "package.json" - "package-lock.json" - "Makefile" - - ".eslintrc.yaml" - - "stylelint.config.js" + - ".eslintrc.cjs" - ".npmrc" docs: @@ -75,7 +77,7 @@ jobs: - "tools/lint-templates-*.js" - "templates/**/*.tmpl" - "pyproject.toml" - - "poetry.lock" + - "uv.lock" docker: - "Dockerfile" @@ -85,6 +87,7 @@ jobs: swagger: - "templates/swagger/v1_json.tmpl" + - "templates/swagger/v1_input.json" - "Makefile" - "package.json" - "package-lock.json" @@ -95,4 +98,3 @@ jobs: - "**/*.yaml" - ".yamllint.yaml" - "pyproject.toml" - - "poetry.lock" diff --git a/.github/workflows/pull-compliance.yml b/.github/workflows/pull-compliance.yml index 7e988e04492..56685ffb460 100644 --- a/.github/workflows/pull-compliance.yml +++ b/.github/workflows/pull-compliance.yml @@ -32,15 +32,13 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: "3.12" + - uses: astral-sh/setup-uv@v6 + - run: uv python install 3.12 - uses: actions/setup-node@v4 with: - node-version: 22 + node-version: 24 cache: npm cache-dependency-path: package-lock.json - - run: pip install poetry - run: make deps-py - run: make deps-frontend - run: make lint-templates @@ -51,10 +49,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: "3.12" - - run: pip install poetry + - uses: astral-sh/setup-uv@v6 + - run: uv python install 3.12 - run: make deps-py - run: make lint-yaml @@ -66,7 +62,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: 22 + node-version: 24 cache: npm cache-dependency-path: package-lock.json - run: make deps-frontend @@ -95,7 +91,7 @@ jobs: go-version-file: go.mod check-latest: true - run: make deps-backend deps-tools - - run: make lint-go-windows lint-go-vet + - run: make lint-go-windows lint-go-gitea-vet env: TAGS: bindata sqlite sqlite_unlock_notify GOOS: windows @@ -137,7 +133,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: 22 + node-version: 24 cache: npm cache-dependency-path: package-lock.json - run: make deps-frontend @@ -186,7 +182,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: 22 + node-version: 24 cache: npm cache-dependency-path: package-lock.json - run: make deps-frontend diff --git a/.github/workflows/pull-db-tests.yml b/.github/workflows/pull-db-tests.yml index 0b23de0a66c..55c2d2bf5e3 100644 --- a/.github/workflows/pull-db-tests.yml +++ b/.github/workflows/pull-db-tests.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest services: pgsql: - image: postgres:12 + image: postgres:14 env: POSTGRES_DB: test POSTGRES_PASSWORD: postgres @@ -98,7 +98,7 @@ jobs: ports: - "9200:9200" meilisearch: - image: getmeili/meilisearch:v1.2.0 + image: getmeili/meilisearch:v1 env: MEILI_ENV: development # disable auth ports: @@ -202,12 +202,10 @@ jobs: test-mssql: if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true' needs: files-changed - # specifying the version of ubuntu in use as mssql fails on newer kernels - # pending resolution from vendor - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest services: mssql: - image: mcr.microsoft.com/mssql/server:2017-latest + image: mcr.microsoft.com/mssql/server:2019-latest env: ACCEPT_EULA: Y MSSQL_PID: Standard diff --git a/.github/workflows/pull-e2e-tests.yml b/.github/workflows/pull-e2e-tests.yml index b84c69e4a0f..cc3fbd9c341 100644 --- a/.github/workflows/pull-e2e-tests.yml +++ b/.github/workflows/pull-e2e-tests.yml @@ -12,7 +12,9 @@ jobs: uses: ./.github/workflows/files-changed.yml test-e2e: - if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.frontend == 'true' || needs.files-changed.outputs.actions == 'true' + # the "test-e2e" won't pass, and it seems that there is no useful test, so skip + # if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.frontend == 'true' || needs.files-changed.outputs.actions == 'true' + if: false needs: files-changed runs-on: ubuntu-latest steps: @@ -23,7 +25,7 @@ jobs: check-latest: true - uses: actions/setup-node@v4 with: - node-version: 22 + node-version: 24 cache: npm cache-dependency-path: package-lock.json - run: make deps-frontend frontend deps-backend diff --git a/.github/workflows/release-nightly.yml b/.github/workflows/release-nightly.yml index 6e1b6e07584..5919a1616ac 100644 --- a/.github/workflows/release-nightly.yml +++ b/.github/workflows/release-nightly.yml @@ -10,7 +10,7 @@ concurrency: jobs: nightly-binary: - runs-on: nscloud + runs-on: namespace-profile-gitea-release-binary steps: - uses: actions/checkout@v4 # fetch all commits instead of only the last as some branches are long lived and could have many between versions @@ -22,7 +22,7 @@ jobs: check-latest: true - uses: actions/setup-node@v4 with: - node-version: 22 + node-version: 24 cache: npm cache-dependency-path: package-lock.json - run: make deps-frontend deps-backend @@ -58,7 +58,9 @@ jobs: run: | aws s3 sync dist/release s3://${{ secrets.AWS_S3_BUCKET }}/gitea/${{ steps.clean_name.outputs.branch }} --no-progress nightly-docker-rootful: - runs-on: ubuntu-latest + runs-on: namespace-profile-gitea-release-docker + permissions: + packages: write # to publish to ghcr.io steps: - uses: actions/checkout@v4 # fetch all commits instead of only the last as some branches are long lived and could have many between versions @@ -73,11 +75,6 @@ jobs: - name: Get cleaned branch name id: clean_name run: | - # if main then say nightly otherwise cleanup name - if [ "${{ github.ref }}" = "refs/heads/main" ]; then - echo "branch=nightly" >> "$GITHUB_OUTPUT" - exit 0 - fi REF_NAME=$(echo "${{ github.ref }}" | sed -e 's/refs\/heads\///' -e 's/refs\/tags\///' -e 's/release\/v//') echo "branch=${REF_NAME}-nightly" >> "$GITHUB_OUTPUT" - name: Login to Docker Hub @@ -85,17 +82,27 @@ jobs: with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Login to GHCR using PAT + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} - name: fetch go modules run: make vendor - name: build rootful docker image uses: docker/build-push-action@v5 with: context: . - platforms: linux/amd64,linux/arm64 + platforms: linux/amd64,linux/arm64,linux/riscv64 push: true - tags: gitea/gitea:${{ steps.clean_name.outputs.branch }} + tags: |- + gitea/gitea:${{ steps.clean_name.outputs.branch }} + ghcr.io/go-gitea/gitea:${{ steps.clean_name.outputs.branch }} nightly-docker-rootless: - runs-on: ubuntu-latest + runs-on: namespace-profile-gitea-release-docker + permissions: + packages: write # to publish to ghcr.io steps: - uses: actions/checkout@v4 # fetch all commits instead of only the last as some branches are long lived and could have many between versions @@ -110,11 +117,6 @@ jobs: - name: Get cleaned branch name id: clean_name run: | - # if main then say nightly otherwise cleanup name - if [ "${{ github.ref }}" = "refs/heads/main" ]; then - echo "branch=nightly" >> "$GITHUB_OUTPUT" - exit 0 - fi REF_NAME=$(echo "${{ github.ref }}" | sed -e 's/refs\/heads\///' -e 's/refs\/tags\///' -e 's/release\/v//') echo "branch=${REF_NAME}-nightly" >> "$GITHUB_OUTPUT" - name: Login to Docker Hub @@ -122,6 +124,12 @@ jobs: with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Login to GHCR using PAT + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} - name: fetch go modules run: make vendor - name: build rootless docker image @@ -131,4 +139,6 @@ jobs: platforms: linux/amd64,linux/arm64 push: true file: Dockerfile.rootless - tags: gitea/gitea:${{ steps.clean_name.outputs.branch }}-rootless + tags: |- + gitea/gitea:${{ steps.clean_name.outputs.branch }}-rootless + ghcr.io/go-gitea/gitea:${{ steps.clean_name.outputs.branch }}-rootless diff --git a/.github/workflows/release-tag-rc.yml b/.github/workflows/release-tag-rc.yml index 41037df29cb..c9c15c31a0d 100644 --- a/.github/workflows/release-tag-rc.yml +++ b/.github/workflows/release-tag-rc.yml @@ -11,7 +11,7 @@ concurrency: jobs: binary: - runs-on: nscloud + runs-on: namespace-profile-gitea-release-binary steps: - uses: actions/checkout@v4 # fetch all commits instead of only the last as some branches are long lived and could have many between versions @@ -23,7 +23,7 @@ jobs: check-latest: true - uses: actions/setup-node@v4 with: - node-version: 22 + node-version: 24 cache: npm cache-dependency-path: package-lock.json - run: make deps-frontend deps-backend @@ -68,7 +68,9 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} docker-rootful: - runs-on: ubuntu-latest + runs-on: namespace-profile-gitea-release-docker + permissions: + packages: write # to publish to ghcr.io steps: - uses: actions/checkout@v4 # fetch all commits instead of only the last as some branches are long lived and could have many between versions @@ -79,7 +81,9 @@ jobs: - uses: docker/metadata-action@v5 id: meta with: - images: gitea/gitea + images: |- + gitea/gitea + ghcr.io/go-gitea/gitea flavor: | latest=false # 1.2.3-rc0 @@ -90,16 +94,24 @@ jobs: with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Login to GHCR using PAT + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} - name: build rootful docker image uses: docker/build-push-action@v5 with: context: . - platforms: linux/amd64,linux/arm64 + platforms: linux/amd64,linux/arm64,linux/riscv64 push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} docker-rootless: - runs-on: ubuntu-latest + runs-on: namespace-profile-gitea-release-docker + permissions: + packages: write # to publish to ghcr.io steps: - uses: actions/checkout@v4 # fetch all commits instead of only the last as some branches are long lived and could have many between versions @@ -110,7 +122,9 @@ jobs: - uses: docker/metadata-action@v5 id: meta with: - images: gitea/gitea + images: |- + gitea/gitea + ghcr.io/go-gitea/gitea # each tag below will have the suffix of -rootless flavor: | latest=false @@ -123,11 +137,17 @@ jobs: with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Login to GHCR using PAT + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} - name: build rootless docker image uses: docker/build-push-action@v5 with: context: . - platforms: linux/amd64,linux/arm64 + platforms: linux/amd64,linux/arm64,linux/riscv64 push: true file: Dockerfile.rootless tags: ${{ steps.meta.outputs.tags }} diff --git a/.github/workflows/release-tag-version.yml b/.github/workflows/release-tag-version.yml index a23e6982000..ae717c7cecc 100644 --- a/.github/workflows/release-tag-version.yml +++ b/.github/workflows/release-tag-version.yml @@ -13,7 +13,9 @@ concurrency: jobs: binary: - runs-on: nscloud + runs-on: namespace-profile-gitea-release-binary + permissions: + packages: write # to publish to ghcr.io steps: - uses: actions/checkout@v4 # fetch all commits instead of only the last as some branches are long lived and could have many between versions @@ -25,7 +27,7 @@ jobs: check-latest: true - uses: actions/setup-node@v4 with: - node-version: 22 + node-version: 24 cache: npm cache-dependency-path: package-lock.json - run: make deps-frontend deps-backend @@ -70,7 +72,9 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} docker-rootful: - runs-on: ubuntu-latest + runs-on: namespace-profile-gitea-release-docker + permissions: + packages: write # to publish to ghcr.io steps: - uses: actions/checkout@v4 # fetch all commits instead of only the last as some branches are long lived and could have many between versions @@ -81,31 +85,39 @@ jobs: - uses: docker/metadata-action@v5 id: meta with: - images: gitea/gitea + images: |- + gitea/gitea + ghcr.io/go-gitea/gitea # this will generate tags in the following format: # latest # 1 # 1.2 # 1.2.3 tags: | + type=semver,pattern={{version}} type=semver,pattern={{major}} type=semver,pattern={{major}}.{{minor}} - type=semver,pattern={{version}} - name: Login to Docker Hub uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Login to GHCR using PAT + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} - name: build rootful docker image uses: docker/build-push-action@v5 with: context: . - platforms: linux/amd64,linux/arm64 + platforms: linux/amd64,linux/arm64,linux/riscv64 push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} docker-rootless: - runs-on: ubuntu-latest + runs-on: namespace-profile-gitea-release-docker steps: - uses: actions/checkout@v4 # fetch all commits instead of only the last as some branches are long lived and could have many between versions @@ -116,7 +128,9 @@ jobs: - uses: docker/metadata-action@v5 id: meta with: - images: gitea/gitea + images: |- + gitea/gitea + ghcr.io/go-gitea/gitea # each tag below will have the suffix of -rootless flavor: | suffix=-rootless,onlatest=true @@ -126,19 +140,25 @@ jobs: # 1.2 # 1.2.3 tags: | + type=semver,pattern={{version}} type=semver,pattern={{major}} type=semver,pattern={{major}}.{{minor}} - type=semver,pattern={{version}} - name: Login to Docker Hub uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Login to GHCR using PAT + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} - name: build rootless docker image uses: docker/build-push-action@v5 with: context: . - platforms: linux/amd64,linux/arm64 + platforms: linux/amd64,linux/arm64,linux/riscv64 push: true file: Dockerfile.rootless tags: ${{ steps.meta.outputs.tags }} diff --git a/.gitignore b/.gitignore index 7889df77b1e..fc2d74a33ae 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,11 @@ _test # IntelliJ .idea +.run + +# IntelliJ Gateway +.uuid + # Goland's output filename can not be set manually /go_build_* /gitea_* @@ -17,6 +22,9 @@ _test .vscode __debug_bin* +# Visual Studio +/.vs/ + *.cgo1.go *.cgo2.c _cgo_defun.c @@ -28,19 +36,16 @@ _testmain.go *.exe *.test *.prof +*.tsbuildinfo *coverage.out coverage.all cpu.out -/modules/migration/bindata.go -/modules/migration/bindata.go.hash -/modules/options/bindata.go -/modules/options/bindata.go.hash -/modules/public/bindata.go -/modules/public/bindata.go.hash -/modules/templates/bindata.go -/modules/templates/bindata.go.hash +/modules/migration/bindata.* +/modules/options/bindata.* +/modules/public/bindata.* +/modules/templates/bindata.* *.db *.log @@ -78,18 +83,6 @@ cpu.out /public/assets/fonts /public/assets/licenses.txt /vendor -/web_src/fomantic/node_modules -/web_src/fomantic/build/* -!/web_src/fomantic/build/semantic.js -!/web_src/fomantic/build/semantic.css -!/web_src/fomantic/build/themes -/web_src/fomantic/build/themes/* -!/web_src/fomantic/build/themes/default -/web_src/fomantic/build/themes/default/assets/* -!/web_src/fomantic/build/themes/default/assets/fonts -/web_src/fomantic/build/themes/default/assets/fonts/* -!/web_src/fomantic/build/themes/default/assets/fonts/icons.woff2 -!/web_src/fomantic/build/themes/default/assets/fonts/outline-icons.woff2 /VERSION /.air /.go-licenses @@ -116,3 +109,15 @@ prime/ # Manpage /man + +# Ignore AI/LLM instruction files +/.claude/ +/.cursorrules +/.cursor/ +/.goosehints +/.windsurfrules +/.github/copilot-instructions.md +/AGENT.md +/CLAUDE.md +/llms.txt + diff --git a/.golangci.yml b/.golangci.yml index 37617ad3652..2ad39fbae2c 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,7 +1,9 @@ +version: "2" +output: + sort-order: + - file linters: - enable-all: false - disable-all: true - fast: false + default: none enable: - bidichk - depguard @@ -9,133 +11,172 @@ linters: - errcheck - forbidigo - gocritic - - gofmt - - gofumpt - - gosimple - govet - ineffassign + - mirror - nakedret - nolintlint + - perfsprint - revive - staticcheck - - stylecheck - - typecheck + - testifylint - unconvert - - unused - unparam + - unused + - usestdlibvars + - usetesting - wastedassign - -run: - timeout: 10m - -output: - sort-results: true - sort-order: [file] - show-stats: true - -linters-settings: - stylecheck: - checks: ["all", "-ST1005", "-ST1003"] - nakedret: - max-func-lines: 0 - gocritic: - disabled-checks: - - ifElseChain - - singleCaseSwitch # Every time this occurred in the code, there was no other way. - revive: - severity: error + settings: + depguard: + rules: + main: + deny: + - pkg: encoding/json + desc: use gitea's modules/json instead of encoding/json + - pkg: github.com/unknwon/com + desc: use gitea's util and replacements + - pkg: io/ioutil + desc: use os or io instead + - pkg: golang.org/x/exp + desc: it's experimental and unreliable + - pkg: code.gitea.io/gitea/modules/git/internal + desc: do not use the internal package, use AddXxx function instead + - pkg: gopkg.in/ini.v1 + desc: do not use the ini package, use gitea's config system instead + - pkg: gitea.com/go-chi/cache + desc: do not use the go-chi cache package, use gitea's cache system + nolintlint: + allow-unused: false + require-explanation: true + require-specific: true + gocritic: + enabled-checks: + - equalFold + disabled-checks: + - ifElseChain + - singleCaseSwitch # Every time this occurred in the code, there was no other way. + revive: + severity: error + rules: + - name: atomic + - name: bare-return + - name: blank-imports + - name: constant-logical-expr + - name: context-as-argument + - name: context-keys-type + - name: dot-imports + - name: duplicated-imports + - name: empty-lines + - name: error-naming + - name: error-return + - name: error-strings + - name: errorf + - name: exported + - name: identical-branches + - name: if-return + - name: increment-decrement + - name: indent-error-flow + - name: modifies-value-receiver + - name: package-comments + - name: range + - name: receiver-naming + - name: redefines-builtin-id + - name: string-of-int + - name: superfluous-else + - name: time-naming + - name: unconditional-recursion + - name: unexported-return + - name: unreachable-code + - name: var-declaration + - name: var-naming + arguments: + - [] # AllowList - do not remove as args for the rule are positional and won't work without lists first + - [] # DenyList + - - skip-package-name-checks: true # supress errors from underscore in migration packages + staticcheck: + checks: + - all + - -ST1003 + - -ST1005 + - -QF1001 + - -QF1006 + - -QF1008 + testifylint: + disable: + - go-require + - require-error + usetesting: + os-temp-dir: true + exclusions: + generated: lax + presets: + - comments + - common-false-positives + - legacy + - std-error-handling rules: - - name: atomic - - name: bare-return - - name: blank-imports - - name: constant-logical-expr - - name: context-as-argument - - name: context-keys-type - - name: dot-imports - - name: duplicated-imports - - name: empty-lines - - name: error-naming - - name: error-return - - name: error-strings - - name: errorf - - name: exported - - name: identical-branches - - name: if-return - - name: increment-decrement - - name: indent-error-flow - - name: modifies-value-receiver - - name: package-comments - - name: range - - name: receiver-naming - - name: redefines-builtin-id - - name: string-of-int - - name: superfluous-else - - name: time-naming - - name: unconditional-recursion - - name: unexported-return - - name: unreachable-code - - name: var-declaration - - name: var-naming - gofumpt: - extra-rules: true - depguard: - rules: - main: - deny: - - pkg: encoding/json - desc: use gitea's modules/json instead of encoding/json - - pkg: github.com/unknwon/com - desc: use gitea's util and replacements - - pkg: io/ioutil - desc: use os or io instead - - pkg: golang.org/x/exp - desc: it's experimental and unreliable - - pkg: code.gitea.io/gitea/modules/git/internal - desc: do not use the internal package, use AddXxx function instead - - pkg: gopkg.in/ini.v1 - desc: do not use the ini package, use gitea's config system instead - - pkg: gitea.com/go-chi/cache - desc: do not use the go-chi cache package, use gitea's cache system - + - linters: + - dupl + - errcheck + - gocyclo + - gosec + - staticcheck + - unparam + path: _test\.go + - linters: + - dupl + - errcheck + - gocyclo + - gosec + path: models/migrations/v + - linters: + - forbidigo + path: cmd + - linters: + - dupl + text: (?i)webhook + - linters: + - gocritic + text: (?i)`ID' should not be capitalized + - linters: + - deadcode + - unused + text: (?i)swagger + - linters: + - staticcheck + text: (?i)argument x is overwritten before first use + - linters: + - gocritic + text: '(?i)commentFormatting: put a space between `//` and comment text' + - linters: + - gocritic + text: '(?i)exitAfterDefer:' + paths: + - node_modules + - public + - web_src + - third_party$ + - builtin$ + - examples$ issues: max-issues-per-linter: 0 max-same-issues: 0 - exclude-dirs: [node_modules, public, web_src] - exclude-case-sensitive: true - exclude-rules: - - path: _test\.go - linters: - - gocyclo - - errcheck - - dupl - - gosec - - unparam - - staticcheck - - path: models/migrations/v - linters: - - gocyclo - - errcheck - - dupl - - gosec - - path: cmd - linters: - - forbidigo - - text: "webhook" - linters: - - dupl - - text: "`ID' should not be capitalized" - linters: - - gocritic - - text: "swagger" - linters: - - unused - - deadcode - - text: "argument x is overwritten before first use" - linters: - - staticcheck - - text: "commentFormatting: put a space between `//` and comment text" - linters: - - gocritic - - text: "exitAfterDefer:" - linters: - - gocritic +formatters: + enable: + - gofmt + - gofumpt + settings: + gofumpt: + extra-rules: true + exclusions: + generated: lax + paths: + - node_modules + - public + - web_src + - third_party$ + - builtin$ + - examples$ + +run: + timeout: 10m diff --git a/.ignore b/.ignore index 5b96dabd38a..29912ad5c31 100644 --- a/.ignore +++ b/.ignore @@ -1,9 +1,6 @@ *.min.css *.min.js /assets/*.json -/modules/options/bindata.go -/modules/public/bindata.go -/modules/templates/bindata.go /options/gitignore /options/license /public/assets diff --git a/.mailmap b/.mailmap new file mode 100644 index 00000000000..88ff1591a4a --- /dev/null +++ b/.mailmap @@ -0,0 +1,2 @@ +Unknwon +Unknwon æ— é—» diff --git a/CHANGELOG.md b/CHANGELOG.md index 04fffd4a4e3..b72ac4849aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,1377 @@ This changelog goes through the changes that have been made in each release without substantial changes to our git log; to see the highlights of what has been added to each release, please refer to the [blog](https://blog.gitea.com). +## [1.24.0](https://github.com/go-gitea/gitea/releases/tag/1.24.0) - 2025-05-26 + +* BREAKING + * Make Gitea always use its internal config, ignore `/etc/gitconfig` (#33076) + * Improve log format (#33814) + * Fix markdown render behaviors (#34122) + * Add package version api endpoints (#34173) + +* FEATURES + * Enforce two-factor auth (2FA: TOTP or WebAuthn) (#34187) + * Add fullscreen mode as a more efficient operation way to view projects (#34081) + * Add anonymous access support for private/unlisted repositories (#34051) + * Support public code/issue access for private repositories (#33127) + * Add middleware for request prioritization (#33951) + * Add cli flags LDAP group configuration (#33933) + * Add file tree to file view page (#32721) + * Add material icons for file list (#33837) + * Artifacts download api for artifact actions v4 (#33510) + * Support choose email when creating a commit via web UI (#33432) + * Add basic auth support to rss/atom feeds (#33371) + * Add sorting by exclusive labels (issue priority) (#33206) + * Add sub issue list support (#32940) + * Private README.md for organization (#32872) + * Email option to embed images as base64 instead of link (#32061) + * Option to delay conflict checking of old pull requests until page view (#27779) + * Worktime tracking for the organization level (#19808) + +* PERFORMANCE + * Add cache for common package queries (#22491) + * Move issue pin to an standalone table for querying performance (#33452) + * Improve commits list performance to reduce unnecessary database queries (#33528) + * Optimize total count of feed when loading activities in user dashboard. (#33841) + * Optimize heatmap query (#33853) + * Only use prev and next buttons for pagination on user dashboard (#33981) + * Improve pull request list API performance (#34052) + * Cache GPG keys, emails and users when list commits (#34086) + * Refactor Git Attribute & performance optimization (#34154) + * Performance optimization for tags synchronization (#34355) #34522 + +* ENHANCEMENTS + * Code + * Display when a release attachment was uploaded (#34261) + * Support creating relative link to raw path in markdown (#34105) + * Improve code block readability and isolate copy button (#34009) + * Improve repository commit view (#33877) + * Full-file syntax highlighting for diff pages (#33766) + * Clone repository with Tea CLI (#33725) + * Improve sync fork behavior (#33319) + * Make git clone URL could use current signed-in user (#33091) + * Add submodule diff links (#33097) + * Link to tree views of submodules if possible (#33424) + * Only keep popular licenses (#33832) + * De-emphasize signed commits (#31160) + + * Actions + * Add flat-square action badge style (#34062) + * Update action status badge layout (#34018) + * Download actions job logs from API (#33858) + * Always show the "rerun" button for action jobs (#33692) + * Add auto-expanding running actions step (#30058) + * Update status check for all supported on.pull_request.types in Gitea (#33117) + * Workflow_dispatch use workflow from trigger branch (#33098) + * Add action auto-scroll (#30057) + * Add workflow_job webhook (#33694) + * Add a button editing action secret (#34462) + + * Pull Request + * Auto expand "New PR" form (#33971) + * Mark parent directory as viewed when all files are viewed (#33958) + * Show info about maintainers are allowed to edit a PR (#33738) + * Automerge supports deleting branch automatically after merging (#32343) + * Add additional command hints for PowerShell & CMD (#33548) + + * Issues + * Allow filtering issues by any assignee (#33343) + * Show warning on navigation if currently editing comment or title (#32920) + * Make tracked time representation display as hours (#33315) + * Add No Results Prompt Message on Issue List Page (#33699) + * Add sort option recentclose for issues and pulls (#34525) #34539 + + * Packages + * Link to nuget dependencies (#26554) + * Add composor source field (#33502) + + * Administration + * Improve navbar: add "admin" tip, add "active" style (#32927) + * Add a option "--user-type bot" to admin user create, improve role display (#27885) + * Improve admin user view page (#33735) + * Support performance trace (#32973) + * Change pprof labels to be prometheus compatible (#32865) + * Allow admins and org owners to change org member public status (#28294) + * Optimize the installation page (#32994) + * Make public URL generation configurable (#34250) + * Add a --fullname arg to gitea admin user create. (#34241) + + * Others + * Improve oauth2 error handling (#33969) + * Fail mirroring more gracefully (#34002) + * Align User Details Page Header Layout with Design Specifications (#34192) + * Webhook add X-Gitea-Hook-Installation-Target-Type Header (#33752) + * Optimize the dashboard (#32990) + * Improve button layout on small screens (#33633) + * Add cropping support when modifying the user/org/repo avatar (#33498) + * Make ROOT_URL support using request Host header (#32564) + * Add `show more` organizations icon in user's profile (#32986) + * Introduce `--page-space-bottom` at 64px (#30692) + * Improve theme display (#30671) + * Add alphabetical project sorting (#33504) + * Add global lock for migrations to make upgrade more safe with multiple replications (#33706) + * Add descriptions for private repo public access settings and improve the UI (#34057) + +* API + * Actions Runner rest api (#33873) + * Inclusion of rename organization api (#33303) + * Add API to support link package to repository and unlink it (#33481) + * Add API endpoint to request contents of multiple files simultaniously (#34139) + * Actions artifacts API list/download check status upload confirmed (#34273) + * Add API routes to lock and unlock issues (#34165) + * Fix some user name usages (#33689) + * Allow filtering /repos/{owner}/{repo}/pulls by target base branch queryparam (#33684) + * Improve swagger generation (#33664) + * Support Ephemeral action runners (#33570) + * Support workflow event dispatch via API (#33545) + * Support workflow event dispatch via API (#32059) + * Added Description Field for Secrets and Variables (#33526) + * Reject star-related requests if stars are disabled (#33208) + * Let API create and edit system webhooks, attempt 2 (#33180) + * Use `Project-URL` metadata field to get a PyPI package's homepage URL (#33089) + * Add `last_committer_date` and `last_author_date` for file contents API (#32921) + +* REFACTORS + * Remove context from git struct (#33793) + * Refactor admin/common.ts (#33788) + * Refactor repo-settings.ts (#33785) + * Refactor repo-issue.ts (#33784) + * Small refactor to reduce unnecessary database queries and remove duplicated functions (#33779) + * Refactor initRepoBranchTagSelector to use new init framework (#33776) + * Refactor buttons to use new init framework (#33774) + * Refactor markup and pdf-viewer to use new init framework (#33772) + * Refactor error system (#33771) + * Refactor mail code (#33768) + * Update TypeScript types (#33799) + * Refactor older tests to use testify (#33140) + * Move notifywatch to service layer (#33825) + * Decouple context from repository related structs (#33823) + * Remove context from mail struct (#33811) + * Refactor dropdown ellipsis (#34123) + * Refactor functions to reduce repopath expose (#33892) + * Refactor repo-diff.ts (#33746) + * Refactor web route handler (#33488) + * Refactor user & avatar (#33433) + * Refactor user package (#33423) + * Refactor decouple context from migration structs (#33399) + * Refactor context flash msg and global variables (#33375) + * Refactor response writer & access logger (#33323) + * Refactor ref type (#33242) + * Refactor context repository (#33202) + * Refactor legacy JS (#33115) + * Refactor legacy line-number and scroll code (#33094) + * Refactor env var related code (#33075) + * Move SetMerged to service layer (#33045) + * Merge updatecommentattachment functions (#33044) + * Refactor pull-request compare&create page (#33071) + * Refactor repo-new.ts (#33070) + * Refactor pagination (#33037) + * Refactor tests (#33021) + * Refactor markup render to fix various path problems (#34114) + * Refactor Branch struct in package modules/git (#33980) + * Don't create duplicated functions for code repositories and wiki repositories (#33924) + * Move git references checking to gitrepo packages to reduce expose of repository path (#33891) + * Refactor cache-control (#33861) + * Decouple diff stats query from actual diffing (#33810) + * Move part of updating protected branch logic to service layer (#33742) + * Decouple Batch from git.Repository to simplify usage without requiring the creation of a Repository struct. (#34001) + * Refactor tmpl and blob_excerpt (#32967) + * Refactor template & test related code (#32938) + * Refactor db package and remove unnecessary `DumpTables` (#32930) + * Refactor pprof labels and process desc (#32909) + * Refactor repo-projects.ts (#32892) + * Refactor getpatch/getdiff functions and remove unnecessary fallback (#32817) + * Uniform all temporary directories and allow customizing temp path (#32352) + * Remove context from retry downloader (#33871) + * Refactor global init code and add more comments (#33755) + * Remove some unnecessary template helpers (#33069) + * Move and rename UpdateRepository (#34136) + * Move hooks function to gitrepo and reduce expose repopath (#33890) + * Add abstraction layer to delete repository from disk (#33879) + * Add abstraction layer to check if the repository exists on disk (#33874) + * Move ParseCommitWithSSHSignature to service layer (#34087) + * Move duplicated functions (#33977) + * Extract code to their own functions for push update (#33944) + * Move gitgraph from modules to services layer (#33527) + * Move commits signature and verify functions to service layers (#33605) + * Use `CloseIssue` and `ReopenIssue` instead of `ChangeStatus` (#32467) + * Refactor arch route handlers (#32993) + * Refactor "string truncate" (#32984) + * Refactor arch route handlers (#32972) + * Clarify path param naming (#32969) + * Refactor request context (#32956) + * Move some errors to their own sub packages (#32880) + * Move RepoTransfer from models to models/repo sub package (#32506) + * Move delete deploy keys into service layer (#32201) + * Refactor webhook events (#33337) + * Move some Actions related functions from `routers` to `services` (#33280) + * Refactor RefName (#33234) + * Refactor context RefName and RepoAssignment (#33226) + * Refactor repository transfer (#33211) + * Refactor error system (#33626) + * Refactor error system (#33610) + * Refactor package (routes and error handling, npm peer dependency) (#33111) + * Use test context in tests and new loop system in benchmarks (#33648) + * Some small refactors (#33144) + * Simplify context ref name (#33267) + +* BUGFIXES + * Fix some dropdown problems on the issue sidebar (#34308) #34327 + * Do not return archive download URLs in API if downloads are disabled (#34324) #34338 + * Fix LFS files being editable in web UI (#34356) #34362 + * Fix only text/* being viewable in web UI (#34374) #34378 + * Fix LFS file not stored in LFS when uploaded/edited via API or web UI (#34367) + * Grey out expired artifact on Artifacts list (#34314) #34404 + * Fix incorrect divergence cache after switching default branch (#34370) #34406 + * Refactor commit message rendering and fix bugs (#34412) #34414 + * Merge and tweak markup editor expander CSS (#34409) #34415 + * Fix GetUsersByEmails (#34423) #34425 + * Only git operations should update last changed of a repository (#34388) #34427 + * Fix comment textarea scroll issue in Firefox (#34438) #34446 + * Fix repo broken check (#34444) #34452 + * Fix remove org user failure on mssql (#34449) #34453 + * Fix Workflow run Not Found page (#34459) #34466 + * When updating comment, if the content is the same, just return and not update the database (#34422) #34464 + * Fix project board view (#34470) #34475 + * Fix get / delete runner to use consistent http 404 and 500 status (#34480) #34488 + * Fix url validation in webhook add/edit API (#34492) #34496 + * Fix edithook api can not update package, status and workflow_job events (#34495) #34499 + * Fix ephemeral runner deletion (#34447) #34513 + * Don't display error log when .git-blame-ignore-revs doesn't exist (#34457) + * Only allow admins to rename default/protected branches (#33276) + * Improve "lock conversation" UI (#34207) + * Fix incorrect file links (#34189) + * Optimize Overflow Menu (#34183) + * Check user/org repo limit instead of doer (#34147) + * Make markdown render match GitHub's behavior (#34129) + * Fix team permission (#34128) + * Correctly handle submodule view and avoid throwing 500 error (#34121) + * Fix users being able bypass limits with repo transfers (#34031) + * Avoid creating unnecessary temporary cat file sub process (#33942) + * Refactor organization menu (#33928) + * Fix various Fomantic UI and htmx problems (#33851) + * Fix 500 error when error occurred in migration page (#33256) + * Validate that the tag doesn't exist when creating a tag via the web (#33241) + * Add missed transaction on setmerged (#33079) + * Rework create/fork/adopt/generate repository to make sure resources will be cleanup once failed (#31035) + * Valid email address should only start with alphanumeric (#28174) + * Fix webhook url (#34186) + * Fix "toAbsoluteLocaleDate" test when system locale is not en-US (#33939) + * Fix file name could not be searched if the file was not a text file when using the Bleve indexer (#33959) + * Fix cannot delete runners via the modal dialog (#33895) + * Fix unpin hint on the pinned pull requests (#33207) + * Fix parentCommit invalid memory address or nil pointer dereference. (#33204) + * Fix comment header padding (#33377) + * Fix some migration and repo name problems (#33986) + * Fix various trivial frontend problems (#34263) + * Fix Set Email Preference dropdown and button placement (#34255) + * Fix quoted replies incorrectly render user input as part of the quote (#34216) + * Fix button alignments and remove unnecessary styles (#34206) + * Restore form inputs on organization create error (#34201) + * Try to fix ACME (3rd) (#33807) + * Fix incorrect ref "blob" (#33240) + * Fix dynamic content loading init problem (#33748) + * Fix git empty check and HEAD request (#33690) + * Fix Untranslated Text on Actions Page (#33635) + * Fix issue label delete incorrect labels webhook payload (#34575) + * Fix incorrect page navigation with up and down arrow on last item of dashboard repos (#34570) + * Fix/improve avatar sync from LDAP (#34573) + * Fix some trivial problems (#34579) + * Retain issue sort type when a keyword search is introduced (#34559) + * Always use an empty line to separate the commit message and trailer (#34512) + * Fix line-button issue after file selection in file tree (#34574) + * Fix doctor deleting orphaned issues attachments (#34142) + * Add webhook assigning test and fix possible bug (#34420) + * Fix possible nil description of pull request when migrating from CodeCommit (#34541) + * Refactor commit reader (#34542) + * Fix possible pull request broken when leave the page immediately after clicking the update button #34509 + * Ignore "Close" error when uploading container blob (#34620) + * Fix missed merge commit sha and time when migrating from codecommit (#34645) + * Fix GetUsersByEmails (#34643) + * Misc CSS fixes (#34638) + * Add codecommit to supported services in api docs (#34626) + * Validate hex colors when creating/editing labels (#34623) + * Fix possible pull request broken when leave the page immediately after clicking the update button (#34509) + * Fix margin issue in markup paragraph rendering (#34599) + * Fix migration pull request title too long (#34577) + * Fix footnote jump behavior on the issue page. (#34621) + * Fix "oras" OCI client compatibility (#34666) + * Fix last admin check when syncing users (#34649) + * Fix skip paths check on tag push events in workflows (#34602) #34670 + +* MISC + + * Bump to alpine 3.22 (#34613) + * Make pull request and issue history more compact (#34588) + * Run integration tests against postgres 14 (#34514) #34536 + * Enable addtional linters (#34085) + * Enable testifylint rules (#34075) + * Enable staticcheck QFxxxx rules (#34064) + * Improve Actions test (#32883) + * Drop fomantic build (#33845) + * Go1.24 (#33562) + * Run yamllint with strict mode, fix issue (#33551) + * Disable cron task to update license (#33486) + * Optimize makefile help information generation (#33390) + * Convert github.com/xanzy/go-gitlab into gitlab.com/gitlab-org/api/client-go (#33126) + * Add missed changelogs (#33649) + * Update .changelog file to add performance label group (#33472) + * Add missing POPULATE_SQUASH_COMMENT_WITH_COMMIT_MESSAGES in app.example.ini (#33363) + * Update README screenshots (#33347) + * Update unrs-resolver (#34279) + * Update go&js dependencies (#34262) + * Optimize the calling code of queryElems (#34235) + * Update protected_branch.tmpl (#34193) + * Feat/optimize span svg layout (#34185) + * Set MERMAID_MAX_SOURCE_CHARACTERS to 50000 (#34152) + * Update JS and PY deps (#34143) + * Add Chinese translations for README files (#34132) + * Use `overflow-wrap: anywhere` to replace `word-break: break-all` (#34126) + * Clarify ownership in password change error messages (#34092) + * Add toggleClass function in dom.ts (#34063) + * Update to golangci-lint v2 (#34054) + * Update Makefile test comments (#34013) + * Update go mod dependencies (#33988) + * Use filepath.Join instead of path.Join for file system file operations (#33978) + * Prepare common tmpl functions in a middleware (#33957) + * Remove unused or abused styles (#33918) + * Update JS and PY deps, misc tweaks (#33903) + * Try to figure out attribute checker problem (#33901) + * Add lock for a repository pull mirror (#33876) + * Fine tune push mirror UI (#33866) + * Improve issue & code search (#33860) + * Use pullrequestlist instead of []*pullrequest (#33765) + * Upgrade act to 0.261.4 and actions-proto-go to v0.4.1 (#33760) + * Align sidebar gears to the right (#33721) + * Update Go dependencies (skip blevesearch, meilisearch) (#33655) + * Add migrations and doctor fixes (#33556) + * Remove "class-name" from svg icon (#33540) + * Update MAINTAINERS (#33529) + * Add "No data available" display when list is empty (#33517) + * Use `git diff-tree` for `DiffFileTree` on diff pages (#33514) + * Give organisation members access to organisation feeds (#33508) + * Update feishu icon (#33470) + * Hide/disable unusable UI elements when a repository is archived (#33459) + * Update `@github/text-expander-element` to 2.9.0 (#33435) + * Do not access GitRepo when a repo is being created (#33380) + * Fix incorrect ref usages (#33301) + * Prepare for support performance trace (#33286) + * Enable Typescript `noImplicitThis` (#33250) + * Remove unused CSS styles and move some styles to proper files (#33217) + * Add .run to gitignore (#33175) + * Fix typo in gitea downloader test and add missing codebase in `ToGitServiceType` (#33146) + * Remove extended glob pattern from branch protection UI (#33125) + * Clean up legacy form CSS styles (#33081) + * Unset XDG_HOME_CONFIG as gitea manages configuration locations (#33067) + * Add IntelliJ Gateway's .uuid to gitignore (#33052) + * User facing messages for AGit errors (#33012) + * Always show assignees on right (#33006) + * Fix eslint (#33002) + * Update JS dependencies (#32914) + * Bump x/net (#32896) (#32900) + * Only activity tab needs heatmap data loading (#34652) + +## [1.23.8](https://github.com/go-gitea/gitea/releases/tag/1.23.8) - 2025-05-11 + +* SECURITY + * Fix a bug when uploading file via lfs ssh command (#34408) (#34411) + * Update net package (#34228) (#34232) +* BUGFIXES + * Fix releases sidebar navigation link (#34436) #34439 + * Fix bug webhook milestone is not right. (#34419) #34429 + * Fix two missed null value checks on the wiki page. (#34205) (#34215) + * Swift files can be passed either as file or as form value (#34068) (#34236) + * Fix bug when API get pull changed files for deleted head repository (#34333) (#34368) + * Upgrade github v61 -> v71 to fix migrating bug (#34389) + * Fix bug when visiting comparation page (#34334) (#34364) + * Fix wrong review requests when updating the pull request (#34286) (#34304) + * Fix github migration error when using multiple tokens (#34144) (#34302) + * Explicitly not update indexes when sync database schemas (#34281) (#34295) + * Fix panic when comment is nil (#34257) (#34277) + * Fix project board links to related Pull Requests (#34213) (#34222) + * Don't assume the default wiki branch is master in the wiki API (#34244) (#34245) +* DOCUMENTATION + * Update token creation API swagger documentation (#34288) (#34296) +* MISC + * Fix CI Build (#34315) + * Add riscv64 support (#34199) (#34204) + * Bump go version in go.mod (#34160) + * remove hardcoded 'code' string in clone_panel.tmpl (#34153) (#34158) + +## [1.23.7](https://github.com/go-gitea/gitea/releases/tag/1.23.7) - 2025-04-07 + +* Enhancements + * Add a config option to block "expensive" pages for anonymous users (#34024) (#34071) + * Also check default ssh-cert location for host (#34099) (#34100) (#34116) +* BUGFIXES + * Fix discord webhook 400 status code when description limit is exceeded (#34084) (#34124) + * Get changed files based on merge base when checking `pull_request` actions trigger (#34106) (#34120) + * Fix invalid version in RPM package path (#34112) (#34115) + * Return default avatar url when user id is zero rather than updating database (#34094) (#34095) + * Add additional ReplaceAll in pathsep to cater for different pathsep (#34061) (#34070) + * Try to fix check-attr bug (#34029) (#34033) + * Git client will follow 301 but 307 (#34005) (#34010) + * Fix block expensive for 1.23 (#34127) + * Fix markdown frontmatter rendering (#34102) (#34107) + * Add new CLI flags to set name and scopes when creating a user with access token (#34080) (#34103) + * Do not show 500 error when default branch doesn't exist (#34096) (#34097) + * Hide activity contributors, recent commits and code frequrency left tabs if there is no code permission (#34053) (#34065) + * Simplify emoji rendering (#34048) (#34049) + * Adjust the layout of the toolbar on the Issues/Projects page (#33667) (#34047) + * Pull request updates will also trigger code owners review requests (#33744) (#34045) + * Fix org repo creation being limited by user limits (#34030) (#34044) + * Fix git client accessing renamed repo (#34034) (#34043) + * Fix the issue with error message logging for the `check-attr` command on Windows OS. (#34035) (#34036) + * Polyfill WeakRef (#34025) (#34028) + +## [1.23.6](https://github.com/go-gitea/gitea/releases/tag/v1.23.6) - 2025-03-24 + +* SECURITY + * Fix LFS URL (#33840) (#33843) + * Update jwt and redis packages (#33984) (#33987) + * Update golang crypto and net (#33989) +* BUGFIXES + * Drop timeout for requests made to the internal hook api (#33947) (#33970) + * Fix maven panic when no package exists (#33888) (#33889) + * Fix markdown render (#33870) (#33875) + * Fix auto concurrency cancellation skips commit status updates (#33764) (#33849) + * Fix oauth2 auth (#33961) (#33962) + * Fix incorrect 1.23 translations (#33932) + * Try to figure out attribute checker problem (#33901) (#33902) + * Ignore trivial errors when updating push data (#33864) (#33887) + * Fix some UI problems for 1.23 (#33856) + * Removing unwanted ui container (#33833) (#33835) + * Support disable passkey auth (#33348) (#33819) + * Do not call "git diff" when listing PRs (#33817) + * Try to fix ACME (3rd) (#33807) (#33808) + * Fix incorrect code search indexer options (#33992) #33999 + +## [1.23.5](https://github.com/go-gitea/gitea/releases/tag/v1.23.5) - 2025-03-04 + +* SECURITY + * Bump x/oauth2 & x/crypto (#33704) (#33727) +* PERFORMANCE + * Optimize user dashboard loading (#33686) (#33708) +* BUGFIXES + * Fix navbar dropdown item align (#33782) + * Fix inconsistent closed issue list icon (#33722) (#33728) + * Fix for Maven Package Naming Convention Handling (#33678) (#33679) + * Improve Open-with URL encoding (#33666) (#33680) + * Deleting repository should unlink all related packages (#33653) (#33673) + * Fix omitempty bug (#33663) (#33670) + * Upgrade go-crypto from 1.1.4 to 1.1.6 (#33745) (#33754) + * Fix OCI image.version annotation for releases to use full semver (#33698) (#33701) + * Try to fix ACME path when renew (#33668) (#33693) + * Fix mCaptcha bug (#33659) (#33661) + * Git graph: don't show detached commits (#33645) (#33650) + * Use MatchPhraseQuery for bleve code search (#33628) + * Adjust appearence of commit status webhook (#33778) #33789 + * Upgrade golang net from 0.35.0 -> 0.36.0 (#33795) #33796 + +## [1.23.4](https://github.com/go-gitea/gitea/releases/tag/v1.23.4) - 2025-02-16 + +* SECURITY + * Enhance routers for the Actions variable operations (#33547) (#33553) + * Enhance routers for the Actions runner operations (#33549) (#33555) + * Fix project issues list and counting (#33594) #33619 +* PERFORMANCES + * Performance optimization for pull request files loading comments attachments (#33585) (#33592) +* BUGFIXES + * Add a transaction to `pickTask` (#33543) (#33563) + * Fix mirror bug (#33597) (#33607) + * Use default Git timeout when checking repo health (#33593) (#33598) + * Fix PR's target branch dropdown (#33589) (#33591) + * Fix various problems (artifact order, api empty slice, assignee check, fuzzy prompt, mirror proxy, adopt git) (#33569) (#33577) + * Rework suggestion backend (#33538) (#33546) + * Fix context usage (#33554) (#33557) + * Only show the latest version in the Arch index (#33262) (#33580) + * Skip deletion error for action artifacts (#33476) (#33568) + * Make actions URL in commit status webhooks absolute (#33620) #33632 + * Add missing locale (#33641) #33642 + +## [1.23.3](https://github.com/go-gitea/gitea/releases/tag/v1.23.3) - 2025-02-06 + +* Security + * Build Gitea with Golang v1.23.6 to fix security bugs +* BUGFIXES + * Fix a bug caused by status webhook template #33512 + +## [1.23.2](https://github.com/go-gitea/gitea/releases/tag/1.23.2) - 2025-02-04 + +* BREAKING + * Add tests for webhook and fix some webhook bugs (#33396) (#33442) + * Package webhook’s Organization was incorrectly used as the User struct. This PR fixes the issue. + * This changelog is just a hint. The change is not really breaking because most fields are the same, most users are not affected. +* ENHANCEMENTS + * Clone button enhancements (#33362) (#33404) + * Repo homepage styling tweaks (#33289) (#33381) + * Add a confirm dialog for "sync fork" (#33270) (#33273) + * Make tracked time representation display as hours (#33315) (#33334) + * Improve sync fork behavior (#33319) (#33332) +* BUGFIXES + * Fix code button alignment (#33345) (#33351) + * Correct bot label `vertical-align` (#33477) (#33480) + * Fix SSH LFS memory usage (#33455) (#33460) + * Fix issue sidebar dropdown keyboard support (#33447) (#33450) + * Fix user avatar (#33439) + * Fix `GetCommitBranchStart` bug (#33298) (#33421) + * Add pubdate for repository rss and add some tests (#33411) (#33416) + * Add missed auto merge feed message on dashboard (#33309) (#33405) + * Fix issue suggestion bug (#33389) (#33391) + * Make issue suggestion work for all editors (#33340) (#33342) + * Fix issue count (#33338) (#33341) + * Fix Account linking page (#33325) (#33327) + * Fix closed dependency title (#33285) (#33287) + * Fix sidebar milestone link (#33269) (#33272) + * Fix missing license when sync mirror (#33255) (#33258) + * Fix upload file form (#33230) (#33233) + * Fix mirror bug (#33224) (#33225) + * Fix system admin cannot fork or get private fork with API (#33401) (#33417) + * Fix push message behavior (#33215) (#33317) + * Trivial fixes (#33304) (#33312) + * Fix "stop time tracking button" on navbar (#33084) (#33300) + * Fix tag route and empty repo (#33253) + * Fix cache test triggered by non memory cache (#33220) (#33221) + * Revert empty lfs ref name (#33454) (#33457) + * Fix flex width (#33414) (#33418) + * Fix commit status events (#33320) #33493 + * Fix unnecessary comment when moving issue on the same project column (#33496) #33499 + * Add timetzdata build tag to binary releases (#33463) #33503 +* MISC + * Use ProtonMail/go-crypto to replace keybase/go-crypto (#33402) (#33410) + * Update katex to latest version (#33361) + * Update go tool dependencies (#32916) (#33355) + +## [1.23.1](https://github.com/go-gitea/gitea/releases/tag/v1.23.1) - 2025-01-09 + +* ENHANCEMENTS + * Move repo size to sidebar (#33155) (#33182) +* BUGFIXES + * Use updated path to s6-svscan after alpine upgrade (#33185) (#33188) + * Fix fuzz test (#33156) (#33158) + * Fix raw file API ref handling (#33172) (#33189) + * Fix ACME panic (#33178) (#33186) + * Fix branch dropdown not display ref name (#33159) (#33183) + * Fix assignee list overlapping in Issue sidebar (#33176) (#33181) + * Fix sync fork for consistency (#33147) #33192 + * Fix editor markdown not incrementing in a numbered list (#33187) #33193 + +## [1.23.0](https://github.com/go-gitea/gitea/releases/tag/v1.23.0) - 2025-01-08 + +* BREAKING + * Rename config option `[camo].Allways` to `[camo].Always` (#32097) + * Remove SHA1 for support for ssh rsa signing (#31857) + * Use UTC as default timezone when schedule Actions cron tasks (#31742) + * Delete Actions logs older than 1 year by default (#31735) + * Make OIDC introspection authentication strictly require Client ID and secret (#31632) + +* SECURITY + * Include file extension checks in attachment API (#32151) + * Include all security fixes which have been backported to v1.22 + +* FEATURES + * Allow to fork repository into the same owner (#32819) + * Support "merge upstream branch" (Sync fork) (#32741) + * Add Arch package registry (#32692) + * Allow to disable the password-based login (sign-in) form (#32687) + * Allow cropping an avatar before setting it (#32565) + * Support quote selected comments to reply (#32431) + * Add reviewers selection to new pull request (#32403) + * Suggestions for issues (#32327) + * Add priority to protected branch (#32286) + * Included tag search capabilities (#32045) + * Add option to filter board cards by labels and assignees (#31999) + * Add automatic light/dark option for the colorblind theme (#31997) + * Support migration from AWS CodeCommit (#31981) + * Introduce globallock as distributed locks (#31908 & #31813) + * Support compression for Actions logs & enable by default (#31761 & #32013) + * Add pure SSH LFS support (#31516) + * Add Passkey login support (#31504) + * Actions support workflow dispatch event (#28163) + * Support repo license (#24872) + * Issue time estimate, meaningful time tracking (#23113) + * GitHub like repo home page (#32213 & #32847) + * Rearrange Clone Panel (#31142) + * Enhancing Gitea OAuth2 Provider with Granular Scopes for Resource Access (#32573) + * Use env GITEA_RUNNER_REGISTRATION_TOKEN as global runner token (#32946) #32964 + * Update i18n.go - Language Picker (#32933) #32935 + +* PERFORMANCE + * Perf: add extra index to notification table (#32395) + * Introduce OrgList and add LoadTeams, optimaze Load teams for orgs (#32543) + * Improve performance of diffs (#32393) + * Make LFS http_client parallel within a batch. (#32369) + * Add new index for action to resolve the performance problem (#32333) + * Improve get feed with pagination (#31821) + * Performance improvements for pull request list API (#30490) + * Use batch database operations instead of one by one to optimze api pulls (#32680) + * Use gitrepo.GetTreePathLatestCommit to get file lastest commit instead from latest commit cache (#32987) #33046 + +* ENHANCEMENTS + * Code + * Remove unnecessary border in repo home page sidebar (#32767) + * Add 'Copy path' button to file view (#32584) + * Improve diff file tree (#32658) + * Add new [lfs_client].BATCH_SIZE and [server].LFS_MAX_BATCH_SIZE config settings. (#32307) + * Updated tokenizer to better matching when search for code snippets (#32261) + * Change the code search to sort results by relevance (#32134) + * Support migrating GitHub/GitLab PR draft status (#32242) + * Move lock icon position and add additional tooltips to branch list page (#31839) + * Add tag name in the commits list (#31082) + * Add `MAX_ROWS` option for CSV rendering (#30268) + * Allow code search by filename (#32210) + * Make git push options accept short name (#32245) + * Repo file list enhancements (#32835) + + * Markdown & Editor + * Refactor markdown math render, add dollor-backquote syntax support (#32831) + * Make Monaco theme follow browser, fully type codeeditor.ts (#32756) + * Refactor markdown editor and use it for milestone description editor (#32688) + * Add some handy markdown editor features (#32400) + * Improve markdown textarea for indentation and lists (#31406) + + * Issue + * Add label/author/assignee filters to the user/org home issue list (#32779) + * Refactor issue filter (labels, poster, assignee) (#32771) + * Style unification for the issue_management area (#32605) + * Add "View all branches/tags" entry to Branch Selector (#32653) + * Improve textarea paste (#31948) + * Add avif image file support (#32508) + * Prevent from submitting issue/comment on uploading (#32263) + * Issue Templates: add option to have dropdown printed list (#31577) + * Allow searching issues by ID (#31479) + * Add `is_archived` option for issue indexer (#32735) + * Improve attachment upload methods (#30513) + * Support issue template assignees (#31083) + * Prevent simultaneous editing of comments and issues (#31053) + * Add issue comment when moving issues from one column to another of the project (#29311) + + * Pull Request + * Display head branch more comfortable on pull request view (#32000) + * Simplify review UI (#31062) + * Allow force push to protected branches (#28086) + * Add line-through for deleted branch on pull request view page (#32500) + * Support requested_reviewers data in comment webhook events (#26178) + * Allow maintainers to view and edit files of private repos when "Allow maintainers to edit" is enabled (#32215) + * Allow including `Reviewed-on`/`Reviewed-by` lines for custom merge messages (#31211) + + * Actions + * Render job title as commit message (#32748) + * Refactor RepoActionView.vue, add `::group::` support (#32713) + * Make RepoActionView.vue support `##[group]` (#32770) + * Support `pull_request_target` event for commit status (#31703) + * Detect whether action view branch was deleted (#32764) + * Allow users with write permission to run actions (#32644) + * Show latest run when visit /run/latest (#31808) + + * Packages + * Improve rubygems package registry (#31357) + * Add support for npm bundleDependencies (#30751) + * Add signature support for the RPM module (#27069) + * Extract and display readme and comments for Composer packages (#30927) + + * Project + * Add title to project view page (#32747) + * Set the columns height to hug all its contents (#31726) + * Rename project `board` -> `column` to make the UI less confusing (#30170) + + * User & Organazition + * Use better name for userinfo structure (#32544) + * Use user.FullName in Oauth2 id_token response (#32542) + * Limit org member view of restricted users (#32211) + * Allow disabling authentication related user features (#31535) + * Add option to change mail from user display name (#31528) + * Use FullName in Emails to address the recipient if possible (#31527) + + * Administration + * Add support for a credentials chain for minio access (#31051) + * Move admin routers from /admin to /-/admin (#32189) + * Add cache test for admins (#31265) + * Add option for mailer to override mail headers (#27860) + * Azure blob storage support (#30995) + * Supports forced use of S3 virtual-hosted style (#30969) + * Move repository visibility to danger zone in the settings area (#31126) + + * Others + * Remove urls from translations (#31950) + * Simplify 404/500 page (#31409) + * Optimize installation-page experience (#32558) + * Refactor login page (#31530) + * Add new event commit status creation and webhook implementation (#27151) + * Repo Activity: count new issues that were closed (#31776) + * Set manual `tabindex`es on login page (#31689) + * Add `YEAR`, `MONTH`, `MONTH_ENGLISH`, `DAY` variables for template repos (#31584) + * Add typescript guideline and typescript-specific eslint plugins and fix issues (#31521) + * Make toast support preventDuplicates (#31501) + * Fix tautological conditions (#30735) + * Issue change title notifications (#33050) #33065 + +* API + * Implement update branch API (#32433) + * Fix missing outputs for jobs with matrix (#32823) + * Make API "compare" accept commit IDs (#32801) + * Add github compatible tarball download API endpoints (#32572) + * Harden runner updateTask and updateLog api (#32462) + * Add `DISABLE_ORGANIZATIONS_PAGE` and `DISABLE_CODE_PAGE` settings for explore pages and fix an issue related to user search (#32288) + * Make admins adhere to branch protection rules (#32248) + * Calculate `PublicOnly` for org membership only once (#32234) + * Allow filtering PRs by poster in the ListPullRequests API (#32209) + * Return 404 instead of error when commit not exist (#31977) + * Save initial signup information for users to aid in spam prevention (#31852) + * Fix upload maven pacakge parallelly (#31851) + * Fix null requested_reviewer from API (#31773) + * Add permission description for API to add repo collaborator (#31744) + * Add return type to GetRawFileOrLFS and GetRawFile (#31680) + * Add skip secondary authorization option for public oauth2 clients (#31454) + * Add tag protection via rest api #17862 (#31295) + * Document possible action types for the user activity feed API (#31196) + * Add topics for repository API (#31127) + * Add support for searching users by email (#30908) + * Add API endpoints for getting action jobs status (#26673) + +* REFACTOR + * Update JS and PY dependencies (#31940) + * Enable `no-jquery/no-parse-html-literal` and fix violation (#31684) + * Refactor image diff (#31444) + * Refactor CSRF token (#32216) + * Fix some typescript issues (#32586) + * Refactor names (#31405) + * Use per package global lock for container uploads instead of memory lock (#31860) + * Move team related functions to service layer (#32537) + * Move GetFeeds to service layer (#32526) + * Resolve lint for unused parameter and unnecessary type arguments (#30750) + * Reimplement GetUserOrgsList to make it simple and clear (#32486) + * Move some functions from issue.go to standalone files (#32468) + * Refactor sidebar assignee&milestone&project selectors (#32465) + * Refactor sidebar label selector (#32460) + * Fix a number of typescript issues (#32459) + * Refactor language menu and dom utils (#32450) + * Refactor issue page info (#32445) + * Split issue sidebar into small templates (#32444) + * Refactor template ctx and render utils (#32422) + * Refactor repo legacy (#32404) + * Refactor markup package (#32399) + * Refactor markup render system (#32533 & #32589 & #32612) + * Refactor the DB migration system slightly (#32344) + * Remove jQuery import from some files (#32512) + * Strict pagination check (#32548) + * Split mail sender sub package from mailer service package (#32618) + * Remove outdated code about fixture generation (#32708) + * Refactor RepoBranchTagSelector (#32681) + * Refactor issue list (#32755) + * Refactor LabelEdit (#32752) + * Split issue/pull view router function as multiple smaller functions (#32749) + * Refactor some LDAP code (#32849) + * Unify repo search order by logic (#30876) + * Remove duplicate empty repo check in delete branch API (#32569) + * Replace deprecated `math/rand` functions (#30733) + * Remove fomantic dimmer module (#30723) + * Add types to fetch,toast,bootstrap,svg (#31627) + * Refactor webhook (#31587) + * Move AddCollabrator and CreateRepositoryByExample to service layer (#32419) + * Refactor RepoRefByType (#32413) + * Refactor: remove redundant err declarations (#32381) + * Refactor markup code (#31399) + * Refactor render system (orgmode) (#32671) + * Refactor render system (#32492) + * Refactor markdown render (#32736 & #32728) + * Refactor repo unit "disabled" check (#31389) + * Refactor route path normalization (#31381) + * Refactor to use UnsafeStringToBytes (#31358) + * Migrate vue components to setup (#32329) + * Refactor globallock (#31933) + * Use correct function name (#31887) + * Use a common message template instead of a special one (#31878) + * Fix a number of Typescript issues (#31877) + * Refactor dropzone (#31482) + * Move custom `tw-` helpers to tailwind plugin (#31184) + * Replace `gt-word-break` with `tw-break-anywhere` (#31183) + * Drop `IDOrderDesc` for listing Actions task and always order by `id DESC` (#31150) + * Split common-global.js into separate files (#31438) + * Improve detecting empty files (#31332) + * Use `querySelector` over alternative DOM methods (#31280) + * Remove jQuery `.text()` (#30506) + * Use repo as of renderctx's member rather than a repoPath on metas (#29222) + * Refactor some frontend problems (#32646) + * Refactor DateUtils and merge TimeSince (#32409) + * Replace DateTime with proper functions (#32402) + * Replace DateTime with DateUtils (#32383) + * Convert frontend code to typescript (#31559) + * Refactor maven package registry (#33049) #33057 + * Refactor testfixtures #33028 + +* BUGFIXES + * Fix issues with inconsistent spacing in areas (#32607) + * Fix incomplete Actions status aggregations (#32859) + * In some lfs server implementations, they require the ref attribute. (#32838) + * Update the list of watchers and stargazers when clicking watch/unwatch or star/unstar (#32570) + * Fix `recentupdate` sorting bugs (#32505) + * Fix incorrect "Target branch does not exist" in PR title (#32222) + * Handle "close" actionable references for manual merges (#31879) + * render plain text file if the LFS object doesn't exist (#31812) + * Fix Null Pointer error for CommitStatusesHideActionsURL (#31731) + * Fix loadRepository error when access user dashboard (#31719) + * Hide the "Details" link of commit status when the user cannot access actions (#30156) + * Fix duplicate dropdown dividers (#32760) + * Fix SSPI button visibility when SSPI is the only enabled method (#32841) + * Fix overflow on org header (#32837) + * Exclude protected branches from recently pushed (#31748) + * Fix large image overflow in comment page (#31740) + * Fix milestone deadline and date related problems (#32339) + * Fix markdown preview $$ support (#31514) + * Fix a compilation error in the Gitpod environment (#32559) + * Fix PR diff review form submit (#32596) + * Fix a number of typescript issues (#32308) + * Fix some function names in comment (#32300) + * Fix absolute-date (#32375) + * Clarify Actions resources ownership (#31724) + * Try to fix ACME directory problem (#33072) #33077 + * Inherit submodules from template repository content (#16237) #33068 + * Use project's redirect url instead of composing url (#33058) #33064 + * Fix toggle commit body button ui when latest commit message is long (#32997) #33034 + * Fix package error handling and npm meta and empty repo guide #33112 + * Fix empty git repo handling logic and fix mobile view (#33101) #33102 + * Fix line-number and scroll bugs (#33094) #33095 + * Fix bleve fuzziness search (#33078) #33087 + * Fix broken forms #33082 + * Fix empty repo updated time (#33120) #33124 + * Add missing transaction when set merge #33113 + * Fix issue comment number (#30556) #33055 + * Fix duplicate co-author in squashed merge commit messages (#33020) #33054 + * Fix Agit pull request permission check (#32999) #33005 + * Fix scoped label ui when contains emoji (#33007) #33014 + * Fix bug on activities (#33008) #33016 + * Fix review code comment avatar alignment (#33031) #33032 + * Fix templating in pull request comparison (#33025) #33038 + * Fix bug automerge cannot be chosed when there is only 1 merge style (#33040) #33043 + * Fix settings not being loaded at CLI (#26402) #33048 + * Support for email addresses containing uppercase characters when activating user account (#32998) #33001 + * Support org labels when adding labels by label names (#32988) #32996 + * Do not render truncated links in markdown (#32980) #32983 + * Demilestone should not include milestone (#32923) #32979 + * Fix Azure blob object Seek (#32974) #32975 + * Fix maven pom inheritance (#32943) #32976 + * Fix textarea newline handle (#32966) #32977 + * Fix outdated tmpl code (#32953) #32961 + * Fix commit range paging (#32944) #32962 + * Fix repo avatar conflict (#32958) #32960 + * Fix trailing comma not matched in the case of alphanumeric issue (#32945) + * Relax the version checking for Arch packages (#32908) #32913 + * Add more load functions to make sure the reference object loaded (#32901) #32912 + * Filter reviews of one pull request in memory instead of database to reduce slow response because of lacking database index (#33106) #33128 + * Fix git remote error check, fix dependencies, fix js error (#33129) #33133 + +* MISC + * Optimize branch protection rule loading (#32280) + * Bump to go 1.23 (#31855) + * Remove unused call to $.HeadRepo in view_title template (#32317) + * Do not display `attestation-manifest` and use short sha256 instead of full sha256 (#32851) + * Upgrade htmx to 2.0.4 (#32834) + * Improve JSX/TSX support in code editor (#32833) + * Add User-Agent for gitea's self-implemented lfs client. (#32832) + * Use errors.New to replace fmt.Errorf with no parameters (#32800) + * Add "n commits" link to contributors in contributors graph page (#32799) + * Update dependencies, tweak eslint (#32719) + * Remove all "floated" CSS styles (#32691) + * Show tag name on branch/tag selector if repo shown from tag ref (#32689) + * Use new mail package instead of an unmintained one (#32682) + * Optimize the styling of icon buttons within file-header-right (#32675) + * Validate OAuth Redirect URIs (#32643) + * Support optional/configurable IAMEndpoint for Minio Client (#32581) (#32581) + * Make search box in issue sidebar dropdown list always show when scrolling (#32576) + * Bump CI,Flake and Snap to Node 22 (#32487) + * Update `github.com/meilisearch/meilisearch-go` (#32484) + * Add `DEFAULT_MIRROR_REPO_UNITS` and `DEFAULT_TEMPLATE_REPO_UNITS` options (#32416) + * Update go dependencies (#32389) + * Update JS and PY dependencies (#32388) + * Upgrade rollup to 4.24.0 (#32312) + * Upgrade vue to 3.5.12 (#32311) + * Improve the maintainblity of the reserved username list (#32229) + * Upgrade htmx to 2.0.3 (#32192) + * Count typescript files as frontend for labeling (#32088) + * Only use Host header from reverse proxy (#32060) + * Failed authentications are logged to level Warning (#32016) + * Enhance USER_DISABLED_FEATURES to allow disabling change username or full name (#31959) + * Distinguish official vs non-official reviews, add tool tips, and upgr… (#31924) + * Update mermaid to v11 (#31913) + * Bump relative-time-element to v4.4.3 (#31910) + * Upgrade `htmx` to `2.0.2` (#31847) + * Add warning message in merge instructions when `AutodetectManualMerge` was not enabled (#31805) + * Add types to various low-level functions (#31781) + * Update JS dependencies (#31766) + * Remove unused code from models/repos/release.go (#31756) + * Support delete user email in admin panel (#31690) + * Add `username` to OIDC introspection response (#31688) + * Use GetDisplayName() instead of DisplayName() to generate rss feeds (#31687) + * Code editor theme enhancements (#31629) + * Update JS dependencies (#31616) + * Add types for js globals (#31586) + * Add back esbuild-loader for .js files (#31585) + * Don't show hidden labels when filling out an issue template (#31576) + * Allow synchronizing user status from OAuth2 login providers (#31572) + * Display app name in the registration email title (#31562) + * Use stable version of fabric (#31526) + * Support legacy _links LFS batch responses (#31513) + * Fix JS error with disabled attachment and easymde (#31511) + * Always use HTML attributes for avatar size (#31509) + * Use nolyfill to remove some polyfills (#31468) + * Disable issue/PR comment button given empty input (#31463) + * Add simple JS init performance trace (#31459) + * Bump htmx to 2.0.0 (#31413) + * Update JS dependencies, remove `eslint-plugin-jquery` (#31402) + * Split org Propfile README to a new tab `overview` (#31373) + * Update nix flake and add gofumpt (#31320) + * Code optimization (#31315) + * Enable poetry non-package mode (#31282) + * Optimize profile layout to enhance visual experience (#31278) + * Update `golang.org/x/net` (#31260) + * Bump `@github/relative-time-element` to v4.4.1 (#31232) + * Remove unnecessary inline style for tab-size (#31224) + * Update golangci-lint to v1.59.0 (#31221) + * Update chroma to v2.14.0 (#31177) + * Update JS dependencies (#31120) + * Improve the handling of `jobs..if` (#31070) + * Clean up revive linter config, tweak golangci output (#30980) + * Use CSS `inset` shorthand (#30939) + * Forbid deprecated `break-word` in CSS (#30934) + * Remove obsolete monaco workaround (#30893) + * Update JS dependencies, add new eslint rules (#30840) + * Fix body margin shifting with modals, fix error on project column edit (#30831) + * Remove disk-clean workflow (#30741) + * Bump `github.com/google/go-github` to v61 (#30738) + * Add built js files to eslint ignore (#30737) + * Use `ProtonMail/go-crypto` for `opengpg` in tests (#30736) + * Upgrade xorm to v1.3.9 and improve some migrations Sync (#29899) + * Added default sorting milestones by name (#27084) + * Enable `unparam` linter (#31277) + * Use Alpine 3.21 for the docker images (#32924) #32951 + * Bump x/net (#32896) #32899 + * Use -s -w ldflags for release artifacts (#33041) #33042 + * Remove aws go sdk package dependency (#33029) #33047 + +## [1.22.6](https://github.com/go-gitea/gitea/releases/tag/v1.22.6) - 2024-12-12 + +* SECURITY + * Fix misuse of PublicKeyCallback(#32810) +* BUGFIXES + * Fix lfs migration (#32812) (#32818) + * Add missing two sync feed for refs/pull (#32815) +* TESTING + * Avoid MacOS keychain dialog in integration tests (#32813) (#32816) + +## [1.22.5](https://github.com/go-gitea/gitea/releases/tag/v1.22.5) - 2024-12-11 + +* SECURITY + * Upgrade crypto library (#32791) + * Fix delete branch perm checking (#32654) (#32707) +* BUGFIXES + * Add standard-compliant route to serve outdated R packages (#32783) (#32789) + * Fix internal server error when updating labels without write permission (#32776) (#32785) + * Add Swift login endpoint (#32693) (#32701) + * Fix fork page branch selection (#32711) (#32725) + * Fix word overflow in file search page (#32695) (#32699) + * Fix gogit `GetRefCommitID` (#32705) (#32712) + * Fix race condition in mermaid observer (#32599) (#32673) + * Fixe a keystring misuse and refactor duplicates keystrings (#32668) (#32792) + * Bump relative-time-element to v4.4.4 (#32739) +* PERFORMANCE + * Make wiki pages visit fast (#32732) (#32745) +* MISC + * Don't create action when syncing mirror pull refs (#32659) (#32664) + +## [1.22.4](https://github.com/go-gitea/gitea/releases/tag/v1.22.4) - 2024-11-14 + +* SECURITY + * Fix basic auth with webauthn (#32531) (#32536) + * Refactor internal routers (partial backport, auth token const time comparing) (#32473) (#32479) +* PERFORMANCE + * Remove transaction for archive download (#32186) (#32520) +* BUGFIXES + * Fix `missing signature key` error when pulling Docker images with `SERVE_DIRECT` enabled (#32365) (#32397) + * Fix get reviewers fails when selecting user without pull request permissions unit (#32415) (#32616) + * Fix adding index files to tmp directory (#32360) (#32593) + * Fix PR creation on forked repositories via API (#31863) (#32591) + * Fix missing menu tabs in organization project view page (#32313) (#32592) + * Support HTTP POST requests to `/userinfo`, aligning to OpenID Core specification (#32578) (#32594) + * Fix debian package clean up cron job (#32351) (#32590) + * Fix GetInactiveUsers (#32540) (#32588) + * Allow the actions user to login via the jwt token (#32527) (#32580) + * Fix submodule parsing (#32571) (#32577) + * Refactor find forks and fix possible bugs that weaken permissions check (#32528) (#32547) + * Fix some places that don't respect org full name setting (#32243) (#32550) + * Refactor push mirror find and add check for updating push mirror (#32539) (#32549) + * Fix basic auth with webauthn (#32531) (#32536) + * Fix artifact v4 upload above 8MB (#31664) (#32523) + * Fix oauth2 error handle not return immediately (#32514) (#32516) + * Fix action not triggered when commit message is too long (#32498) (#32507) + * Fix `GetRepoLink` nil pointer dereference on dashboard feed page when repo is deleted with actions enabled (#32501) (#32502) + * Fix `missing signature key` error when pulling Docker images with `SERVE_DIRECT` enabled (#32397) (#32397) + * Fix the permission check for user search API and limit the number of returned users for `/user/search` (#32310) + * Fix SearchIssues swagger docs (#32208) (#32298) + * Fix dropdown content overflow (#31610) (#32250) + * Disable Oauth check if oauth disabled (#32368) (#32480) + * Respect renamed dependencies of Cargo registry (#32430) (#32478) + * Fix mermaid diagram height when initially hidden (#32457) (#32464) + * Fix broken releases when re-pushing tags (#32435) (#32449) + * Only provide the commit summary for Discord webhook push events (#32432) (#32447) + * Only query team tables if repository is under org when getting assignees (#32414) (#32426) + * Fix created_unix for mirroring (#32342) (#32406) + * Respect UI.ExploreDefaultSort setting again (#32357) (#32385) + * Fix broken image when editing comment with non-image attachments (#32319) (#32345) + * Fix disable 2fa bug (#32320) (#32330) + * Always update expiration time when creating an artifact (#32281) (#32285) + * Fix null errors on conversation holder (#32258) (#32266) (#32282) + * Only rename a user when they should receive a different name (#32247) (#32249) + * Fix checkbox bug on private/archive filter (#32236) (#32240) + * Add a doctor check to disable the "Actions" unit for mirrors (#32424) (#32497) + * Quick fix milestone deadline 9999 (#32423) + * Make `show stats` work when only one file changed (#32244) (#32268) + * Make `owner/repo/pulls` handlers use "PR reader" permission (#32254) (#32265) + * Update scheduled tasks even if changes are pushed by "ActionsUser" (#32246) (#32252) +* MISC + * Remove unnecessary code: `GetPushMirrorsByRepoID` called on all repo pages (#32560) (#32567) + * Improve some sanitizer rules (#32534) + * Update nix development environment vor v1.22.x (#32495) + * Add warn log when deleting inactive users (#32318) (#32321) + * Update github.com/go-enry/go-enry to v2.9.1 (#32295) (#32296) + * Warn users when they try to use a non-root-url to sign in/up (#32272) (#32273) + +## [1.22.3](https://github.com/go-gitea/gitea/releases/tag/v1.22.3) - 2024-10-08 + +* SECURITY + * Fix bug when a token is given public only (#32204) (#32218) +* PERFORMANCE + * Increase `cacheContextLifetime` to reduce false reports (#32011) (#32023) + * Don't join repository when loading action table data (#32127) (#32143) +* BUGFIXES + * Fix javascript error when an anonymous user visits migration page (#32144) (#32179) + * Don't init signing keys if oauth2 provider is disabled (#32177) + * Fix wrong status of `Set up Job` when first step is skipped (#32120) (#32125) + * Fix bug when deleting a migrated branch (#32075) (#32123) + * Truncate commit message during Discord webhook push events (#31970) (#32121) + * Allow to set branch protection in an empty repository (#32095) (#32119) + * Fix panic when cloning with wrong ssh format. (#32076) (#32118) + * Fix rename branch permission bug (#32066) (#32108) + * Fix: database not update release when using `git push --tags --force` (#32040) (#32074) + * Add missing comment reply handling (#32050) (#32065) + * Do not escape relative path in RPM primary index (#32038) (#32054) + * Fix `/repos/{owner}/{repo}/pulls/{index}/files` endpoint not populating `previous_filename` (#32017) (#32028) + * Support allowed hosts for migrations to work with proxy (#32025) (#32026) + * Fix the logic of finding the latest pull review commit ID (#32139) (#32165) + * Fix bug in getting merged pull request by commit (#32079) (#32117) + * Fix wrong last modify time (#32102) (#32104) + * Fix incorrect `/tokens` api (#32085) (#32092) + * Handle invalid target when creating releases using API (#31841) (#32043) + * Check if the `due_date` is nil when editing issues (#32035) (#32042) + * Fix container parallel upload bugs (#32022) + * Fixed race condition when deleting documents by repoId in ElasticSearch (#32185) (#32188) + * Refactor CSRF protector (#32057) (#32069) + * Fix Bug in Issue/pulls list (#32081) (#32115) + * Include collaboration repositories on dashboard source/forks/mirrors list (#31946) (#32122) + * Add null check for responseData.invalidTopics (#32212) (#32217) +* TESTING + * Fix mssql ci with a new mssql version on ci (#32094) +* MISC + * Upgrade some dependencies include minio-go (#32166) + * Add bin to Composer Metadata (#32099) (#32106) + * Lazy load avatar images (#32051) (#32063) + * Upgrade cache to v0.2.1 (#32003) (#32009) + +## [1.22.2](https://github.com/go-gitea/gitea/releases/tag/v1.22.2) - 2024-08-28 + +* Security + * Replace v-html with v-text in search inputbox (#31966) (#31973) + * Fix nuget/conan/container packages upload bugs (#31967) (#31982) +* PERFORMANCE + * Refactor the usage of batch catfile (#31754) (#31889) +* BUGFIXES + * Fix overflowing content in action run log (#31842) (#31853) + * Scroll images in project issues separately from the remaining issue (#31683) (#31823) + * Add `:focus-visible` style to buttons (#31799) (#31819) + * Fix the display of project type for deleted projects (#31732) (#31734) + * Fix API owner ID should be zero when created repo secret (#31715) (#31811) + * Set owner id to zero when GetRegistrationToken for repo (#31725) (#31729) + * Fix API endpoint for registration-token (#31722) (#31728) + * Add permission check when creating PR (#31033) (#31720) + * Don't return 500 if mirror url contains special chars (#31859) (#31895) + * Fix agit automerge (#31207) (#31881) + * Add CfTurnstileSitekey context data to all captcha templates (#31874) (#31876) + * Avoid returning without written ctx when posting PR (#31843) (#31848) + * Fix raw wiki links (#31825) (#31845) + * Fix panic of ssh public key page after deletion of auth source (#31829) (#31836) + * Fixes for unreachable project issues when transfer repository from organization (#31770) (#31828) + * Show lock owner instead of repo owner on LFS setting page (#31788) (#31817) + * Fix `IsObjectExist` with gogit (#31790) (#31806) + * Fix protected branch files detection on pre_receive hook (#31778) (#31796) + * Add `TAGS` to `TEST_TAGS` and fix bugs found with gogit (#31791) (#31795) + * Rename head branch of pull requests when renaming a branch (#31759) (#31774) + * Fix wiki revision pagination (#31760) (#31772) + * Bump vue-bar-graph (#31705) (#31753) + * Distinguish LFS object errors to ignore missing objects during migration (#31702) (#31745) + * Make GetRepositoryByName more safer (#31712) (#31718) + * Fix a branch divergence cache bug (#31659) (#31661) + * Allow org team names of length 255 in create team form (#31564) (#31603) + * Use old behavior for telegram webhook (#31588) + * Bug fix for translation in ru (#31892) + * Fix actions notify bug (#31866) (#31875) + * Fix the component of access token list not mounted (#31824) (#31868) + * Add missing repository type filter parameters to pager (#31832) (#31837) + * Fix dates displaying in a wrong manner when we're close to the end of… (#31750) + * Fix "Filter by commit" Dropdown (#31695) (#31696) + * Properly filter issue list given no assignees filter (#31522) (#31685) + * Prevent update pull refs manually and will not affect other refs update (#31931)(#31955) + * Fix sort order for organization home and user profile page (#31921) (#31922) + * Fix search team (#31923) (#31942) + * Fix 500 error when state params is set when editing issue/PR by API (#31880) (#31952) + * Fix index too many file names bug (#31903) (#31953) + * Add lock for parallel maven upload (#31851) (#31954) +* MISC + * Remove "dsa-1024" testcases from Test_SSHParsePublicKey and Test_calcFingerprint (#31905) (#31914) + * Upgrade bleve to 2.4.2 (#31894) + * Remove unneccessary uses of `word-break: break-all` (#31637) (#31652) + * Return an empty string when a repo has no avatar in the repo API (#31187) (#31567) + * Upgrade micromatch to 4.0.8 (#31944) + * Update webpack to 5.94.0 (#31941) + +## [1.22.1](https://github.com/go-gitea/gitea/releases/tag/v1.22.1) - 2024-07-04 + +* SECURITY + * Add replacement module for `mholt/archiver` (#31267) (#31270) +* API + * Fix missing images in editor preview due to wrong links (#31299) (#31393) + * Fix duplicate sub-path for avatars (#31365) (#31368) + * Reduce memory usage for chunked artifact uploads to MinIO (#31325) (#31338) + * Remove sub-path from container registry realm (#31293) (#31300) + * Fix NuGet Package API for $filter with Id equality (#31188) (#31242) + * Add an immutable tarball link to archive download headers for Nix (#31139) (#31145) + * Add missed return after `ctx.ServerError` (#31130) (#31133) +* BUGFIXES + * Fix avatar radius problem on the new issue page (#31506) (#31508) + * Fix overflow menu flickering on mobile (#31484) (#31488) + * Fix poor table column width due to breaking words (#31473) (#31477) + * Support relative paths to videos from Wiki pages (#31061) (#31453) + * Fix new issue/pr avatar (#31419) (#31424) + * Increase max length of org team names from 30 to 255 characters (#31410) (#31421) + * Fix line number width in code preview (#31307) (#31316) + * Optimize runner-tags layout to enhance visual experience (#31258) (#31263) + * Fix overflow on push notification (#31179) (#31238) + * Fix overflow on notifications (#31178) (#31237) + * Fix overflow in issue card (#31203) (#31225) + * Split sanitizer functions and fine-tune some tests (#31192) (#31200) + * use correct l10n string (#31487) (#31490) + * Fix dropzone JS error when attachment is disabled (#31486) + * Fix web notification icon not updated once you read all notifications (#31447) (#31466) + * Switch to "Write" tab when edit comment again (#31445) (#31461) + * Fix the link for .git-blame-ignore-revs bypass (#31432) (#31442) + * Fix the wrong line number in the diff view page when expanded twice. (#31431) (#31440) + * Fix labels and projects menu overflow on issue page (#31435) (#31439) + * Fix Account Linking UpdateMigrationsByType (#31428) (#31434) + * Fix markdown math brackets render problem (#31420) (#31430) + * Fix rendered wiki page link (#31398) (#31407) + * Fix natural sort (#31384) (#31394) + * Allow downloading attachments of draft releases (#31369) (#31380) + * Fix repo graph JS (#31377) + * Fix incorrect localization `explorer.go` (#31348) (#31350) + * Fix hash render end with colon (#31319) (#31346) + * Fix line number widths (#31341) (#31343) + * Fix navbar `+` menu flashing on page load (#31281) (#31342) + * Fix adopt repository has empty object name in database (#31333) (#31335) + * Delete legacy cookie before setting new cookie (#31306) (#31317) + * Fix some URLs whose sub-path is missing (#31289) (#31292) + * Fix admin oauth2 custom URL settings (#31246) (#31247) + * Make pasted "img" tag has the same behavior as markdown image (#31235) (#31243) + * Fix agit checkout command line hint & fix ShowMergeInstructions checking (#31219) (#31222) + * Fix the possible migration failure on 286 with postgres 16 (#31209) (#31218) + * Fix branch order (#31174) (#31193) + * Fix markup preview (#31158) (#31166) + * Fix push multiple branches error with tests (#31151) (#31153) + * Fix API repository object format missed (#31118) (#31132) + * Fix missing memcache import (#31105) (#31109) + * Upgrade `github.com/hashicorp/go-retryablehttp` (#31499) + * Fix double border in system status table (#31363) (#31401) + * Fix bug filtering issues which have no project (#31337) (#31367) + * Fix #31185 try fix lfs download from bitbucket failed (#31201) (#31329) + * Add nix flake for dev shell (#30967) (#31310) + * Fix and clean up `ConfirmModal` (#31283) (#31291) + * Optimize repo-list layout to enhance visual experience (#31272) (#31276) + * fixed the dropdown menu for the top New button to expand to the left (#31273) (#31275) + * Fix Activity Page Contributors dropdown (#31264) (#31269) + * fix: allow actions artifacts storage migration to complete succesfully (#31251) (#31257) + * Make blockquote attention recognize more syntaxes (#31240) (#31250) + * Remove .segment from .project-column (#31204) (#31239) + * Ignore FindRecentlyPushedNewBranches err (#31164) (#31171) + * Use vertical layout for multiple code expander buttons (#31122) (#31152) + * Remove duplicate `ProxyPreserveHost` in Apache httpd doc (#31143) (#31147) + * Improve mobile review ui (#31091) (#31136) + * Fix DashboardRepoList margin (#31121) (#31128) + * Update pip related commands for docker (#31106) (#31111) + +## [1.22.0](https://github.com/go-gitea/gitea/releases/tag/v1.22.0) - 2024-05-27 + +This release stands as a monumental milestone in our development journey with a record-breaking incorporation of [1528](https://github.com/go-gitea/gitea/pulls?q=is%3Apr+milestone%3A1.22.0+is%3Amerged) pull requests. It marks the most extensive update in Gitea's history, showcasing a plethora of new features and infrastructure improvements. + +Noteworthy advancements in this release include the introduction of `HTMX` and `Tailwind`, signaling a strategic shift as we gradually phase out `jquery` and `Fomantic UI`. These changes reflect our commitment to embracing modern technologies and enhancing the user experience. + +Key highlights of this release encompass significant changes categorized under `BREAKING`, `FEATURES`, `ENHANCEMENTS`, and `PERFORMANCE`, each contributing to a more robust and efficient Gitea platform. + +* BREAKING + * Improve reverse proxy documents and clarify the AppURL guessing behavior (#31003) (#31020) + * Remember log in for a month by default (#30150) + * Breaking summary for template refactoring (#29395) + * All custom templates need to follow these changes + * Recommend/convert to use case-sensitive collation for MySQL/MSSQL (#28662) + * Make offline mode as default to not connect external avatar service by default (#28548) + * Include public repos in the doer's dashboard for issue search (#28304) + * Use restricted sanitizer for repository description (#28141) + * Support storage base path as prefix (#27827) + * Enhanced auth token / remember me (#27606) + * Rename the default themes to `gitea-light`, `gitea-dark`, `gitea-auto` (#27419) + * If you didn't see the new themes, please remove the `[ui].THEMES` config option from `app.ini` + * Require MySQL 8.0, PostgreSQL 12, MSSQL 2012 (#27337) +* FEATURES + * Allow everyone to read or write a wiki by a repo unit setting (#30495) + * Use raw Wiki links for non-renderable Wiki files (#30273) + * Render embedded code preview by permalink in markdown (#30234) (#30249) + * Support repo code search without setting up an indexer (#29998) + * Support pasting URLs over markdown text (#29566) + * Allow to change primary email before account activation (#29412) + * Customizable "Open with" applications for repository clone (#29320) + * Allow options to disable user deletion from the interface on app.ini (#29275) + * Extend issue template YAML engine (#29274) + * Add support for `linguist-detectable` and `linguist-documentation` (#29267) + * Implement code frequency graph (#29191) + * Show commit status for releases (#29149) + * Add user blocking (#29028) + * Actions Artifacts v4 backend (#28965) + * Add merge style `fast-forward-only` (#28954) + * Retarget depending pulls when the parent branch is deleted (#28686) + * Add global setting on how timestamps should be rendered (#28657) + * Implement actions badge SVGs (#28102) + * Add skip ci functionality (#28075) + * Show latest commit for file (#28067) + * Allow to sync tags from the admin dashboard (#28045) + * Add Profile Readme for Organisations (#27955) + * Implement contributors graph (#27882) + * Artifact deletion in actions ui (#27172) + * Add API routes to get runner registration token (#27144) + * Add support for forking single branch (#25821) + * Add support for sha256 repositories (#23894) + * Add admin API route for managing user's badges (#23106) +* ENHANCEMENTS + * Make gitea webhooks openproject compatible (#28435) (#31081) + * Support using label names when changing issue labels (#30943) (#30958) + * Fix various problems around project board view (#30696) (#30902) + * Improve context popup rendering (#30824) (#30829) + * Allow to save empty comment (#30706) + * Prevent allow/reject reviews on merged/closed PRs (#30686) + * Initial support for colorblindness-friendly themes (#30625) + * Some NuGet package enhancements (#30280) (#30324) + * Markup color and font size fixes (#30282) (#30310) + * Show 12 lines in markup code preview (#30255) (#30257) + * Add `[other].SHOW_FOOTER_POWERED_BY` setting to hide `Powered by` (#30253) + * Pulse page improvements (#30149) + * Render code tags in commit messages (#30146) + * Prevent re-review and dismiss review actions on closed and merged PRs (#30065) + * Cancel previous runs of the same PR automatically (#29961) + * Drag-and-drop improvements for projects and issue pins (#29875) + * Add default board to new projects, remove uncategorized pseudo-board (#29874) + * Prevent layout shift in `` items (#29831) + * Add skip ci support for pull request title (#29774) + * Add more stats tables (#29730) + * Update API to return 'source_id' for users (#29718) + * Determine fuzziness of bleve indexer by keyword length (#29706) + * Expose fuzzy search for issues/pulls (#29701) + * Put an edit file button on pull request files to allow a quick operation (#29697) + * Fix action runner offline label padding (#29691) + * Update allowed attachment types (#29688) + * Completely style the webkit autofill (#29683) + * Highlight archived labels (#29680) + * Add a warning for disallowed email domains (#29658) + * Set user's 24h preference from their current OS locale (#29651) + * Add setting to disable user features when user login type is not plain (#29615) + * Improve natural sort (#29611) + * Make wiki default branch name changeable (#29603) + * Unify search boxes (#29530) + * Add support for API blob upload of release attachments (#29507) + * Detect broken git hooks (#29494) + * Sync branches to DB immediately when handling git hook calling (#29493) + * Allow options to disable user GPG key configuration from the interface on app.ini (#29486) + * Allow options to disable user SSH key configuration from the interface on app.ini (#29447) + * Use relative links for commits, mentions, and issues in markdown (#29427) + * Add ``, rename webcomponents (#29400) + * Include resource state events in Gitlab downloads (#29382) + * Properly migrate target branch change GitLab comment (#29340) + * Recolor dark theme to blue shade (#29283) + * Partially enable MSSQL case-sensitive collation support (#29238) + * Auto-update the system status in the admin dashboard (#29163) + * Integrate alpine `noarch` packages into other architectures index (#29137) + * Document how the TOC election process works (#29135) + * Tweak repo header (#29134) + * Make blockquote border size less aggressive (#29124) + * Downscale pasted PNG images based on metadata (#29123) + * Show `View at this point in history` for every commit (#29122) + * Add support for action artifact serve direct (#29120) + * Change webhook-type in create-view (#29114) + * Drop "@" from the email sender to avoid spam filters (#29109) + * Allow non-admin users to delete review requests (#29057) + * Improve user search display name (#29002) + * Include username in email headers (#28981) + * Show whether a PR is WIP inside popups (#28975) + * Also match weakly validated ETags (#28957) + * Support nuspec manifest download for Nuget packages (#28921) + * Fix hardcoded GitHub icon used as migrated release avatar (#28910) + * Propagate install_if and provider_priority to APKINDEX (#28899) + * Add artifacts v4 JWT to job message and accept it (#28885) + * Enable/disable owner and repo projects independently (#28805) + * Add non-JS fallback for reaction tooltips (#28785) + * Add the ability to see open and closed issues at the same time (#28757) + * Move sign-in labels to be above inputs (#28753) + * Display the latest sync time for pull mirrors on the repo page (#28712) + * Show in Web UI if the file is vendored and generated (#28620) + * Add orphaned topic consistency check (#28507) + * Add branch protection setting for ignoring stale approvals (#28498) + * Add option to set language in admin user view (#28449) + * Fix incorrect run order of action jobs (#28367) + * Add missing exclusive in advanced label options (#28322) + * Added instance-level variables (#28115) + * Add edit option for README.md (#28071) + * Fix link to `Code` tab on wiki commits (#28041) + * Allow to set explore page default sort (#27951) + * Improve PR diff view on mobile (#27883) + * Properly migrate automatic merge GitLab comments (#27873) + * Display issue task list on project cards (#27865) + * Add Index to pull_auto_merge.doer_id (#27811) + * Fix display member unit in the menu bar if there are no hidden members in public org (#27795) + * List all Debian package versions in `Packages` (#27786) + * Allow pull requests Manually Merged option to be used by non-admins (#27780) + * Only show diff file tree when more than one file changed (#27775) + * Show placeholder email in privacy popup (#27770) + * Revamp repo header (#27760) + * Add `must-change-password` command line parameter (#27626) + * Unify password changing and invalidate auth tokens (#27625) + * Add border to file tree 'sub-items' and add padding to 'item-file' (#27593) + * Add slow SQL query warning (#27545) + * Pre-register OAuth application for tea (#27509) + * Differentiate between `push` and `pull` `mirror sync in progress` (#27390) + * Link to file from its history (#27354) + * Add a shortcut to user's profile page to admin user details (#27299) + * Doctor: delete action entries without existing user (#27292) + * Show total TrackedTime on issue/pull/milestone lists (#26672) + * Don't show the new pull request button when the page is not compare pull (#26431) + * Add `Hide/Show all checks` button to commit status check (#26284) + * Improvements of releases list and tags list (#25859) +* PERFORMANCE + * Fix package list performance (#30520) (#30616) + * Add commit status summary table to reduce query from commit status table (#30223) + * Refactor markup/csv: don't read all to memory (#29760) + * Lazy load object format with command line and don't do it in OpenRepository (#29712) + * Add cache for branch divergence on branch list page (#29577) + * Do some performance optimization for issues list and view issue/pull (#29515) + * Cache repository default branch commit status to reduce query on commit status table (#29444) + * Use `crypto/sha256` (#29386) + * Some performance optimization on the dashboard and issues page (#29010) + * Add combined index for issue_user.uid and issue_id (#28080) + ## [1.21.11](https://github.com/go-gitea/gitea/releases/tag/v1.21.11) - 2024-04-07 * SECURITY diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 979831eb9b8..6a7126388ef 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -30,7 +30,7 @@ These are the values to which people in the Gitea community should aspire. - **Be constructive.** - Avoid derailing: stay on topic; if you want to talk about something else, start a new conversation. - Avoid unconstructive criticism: don't merely decry the current state of affairs; offer—or at least solicit—suggestions as to how things may be improved. - - Avoid snarking (pithy, unproductive, sniping comments) + - Avoid snarking (pithy, unproductive, sniping comments). - Avoid discussing potentially offensive or sensitive issues; this all too often leads to unnecessary conflict. - Avoid microaggressions (brief and commonplace verbal, behavioral and environmental indignities that communicate hostile, derogatory or negative slights and insults to a person or group). - **Be responsible.** @@ -42,7 +42,7 @@ People are complicated. You should expect to be misunderstood and to misundersta ### Our Pledge -In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. +In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to make participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. ### Our Standards diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 60146276dba..96e05c578fc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -182,7 +182,7 @@ Here's how to run the test suite: ## Translation -All translation work happens on [Crowdin](https://crowdin.com/project/gitea). +All translation work happens on [Crowdin](https://translate.gitea.com). The only translation that is maintained in this repository is [the English translation](https://github.com/go-gitea/gitea/blob/main/options/locale/locale_en-US.ini). It is synced regularly with Crowdin. \ Other locales on main branch **should not** be updated manually as they will be overwritten with each sync. \ @@ -591,7 +591,7 @@ be reviewed by two maintainers and must pass the automatic tests. ## Releasing Gitea - Let $vmaj, $vmin and $vpat be Major, Minor and Patch version numbers, $vpat should be rc1, rc2, 0, 1, ...... $vmaj.$vmin will be kept the same as milestones on github or gitea in future. -- Before releasing, confirm all the version's milestone issues or PRs has been resolved. Then discuss the release on Discord channel #maintainers and get agreed with almost all the owners and mergers. Or you can declare the version and if nobody against in about serval hours. +- Before releasing, confirm all the version's milestone issues or PRs has been resolved. Then discuss the release on Discord channel #maintainers and get agreed with almost all the owners and mergers. Or you can declare the version and if nobody is against it in about several hours. - If this is a big version first you have to create PR for changelog on branch `main` with PRs with label `changelog` and after it has been merged do following steps: - Create `-dev` tag as `git tag -s -F release.notes v$vmaj.$vmin.0-dev` and push the tag as `git push origin v$vmaj.$vmin.0-dev`. - When CI has finished building tag then you have to create a new branch named `release/v$vmaj.$vmin` diff --git a/Dockerfile b/Dockerfile index 2a6b1dd6b7d..014eed9d467 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Build stage -FROM docker.io/library/golang:1.23-alpine3.20 AS build-env +FROM docker.io/library/golang:1.25-alpine3.22 AS build-env ARG GOPROXY ENV GOPROXY=${GOPROXY:-direct} @@ -39,9 +39,8 @@ RUN chmod 755 /tmp/local/usr/bin/entrypoint \ /tmp/local/etc/s6/.s6-svscan/* \ /go/src/code.gitea.io/gitea/gitea \ /go/src/code.gitea.io/gitea/environment-to-ini -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.22 LABEL maintainer="maintainers@gitea.io" EXPOSE 22 3000 @@ -78,9 +77,8 @@ ENV GITEA_CUSTOM=/data/gitea VOLUME ["/data"] 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 /go/src/code.gitea.io/gitea/gitea /app/gitea/gitea COPY --from=build-env /go/src/code.gitea.io/gitea/environment-to-ini /usr/local/bin/environment-to-ini -COPY --from=build-env /go/src/code.gitea.io/gitea/contrib/autocompletion/bash_autocomplete /etc/profile.d/gitea_bash_autocomplete.sh diff --git a/Dockerfile.rootless b/Dockerfile.rootless index 26f02205a7a..677d36bbcc0 100644 --- a/Dockerfile.rootless +++ b/Dockerfile.rootless @@ -1,5 +1,5 @@ # Build stage -FROM docker.io/library/golang:1.23-alpine3.20 AS build-env +FROM docker.io/library/golang:1.25-alpine3.22 AS build-env ARG GOPROXY ENV GOPROXY=${GOPROXY:-direct} @@ -37,9 +37,8 @@ RUN chmod 755 /tmp/local/usr/local/bin/docker-entrypoint.sh \ /tmp/local/usr/local/bin/gitea \ /go/src/code.gitea.io/gitea/gitea \ /go/src/code.gitea.io/gitea/environment-to-ini -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.22 LABEL maintainer="maintainers@gitea.io" EXPOSE 2222 3000 @@ -52,6 +51,7 @@ RUN apk --no-cache add \ git \ curl \ gnupg \ + openssh-keygen \ && rm -rf /var/cache/apk/* RUN addgroup \ @@ -71,7 +71,6 @@ RUN chown git:git /var/lib/gitea /etc/gitea COPY --from=build-env /tmp/local / COPY --from=build-env --chown=root:root /go/src/code.gitea.io/gitea/gitea /app/gitea/gitea COPY --from=build-env --chown=root:root /go/src/code.gitea.io/gitea/environment-to-ini /usr/local/bin/environment-to-ini -COPY --from=build-env /go/src/code.gitea.io/gitea/contrib/autocompletion/bash_autocomplete /etc/profile.d/gitea_bash_autocomplete.sh # git:git USER 1000:1000 diff --git a/MAINTAINERS b/MAINTAINERS index 426181cbcf1..63da471f9b1 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -31,22 +31,18 @@ Gary Kim (@gary-kim) Guillermo Prandi (@guillep2k) Mura Li (@typeless) 6543 <6543@obermui.de> (@6543) -jaqra (@jaqra) David Svantesson (@davidsvantesson) a1012112796 <1012112796@qq.com> (@a1012112796) Karl Heinz Marbaise (@khmarbaise) Norwin Roosen (@noerw) Kyle Dumont (@kdumontnu) -Patrick Schratz (@pat-s) Janis Estelmann (@KN4CK3R) -Steven Kriegler (@justusbunsi) Jimmy Praet (@jpraet) Leon Hofmeister (@delvh) Wim (@42wim) Jason Song (@wolfogre) Yarden Shoham (@yardenshoham) Yu Tian (@Zettat123) -Eddie Yang <576951401@qq.com> (@yp05327) Dong Ge (@sillyguodong) Xinyi Gong (@HesterG) wxiaoguang (@wxiaoguang) @@ -64,3 +60,6 @@ Yu Liu <1240335630@qq.com> (@HEREYUA) Kemal Zebari (@kemzeb) Rowan Bohde (@bohde) hiifong (@hiifong) +metiftikci (@metiftikci) +Christopher Homberger (@ChristopherHX) +Tobias Balle-Petersen (@tobiasbp) diff --git a/Makefile b/Makefile index 0cd6936b3bb..9cd32e4c33f 100644 --- a/Makefile +++ b/Makefile @@ -23,20 +23,21 @@ SHASUM ?= shasum -a 256 HAS_GO := $(shell hash $(GO) > /dev/null 2>&1 && echo yes) COMMA := , -XGO_VERSION := go-1.23.x +XGO_VERSION := go-1.25.x AIR_PACKAGE ?= github.com/air-verse/air@v1 -EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/cmd/editorconfig-checker@2.7.0 -GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.7.0 -GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/cmd/golangci-lint@v1.60.3 -GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.11 -MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.5.1 -SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.31.0 +EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/v3/cmd/editorconfig-checker@v3 +GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.8.0 +GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.4.0 +GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.12 +MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.7.0 +SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.32.3 XGO_PACKAGE ?= src.techknowlogick.com/xgo@latest GO_LICENSES_PACKAGE ?= github.com/google/go-licenses@v1 GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1 ACTIONLINT_PACKAGE ?= github.com/rhysd/actionlint/cmd/actionlint@v1 -GOPLS_PACKAGE ?= golang.org/x/tools/gopls@v0.15.3 +GOPLS_PACKAGE ?= golang.org/x/tools/gopls@v0.20.0 +GOPLS_MODERNIZE_PACKAGE ?= golang.org/x/tools/gopls/internal/analysis/modernize/cmd/modernize@v0.20.0 DOCKER_IMAGE ?= gitea/gitea DOCKER_TAG ?= latest @@ -47,6 +48,17 @@ ifeq ($(HAS_GO), yes) CGO_CFLAGS ?= $(shell $(GO) env CGO_CFLAGS) $(CGO_EXTRA_CFLAGS) endif +CGO_ENABLED ?= 0 +ifneq (,$(findstring sqlite,$(TAGS))$(findstring pam,$(TAGS))) + CGO_ENABLED = 1 +endif + +STATIC ?= +EXTLDFLAGS ?= +ifneq ($(STATIC),) + EXTLDFLAGS = -extldflags "-static" +endif + ifeq ($(GOOS),windows) IS_WINDOWS := yes else ifeq ($(patsubst Windows%,Windows,$(OS)),Windows) @@ -73,13 +85,13 @@ EXTRA_GOFLAGS ?= MAKE_VERSION := $(shell "$(MAKE)" -v | cat | head -n 1) MAKE_EVIDENCE_DIR := .make_evidence +GOTESTFLAGS ?= ifeq ($(RACE_ENABLED),true) GOFLAGS += -race GOTESTFLAGS += -race endif STORED_VERSION_FILE := VERSION -HUGO_VERSION ?= 0.111.3 GITHUB_REF_TYPE ?= branch GITHUB_REF_NAME ?= $(shell git rev-parse --abbrev-ref HEAD) @@ -109,20 +121,17 @@ endif LDFLAGS := $(LDFLAGS) -X "main.MakeVersion=$(MAKE_VERSION)" -X "main.Version=$(GITEA_VERSION)" -X "main.Tags=$(TAGS)" -LINUX_ARCHS ?= linux/amd64,linux/386,linux/arm-5,linux/arm-6,linux/arm64 +LINUX_ARCHS ?= linux/amd64,linux/386,linux/arm-5,linux/arm-6,linux/arm64,linux/riscv64 GO_TEST_PACKAGES ?= $(filter-out $(shell $(GO) list code.gitea.io/gitea/models/migrations/...) code.gitea.io/gitea/tests/integration/migration-test code.gitea.io/gitea/tests code.gitea.io/gitea/tests/integration code.gitea.io/gitea/tests/e2e,$(shell $(GO) list ./... | grep -v /vendor/)) MIGRATE_TEST_PACKAGES ?= $(shell $(GO) list code.gitea.io/gitea/models/migrations/...) -FOMANTIC_WORK_DIR := web_src/fomantic - WEBPACK_SOURCES := $(shell find web_src/js web_src/css -type f) WEBPACK_CONFIGS := webpack.config.js tailwind.config.js WEBPACK_DEST := public/assets/js/index.js public/assets/css/index.css WEBPACK_DEST_ENTRIES := public/assets/js public/assets/css public/assets/fonts -BINDATA_DEST := modules/public/bindata.go modules/options/bindata.go modules/templates/bindata.go -BINDATA_HASH := $(addsuffix .hash,$(BINDATA_DEST)) +BINDATA_DEST_WILDCARD := modules/migration/bindata.* modules/public/bindata.* modules/options/bindata.* modules/templates/bindata.* GENERATED_GO_DEST := modules/charset/invisible_gen.go modules/charset/ambiguous_gen.go @@ -139,25 +148,19 @@ TAGS_EVIDENCE := $(MAKE_EVIDENCE_DIR)/tags TEST_TAGS ?= $(TAGS_SPLIT) sqlite sqlite_unlock_notify -TAR_EXCLUDES := .git data indexers queues log node_modules $(EXECUTABLE) $(FOMANTIC_WORK_DIR)/node_modules $(DIST) $(MAKE_EVIDENCE_DIR) $(AIR_TMP_DIR) $(GO_LICENSE_TMP_DIR) +TAR_EXCLUDES := .git data indexers queues log node_modules $(EXECUTABLE) $(DIST) $(MAKE_EVIDENCE_DIR) $(AIR_TMP_DIR) $(GO_LICENSE_TMP_DIR) GO_DIRS := build cmd models modules routers services tests WEB_DIRS := web_src/js web_src/css -ESLINT_FILES := web_src/js tools *.js *.ts tests/e2e +ESLINT_FILES := web_src/js tools *.js *.ts *.cjs tests/e2e STYLELINT_FILES := web_src/css web_src/js/components/*.vue -SPELLCHECK_FILES := $(GO_DIRS) $(WEB_DIRS) templates options/locale/locale_en-US.ini .github $(filter-out CHANGELOG.md, $(wildcard *.go *.js *.md *.yml *.yaml *.toml)) +SPELLCHECK_FILES := $(GO_DIRS) $(WEB_DIRS) templates options/locale/locale_en-US.ini .github $(filter-out CHANGELOG.md, $(wildcard *.go *.js *.md *.yml *.yaml *.toml)) $(filter-out tools/misspellings.csv, $(wildcard tools/*)) EDITORCONFIG_FILES := templates .github/workflows options/locale/locale_en-US.ini GO_SOURCES := $(wildcard *.go) -GO_SOURCES += $(shell find $(GO_DIRS) -type f -name "*.go" ! -path modules/options/bindata.go ! -path modules/public/bindata.go ! -path modules/templates/bindata.go) +GO_SOURCES += $(shell find $(GO_DIRS) -type f -name "*.go") GO_SOURCES += $(GENERATED_GO_DEST) -GO_SOURCES_NO_BINDATA := $(GO_SOURCES) - -ifeq ($(filter $(TAGS_SPLIT),bindata),bindata) - GO_SOURCES += $(BINDATA_DEST) - GENERATED_GO_DEST += $(BINDATA_DEST) -endif # Force installation of playwright dependencies by setting this flag ifdef DEPS_PLAYWRIGHT @@ -165,10 +168,8 @@ ifdef DEPS_PLAYWRIGHT endif SWAGGER_SPEC := templates/swagger/v1_json.tmpl -SWAGGER_SPEC_S_TMPL := s|"basePath": *"/api/v1"|"basePath": "{{AppSubUrl \| JSEscape}}/api/v1"|g -SWAGGER_SPEC_S_JSON := s|"basePath": *"{{AppSubUrl \| JSEscape}}/api/v1"|"basePath": "/api/v1"|g +SWAGGER_SPEC_INPUT := templates/swagger/v1_input.json SWAGGER_EXCLUDE := code.gitea.io/sdk -SWAGGER_NEWLINE_COMMAND := -e '$$a\' TEST_MYSQL_HOST ?= mysql:3306 TEST_MYSQL_DBNAME ?= testgitea @@ -189,67 +190,11 @@ TEST_MSSQL_PASSWORD ?= MwantsaSecurePassword1 all: build .PHONY: help -help: - @echo "Make Routines:" - @echo " - \"\" equivalent to \"build\"" - @echo " - build build everything" - @echo " - frontend build frontend files" - @echo " - backend build backend files" - @echo " - watch watch everything and continuously rebuild" - @echo " - watch-frontend watch frontend files and continuously rebuild" - @echo " - watch-backend watch backend files and continuously rebuild" - @echo " - clean delete backend and integration files" - @echo " - clean-all delete backend, frontend and integration files" - @echo " - deps install dependencies" - @echo " - deps-frontend install frontend dependencies" - @echo " - deps-backend install backend dependencies" - @echo " - deps-tools install tool dependencies" - @echo " - deps-py install python dependencies" - @echo " - lint lint everything" - @echo " - lint-fix lint everything and fix issues" - @echo " - lint-actions lint action workflow files" - @echo " - lint-frontend lint frontend files" - @echo " - lint-frontend-fix lint frontend files and fix issues" - @echo " - lint-backend lint backend files" - @echo " - lint-backend-fix lint backend files and fix issues" - @echo " - lint-go lint go files" - @echo " - lint-go-fix lint go files and fix issues" - @echo " - lint-go-vet lint go files with vet" - @echo " - lint-go-gopls lint go files with gopls" - @echo " - lint-js lint js files" - @echo " - lint-js-fix lint js files and fix issues" - @echo " - lint-css lint css files" - @echo " - lint-css-fix lint css files and fix issues" - @echo " - lint-md lint markdown files" - @echo " - lint-swagger lint swagger files" - @echo " - lint-templates lint template files" - @echo " - lint-yaml lint yaml files" - @echo " - lint-spell lint spelling" - @echo " - lint-spell-fix lint spelling and fix issues" - @echo " - checks run various consistency checks" - @echo " - checks-frontend check frontend files" - @echo " - checks-backend check backend files" - @echo " - test test everything" - @echo " - test-frontend test frontend files" - @echo " - test-backend test backend files" - @echo " - test-e2e[\#TestSpecificName] test end to end using playwright" - @echo " - update update js and py dependencies" - @echo " - update-js update js dependencies" - @echo " - update-py update py dependencies" - @echo " - webpack build webpack files" - @echo " - svg build svg files" - @echo " - fomantic build fomantic files" - @echo " - generate run \"go generate\"" - @echo " - fmt format the Go code" - @echo " - generate-license update license files" - @echo " - generate-gitignore update gitignore files" - @echo " - generate-manpage generate manpage" - @echo " - generate-swagger generate the swagger spec from code comments" - @echo " - swagger-validate check if the swagger spec is valid" - @echo " - go-licenses regenerate go licenses" - @echo " - tidy run go mod tidy" - @echo " - test[\#TestSpecificName] run unit test" - @echo " - test-sqlite[\#TestSpecificName] run integration test for sqlite" +help: Makefile ## print Makefile help information. + @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m[TARGETS] default target: build\033[0m\n\n\033[35mTargets:\033[0m\n"} /^[0-9A-Za-z._-]+:.*?##/ { printf " \033[36m%-45s\033[0m %s\n", $$1, $$2 }' Makefile #$(MAKEFILE_LIST) + @printf " \033[36m%-46s\033[0m %s\n" "test-e2e[#TestSpecificName]" "test end to end using playwright" + @printf " \033[36m%-46s\033[0m %s\n" "test[#TestSpecificName]" "run unit test" + @printf " \033[36m%-46s\033[0m %s\n" "test-sqlite[#TestSpecificName]" "run integration test for sqlite" .PHONY: go-check go-check: @@ -280,12 +225,12 @@ node-check: fi .PHONY: clean-all -clean-all: clean +clean-all: clean ## delete backend, frontend and integration files rm -rf $(WEBPACK_DEST_ENTRIES) node_modules .PHONY: clean -clean: - rm -rf $(EXECUTABLE) $(DIST) $(BINDATA_DEST) $(BINDATA_HASH) \ +clean: ## delete backend and integration files + rm -rf $(EXECUTABLE) $(DIST) $(BINDATA_DEST_WILDCARD) \ integrations*.test \ e2e*.test \ tests/integration/gitea-integration-* \ @@ -296,7 +241,7 @@ clean: tests/e2e/reports/ tests/e2e/test-artifacts/ tests/e2e/test-snapshots/ .PHONY: fmt -fmt: +fmt: ## format the Go and template code @GOFUMPT_PACKAGE=$(GOFUMPT_PACKAGE) $(GO) run build/code-batch-process.go gitea-fmt -w '{file-list}' $(eval TEMPLATES := $(shell find templates -type f -name '*.tmpl')) @# strip whitespace after '{{' or '(' and before '}}' or ')' unless there is only @@ -311,7 +256,20 @@ fmt-check: fmt @diff=$$(git diff --color=always $(GO_SOURCES) templates $(WEB_DIRS)); \ if [ -n "$$diff" ]; then \ echo "Please run 'make fmt' and commit the result:"; \ - echo "$${diff}"; \ + printf "%s" "$${diff}"; \ + exit 1; \ + fi + +.PHONY: fix +fix: ## apply automated fixes to Go code + $(GO) run $(GOPLS_MODERNIZE_PACKAGE) -fix ./... + +.PHONY: fix-check +fix-check: fix + @diff=$$(git diff --color=always $(GO_SOURCES)); \ + if [ -n "$$diff" ]; then \ + echo "Please run 'make fix' and commit the result:"; \ + printf "%s" "$${diff}"; \ exit 1; \ fi @@ -325,95 +283,95 @@ TAGS_PREREQ := $(TAGS_EVIDENCE) endif .PHONY: generate-swagger -generate-swagger: $(SWAGGER_SPEC) +generate-swagger: $(SWAGGER_SPEC) ## generate the swagger spec from code comments -$(SWAGGER_SPEC): $(GO_SOURCES_NO_BINDATA) - $(GO) run $(SWAGGER_PACKAGE) generate spec -x "$(SWAGGER_EXCLUDE)" -o './$(SWAGGER_SPEC)' - $(SED_INPLACE) '$(SWAGGER_SPEC_S_TMPL)' './$(SWAGGER_SPEC)' - $(SED_INPLACE) $(SWAGGER_NEWLINE_COMMAND) './$(SWAGGER_SPEC)' +$(SWAGGER_SPEC): $(GO_SOURCES) $(SWAGGER_SPEC_INPUT) + $(GO) run $(SWAGGER_PACKAGE) generate spec --exclude "$(SWAGGER_EXCLUDE)" --input "$(SWAGGER_SPEC_INPUT)" --output './$(SWAGGER_SPEC)' .PHONY: swagger-check swagger-check: generate-swagger @diff=$$(git diff --color=always '$(SWAGGER_SPEC)'); \ if [ -n "$$diff" ]; then \ echo "Please run 'make generate-swagger' and commit the result:"; \ - echo "$${diff}"; \ + printf "%s" "$${diff}"; \ exit 1; \ fi .PHONY: swagger-validate -swagger-validate: - $(SED_INPLACE) '$(SWAGGER_SPEC_S_JSON)' './$(SWAGGER_SPEC)' +swagger-validate: ## check if the swagger spec is valid + @# swagger "validate" requires that the "basePath" must start with a slash, but we are using Golang template "{{...}}" + @$(SED_INPLACE) -E -e 's|"basePath":( *)"(.*)"|"basePath":\1"/\2"|g' './$(SWAGGER_SPEC)' # add a prefix slash to basePath + @# FIXME: there are some warnings $(GO) run $(SWAGGER_PACKAGE) validate './$(SWAGGER_SPEC)' - $(SED_INPLACE) '$(SWAGGER_SPEC_S_TMPL)' './$(SWAGGER_SPEC)' + @$(SED_INPLACE) -E -e 's|"basePath":( *)"/(.*)"|"basePath":\1"\2"|g' './$(SWAGGER_SPEC)' # remove the prefix slash from basePath .PHONY: checks -checks: checks-frontend checks-backend +checks: checks-frontend checks-backend ## run various consistency checks .PHONY: checks-frontend -checks-frontend: lockfile-check svg-check +checks-frontend: lockfile-check svg-check ## check frontend files .PHONY: checks-backend -checks-backend: tidy-check swagger-check fmt-check swagger-validate security-check +checks-backend: tidy-check swagger-check fmt-check fix-check swagger-validate security-check ## check backend files .PHONY: lint -lint: lint-frontend lint-backend lint-spell +lint: lint-frontend lint-backend lint-spell ## lint everything .PHONY: lint-fix -lint-fix: lint-frontend-fix lint-backend-fix lint-spell-fix +lint-fix: lint-frontend-fix lint-backend-fix lint-spell-fix ## lint everything and fix issues .PHONY: lint-frontend -lint-frontend: lint-js lint-css +lint-frontend: lint-js lint-css ## lint frontend files .PHONY: lint-frontend-fix -lint-frontend-fix: lint-js-fix lint-css-fix +lint-frontend-fix: lint-js-fix lint-css-fix ## lint frontend files and fix issues .PHONY: lint-backend -lint-backend: lint-go lint-go-vet lint-go-gopls lint-editorconfig +lint-backend: lint-go lint-go-gitea-vet lint-go-gopls lint-editorconfig ## lint backend files .PHONY: lint-backend-fix -lint-backend-fix: lint-go-fix lint-go-vet lint-editorconfig +lint-backend-fix: lint-go-fix lint-go-gitea-vet lint-editorconfig ## lint backend files and fix issues .PHONY: lint-js -lint-js: node_modules +lint-js: node_modules ## lint js files npx eslint --color --max-warnings=0 --ext js,ts,vue $(ESLINT_FILES) -# npx vue-tsc + npx vue-tsc .PHONY: lint-js-fix -lint-js-fix: node_modules +lint-js-fix: node_modules ## lint js files and fix issues npx eslint --color --max-warnings=0 --ext js,ts,vue $(ESLINT_FILES) --fix -# npx vue-tsc + npx vue-tsc .PHONY: lint-css -lint-css: node_modules +lint-css: node_modules ## lint css files npx stylelint --color --max-warnings=0 $(STYLELINT_FILES) .PHONY: lint-css-fix -lint-css-fix: node_modules +lint-css-fix: node_modules ## lint css files and fix issues npx stylelint --color --max-warnings=0 $(STYLELINT_FILES) --fix .PHONY: lint-swagger -lint-swagger: node_modules +lint-swagger: node_modules ## lint swagger files npx spectral lint -q -F hint $(SWAGGER_SPEC) .PHONY: lint-md -lint-md: node_modules +lint-md: node_modules ## lint markdown files npx markdownlint *.md .PHONY: lint-spell -lint-spell: +lint-spell: ## lint spelling @go run $(MISSPELL_PACKAGE) -dict tools/misspellings.csv -error $(SPELLCHECK_FILES) .PHONY: lint-spell-fix -lint-spell-fix: +lint-spell-fix: ## lint spelling and fix issues @go run $(MISSPELL_PACKAGE) -dict tools/misspellings.csv -w $(SPELLCHECK_FILES) .PHONY: lint-go -lint-go: +lint-go: ## lint go files $(GO) run $(GOLANGCI_LINT_PACKAGE) run .PHONY: lint-go-fix -lint-go-fix: +lint-go-fix: ## lint go files and fix issues $(GO) run $(GOLANGCI_LINT_PACKAGE) run --fix # workaround step for the lint-go-windows CI task because 'go run' can not @@ -423,61 +381,58 @@ lint-go-windows: @GOOS= GOARCH= $(GO) install $(GOLANGCI_LINT_PACKAGE) golangci-lint run -.PHONY: lint-go-vet -lint-go-vet: - @echo "Running go vet..." +.PHONY: lint-go-gitea-vet +lint-go-gitea-vet: ## lint go files with gitea-vet + @echo "Running gitea-vet..." @GOOS= GOARCH= $(GO) build code.gitea.io/gitea-vet @$(GO) vet -vettool=gitea-vet ./... .PHONY: lint-go-gopls -lint-go-gopls: +lint-go-gopls: ## lint go files with gopls @echo "Running gopls check..." - @GO=$(GO) GOPLS_PACKAGE=$(GOPLS_PACKAGE) tools/lint-go-gopls.sh $(GO_SOURCES_NO_BINDATA) + @GO=$(GO) GOPLS_PACKAGE=$(GOPLS_PACKAGE) tools/lint-go-gopls.sh $(GO_SOURCES) .PHONY: lint-editorconfig lint-editorconfig: + @echo "Running editorconfig check..." @$(GO) run $(EDITORCONFIG_CHECKER_PACKAGE) $(EDITORCONFIG_FILES) .PHONY: lint-actions -lint-actions: +lint-actions: ## lint action workflow files $(GO) run $(ACTIONLINT_PACKAGE) .PHONY: lint-templates -lint-templates: .venv node_modules +lint-templates: .venv node_modules ## lint template files @node tools/lint-templates-svg.js - @poetry run djlint $(shell find templates -type f -iname '*.tmpl') + @uv run --frozen djlint $(shell find templates -type f -iname '*.tmpl') .PHONY: lint-yaml -lint-yaml: .venv - @poetry run yamllint . - -.PHONY: tsc -tsc: - npx vue-tsc +lint-yaml: .venv ## lint yaml files + @uv run --frozen yamllint -s . .PHONY: watch -watch: +watch: ## watch everything and continuously rebuild @bash tools/watch.sh .PHONY: watch-frontend -watch-frontend: node-check node_modules +watch-frontend: node-check node_modules ## watch frontend files and continuously rebuild @rm -rf $(WEBPACK_DEST_ENTRIES) NODE_ENV=development npx webpack --watch --progress .PHONY: watch-backend -watch-backend: go-check +watch-backend: go-check ## watch backend files and continuously rebuild GITEA_RUN_MODE=dev $(GO) run $(AIR_PACKAGE) -c .air.toml .PHONY: test -test: test-frontend test-backend +test: test-frontend test-backend ## test everything .PHONY: test-backend -test-backend: +test-backend: ## test backend files @echo "Running go test with $(GOTESTFLAGS) -tags '$(TEST_TAGS)'..." @$(GO) test $(GOTESTFLAGS) -tags='$(TEST_TAGS)' $(GO_TEST_PACKAGES) .PHONY: test-frontend -test-frontend: node_modules +test-frontend: node_modules ## test frontend files npx vitest .PHONY: test-check @@ -486,7 +441,7 @@ test-check: @diff=$$(git status -s); \ if [ -n "$$diff" ]; then \ echo "make test-backend has changed files in the source tree:"; \ - echo "$${diff}"; \ + printf "%s" "$${diff}"; \ echo "You should change the tests to create these files in a temporary directory."; \ echo "Do not simply add these files to .gitignore"; \ exit 1; \ @@ -509,7 +464,7 @@ unit-test-coverage: @$(GO) test $(GOTESTFLAGS) -timeout=20m -tags='$(TEST_TAGS)' -cover -coverprofile coverage.out $(GO_TEST_PACKAGES) && echo "\n==>\033[32m Ok\033[m\n" || exit 1 .PHONY: tidy -tidy: +tidy: ## run go mod tidy $(eval MIN_GO_VERSION := $(shell grep -Eo '^go\s+[0-9]+\.[0-9.]+' go.mod | cut -d' ' -f2)) $(GO) mod tidy -compat=$(MIN_GO_VERSION) @$(MAKE) --no-print-directory $(GO_LICENSE_FILE) @@ -523,15 +478,17 @@ tidy-check: tidy @diff=$$(git diff --color=always go.mod go.sum $(GO_LICENSE_FILE)); \ if [ -n "$$diff" ]; then \ echo "Please run 'make tidy' and commit the result:"; \ - echo "$${diff}"; \ + printf "%s" "$${diff}"; \ exit 1; \ fi .PHONY: go-licenses -go-licenses: $(GO_LICENSE_FILE) +go-licenses: $(GO_LICENSE_FILE) ## regenerate go licenses $(GO_LICENSE_FILE): go.mod go.sum - -$(GO) run $(GO_LICENSES_PACKAGE) save . --force --save_path=$(GO_LICENSE_TMP_DIR) 2>/dev/null + @rm -rf $(GO_LICENSE_FILE) + $(GO) install $(GO_LICENSES_PACKAGE) + -GOOS=linux CGO_ENABLED=1 go-licenses save . --force --save_path=$(GO_LICENSE_TMP_DIR) 2>/dev/null $(GO) run build/generate-go-licenses.go $(GO_LICENSE_TMP_DIR) $(GO_LICENSE_FILE) @rm -rf $(GO_LICENSE_TMP_DIR) @@ -775,17 +732,17 @@ install: $(wildcard *.go) CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) install -v -tags '$(TAGS)' -ldflags '-s -w $(LDFLAGS)' .PHONY: build -build: frontend backend +build: frontend backend ## build everything .PHONY: frontend -frontend: $(WEBPACK_DEST) +frontend: $(WEBPACK_DEST) ## build frontend files .PHONY: backend -backend: go-check generate-backend $(EXECUTABLE) +backend: go-check generate-backend $(EXECUTABLE) ## build backend files # We generate the backend before the frontend in case we in future we want to generate things in the frontend from generated files in backend .PHONY: generate -generate: generate-backend +generate: generate-backend ## run "go generate" .PHONY: generate-backend generate-backend: $(TAGS_PREREQ) generate-go @@ -797,10 +754,13 @@ generate-go: $(TAGS_PREREQ) .PHONY: security-check security-check: - go run $(GOVULNCHECK_PACKAGE) ./... + go run $(GOVULNCHECK_PACKAGE) -show color ./... $(EXECUTABLE): $(GO_SOURCES) $(TAGS_PREREQ) - CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) build $(GOFLAGS) $(EXTRA_GOFLAGS) -tags '$(TAGS)' -ldflags '-s -w $(LDFLAGS)' -o $@ +ifneq ($(and $(STATIC),$(findstring pam,$(TAGS))),) + $(error pam support set via TAGS doesn't support static builds) +endif + CGO_ENABLED="$(CGO_ENABLED)" CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) build $(GOFLAGS) $(EXTRA_GOFLAGS) -tags '$(TAGS)' -ldflags '-s -w $(EXTLDFLAGS) $(LDFLAGS)' -o $@ .PHONY: release release: frontend generate release-windows release-linux release-darwin release-freebsd release-copy release-compress vendor release-sources release-check @@ -810,22 +770,22 @@ $(DIST_DIRS): .PHONY: release-windows release-windows: | $(DIST_DIRS) - CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -buildmode exe -dest $(DIST)/binaries -tags 'osusergo $(TAGS)' -ldflags '-linkmode external -extldflags "-static" $(LDFLAGS)' -targets 'windows/*' -out gitea-$(VERSION) . + CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -buildmode exe -dest $(DIST)/binaries -tags 'osusergo $(TAGS)' -ldflags '-s -w -linkmode external -extldflags "-static" $(LDFLAGS)' -targets 'windows/*' -out gitea-$(VERSION) . ifeq (,$(findstring gogit,$(TAGS))) - CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -buildmode exe -dest $(DIST)/binaries -tags 'osusergo gogit $(TAGS)' -ldflags '-linkmode external -extldflags "-static" $(LDFLAGS)' -targets 'windows/*' -out gitea-$(VERSION)-gogit . + CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -buildmode exe -dest $(DIST)/binaries -tags 'osusergo gogit $(TAGS)' -ldflags '-s -w -linkmode external -extldflags "-static" $(LDFLAGS)' -targets 'windows/*' -out gitea-$(VERSION)-gogit . endif .PHONY: release-linux release-linux: | $(DIST_DIRS) - CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '-linkmode external -extldflags "-static" $(LDFLAGS)' -targets '$(LINUX_ARCHS)' -out gitea-$(VERSION) . + CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '-s -w -linkmode external -extldflags "-static" $(LDFLAGS)' -targets '$(LINUX_ARCHS)' -out gitea-$(VERSION) . .PHONY: release-darwin release-darwin: | $(DIST_DIRS) - CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '$(LDFLAGS)' -targets 'darwin-10.12/amd64,darwin-10.12/arm64' -out gitea-$(VERSION) . + CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '-s -w $(LDFLAGS)' -targets 'darwin-10.12/amd64,darwin-10.12/arm64' -out gitea-$(VERSION) . .PHONY: release-freebsd release-freebsd: | $(DIST_DIRS) - CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '$(LDFLAGS)' -targets 'freebsd/amd64' -out gitea-$(VERSION) . + CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '-s -w $(LDFLAGS)' -targets 'freebsd/amd64' -out gitea-$(VERSION) . .PHONY: release-copy release-copy: | $(DIST_DIRS) @@ -850,20 +810,20 @@ release-sources: | $(DIST_DIRS) rm -f $(STORED_VERSION_FILE) .PHONY: deps -deps: deps-frontend deps-backend deps-tools deps-py +deps: deps-frontend deps-backend deps-tools deps-py ## install dependencies .PHONY: deps-py -deps-py: .venv +deps-py: .venv ## install python dependencies .PHONY: deps-frontend -deps-frontend: node_modules +deps-frontend: node_modules ## install frontend dependencies .PHONY: deps-backend -deps-backend: +deps-backend: ## install backend dependencies $(GO) mod download .PHONY: deps-tools -deps-tools: +deps-tools: ## install tool dependencies $(GO) install $(AIR_PACKAGE) & \ $(GO) install $(EDITORCONFIG_CHECKER_PACKAGE) & \ $(GO) install $(GOFUMPT_PACKAGE) & \ @@ -876,21 +836,22 @@ deps-tools: $(GO) install $(GOVULNCHECK_PACKAGE) & \ $(GO) install $(ACTIONLINT_PACKAGE) & \ $(GO) install $(GOPLS_PACKAGE) & \ + $(GO) install $(GOPLS_MODERNIZE_PACKAGE) & \ wait node_modules: package-lock.json npm install --no-save @touch node_modules -.venv: poetry.lock - poetry install +.venv: uv.lock + uv sync @touch .venv .PHONY: update -update: update-js update-py +update: update-js update-py ## update js and py dependencies .PHONY: update-js -update-js: node-check | node_modules +update-js: node-check | node_modules ## update js dependencies npx updates -u -f package.json rm -rf node_modules package-lock.json npm install --package-lock @@ -899,27 +860,14 @@ update-js: node-check | node_modules @touch node_modules .PHONY: update-py -update-py: node-check | node_modules +update-py: node-check | node_modules ## update py dependencies npx updates -u -f pyproject.toml - rm -rf .venv poetry.lock - poetry install + rm -rf .venv uv.lock + uv sync @touch .venv -.PHONY: fomantic -fomantic: - rm -rf $(FOMANTIC_WORK_DIR)/build - cd $(FOMANTIC_WORK_DIR) && npm install --no-save - cp -f $(FOMANTIC_WORK_DIR)/theme.config.less $(FOMANTIC_WORK_DIR)/node_modules/fomantic-ui/src/theme.config - cp -rf $(FOMANTIC_WORK_DIR)/_site $(FOMANTIC_WORK_DIR)/node_modules/fomantic-ui/src/ - $(SED_INPLACE) -e 's/ overrideBrowserslist\r/ overrideBrowserslist: ["defaults"]\r/g' $(FOMANTIC_WORK_DIR)/node_modules/fomantic-ui/tasks/config/tasks.js - cd $(FOMANTIC_WORK_DIR) && npx gulp -f node_modules/fomantic-ui/gulpfile.js build - # fomantic uses "touchstart" as click event for some browsers, it's not ideal, so we force fomantic to always use "click" as click event - $(SED_INPLACE) -e 's/clickEvent[ \t]*=/clickEvent = "click", unstableClickEvent =/g' $(FOMANTIC_WORK_DIR)/build/semantic.js - $(SED_INPLACE) -e 's/\r//g' $(FOMANTIC_WORK_DIR)/build/semantic.css $(FOMANTIC_WORK_DIR)/build/semantic.js - rm -f $(FOMANTIC_WORK_DIR)/build/*.min.* - .PHONY: webpack -webpack: $(WEBPACK_DEST) +webpack: $(WEBPACK_DEST) ## build webpack files $(WEBPACK_DEST): $(WEBPACK_SOURCES) $(WEBPACK_CONFIGS) package-lock.json @$(MAKE) -s node-check node_modules @@ -929,7 +877,7 @@ $(WEBPACK_DEST): $(WEBPACK_SOURCES) $(WEBPACK_CONFIGS) package-lock.json @touch $(WEBPACK_DEST) .PHONY: svg -svg: node-check | node_modules +svg: node-check | node_modules ## build svg files rm -rf $(SVG_DEST_DIR) node tools/generate-svg.js @@ -939,7 +887,7 @@ svg-check: svg @diff=$$(git diff --color=always --cached $(SVG_DEST_DIR)); \ if [ -n "$$diff" ]; then \ echo "Please run 'make svg' and 'git add $(SVG_DEST_DIR)' and commit the result:"; \ - echo "$${diff}"; \ + printf "%s" "$${diff}"; \ exit 1; \ fi @@ -950,7 +898,7 @@ lockfile-check: if [ -n "$$diff" ]; then \ echo "package-lock.json is inconsistent with package.json"; \ echo "Please run 'npm install --package-lock-only' and commit the result:"; \ - echo "$${diff}"; \ + printf "%s" "$${diff}"; \ exit 1; \ fi @@ -964,12 +912,8 @@ update-translations: mv ./translations/*.ini ./options/locale/ rmdir ./translations -.PHONY: generate-license -generate-license: - $(GO) run build/generate-licenses.go - .PHONY: generate-gitignore -generate-gitignore: +generate-gitignore: ## update gitignore files $(GO) run build/generate-gitignores.go .PHONY: generate-images @@ -978,7 +922,7 @@ generate-images: | node_modules node tools/generate-images.js $(TAGS) .PHONY: generate-manpage -generate-manpage: +generate-manpage: ## generate manpage @[ -f gitea ] || make backend @mkdir -p man/man1/ man/man5 @./gitea docs --man > man/man1/gitea.1 diff --git a/README.md b/README.md index c280c832ac7..aa2c6010fa3 100644 --- a/README.md +++ b/README.md @@ -9,9 +9,9 @@ [![](https://opencollective.com/gitea/tiers/backers/badge.svg?label=backers&color=brightgreen)](https://opencollective.com/gitea "Become a backer/sponsor of gitea") [![](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT "License: MIT") [![Contribute with Gitpod](https://img.shields.io/badge/Contribute%20with-Gitpod-908a85?logo=gitpod&color=green)](https://gitpod.io/#https://github.com/go-gitea/gitea) -[![](https://badges.crowdin.net/gitea/localized.svg)](https://crowdin.com/project/gitea "Crowdin") +[![](https://badges.crowdin.net/gitea/localized.svg)](https://translate.gitea.com "Crowdin") -[View this document in Chinese](./README_ZH.md) +[ç¹é«”中文](./README.zh-tw.md) | [简体中文](./README.zh-cn.md) ## Purpose @@ -31,6 +31,14 @@ For accessing free Gitea service (with a limited number of repositories), you ca To quickly deploy your own dedicated Gitea instance on Gitea Cloud, you can start a free trial at [cloud.gitea.com](https://cloud.gitea.com). +## Documentation + +You can find comprehensive documentation on our official [documentation website](https://docs.gitea.com/). + +It includes installation, administration, usage, development, contributing guides, and more to help you get started and explore all features effectively. + +If you have any suggestions or would like to contribute to it, you can visit the [documentation repository](https://gitea.com/gitea/docs) + ## Building From the root of the source tree, run: @@ -52,6 +60,8 @@ More info: https://docs.gitea.com/installation/install-from-source ## Using +After building, a binary file named `gitea` will be generated in the root of the source tree by default. To run it, use: + ./gitea web > [!NOTE] @@ -68,22 +78,25 @@ Expected workflow is: Fork -> Patch -> Push -> Pull Request ## Translating -Translations are done through Crowdin. If you want to translate to a new language ask one of the managers in the Crowdin project to add a new language there. +[![Crowdin](https://badges.crowdin.net/gitea/localized.svg)](https://translate.gitea.com) -You can also just create an issue for adding a language or ask on discord on the #translation channel. If you need context or find some translation issues, you can leave a comment on the string or ask on Discord. For general translation questions there is a section in the docs. Currently a bit empty but we hope to fill it as questions pop up. +Translations are done through [Crowdin](https://translate.gitea.com). If you want to translate to a new language, ask one of the managers in the Crowdin project to add a new language there. -https://docs.gitea.com/contributing/localization +You can also just create an issue for adding a language or ask on Discord on the #translation channel. If you need context or find some translation issues, you can leave a comment on the string or ask on Discord. For general translation questions there is a section in the docs. Currently a bit empty, but we hope to fill it as questions pop up. -[![Crowdin](https://badges.crowdin.net/gitea/localized.svg)](https://crowdin.com/project/gitea) +Get more information from [documentation](https://docs.gitea.com/contributing/localization). -## Further information +## Official and Third-Party Projects -For more information and instructions about how to install Gitea, please look at our [documentation](https://docs.gitea.com/). -If you have questions that are not covered by the documentation, you can get in contact with us on our [Discord server](https://discord.gg/Gitea) or create a post in the [discourse forum](https://forum.gitea.com/). +We provide an official [go-sdk](https://gitea.com/gitea/go-sdk), a CLI tool called [tea](https://gitea.com/gitea/tea) and an [action runner](https://gitea.com/gitea/act_runner) for Gitea Action. -We maintain a list of Gitea-related projects at [gitea/awesome-gitea](https://gitea.com/gitea/awesome-gitea). +We maintain a list of Gitea-related projects at [gitea/awesome-gitea](https://gitea.com/gitea/awesome-gitea), where you can discover more third-party projects, including SDKs, plugins, themes, and more. -The official Gitea CLI is developed at [gitea/tea](https://gitea.com/gitea/tea). +## Communication + +[![](https://img.shields.io/discord/322538954119184384.svg?logo=discord&logoColor=white&label=Discord&color=5865F2)](https://discord.gg/Gitea "Join the Discord chat at https://discord.gg/Gitea") + +If you have questions that are not covered by the [documentation](https://docs.gitea.com/), you can get in contact with us on our [Discord server](https://discord.gg/Gitea) or create a post in the [discourse forum](https://forum.gitea.com/). ## Authors @@ -122,18 +135,79 @@ Gitea is pronounced [/ɡɪ’ti:/](https://youtu.be/EM71-2uDAoY) as in "gi-tea" We're [working on it](https://github.com/go-gitea/gitea/issues/1029). +**Where can I find the security patches?** + +In the [release log](https://github.com/go-gitea/gitea/releases) or the [change log](https://github.com/go-gitea/gitea/blob/main/CHANGELOG.md), search for the keyword `SECURITY` to find the security patches. + ## License This project is licensed under the MIT License. See the [LICENSE](https://github.com/go-gitea/gitea/blob/main/LICENSE) file for the full license text. -## Screenshots +## Further information -Looking for an overview of the interface? Check it out! +
+Looking for an overview of the interface? Check it out! -|![Dashboard](https://dl.gitea.com/screenshots/home_timeline.png)|![User Profile](https://dl.gitea.com/screenshots/user_profile.png)|![Global Issues](https://dl.gitea.com/screenshots/global_issues.png)| -|:---:|:---:|:---:| -|![Branches](https://dl.gitea.com/screenshots/branches.png)|![Web Editor](https://dl.gitea.com/screenshots/web_editor.png)|![Activity](https://dl.gitea.com/screenshots/activity.png)| -|![New Migration](https://dl.gitea.com/screenshots/migration.png)|![Migrating](https://dl.gitea.com/screenshots/migration.gif)|![Pull Request View](https://image.ibb.co/e02dSb/6.png)| -|![Pull Request Dark](https://dl.gitea.com/screenshots/pull_requests_dark.png)|![Diff Review Dark](https://dl.gitea.com/screenshots/review_dark.png)|![Diff Dark](https://dl.gitea.com/screenshots/diff_dark.png)| +### Login/Register Page + +![Login](https://dl.gitea.com/screenshots/login.png) +![Register](https://dl.gitea.com/screenshots/register.png) + +### User Dashboard + +![Home](https://dl.gitea.com/screenshots/home.png) +![Issues](https://dl.gitea.com/screenshots/issues.png) +![Pull Requests](https://dl.gitea.com/screenshots/pull_requests.png) +![Milestones](https://dl.gitea.com/screenshots/milestones.png) + +### User Profile + +![Profile](https://dl.gitea.com/screenshots/user_profile.png) + +### Explore + +![Repos](https://dl.gitea.com/screenshots/explore_repos.png) +![Users](https://dl.gitea.com/screenshots/explore_users.png) +![Orgs](https://dl.gitea.com/screenshots/explore_orgs.png) + +### Repository + +![Home](https://dl.gitea.com/screenshots/repo_home.png) +![Commits](https://dl.gitea.com/screenshots/repo_commits.png) +![Branches](https://dl.gitea.com/screenshots/repo_branches.png) +![Labels](https://dl.gitea.com/screenshots/repo_labels.png) +![Milestones](https://dl.gitea.com/screenshots/repo_milestones.png) +![Releases](https://dl.gitea.com/screenshots/repo_releases.png) +![Tags](https://dl.gitea.com/screenshots/repo_tags.png) + +#### Repository Issue + +![List](https://dl.gitea.com/screenshots/repo_issues.png) +![Issue](https://dl.gitea.com/screenshots/repo_issue.png) + +#### Repository Pull Requests + +![List](https://dl.gitea.com/screenshots/repo_pull_requests.png) +![Pull Request](https://dl.gitea.com/screenshots/repo_pull_request.png) +![File](https://dl.gitea.com/screenshots/repo_pull_request_file.png) +![Commits](https://dl.gitea.com/screenshots/repo_pull_request_commits.png) + +#### Repository Actions + +![List](https://dl.gitea.com/screenshots/repo_actions.png) +![Details](https://dl.gitea.com/screenshots/repo_actions_run.png) + +#### Repository Activity + +![Activity](https://dl.gitea.com/screenshots/repo_activity.png) +![Contributors](https://dl.gitea.com/screenshots/repo_contributors.png) +![Code Frequency](https://dl.gitea.com/screenshots/repo_code_frequency.png) +![Recent Commits](https://dl.gitea.com/screenshots/repo_recent_commits.png) + +### Organization + +![Home](https://dl.gitea.com/screenshots/org_home.png) + +
diff --git a/README.zh-cn.md b/README.zh-cn.md new file mode 100644 index 00000000000..f34b25b9454 --- /dev/null +++ b/README.zh-cn.md @@ -0,0 +1,206 @@ +# Gitea + +[![](https://github.com/go-gitea/gitea/actions/workflows/release-nightly.yml/badge.svg?branch=main)](https://github.com/go-gitea/gitea/actions/workflows/release-nightly.yml?query=branch%3Amain "Release Nightly") +[![](https://img.shields.io/discord/322538954119184384.svg?logo=discord&logoColor=white&label=Discord&color=5865F2)](https://discord.gg/Gitea "Join the Discord chat at https://discord.gg/Gitea") +[![](https://goreportcard.com/badge/code.gitea.io/gitea)](https://goreportcard.com/report/code.gitea.io/gitea "Go Report Card") +[![](https://pkg.go.dev/badge/code.gitea.io/gitea?status.svg)](https://pkg.go.dev/code.gitea.io/gitea "GoDoc") +[![](https://img.shields.io/github/release/go-gitea/gitea.svg)](https://github.com/go-gitea/gitea/releases/latest "GitHub release") +[![](https://www.codetriage.com/go-gitea/gitea/badges/users.svg)](https://www.codetriage.com/go-gitea/gitea "Help Contribute to Open Source") +[![](https://opencollective.com/gitea/tiers/backers/badge.svg?label=backers&color=brightgreen)](https://opencollective.com/gitea "Become a backer/sponsor of gitea") +[![](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT "License: MIT") +[![Contribute with Gitpod](https://img.shields.io/badge/Contribute%20with-Gitpod-908a85?logo=gitpod&color=green)](https://gitpod.io/#https://github.com/go-gitea/gitea) +[![](https://badges.crowdin.net/gitea/localized.svg)](https://translate.gitea.com "Crowdin") + +[English](./README.md) | [ç¹é«”中文](./README.zh-tw.md) + +## 目的 + +这个项目的目标是æä¾›æœ€ç®€å•ã€æœ€å¿«é€Ÿã€æœ€æ— ç—›çš„æ–¹å¼æ¥è®¾ç½®è‡ªæ‰˜ç®¡çš„ Git æœåŠ¡ã€‚ + +由于 Gitea 是用 Go 语言编写的,它å¯ä»¥åœ¨ Go 支æŒçš„æ‰€æœ‰å¹³å°å’Œæž¶æž„上è¿è¡Œï¼ŒåŒ…括 Linuxã€macOS å’Œ Windows çš„ x86ã€amd64ã€ARM å’Œ PowerPC 架构。这个项目自 2016 å¹´ 11 月从 [Gogs](https://gogs.io) [分å‰](https://blog.gitea.com/welcome-to-gitea/) 而æ¥ï¼Œä½†å·²ç»æœ‰äº†å¾ˆå¤šå˜åŒ–。 + +在线演示å¯ä»¥è®¿é—® [demo.gitea.com](https://demo.gitea.com)。 + +è¦è®¿é—®å…费的 Gitea æœåŠ¡ï¼ˆæœ‰ä¸€å®šæ•°é‡çš„仓库é™åˆ¶ï¼‰ï¼Œå¯ä»¥è®¿é—® [gitea.com](https://gitea.com/user/login)。 + +è¦å¿«é€Ÿéƒ¨ç½²æ‚¨è‡ªå·±çš„专用 Gitea 实例,å¯ä»¥åœ¨ [cloud.gitea.com](https://cloud.gitea.com) 开始å…费试用。 + +## 文件 + +您å¯ä»¥åœ¨æˆ‘们的官方 [文件网站](https://docs.gitea.com/) 上找到全é¢çš„æ–‡ä»¶ã€‚ + +它包括安装ã€ç®¡ç†ã€ä½¿ç”¨ã€å¼€å‘ã€è´¡çŒ®æŒ‡å—等,帮助您快速入门并有效地探索所有功能。 + +如果您有任何建议或想è¦è´¡çŒ®ï¼Œå¯ä»¥è®¿é—® [文件仓库](https://gitea.com/gitea/docs) + +## 构建 + +从æºä»£ç æ ‘的根目录è¿è¡Œï¼š + + TAGS="bindata" make build + +å¦‚æžœéœ€è¦ SQLite 支æŒï¼š + + TAGS="bindata sqlite sqlite_unlock_notify" make build + +`build` 目标分为两个å­ç›®æ ‡ï¼š + +- `make backend` éœ€è¦ [Go Stable](https://go.dev/dl/),所需版本在 [go.mod](/go.mod) 中定义。 +- `make frontend` éœ€è¦ [Node.js LTS](https://nodejs.org/en/download/) 或更高版本。 + +需è¦äº’è”网连接æ¥ä¸‹è½½ go å’Œ npm 模å—。从包å«é¢„构建å‰ç«¯æ–‡ä»¶çš„官方æºä»£ç åŽ‹ç¼©åŒ…æž„å»ºæ—¶ï¼Œä¸ä¼šè§¦å‘ `frontend` 目标,因此å¯ä»¥åœ¨æ²¡æœ‰ Node.js 的情况下构建。 + +更多信æ¯ï¼šhttps://docs.gitea.com/installation/install-from-source + +## 使用 + +构建åŽï¼Œé»˜è®¤æƒ…况下会在æºä»£ç æ ‘的根目录生æˆä¸€ä¸ªå为 `gitea` 的二进制文件。è¦è¿è¡Œå®ƒï¼Œè¯·ä½¿ç”¨ï¼š + + ./gitea web + +> [!注æ„] +> 如果您对使用我们的 API 感兴趣,我们æä¾›äº†å®žéªŒæ€§æ”¯æŒï¼Œå¹¶é™„有 [文件](https://docs.gitea.com/api)。 + +## 贡献 + +预期的工作æµç¨‹æ˜¯ï¼šFork -> Patch -> Push -> Pull Request + +> [!注æ„] +> +> 1. **在开始进行 Pull Request 之å‰ï¼Œæ‚¨å¿…须阅读 [贡献者指å—](CONTRIBUTING.md)。** +> 2. 如果您在项目中å‘çŽ°äº†æ¼æ´žï¼Œè¯·ç§ä¸‹å†™ä¿¡ç»™ **security@gitea.io**ã€‚è°¢è°¢ï¼ + +## 翻译 + +[![Crowdin](https://badges.crowdin.net/gitea/localized.svg)](https://translate.gitea.com) + +翻译通过 [Crowdin](https://translate.gitea.com) è¿›è¡Œã€‚å¦‚æžœæ‚¨æƒ³ç¿»è¯‘æˆæ–°çš„语言,请在 Crowdin 项目中请求管ç†å‘˜æ·»åŠ æ–°è¯­è¨€ã€‚ + +您也å¯ä»¥åˆ›å»ºä¸€ä¸ª issue æ¥æ·»åŠ è¯­è¨€ï¼Œæˆ–è€…åœ¨ discord çš„ #translation 频é“上询问。如果您需è¦ä¸Šä¸‹æ–‡æˆ–å‘现一些翻译问题,å¯ä»¥åœ¨å­—符串上留言或在 Discord ä¸Šè¯¢é—®ã€‚å¯¹äºŽä¸€èˆ¬çš„ç¿»è¯‘é—®é¢˜ï¼Œæ–‡æ¡£ä¸­æœ‰ä¸€ä¸ªéƒ¨åˆ†ã€‚ç›®å‰æœ‰ç‚¹ç©ºï¼Œä½†æˆ‘们希望éšç€é—®é¢˜çš„出现而填充它。 + +更多信æ¯è¯·å‚阅 [文件](https://docs.gitea.com/contributing/localization)。 + +## 官方和第三方项目 + +我们æä¾›äº†ä¸€ä¸ªå®˜æ–¹çš„ [go-sdk](https://gitea.com/gitea/go-sdk),一个å为 [tea](https://gitea.com/gitea/tea) çš„ CLI 工具和一个 Gitea Action çš„ [action runner](https://gitea.com/gitea/act_runner)。 + +我们在 [gitea/awesome-gitea](https://gitea.com/gitea/awesome-gitea) 维护了一个 Gitea 相关项目的列表,您å¯ä»¥åœ¨é‚£é‡Œå‘现更多的第三方项目,包括 SDKã€æ’ä»¶ã€ä¸»é¢˜ç­‰ã€‚ + +## 通讯 + +[![](https://img.shields.io/discord/322538954119184384.svg?logo=discord&logoColor=white&label=Discord&color=5865F2)](https://discord.gg/Gitea "Join the Discord chat at https://discord.gg/Gitea") + +如果您有任何文件未涵盖的问题,å¯ä»¥åœ¨æˆ‘们的 [Discord æœåС噍](https://discord.gg/Gitea) 上与我们è”系,或者在 [discourse 论å›](https://forum.gitea.com/) 上创建帖å­ã€‚ + +## 作者 + +- [维护者](https://github.com/orgs/go-gitea/people) +- [贡献者](https://github.com/go-gitea/gitea/graphs/contributors) +- [翻译者](options/locale/TRANSLATORS) + +## 支æŒè€… + +感谢所有支æŒè€…ï¼ ðŸ™ [[æˆä¸ºæ”¯æŒè€…](https://opencollective.com/gitea#backer)] + + + +## 赞助商 + +通过æˆä¸ºèµžåŠ©å•†æ¥æ”¯æŒè¿™ä¸ªé¡¹ç›®ã€‚您的标志将显示在这里,并带有链接到您的网站。 [[æˆä¸ºèµžåЩ商](https://opencollective.com/gitea#sponsor)] + + + + + + + + + + + + +## 常è§é—®é¢˜ + +**Gitea 怎么å‘音?** + +Gitea çš„å‘音是 [/ɡɪ’ti:/](https://youtu.be/EM71-2uDAoY)ï¼Œå°±åƒ "gi-tea" 一样,g 是硬音。 + +**为什么这个项目没有托管在 Gitea 实例上?** + +我们正在 [努力](https://github.com/go-gitea/gitea/issues/1029)。 + +**在哪里å¯ä»¥æ‰¾åˆ°å®‰å…¨è¡¥ä¸ï¼Ÿ** + +在 [å‘布日志](https://github.com/go-gitea/gitea/releases) 或 [å˜æ›´æ—¥å¿—](https://github.com/go-gitea/gitea/blob/main/CHANGELOG.md) 中,æœç´¢å…³é”®è¯ `SECURITY` 以找到安全补ä¸ã€‚ + +## 许å¯è¯ + +è¿™ä¸ªé¡¹ç›®æ˜¯æ ¹æ® MIT 许å¯è¯æŽˆæƒçš„。 +请å‚阅 [LICENSE](https://github.com/go-gitea/gitea/blob/main/LICENSE) 文件以获å–完整的许å¯è¯æ–‡æœ¬ã€‚ + +## è¿›ä¸€æ­¥ä¿¡æ¯ + +
+å¯»æ‰¾ç•Œé¢æ¦‚è¿°ï¼ŸæŸ¥çœ‹è¿™é‡Œï¼ + +### 登录/æ³¨å†Œé¡µé¢ + +![Login](https://dl.gitea.com/screenshots/login.png) +![Register](https://dl.gitea.com/screenshots/register.png) + +### ç”¨æˆ·ä»ªè¡¨æ¿ + +![Home](https://dl.gitea.com/screenshots/home.png) +![Issues](https://dl.gitea.com/screenshots/issues.png) +![Pull Requests](https://dl.gitea.com/screenshots/pull_requests.png) +![Milestones](https://dl.gitea.com/screenshots/milestones.png) + +### 用户资料 + +![Profile](https://dl.gitea.com/screenshots/user_profile.png) + +### 探索 + +![Repos](https://dl.gitea.com/screenshots/explore_repos.png) +![Users](https://dl.gitea.com/screenshots/explore_users.png) +![Orgs](https://dl.gitea.com/screenshots/explore_orgs.png) + +### 仓库 + +![Home](https://dl.gitea.com/screenshots/repo_home.png) +![Commits](https://dl.gitea.com/screenshots/repo_commits.png) +![Branches](https://dl.gitea.com/screenshots/repo_branches.png) +![Labels](https://dl.gitea.com/screenshots/repo_labels.png) +![Milestones](https://dl.gitea.com/screenshots/repo_milestones.png) +![Releases](https://dl.gitea.com/screenshots/repo_releases.png) +![Tags](https://dl.gitea.com/screenshots/repo_tags.png) + +#### 仓库问题 + +![List](https://dl.gitea.com/screenshots/repo_issues.png) +![Issue](https://dl.gitea.com/screenshots/repo_issue.png) + +#### 仓库拉å–请求 + +![List](https://dl.gitea.com/screenshots/repo_pull_requests.png) +![Pull Request](https://dl.gitea.com/screenshots/repo_pull_request.png) +![File](https://dl.gitea.com/screenshots/repo_pull_request_file.png) +![Commits](https://dl.gitea.com/screenshots/repo_pull_request_commits.png) + +#### 仓库æ“作 + +![List](https://dl.gitea.com/screenshots/repo_actions.png) +![Details](https://dl.gitea.com/screenshots/repo_actions_run.png) + +#### 仓库活动 + +![Activity](https://dl.gitea.com/screenshots/repo_activity.png) +![Contributors](https://dl.gitea.com/screenshots/repo_contributors.png) +![Code Frequency](https://dl.gitea.com/screenshots/repo_code_frequency.png) +![Recent Commits](https://dl.gitea.com/screenshots/repo_recent_commits.png) + +### 组织 + +![Home](https://dl.gitea.com/screenshots/org_home.png) + +
diff --git a/README.zh-tw.md b/README.zh-tw.md new file mode 100644 index 00000000000..9de3f85dd56 --- /dev/null +++ b/README.zh-tw.md @@ -0,0 +1,206 @@ +# Gitea + +[![](https://github.com/go-gitea/gitea/actions/workflows/release-nightly.yml/badge.svg?branch=main)](https://github.com/go-gitea/gitea/actions/workflows/release-nightly.yml?query=branch%3Amain "Release Nightly") +[![](https://img.shields.io/discord/322538954119184384.svg?logo=discord&logoColor=white&label=Discord&color=5865F2)](https://discord.gg/Gitea "Join the Discord chat at https://discord.gg/Gitea") +[![](https://goreportcard.com/badge/code.gitea.io/gitea)](https://goreportcard.com/report/code.gitea.io/gitea "Go Report Card") +[![](https://pkg.go.dev/badge/code.gitea.io/gitea?status.svg)](https://pkg.go.dev/code.gitea.io/gitea "GoDoc") +[![](https://img.shields.io/github/release/go-gitea/gitea.svg)](https://github.com/go-gitea/gitea/releases/latest "GitHub release") +[![](https://www.codetriage.com/go-gitea/gitea/badges/users.svg)](https://www.codetriage.com/go-gitea/gitea "Help Contribute to Open Source") +[![](https://opencollective.com/gitea/tiers/backers/badge.svg?label=backers&color=brightgreen)](https://opencollective.com/gitea "Become a backer/sponsor of gitea") +[![](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT "License: MIT") +[![Contribute with Gitpod](https://img.shields.io/badge/Contribute%20with-Gitpod-908a85?logo=gitpod&color=green)](https://gitpod.io/#https://github.com/go-gitea/gitea) +[![](https://badges.crowdin.net/gitea/localized.svg)](https://translate.gitea.com "Crowdin") + +[English](./README.md) | [简体中文](./README.zh-cn.md) + +## 目的 + +這個項目的目標是æä¾›æœ€ç°¡å–®ã€æœ€å¿«é€Ÿã€æœ€ç„¡ç—›çš„æ–¹å¼ä¾†è¨­ç½®è‡ªè¨—管的 Git æœå‹™ã€‚ + +由於 Gitea 是用 Go 語言編寫的,它å¯ä»¥åœ¨ Go 支æ´çš„æ‰€æœ‰å¹³å°å’Œæž¶æ§‹ä¸Šé‹è¡Œï¼ŒåŒ…括 Linuxã€macOS å’Œ Windows çš„ x86ã€amd64ã€ARM å’Œ PowerPC 架構。這個項目自 2016 å¹´ 11 月從 [Gogs](https://gogs.io) [分å‰](https://blog.gitea.com/welcome-to-gitea/) 而來,但已經有了很多變化。 + +在線演示å¯ä»¥è¨ªå• [demo.gitea.com](https://demo.gitea.com)。 + +è¦è¨ªå•å…費的 Gitea æœå‹™ï¼ˆæœ‰ä¸€å®šæ•¸é‡çš„倉庫é™åˆ¶ï¼‰ï¼Œå¯ä»¥è¨ªå• [gitea.com](https://gitea.com/user/login)。 + +è¦å¿«é€Ÿéƒ¨ç½²æ‚¨è‡ªå·±çš„專用 Gitea 實例,å¯ä»¥åœ¨ [cloud.gitea.com](https://cloud.gitea.com) é–‹å§‹å…費試用。 + +## 文件 + +您å¯ä»¥åœ¨æˆ‘們的官方 [文件網站](https://docs.gitea.com/) 上找到全é¢çš„æ–‡ä»¶ã€‚ + +它包括安è£ã€ç®¡ç†ã€ä½¿ç”¨ã€é–‹ç™¼ã€è²¢ç»æŒ‡å—等,幫助您快速入門並有效地探索所有功能。 + +如果您有任何建議或想è¦è²¢ç»ï¼Œå¯ä»¥è¨ªå• [文件倉庫](https://gitea.com/gitea/docs) + +## 構建 + +從æºä»£ç¢¼æ¨¹çš„æ ¹ç›®éŒ„é‹è¡Œï¼š + + TAGS="bindata" make build + +å¦‚æžœéœ€è¦ SQLite 支æ´ï¼š + + TAGS="bindata sqlite sqlite_unlock_notify" make build + +`build` 目標分為兩個å­ç›®æ¨™ï¼š + +- `make backend` éœ€è¦ [Go Stable](https://go.dev/dl/),所需版本在 [go.mod](/go.mod) 中定義。 +- `make frontend` éœ€è¦ [Node.js LTS](https://nodejs.org/en/download/) 或更高版本。 + +需è¦äº’è¯ç¶²é€£æŽ¥ä¾†ä¸‹è¼‰ go å’Œ npm 模塊。從包å«é æ§‹å»ºå‰ç«¯æ–‡ä»¶çš„官方æºä»£ç¢¼å£“ç¸®åŒ…æ§‹å»ºæ™‚ï¼Œä¸æœƒè§¸ç™¼ `frontend` 目標,因此å¯ä»¥åœ¨æ²’有 Node.js 的情æ³ä¸‹æ§‹å»ºã€‚ + +更多信æ¯ï¼šhttps://docs.gitea.com/installation/install-from-source + +## 使用 + +æ§‹å»ºå¾Œï¼Œé»˜èªæƒ…æ³ä¸‹æœƒåœ¨æºä»£ç¢¼æ¨¹çš„æ ¹ç›®éŒ„生æˆä¸€å€‹å為 `gitea` 的二進制文件。è¦é‹è¡Œå®ƒï¼Œè«‹ä½¿ç”¨ï¼š + + ./gitea web + +> [!注æ„] +> 如果您å°ä½¿ç”¨æˆ‘們的 API 感興趣,我們æä¾›äº†å¯¦é©—性支æ´ï¼Œä¸¦é™„有 [文件](https://docs.gitea.com/api)。 + +## è²¢ç» + +é æœŸçš„工作æµç¨‹æ˜¯ï¼šFork -> Patch -> Push -> Pull Request + +> [!注æ„] +> +> 1. **在開始進行 Pull Request 之å‰ï¼Œæ‚¨å¿…須閱讀 [è²¢ç»è€…指å—](CONTRIBUTING.md)。** +> 2. 如果您在項目中發ç¾äº†æ¼æ´žï¼Œè«‹ç§ä¸‹å¯«ä¿¡çµ¦ **security@gitea.io**。è¬è¬ï¼ + +## 翻譯 + +[![Crowdin](https://badges.crowdin.net/gitea/localized.svg)](https://translate.gitea.com) + +ç¿»è­¯é€šéŽ [Crowdin](https://translate.gitea.com) é€²è¡Œã€‚å¦‚æžœæ‚¨æƒ³ç¿»è­¯æˆæ–°çš„語言,請在 Crowdin 項目中請求管ç†å“¡æ·»åŠ æ–°èªžè¨€ã€‚ + +您也å¯ä»¥å‰µå»ºä¸€å€‹ issue 來添加語言,或者在 discord çš„ #translation é »é“上詢å•。如果您需è¦ä¸Šä¸‹æ–‡æˆ–發ç¾ä¸€äº›ç¿»è­¯å•題,å¯ä»¥åœ¨å­—符串上留言或在 Discord 上詢å•ã€‚å°æ–¼ä¸€èˆ¬çš„翻譯å•é¡Œï¼Œæ–‡æª”ä¸­æœ‰ä¸€å€‹éƒ¨åˆ†ã€‚ç›®å‰æœ‰é»žç©ºï¼Œä½†æˆ‘們希望隨著å•題的出ç¾è€Œå¡«å……它。 + +更多信æ¯è«‹åƒé–± [文件](https://docs.gitea.com/contributing/localization)。 + +## 官方和第三方項目 + +我們æä¾›äº†ä¸€å€‹å®˜æ–¹çš„ [go-sdk](https://gitea.com/gitea/go-sdk),一個å為 [tea](https://gitea.com/gitea/tea) çš„ CLI 工具和一個 Gitea Action çš„ [action runner](https://gitea.com/gitea/act_runner)。 + +我們在 [gitea/awesome-gitea](https://gitea.com/gitea/awesome-gitea) 維護了一個 Gitea 相關項目的列表,您å¯ä»¥åœ¨é‚£è£¡ç™¼ç¾æ›´å¤šçš„第三方項目,包括 SDKã€æ’ä»¶ã€ä¸»é¡Œç­‰ã€‚ + +## 通訊 + +[![](https://img.shields.io/discord/322538954119184384.svg?logo=discord&logoColor=white&label=Discord&color=5865F2)](https://discord.gg/Gitea "Join the Discord chat at https://discord.gg/Gitea") + +如果您有任何文件未涵蓋的å•題,å¯ä»¥åœ¨æˆ‘們的 [Discord æœå‹™å™¨](https://discord.gg/Gitea) 上與我們è¯ç¹«ï¼Œæˆ–者在 [discourse 論壇](https://forum.gitea.com/) 上創建帖å­ã€‚ + +## 作者 + +- [維護者](https://github.com/orgs/go-gitea/people) +- [è²¢ç»è€…](https://github.com/go-gitea/gitea/graphs/contributors) +- [翻譯者](options/locale/TRANSLATORS) + +## 支æŒè€… + +æ„Ÿè¬æ‰€æœ‰æ”¯æŒè€…ï¼ ðŸ™ [[æˆç‚ºæ”¯æŒè€…](https://opencollective.com/gitea#backer)] + + + +## 贊助商 + +é€šéŽæˆç‚ºè´ŠåŠ©å•†ä¾†æ”¯æŒé€™å€‹é …ç›®ã€‚æ‚¨çš„æ¨™èªŒå°‡é¡¯ç¤ºåœ¨é€™è£¡ï¼Œä¸¦å¸¶æœ‰éˆæŽ¥åˆ°æ‚¨çš„ç¶²ç«™ã€‚ [[æˆç‚ºè´ŠåЩ商](https://opencollective.com/gitea#sponsor)] + + + + + + + + + + + + +## 常見å•題 + +**Gitea 怎麼發音?** + +Gitea 的發音是 [/ɡɪ’ti:/](https://youtu.be/EM71-2uDAoY)ï¼Œå°±åƒ "gi-tea" 一樣,g 是硬音。 + +**為什麼這個項目沒有託管在 Gitea 實例上?** + +我們正在 [努力](https://github.com/go-gitea/gitea/issues/1029)。 + +**在哪裡å¯ä»¥æ‰¾åˆ°å®‰å…¨è£œä¸ï¼Ÿ** + +在 [發佈日誌](https://github.com/go-gitea/gitea/releases) 或 [變更日誌](https://github.com/go-gitea/gitea/blob/main/CHANGELOG.md) 中,æœç´¢é—œéµè©ž `SECURITY` 以找到安全補ä¸ã€‚ + +## 許å¯è­‰ + +這個項目是根據 MIT 許å¯è­‰æŽˆæ¬Šçš„。 +è«‹åƒé–± [LICENSE](https://github.com/go-gitea/gitea/blob/main/LICENSE) 文件以ç²å–完整的許å¯è­‰æ–‡æœ¬ã€‚ + +## é€²ä¸€æ­¥ä¿¡æ¯ + +
+å°‹æ‰¾ç•Œé¢æ¦‚è¿°ï¼ŸæŸ¥çœ‹é€™è£¡ï¼ + +### 登錄/註冊é é¢ + +![Login](https://dl.gitea.com/screenshots/login.png) +![Register](https://dl.gitea.com/screenshots/register.png) + +### ç”¨æˆ¶å„€è¡¨æ¿ + +![Home](https://dl.gitea.com/screenshots/home.png) +![Issues](https://dl.gitea.com/screenshots/issues.png) +![Pull Requests](https://dl.gitea.com/screenshots/pull_requests.png) +![Milestones](https://dl.gitea.com/screenshots/milestones.png) + +### 用戶資料 + +![Profile](https://dl.gitea.com/screenshots/user_profile.png) + +### 探索 + +![Repos](https://dl.gitea.com/screenshots/explore_repos.png) +![Users](https://dl.gitea.com/screenshots/explore_users.png) +![Orgs](https://dl.gitea.com/screenshots/explore_orgs.png) + +### 倉庫 + +![Home](https://dl.gitea.com/screenshots/repo_home.png) +![Commits](https://dl.gitea.com/screenshots/repo_commits.png) +![Branches](https://dl.gitea.com/screenshots/repo_branches.png) +![Labels](https://dl.gitea.com/screenshots/repo_labels.png) +![Milestones](https://dl.gitea.com/screenshots/repo_milestones.png) +![Releases](https://dl.gitea.com/screenshots/repo_releases.png) +![Tags](https://dl.gitea.com/screenshots/repo_tags.png) + +#### 倉庫å•題 + +![List](https://dl.gitea.com/screenshots/repo_issues.png) +![Issue](https://dl.gitea.com/screenshots/repo_issue.png) + +#### 倉庫拉å–請求 + +![List](https://dl.gitea.com/screenshots/repo_pull_requests.png) +![Pull Request](https://dl.gitea.com/screenshots/repo_pull_request.png) +![File](https://dl.gitea.com/screenshots/repo_pull_request_file.png) +![Commits](https://dl.gitea.com/screenshots/repo_pull_request_commits.png) + +#### 倉庫æ“作 + +![List](https://dl.gitea.com/screenshots/repo_actions.png) +![Details](https://dl.gitea.com/screenshots/repo_actions_run.png) + +#### 倉庫活動 + +![Activity](https://dl.gitea.com/screenshots/repo_activity.png) +![Contributors](https://dl.gitea.com/screenshots/repo_contributors.png) +![Code Frequency](https://dl.gitea.com/screenshots/repo_code_frequency.png) +![Recent Commits](https://dl.gitea.com/screenshots/repo_recent_commits.png) + +### 組織 + +![Home](https://dl.gitea.com/screenshots/org_home.png) + +
diff --git a/README_ZH.md b/README_ZH.md deleted file mode 100644 index a2e36dc22f1..00000000000 --- a/README_ZH.md +++ /dev/null @@ -1,61 +0,0 @@ -# Gitea - -[![](https://github.com/go-gitea/gitea/actions/workflows/release-nightly.yml/badge.svg?branch=main)](https://github.com/go-gitea/gitea/actions/workflows/release-nightly.yml?query=branch%3Amain "Release Nightly") -[![](https://img.shields.io/discord/322538954119184384.svg?logo=discord&logoColor=white&label=Discord&color=5865F2)](https://discord.gg/Gitea "Join the Discord chat at https://discord.gg/Gitea") -[![](https://goreportcard.com/badge/code.gitea.io/gitea)](https://goreportcard.com/report/code.gitea.io/gitea "Go Report Card") -[![](https://pkg.go.dev/badge/code.gitea.io/gitea?status.svg)](https://pkg.go.dev/code.gitea.io/gitea "GoDoc") -[![](https://img.shields.io/github/release/go-gitea/gitea.svg)](https://github.com/go-gitea/gitea/releases/latest "GitHub release") -[![](https://www.codetriage.com/go-gitea/gitea/badges/users.svg)](https://www.codetriage.com/go-gitea/gitea "Help Contribute to Open Source") -[![](https://opencollective.com/gitea/tiers/backers/badge.svg?label=backers&color=brightgreen)](https://opencollective.com/gitea "Become a backer/sponsor of gitea") -[![](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT "License: MIT") -[![Contribute with Gitpod](https://img.shields.io/badge/Contribute%20with-Gitpod-908a85?logo=gitpod&color=green)](https://gitpod.io/#https://github.com/go-gitea/gitea) -[![](https://badges.crowdin.net/gitea/localized.svg)](https://crowdin.com/project/gitea "Crowdin") - -[View this document in English](./README.md) - -## 目标 - -Gitea 的首è¦ç›®æ ‡æ˜¯åˆ›å»ºä¸€ä¸ªæžæ˜“安装,è¿è¡Œéžå¸¸å¿«é€Ÿï¼Œå®‰è£…和使用体验良好的自建 Git æœåŠ¡ã€‚æˆ‘ä»¬é‡‡ç”¨ Go 作为åŽç«¯è¯­è¨€ï¼Œè¿™ä½¿æˆ‘们åªè¦ç”Ÿæˆä¸€ä¸ªå¯æ‰§è¡Œç¨‹åºå³å¯ã€‚并且他还支æŒè·¨å¹³å°ï¼Œæ”¯æŒ Linux, macOS å’Œ Windows 以åŠå„ç§æž¶æž„,除了 x86,amd64,还包括 ARM å’Œ PowerPC。 - -如果你想试用在线演示和报告问题,请访问 [demo.gitea.com](https://demo.gitea.com/)。 - -如果你想使用å…费的 Gitea æœåŠ¡ï¼ˆæœ‰ä»“åº“æ•°é‡é™åˆ¶ï¼‰ï¼Œè¯·è®¿é—® [gitea.com](https://gitea.com/user/login)。 - -如果你想在 Gitea Cloud 上快速部署你自己独享的 Gitea 实例,请访问 [cloud.gitea.com](https://cloud.gitea.com) 开始å…费试用。 - -## æç¤º - -1. **开始贡献代ç ä¹‹å‰è¯·ç¡®ä¿ä½ å·²ç»çœ‹è¿‡äº† [贡献者å‘导(英文)](CONTRIBUTING.md)**. -2. 所有的安全问题,请ç§ä¸‹å‘é€é‚®ä»¶ç»™ **security@gitea.io**ã€‚è°¢è°¢ï¼ -3. 如果你è¦ä½¿ç”¨API,请å‚è§ [API 文档](https://godoc.org/code.gitea.io/sdk/gitea). - -## 文档 - -关于如何安装请访问我们的 [文档站](https://docs.gitea.com/zh-cn/category/installation),如果没有找到对应的文档,你也å¯ä»¥é€šè¿‡ [Discord - 英文](https://discord.gg/gitea) å’Œ QQ群 328432459 æ¥å’Œæˆ‘们交æµã€‚ - -## 贡献æµç¨‹ - -Fork -> Patch -> Push -> Pull Request - -## 翻译 - -多语言翻译是基于Crowdin进行的. -[![Crowdin](https://badges.crowdin.net/gitea/localized.svg)](https://crowdin.com/project/gitea) - -## 作者 - -* [Maintainers](https://github.com/orgs/go-gitea/people) -* [Contributors](https://github.com/go-gitea/gitea/graphs/contributors) -* [Translators](options/locale/TRANSLATORS) - -## 授æƒè®¸å¯ - -本项目采用 MIT å¼€æºæŽˆæƒè®¸å¯è¯ï¼Œå®Œæ•´çš„æŽˆæƒè¯´æ˜Žå·²æ”¾ç½®åœ¨ [LICENSE](https://github.com/go-gitea/gitea/blob/main/LICENSE) 文件中。 - -## 截图 - -|![Dashboard](https://dl.gitea.com/screenshots/home_timeline.png)|![User Profile](https://dl.gitea.com/screenshots/user_profile.png)|![Global Issues](https://dl.gitea.com/screenshots/global_issues.png)| -|:---:|:---:|:---:| -|![Branches](https://dl.gitea.com/screenshots/branches.png)|![Web Editor](https://dl.gitea.com/screenshots/web_editor.png)|![Activity](https://dl.gitea.com/screenshots/activity.png)| -|![New Migration](https://dl.gitea.com/screenshots/migration.png)|![Migrating](https://dl.gitea.com/screenshots/migration.gif)|![Pull Request View](https://image.ibb.co/e02dSb/6.png)| -|![Pull Request Dark](https://dl.gitea.com/screenshots/pull_requests_dark.png)|![Diff Review Dark](https://dl.gitea.com/screenshots/review_dark.png)|![Diff Dark](https://dl.gitea.com/screenshots/diff_dark.png)| diff --git a/SECURITY.md b/SECURITY.md index c9dbf859f58..d7c27ea6136 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -14,12 +14,12 @@ Please **DO NOT** file a public issue, instead send your report privately to `se Due to the sensitive nature of security information, you can use the below GPG public key to encrypt your mail body. -The PGP key is valid until July 9, 2025. +The PGP key is valid until July 4, 2026. ``` Key ID: 6FCD2D5B Key Type: RSA -Expires: 7/9/2025 +Expires: 7/4/2026 Key Size: 4096/4096 Fingerprint: 3DE0 3D1E 144A 7F06 9359 99DC AAFD 2381 6FCD 2D5B ``` @@ -42,18 +42,18 @@ lzpAjnN9/KLtQroutrm+Ft0mdjDiJUeFVl1cOHDhoyfCsQh62HumoyZoZvqzQd6e AbN11nq6aViMe2Q3je1AbiBnRnQSHxt1Tc8X4IshO3MQK1Sk7oPI6LA5oQARAQAB tCJHaXRlYSBTZWN1cml0eSA8c2VjdXJpdHlAZ2l0ZWEuaW8+iQJXBBMBCABBAhsD BQsJCAcCAiICBhUKCQgLAgQWAgMBAh4HAheAFiEEPeA9HhRKfwaTWZncqv0jgW/N -LVsFAmaMse0FCQW4fW8ACgkQqv0jgW/NLVtXLg/+PF4G9Jhlui15BTNlEBJAV2P/ -1QlAV2krk0fP7tykn0FR9RfGIfVV/kwC1f+ouosYPQDDevl9LWdUIM+g94DtNo2o -7ACpcL3morvt5lVGpIZHL8TbX0qmFRXL/pB/cB+K6IwYvh2mrbp2zH+r4SCRyFYq -BjgXYFTI1MylJ1ShAjU6Z+m3oJ+2xs5LzHS0X6zkTjzA2Zl4zQzciQ9T+wJcE7Zi -HXdM1+YMF8KGNP8J9Rpug5oNDJ98lgZirRY7c3A/1xmYBiPnULwuuymdqEZO7l70 -SeAlE1RWYX8kbOBnBb/KY4XwE3Vic1oEzc9DiPWVH1ElX86WNNsFzuyULiwoBoWg -pqZGhL9x1p5+46RGQSDczsHM7YGVtfYOiDo2PAVrmwsT0BnXnK8Oe3YIkvmUPEJu -OkLt0Z6A5n8pz8zhQzuApwBsK4ncJ8zTCpvz/pfKKqZC/Vnoh3gKGhDGvOZ+b5IJ -0kUTe2JsbnwFixDUMDtacQ1op8XOyLoLVmgqLn0+Pws4XPBlMof2bioFir3yHKnP -gNchsF1agrlSIo5GA8u4ga+IlCSfvFIKrl7+cxacKcJYt/vbOU5KcvVJI5EtHKCG -xfHjHY2ah1Qww7SxW6IXiRZZzPpsL2mBM2CD7N3qh9bV2s27wxYCdUodsIZbiyHe -oWPzfBnkmiAN8KlZxHm5Ag0EYrVn/gEQALrFLQjCR3GjuHSindz0rd3Fnx/t7Sen +LVsFAmhoHmkFCQeT6esACgkQqv0jgW/NLVuFLRAAmjBQSKRAgs2bFIEj7HLAbDp4 +f+XkdH+GsT3jRPOZ9QZgmtM+TfoE4yNgIVfOl+s4RdjM/W4QzqZuPQ55hbEHd056 +cJmm7B+6GsHFcdrPmh65sOCEIyh4+t45dUfeWpFsDPqm9j1UHXAJQIpB8vDEVAPH +t+3wLCk8GMPJs1o5tIyMmaO23ngvkwn8eG7KgY+rp2PzObrb5g7ppci0ILzILkrp +HVjZsEfUWRgSVF7LuU5ppqDKrlcqwUpQq6n3kGMZcLrCp6ACKP04TBmTfUxNwdL7 +I0N7apI2Pbct9T1Gv/lYAUFWyU2c3gh/EBLbO6BukaLOFRQHrtNfdJV/YnMPlcXr +LUJjK9K4eAH9DsrZqrisz/LthsC2BaNIN3KRMTk5YTYgmIh8GXzSgihORmtDFELC +RroID3pTuS0zjXh+wpY9GuPTh7UW23p42Daxca4fAT4k5EclvDRUrL21xMopPMiL +HuNdELz4FVchRTy05PjzKVyjVInDNojE2KUxnjxZDzYJ6aT/g+coD5yfntYm8BEj ++ZzL0ndZES54hzKLpv7zwBQwFzam68clZYmDPILOPTflQDfpGEWmJK4undFU5obz +ZsQRz0R3ulspChATbZxO0d5LX2obLpKO9X3b5VoO1KF+R8Vjw1Y0KxrNZ6rIcfqH +Z50QVQKSe9dm08K0ON+5Ag0EYrVn/gEQALrFLQjCR3GjuHSindz0rd3Fnx/t7Sen T+p07yCSSoSlmnJHCQmwh4vfg1blyz0zZ4vkIhtpHsEgc+ZAG+WQXSsJ2iRz+eSN GwoOQl4XC3n+QWkc1ws+btr48+6UqXIQU+F8TPQyx/PIgi2nZXJB7f5+mjCqsk46 XvH4nTr4kJjuqMSR/++wvre2qNQRa/q/dTsK0OaN/mJsdX6Oi+aGNaQJUhIG7F+E @@ -65,19 +65,19 @@ s+GsP9I3cmWWQcKYxWHtE8xTXnNCVPFZQj2nwhJzae8ypfOtulBRA3dUKWGKuDH/ axFENhUsT397aOU3qkP/od4a64JyNIEo4CTTSPVeWd7njsGqli2U3A4xL2CcyYvt D/MWcMBGEoLSNTswwKdom4FaJpn5KThnK/T0bQcmJblJhoCtppXisbexZnCpuS0x Zdlm2T14KJ3LABEBAAGJAjwEGAEIACYCGwwWIQQ94D0eFEp/BpNZmdyq/SOBb80t -WwUCZoyyjQUJBbh+DwAKCRCq/SOBb80tW18XD/9MXztmf01MT+1kZdBouZ/7Rp/7 -9kuqo//B1G+RXau4oFtPqb67kNe2WaIc3u5B73PUHsMf3i6z4ib2KbMhZZerLn0O -dRglcuPeNWmsASY3dH/XVG0cT0zvvWegagd12TJEl3Vs+7XNrOw4cwDj9L1+GH9m -kSt4uaANWn/6a3RvMRhiVEYuNwhAzcKaactPmYqrLJgoVLbRSDkgyHaMQ2jKgLxk -ifS/fvluGV0ub2Po6DJiqfRpd1tDvPhe9y1+r1WFDZsOcvTcZUfSt/7dXMGfqGu0 -2daVFlfeSXSALrDE5uc0UxodHCpP3sqRYDZevGLBRaaTkIjYXG/+N898+7K5WJF4 -xXOLWxM2cwGkG7eC9pugcDnBp9XlF7O+GBiZ05JUe5flXDQFZ+h3exjopu6KHF1B -RnzNy8LC0UKb+AuvRIOLV92a9Q9wGWU/jaVDu6nZ0umAeuSzxiHoDsonm0Fl9QAz -2/xCokebuoeLrEK7R2af3X86mqq3sVO4ax+HPYChzOaVQBiHUW/TAldWcldYYphR -/e2WsbmQfvCRtz/bZfo+aUVnrHNjzVMtF2SszdVmA/04Y8pS28MqtuRqhm5DPOOd -g1YeUywK5jRZ1twyo1kzJEFPLaoeaXaycsR1PMVBW0Urik5mrR/pOWq7PPoZoKb2 -lXYLE8bwkuQTmsyL1g== -=9i7d +WwUCaGgeJAUJB5PppgAKCRCq/SOBb80tW/NWEACB6Jrf0gWlk7e+hNCdnbM0ZVWU +f2sHNFfXxxsdhpcDgKbNHtkZb8nZgv8AX+5fTtUwMVa3vKcdw30xFiIM5N7cCIPV +vg/5z5BtfEaitnabEUG2iiVDIy8IHXIcK10rX+7BosA3QDl2PsiBHwyi5G13lRk8 +zGTSNDuOalug33h5/lr2dPigamkq74Aoy29q8Rjad6GfWHipL2bFimgtY+Zdi0BH +NLk4EJXxj1SgVx5dtkQzWJReBA5M+FQ4QYQZBO+f4TDoOLmjui152uhkoLBQbGAa +WWJFTVxm0bG5MXloEL3gA8DfU7XDwuW/sHJC5pBko8RpQViooOhckMepZV3Y83DK +bwLYa3JmPgj2rEv4993dvrJbQhpGd082HOxOsllCs8pgNq1SnXpWYfcGTgGKC3ts +U8YZUUJUQ7mi2L8Tv3ix20c9EiGmA30JAmA8eZTC3cWup91ZkkVBFRml2czTXajd +RWZ6GbHV5503ueDQcB8yBVgF3CSixs67+dGSbD3p86OqGrjAcJzM5TFbNKcnGLdE +kGbZpNwAISy750lXzXKmyrh5RTCeTOQerbwCMBvHZO+HAevA/LXDTw2OAiSIQlP5 +sYA4sFYLQ30OAkgJcmdp/pSgVj/erNtSN07ClrOpDb/uFpQymO6K2h0Pst3feNVK +9M2VbqL9C51z/wyHLg== +=SfZA -----END PGP PUBLIC KEY BLOCK----- ``` diff --git a/assets/go-licenses.json b/assets/go-licenses.json index fcfde088004..d9614442396 100644 --- a/assets/go-licenses.json +++ b/assets/go-licenses.json @@ -104,16 +104,6 @@ "path": "github.com/Azure/go-ntlmssp/LICENSE", "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2016 Microsoft\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n" }, - { - "name": "github.com/ClickHouse/ch-go", - "path": "github.com/ClickHouse/ch-go/LICENSE", - "licenseText": "Copyright 2016-2023 ClickHouse, Inc.\n Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright 2016-2023 ClickHouse, Inc.\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n" - }, - { - "name": "github.com/ClickHouse/clickhouse-go/v2", - "path": "github.com/ClickHouse/clickhouse-go/v2/LICENSE", - "licenseText": "Copyright 2016-2023 ClickHouse, Inc.\n\n Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright 2016-2023 ClickHouse, Inc.\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n" - }, { "name": "github.com/DataDog/zstd", "path": "github.com/DataDog/zstd/LICENSE", @@ -125,8 +115,8 @@ "licenseText": "Copyright (c) 2009 The Go Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" }, { - "name": "github.com/RoaringBitmap/roaring", - "path": "github.com/RoaringBitmap/roaring/LICENSE", + "name": "github.com/RoaringBitmap/roaring/v2", + "path": "github.com/RoaringBitmap/roaring/v2/LICENSE", "licenseText": "\n Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright 2016 by the authors\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n\n================================================================================\n\nPortions of runcontainer.go are from the Go standard library, which is licensed\nunder:\n\nCopyright (c) 2009 The Go Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\n notice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\n copyright notice, this list of conditions and the following disclaimer\n in the documentation and/or other materials provided with the\n distribution.\n * Neither the name of Google Inc. nor the names of its\n contributors may be used to endorse or promote products derived from\n this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" }, { @@ -179,16 +169,6 @@ "path": "github.com/aws/aws-sdk-go-v2/service/codecommit/LICENSE.txt", "licenseText": "\n Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n" }, - { - "name": "github.com/aws/aws-sdk-go", - "path": "github.com/aws/aws-sdk-go/LICENSE.txt", - "licenseText": "\n Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n" - }, - { - "name": "github.com/aws/aws-sdk-go/internal/sync/singleflight", - "path": "github.com/aws/aws-sdk-go/internal/sync/singleflight/LICENSE", - "licenseText": "Copyright (c) 2009 The Go Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" - }, { "name": "github.com/aws/smithy-go", "path": "github.com/aws/smithy-go/LICENSE", @@ -309,6 +289,16 @@ "path": "github.com/blevesearch/zapx/v16/LICENSE", "licenseText": "\n Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License." }, + { + "name": "github.com/bmatcuk/doublestar/v4", + "path": "github.com/bmatcuk/doublestar/v4/LICENSE", + "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2014 Bob Matcuk\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n" + }, + { + "name": "github.com/bohde/codel", + "path": "github.com/bohde/codel/LICENSE", + "licenseText": "BSD 3-Clause License\n\nCopyright (c) 2018, Rowan Bohde\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n* Redistributions of source code must retain the above copyright notice, this\n list of conditions and the following disclaimer.\n\n* Redistributions in binary form must reproduce the above copyright notice,\n this list of conditions and the following disclaimer in the documentation\n and/or other materials provided with the distribution.\n\n* Neither the name of the copyright holder nor the names of its\n contributors may be used to endorse or promote products derived from\n this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" + }, { "name": "github.com/boombuler/barcode", "path": "github.com/boombuler/barcode/LICENSE", @@ -519,16 +509,6 @@ "path": "github.com/go-enry/go-enry/v2/LICENSE", "licenseText": "\n Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License." }, - { - "name": "github.com/go-faster/city", - "path": "github.com/go-faster/city/LICENSE", - "licenseText": "MIT License\n\nCopyright (c) 2018 tenfy\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n" - }, - { - "name": "github.com/go-faster/errors", - "path": "github.com/go-faster/errors/LICENSE", - "licenseText": "Copyright (c) 2009 The Go Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" - }, { "name": "github.com/go-fed/httpsig", "path": "github.com/go-fed/httpsig/LICENSE", @@ -557,7 +537,7 @@ { "name": "github.com/go-ldap/ldap/v3", "path": "github.com/go-ldap/ldap/v3/LICENSE", - "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2011-2015 Michael Mitton (mmitton@gmail.com)\nPortions copyright (c) 2015-2016 go-ldap Authors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n" + "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2011-2015 Michael Mitton (mmitton@gmail.com)\nPortions copyright (c) 2015-2024 go-ldap Authors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n" }, { "name": "github.com/go-redsync/redsync/v4", @@ -569,11 +549,6 @@ "path": "github.com/go-sql-driver/mysql/LICENSE", "licenseText": "Mozilla Public License Version 2.0\n==================================\n\n1. Definitions\n--------------\n\n1.1. \"Contributor\"\n means each individual or legal entity that creates, contributes to\n the creation of, or owns Covered Software.\n\n1.2. \"Contributor Version\"\n means the combination of the Contributions of others (if any) used\n by a Contributor and that particular Contributor's Contribution.\n\n1.3. \"Contribution\"\n means Covered Software of a particular Contributor.\n\n1.4. \"Covered Software\"\n means Source Code Form to which the initial Contributor has attached\n the notice in Exhibit A, the Executable Form of such Source Code\n Form, and Modifications of such Source Code Form, in each case\n including portions thereof.\n\n1.5. \"Incompatible With Secondary Licenses\"\n means\n\n (a) that the initial Contributor has attached the notice described\n in Exhibit B to the Covered Software; or\n\n (b) that the Covered Software was made available under the terms of\n version 1.1 or earlier of the License, but not also under the\n terms of a Secondary License.\n\n1.6. \"Executable Form\"\n means any form of the work other than Source Code Form.\n\n1.7. \"Larger Work\"\n means a work that combines Covered Software with other material, in \n a separate file or files, that is not Covered Software.\n\n1.8. \"License\"\n means this document.\n\n1.9. \"Licensable\"\n means having the right to grant, to the maximum extent possible,\n whether at the time of the initial grant or subsequently, any and\n all of the rights conveyed by this License.\n\n1.10. \"Modifications\"\n means any of the following:\n\n (a) any file in Source Code Form that results from an addition to,\n deletion from, or modification of the contents of Covered\n Software; or\n\n (b) any new file in Source Code Form that contains any Covered\n Software.\n\n1.11. \"Patent Claims\" of a Contributor\n means any patent claim(s), including without limitation, method,\n process, and apparatus claims, in any patent Licensable by such\n Contributor that would be infringed, but for the grant of the\n License, by the making, using, selling, offering for sale, having\n made, import, or transfer of either its Contributions or its\n Contributor Version.\n\n1.12. \"Secondary License\"\n means either the GNU General Public License, Version 2.0, the GNU\n Lesser General Public License, Version 2.1, the GNU Affero General\n Public License, Version 3.0, or any later versions of those\n licenses.\n\n1.13. \"Source Code Form\"\n means the form of the work preferred for making modifications.\n\n1.14. \"You\" (or \"Your\")\n means an individual or a legal entity exercising rights under this\n License. For legal entities, \"You\" includes any entity that\n controls, is controlled by, or is under common control with You. For\n purposes of this definition, \"control\" means (a) the power, direct\n or indirect, to cause the direction or management of such entity,\n whether by contract or otherwise, or (b) ownership of more than\n fifty percent (50%) of the outstanding shares or beneficial\n ownership of such entity.\n\n2. License Grants and Conditions\n--------------------------------\n\n2.1. Grants\n\nEach Contributor hereby grants You a world-wide, royalty-free,\nnon-exclusive license:\n\n(a) under intellectual property rights (other than patent or trademark)\n Licensable by such Contributor to use, reproduce, make available,\n modify, display, perform, distribute, and otherwise exploit its\n Contributions, either on an unmodified basis, with Modifications, or\n as part of a Larger Work; and\n\n(b) under Patent Claims of such Contributor to make, use, sell, offer\n for sale, have made, import, and otherwise transfer either its\n Contributions or its Contributor Version.\n\n2.2. Effective Date\n\nThe licenses granted in Section 2.1 with respect to any Contribution\nbecome effective for each Contribution on the date the Contributor first\ndistributes such Contribution.\n\n2.3. Limitations on Grant Scope\n\nThe licenses granted in this Section 2 are the only rights granted under\nthis License. No additional rights or licenses will be implied from the\ndistribution or licensing of Covered Software under this License.\nNotwithstanding Section 2.1(b) above, no patent license is granted by a\nContributor:\n\n(a) for any code that a Contributor has removed from Covered Software;\n or\n\n(b) for infringements caused by: (i) Your and any other third party's\n modifications of Covered Software, or (ii) the combination of its\n Contributions with other software (except as part of its Contributor\n Version); or\n\n(c) under Patent Claims infringed by Covered Software in the absence of\n its Contributions.\n\nThis License does not grant any rights in the trademarks, service marks,\nor logos of any Contributor (except as may be necessary to comply with\nthe notice requirements in Section 3.4).\n\n2.4. Subsequent Licenses\n\nNo Contributor makes additional grants as a result of Your choice to\ndistribute the Covered Software under a subsequent version of this\nLicense (see Section 10.2) or under the terms of a Secondary License (if\npermitted under the terms of Section 3.3).\n\n2.5. Representation\n\nEach Contributor represents that the Contributor believes its\nContributions are its original creation(s) or it has sufficient rights\nto grant the rights to its Contributions conveyed by this License.\n\n2.6. Fair Use\n\nThis License is not intended to limit any rights You have under\napplicable copyright doctrines of fair use, fair dealing, or other\nequivalents.\n\n2.7. Conditions\n\nSections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted\nin Section 2.1.\n\n3. Responsibilities\n-------------------\n\n3.1. Distribution of Source Form\n\nAll distribution of Covered Software in Source Code Form, including any\nModifications that You create or to which You contribute, must be under\nthe terms of this License. You must inform recipients that the Source\nCode Form of the Covered Software is governed by the terms of this\nLicense, and how they can obtain a copy of this License. You may not\nattempt to alter or restrict the recipients' rights in the Source Code\nForm.\n\n3.2. Distribution of Executable Form\n\nIf You distribute Covered Software in Executable Form then:\n\n(a) such Covered Software must also be made available in Source Code\n Form, as described in Section 3.1, and You must inform recipients of\n the Executable Form how they can obtain a copy of such Source Code\n Form by reasonable means in a timely manner, at a charge no more\n than the cost of distribution to the recipient; and\n\n(b) You may distribute such Executable Form under the terms of this\n License, or sublicense it under different terms, provided that the\n license for the Executable Form does not attempt to limit or alter\n the recipients' rights in the Source Code Form under this License.\n\n3.3. Distribution of a Larger Work\n\nYou may create and distribute a Larger Work under terms of Your choice,\nprovided that You also comply with the requirements of this License for\nthe Covered Software. If the Larger Work is a combination of Covered\nSoftware with a work governed by one or more Secondary Licenses, and the\nCovered Software is not Incompatible With Secondary Licenses, this\nLicense permits You to additionally distribute such Covered Software\nunder the terms of such Secondary License(s), so that the recipient of\nthe Larger Work may, at their option, further distribute the Covered\nSoftware under the terms of either this License or such Secondary\nLicense(s).\n\n3.4. Notices\n\nYou may not remove or alter the substance of any license notices\n(including copyright notices, patent notices, disclaimers of warranty,\nor limitations of liability) contained within the Source Code Form of\nthe Covered Software, except that You may alter any license notices to\nthe extent required to remedy known factual inaccuracies.\n\n3.5. Application of Additional Terms\n\nYou may choose to offer, and to charge a fee for, warranty, support,\nindemnity or liability obligations to one or more recipients of Covered\nSoftware. However, You may do so only on Your own behalf, and not on\nbehalf of any Contributor. You must make it absolutely clear that any\nsuch warranty, support, indemnity, or liability obligation is offered by\nYou alone, and You hereby agree to indemnify every Contributor for any\nliability incurred by such Contributor as a result of warranty, support,\nindemnity or liability terms You offer. You may include additional\ndisclaimers of warranty and limitations of liability specific to any\njurisdiction.\n\n4. Inability to Comply Due to Statute or Regulation\n---------------------------------------------------\n\nIf it is impossible for You to comply with any of the terms of this\nLicense with respect to some or all of the Covered Software due to\nstatute, judicial order, or regulation then You must: (a) comply with\nthe terms of this License to the maximum extent possible; and (b)\ndescribe the limitations and the code they affect. Such description must\nbe placed in a text file included with all distributions of the Covered\nSoftware under this License. Except to the extent prohibited by statute\nor regulation, such description must be sufficiently detailed for a\nrecipient of ordinary skill to be able to understand it.\n\n5. Termination\n--------------\n\n5.1. The rights granted under this License will terminate automatically\nif You fail to comply with any of its terms. However, if You become\ncompliant, then the rights granted under this License from a particular\nContributor are reinstated (a) provisionally, unless and until such\nContributor explicitly and finally terminates Your grants, and (b) on an\nongoing basis, if such Contributor fails to notify You of the\nnon-compliance by some reasonable means prior to 60 days after You have\ncome back into compliance. Moreover, Your grants from a particular\nContributor are reinstated on an ongoing basis if such Contributor\nnotifies You of the non-compliance by some reasonable means, this is the\nfirst time You have received notice of non-compliance with this License\nfrom such Contributor, and You become compliant prior to 30 days after\nYour receipt of the notice.\n\n5.2. If You initiate litigation against any entity by asserting a patent\ninfringement claim (excluding declaratory judgment actions,\ncounter-claims, and cross-claims) alleging that a Contributor Version\ndirectly or indirectly infringes any patent, then the rights granted to\nYou by any and all Contributors for the Covered Software under Section\n2.1 of this License shall terminate.\n\n5.3. In the event of termination under Sections 5.1 or 5.2 above, all\nend user license agreements (excluding distributors and resellers) which\nhave been validly granted by You or Your distributors under this License\nprior to termination shall survive termination.\n\n************************************************************************\n* *\n* 6. Disclaimer of Warranty *\n* ------------------------- *\n* *\n* Covered Software is provided under this License on an \"as is\" *\n* basis, without warranty of any kind, either expressed, implied, or *\n* statutory, including, without limitation, warranties that the *\n* Covered Software is free of defects, merchantable, fit for a *\n* particular purpose or non-infringing. The entire risk as to the *\n* quality and performance of the Covered Software is with You. *\n* Should any Covered Software prove defective in any respect, You *\n* (not any Contributor) assume the cost of any necessary servicing, *\n* repair, or correction. This disclaimer of warranty constitutes an *\n* essential part of this License. No use of any Covered Software is *\n* authorized under this License except under this disclaimer. *\n* *\n************************************************************************\n\n************************************************************************\n* *\n* 7. Limitation of Liability *\n* -------------------------- *\n* *\n* Under no circumstances and under no legal theory, whether tort *\n* (including negligence), contract, or otherwise, shall any *\n* Contributor, or anyone who distributes Covered Software as *\n* permitted above, be liable to You for any direct, indirect, *\n* special, incidental, or consequential damages of any character *\n* including, without limitation, damages for lost profits, loss of *\n* goodwill, work stoppage, computer failure or malfunction, or any *\n* and all other commercial damages or losses, even if such party *\n* shall have been informed of the possibility of such damages. This *\n* limitation of liability shall not apply to liability for death or *\n* personal injury resulting from such party's negligence to the *\n* extent applicable law prohibits such limitation. Some *\n* jurisdictions do not allow the exclusion or limitation of *\n* incidental or consequential damages, so this exclusion and *\n* limitation may not apply to You. *\n* *\n************************************************************************\n\n8. Litigation\n-------------\n\nAny litigation relating to this License may be brought only in the\ncourts of a jurisdiction where the defendant maintains its principal\nplace of business and such litigation shall be governed by laws of that\njurisdiction, without reference to its conflict-of-law provisions.\nNothing in this Section shall prevent a party's ability to bring\ncross-claims or counter-claims.\n\n9. Miscellaneous\n----------------\n\nThis License represents the complete agreement concerning the subject\nmatter hereof. If any provision of this License is held to be\nunenforceable, such provision shall be reformed only to the extent\nnecessary to make it enforceable. Any law or regulation which provides\nthat the language of a contract shall be construed against the drafter\nshall not be used to construe this License against a Contributor.\n\n10. Versions of the License\n---------------------------\n\n10.1. New Versions\n\nMozilla Foundation is the license steward. Except as provided in Section\n10.3, no one other than the license steward has the right to modify or\npublish new versions of this License. Each version will be given a\ndistinguishing version number.\n\n10.2. Effect of New Versions\n\nYou may distribute the Covered Software under the terms of the version\nof the License under which You originally received the Covered Software,\nor under the terms of any subsequent version published by the license\nsteward.\n\n10.3. Modified Versions\n\nIf you create software not governed by this License, and you want to\ncreate a new license for such software, you may create and use a\nmodified version of this License if you rename the license and remove\nany references to the name of the license steward (except to note that\nsuch modified license differs from this License).\n\n10.4. Distributing Source Code Form that is Incompatible With Secondary\nLicenses\n\nIf You choose to distribute Source Code Form that is Incompatible With\nSecondary Licenses under the terms of this version of the License, the\nnotice described in Exhibit B of this License must be attached.\n\nExhibit A - Source Code Form License Notice\n-------------------------------------------\n\n This Source Code Form is subject to the terms of the Mozilla Public\n License, v. 2.0. If a copy of the MPL was not distributed with this\n file, You can obtain one at http://mozilla.org/MPL/2.0/.\n\nIf it is not possible or desirable to put the notice in a particular\nfile, then You may include the notice in a location (such as a LICENSE\nfile in a relevant directory) where a recipient would be likely to look\nfor such a notice.\n\nYou may add additional accurate notices of copyright ownership.\n\nExhibit B - \"Incompatible With Secondary Licenses\" Notice\n---------------------------------------------------------\n\n This Source Code Form is \"Incompatible With Secondary Licenses\", as\n defined by the Mozilla Public License, v. 2.0.\n" }, - { - "name": "github.com/go-testfixtures/testfixtures/v3", - "path": "github.com/go-testfixtures/testfixtures/v3/LICENSE", - "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2016 Andrey Nering\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n" - }, { "name": "github.com/go-webauthn/webauthn", "path": "github.com/go-webauthn/webauthn/LICENSE", @@ -624,11 +599,6 @@ "path": "github.com/golang-sql/sqlexp/LICENSE", "licenseText": "Copyright (c) 2017 The Go Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" }, - { - "name": "github.com/golang/geo", - "path": "github.com/golang/geo/LICENSE", - "licenseText": "\n Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n" - }, { "name": "github.com/golang/groupcache/lru", "path": "github.com/golang/groupcache/lru/LICENSE", @@ -650,8 +620,13 @@ "licenseText": "\n Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n" }, { - "name": "github.com/google/go-github/v61/github", - "path": "github.com/google/go-github/v61/github/LICENSE", + "name": "github.com/google/flatbuffers/go", + "path": "github.com/google/flatbuffers/go/LICENSE", + "licenseText": "\n Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n" + }, + { + "name": "github.com/google/go-github/v71/github", + "path": "github.com/google/go-github/v71/github/LICENSE", "licenseText": "Copyright (c) 2013 The go-github AUTHORS. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" }, { @@ -779,11 +754,6 @@ "path": "github.com/kevinburke/ssh_config/LICENSE", "licenseText": "Copyright (c) 2017 Kevin Burke.\n\nPermission is hereby granted, free of charge, to any person\nobtaining a copy of this software and associated documentation\nfiles (the \"Software\"), to deal in the Software without\nrestriction, including without limitation the rights to use,\ncopy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the\nSoftware is furnished to do so, subject to the following\nconditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES\nOF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\nHOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nWHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\nOTHER DEALINGS IN THE SOFTWARE.\n\n===================\n\nThe lexer and parser borrow heavily from github.com/pelletier/go-toml. The\nlicense for that project is copied below.\n\nThe MIT License (MIT)\n\nCopyright (c) 2013 - 2017 Thomas Pelletier, Eric Anderton\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n" }, - { - "name": "github.com/keybase/go-crypto", - "path": "github.com/keybase/go-crypto/LICENSE", - "licenseText": "Copyright (c) 2009 The Go Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" - }, { "name": "github.com/klauspost/compress", "path": "github.com/klauspost/compress/LICENSE", @@ -860,13 +830,18 @@ "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2016 Yasuhiro Matsumoto\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n" }, { - "name": "github.com/meilisearch/meilisearch-go", - "path": "github.com/meilisearch/meilisearch-go/LICENSE", - "licenseText": "MIT License\n\nCopyright (c) 2020-2024 Meili SAS\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n" + "name": "github.com/mattn/go-shellwords", + "path": "github.com/mattn/go-shellwords/LICENSE", + "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2017 Yasuhiro Matsumoto\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n" }, { - "name": "github.com/mholt/acmez/v2", - "path": "github.com/mholt/acmez/v2/LICENSE", + "name": "github.com/meilisearch/meilisearch-go", + "path": "github.com/meilisearch/meilisearch-go/LICENSE", + "licenseText": "MIT License\n\nCopyright (c) 2020-2025 Meili SAS\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n" + }, + { + "name": "github.com/mholt/acmez/v3", + "path": "github.com/mholt/acmez/v3/LICENSE", "licenseText": " Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n" }, { @@ -894,6 +869,11 @@ "path": "github.com/miekg/dns/LICENSE", "licenseText": "BSD 3-Clause License\n\nCopyright (c) 2009, The Go Authors. Extensions copyright (c) 2011, Miek Gieben. \nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n1. Redistributions of source code must retain the above copyright notice, this\n list of conditions and the following disclaimer.\n\n2. Redistributions in binary form must reproduce the above copyright notice,\n this list of conditions and the following disclaimer in the documentation\n and/or other materials provided with the distribution.\n\n3. Neither the name of the copyright holder nor the names of its\n contributors may be used to endorse or promote products derived from\n this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" }, + { + "name": "github.com/minio/crc64nvme", + "path": "github.com/minio/crc64nvme/LICENSE", + "licenseText": "\n Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n" + }, { "name": "github.com/minio/md5-simd", "path": "github.com/minio/md5-simd/LICENSE", @@ -964,11 +944,6 @@ "path": "github.com/opencontainers/image-spec/specs-go/LICENSE", "licenseText": "\n Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n Copyright 2016 The Linux Foundation.\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n" }, - { - "name": "github.com/paulmach/orb", - "path": "github.com/paulmach/orb/LICENSE.md", - "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2017 Paul Mach\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n" - }, { "name": "github.com/pierrec/lz4/v4", "path": "github.com/pierrec/lz4/v4/LICENSE", @@ -977,7 +952,7 @@ { "name": "github.com/pjbgf/sha1cd", "path": "github.com/pjbgf/sha1cd/LICENSE", - "licenseText": " Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n" + "licenseText": " Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright 2023 pjbgf\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n" }, { "name": "github.com/pkg/errors", @@ -1059,21 +1034,11 @@ "path": "github.com/sassoftware/go-rpmutils/LICENSE", "licenseText": "\n Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n" }, - { - "name": "github.com/segmentio/asm", - "path": "github.com/segmentio/asm/LICENSE", - "licenseText": "MIT License\n\nCopyright (c) 2021 Segment\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n" - }, { "name": "github.com/sergi/go-diff/diffmatchpatch", "path": "github.com/sergi/go-diff/diffmatchpatch/LICENSE", "licenseText": "Copyright (c) 2012-2016 The go-diff Authors. All rights reserved.\n\nPermission is hereby granted, free of charge, to any person obtaining a\ncopy of this software and associated documentation files (the \"Software\"),\nto deal in the Software without restriction, including without limitation\nthe rights to use, copy, modify, merge, publish, distribute, sublicense,\nand/or sell copies of the Software, and to permit persons to whom the\nSoftware is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included\nin all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS\nOR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER\nDEALINGS IN THE SOFTWARE.\n\n" }, - { - "name": "github.com/shopspring/decimal", - "path": "github.com/shopspring/decimal/LICENSE", - "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2015 Spring, Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n\n- Based on https://github.com/oguzbilgic/fpd, which has the following license:\n\"\"\"\nThe MIT License (MIT)\n\nCopyright (c) 2013 Oguz Bilgic\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\"\"\"\n" - }, { "name": "github.com/sirupsen/logrus", "path": "github.com/sirupsen/logrus/LICENSE", @@ -1115,35 +1080,40 @@ "licenseText": "Apache License\nVersion 2.0, January 2004\nhttp://www.apache.org/licenses/\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n1. Definitions.\n\n\"License\" shall mean the terms and conditions for use, reproduction, and\ndistribution as defined by Sections 1 through 9 of this document.\n\n\"Licensor\" shall mean the copyright owner or entity authorized by the copyright\nowner that is granting the License.\n\n\"Legal Entity\" shall mean the union of the acting entity and all other entities\nthat control, are controlled by, or are under common control with that entity.\nFor the purposes of this definition, \"control\" means (i) the power, direct or\nindirect, to cause the direction or management of such entity, whether by\ncontract or otherwise, or (ii) ownership of fifty percent (50%) or more of the\noutstanding shares, or (iii) beneficial ownership of such entity.\n\n\"You\" (or \"Your\") shall mean an individual or Legal Entity exercising\npermissions granted by this License.\n\n\"Source\" form shall mean the preferred form for making modifications, including\nbut not limited to software source code, documentation source, and configuration\nfiles.\n\n\"Object\" form shall mean any form resulting from mechanical transformation or\ntranslation of a Source form, including but not limited to compiled object code,\ngenerated documentation, and conversions to other media types.\n\n\"Work\" shall mean the work of authorship, whether in Source or Object form, made\navailable under the License, as indicated by a copyright notice that is included\nin or attached to the work (an example is provided in the Appendix below).\n\n\"Derivative Works\" shall mean any work, whether in Source or Object form, that\nis based on (or derived from) the Work and for which the editorial revisions,\nannotations, elaborations, or other modifications represent, as a whole, an\noriginal work of authorship. For the purposes of this License, Derivative Works\nshall not include works that remain separable from, or merely link (or bind by\nname) to the interfaces of, the Work and Derivative Works thereof.\n\n\"Contribution\" shall mean any work of authorship, including the original version\nof the Work and any modifications or additions to that Work or Derivative Works\nthereof, that is intentionally submitted to Licensor for inclusion in the Work\nby the copyright owner or by an individual or Legal Entity authorized to submit\non behalf of the copyright owner. For the purposes of this definition,\n\"submitted\" means any form of electronic, verbal, or written communication sent\nto the Licensor or its representatives, including but not limited to\ncommunication on electronic mailing lists, source code control systems, and\nissue tracking systems that are managed by, or on behalf of, the Licensor for\nthe purpose of discussing and improving the Work, but excluding communication\nthat is conspicuously marked or otherwise designated in writing by the copyright\nowner as \"Not a Contribution.\"\n\n\"Contributor\" shall mean Licensor and any individual or Legal Entity on behalf\nof whom a Contribution has been received by Licensor and subsequently\nincorporated within the Work.\n\n2. Grant of Copyright License.\n\nSubject to the terms and conditions of this License, each Contributor hereby\ngrants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,\nirrevocable copyright license to reproduce, prepare Derivative Works of,\npublicly display, publicly perform, sublicense, and distribute the Work and such\nDerivative Works in Source or Object form.\n\n3. Grant of Patent License.\n\nSubject to the terms and conditions of this License, each Contributor hereby\ngrants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,\nirrevocable (except as stated in this section) patent license to make, have\nmade, use, offer to sell, sell, import, and otherwise transfer the Work, where\nsuch license applies only to those patent claims licensable by such Contributor\nthat are necessarily infringed by their Contribution(s) alone or by combination\nof their Contribution(s) with the Work to which such Contribution(s) was\nsubmitted. If You institute patent litigation against any entity (including a\ncross-claim or counterclaim in a lawsuit) alleging that the Work or a\nContribution incorporated within the Work constitutes direct or contributory\npatent infringement, then any patent licenses granted to You under this License\nfor that Work shall terminate as of the date such litigation is filed.\n\n4. Redistribution.\n\nYou may reproduce and distribute copies of the Work or Derivative Works thereof\nin any medium, with or without modifications, and in Source or Object form,\nprovided that You meet the following conditions:\n\nYou must give any other recipients of the Work or Derivative Works a copy of\nthis License; and\nYou must cause any modified files to carry prominent notices stating that You\nchanged the files; and\nYou must retain, in the Source form of any Derivative Works that You distribute,\nall copyright, patent, trademark, and attribution notices from the Source form\nof the Work, excluding those notices that do not pertain to any part of the\nDerivative Works; and\nIf the Work includes a \"NOTICE\" text file as part of its distribution, then any\nDerivative Works that You distribute must include a readable copy of the\nattribution notices contained within such NOTICE file, excluding those notices\nthat do not pertain to any part of the Derivative Works, in at least one of the\nfollowing places: within a NOTICE text file distributed as part of the\nDerivative Works; within the Source form or documentation, if provided along\nwith the Derivative Works; or, within a display generated by the Derivative\nWorks, if and wherever such third-party notices normally appear. The contents of\nthe NOTICE file are for informational purposes only and do not modify the\nLicense. You may add Your own attribution notices within Derivative Works that\nYou distribute, alongside or as an addendum to the NOTICE text from the Work,\nprovided that such additional attribution notices cannot be construed as\nmodifying the License.\nYou may add Your own copyright statement to Your modifications and may provide\nadditional or different license terms and conditions for use, reproduction, or\ndistribution of Your modifications, or for any such Derivative Works as a whole,\nprovided Your use, reproduction, and distribution of the Work otherwise complies\nwith the conditions stated in this License.\n\n5. Submission of Contributions.\n\nUnless You explicitly state otherwise, any Contribution intentionally submitted\nfor inclusion in the Work by You to the Licensor shall be under the terms and\nconditions of this License, without any additional terms or conditions.\nNotwithstanding the above, nothing herein shall supersede or modify the terms of\nany separate license agreement you may have executed with Licensor regarding\nsuch Contributions.\n\n6. Trademarks.\n\nThis License does not grant permission to use the trade names, trademarks,\nservice marks, or product names of the Licensor, except as required for\nreasonable and customary use in describing the origin of the Work and\nreproducing the content of the NOTICE file.\n\n7. Disclaimer of Warranty.\n\nUnless required by applicable law or agreed to in writing, Licensor provides the\nWork (and each Contributor provides its Contributions) on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,\nincluding, without limitation, any warranties or conditions of TITLE,\nNON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are\nsolely responsible for determining the appropriateness of using or\nredistributing the Work and assume any risks associated with Your exercise of\npermissions under this License.\n\n8. Limitation of Liability.\n\nIn no event and under no legal theory, whether in tort (including negligence),\ncontract, or otherwise, unless required by applicable law (such as deliberate\nand grossly negligent acts) or agreed to in writing, shall any Contributor be\nliable to You for damages, including any direct, indirect, special, incidental,\nor consequential damages of any character arising as a result of this License or\nout of the use or inability to use the Work (including but not limited to\ndamages for loss of goodwill, work stoppage, computer failure or malfunction, or\nany and all other commercial damages or losses), even if such Contributor has\nbeen advised of the possibility of such damages.\n\n9. Accepting Warranty or Additional Liability.\n\nWhile redistributing the Work or Derivative Works thereof, You may choose to\noffer, and charge a fee for, acceptance of support, warranty, indemnity, or\nother liability obligations and/or rights consistent with this License. However,\nin accepting such obligations, You may act only on Your own behalf and on Your\nsole responsibility, not on behalf of any other Contributor, and only if You\nagree to indemnify, defend, and hold each Contributor harmless for any liability\nincurred by, or claims asserted against, such Contributor by reason of your\naccepting any such warranty or additional liability.\n\nEND OF TERMS AND CONDITIONS\n\nAPPENDIX: How to apply the Apache License to your work\n\nTo apply the Apache License to your work, attach the following boilerplate\nnotice, with the fields enclosed by brackets \"[]\" replaced with your own\nidentifying information. (Don't include the brackets!) The text should be\nenclosed in the appropriate comment syntax for the file format. We also\nrecommend that a file or class name and description of purpose be included on\nthe same \"printed page\" as the copyright notice for easier identification within\nthird-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License." }, { - "name": "github.com/urfave/cli/v2", - "path": "github.com/urfave/cli/v2/LICENSE", - "licenseText": "MIT License\n\nCopyright (c) 2022 urfave/cli maintainers\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n" + "name": "github.com/urfave/cli-docs/v3", + "path": "github.com/urfave/cli-docs/v3/LICENSE", + "licenseText": "MIT License\n\nCopyright (c) 2023 urfave/cli maintainers\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n" + }, + { + "name": "github.com/urfave/cli/v3", + "path": "github.com/urfave/cli/v3/LICENSE", + "licenseText": "MIT License\n\nCopyright (c) 2023 urfave/cli maintainers\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n" }, { "name": "github.com/valyala/fastjson", "path": "github.com/valyala/fastjson/LICENSE", "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2018 Aliaksandr Valialkin\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n" }, + { + "name": "github.com/wneessen/go-mail", + "path": "github.com/wneessen/go-mail/LICENSE", + "licenseText": "MIT License\n\nCopyright (c) 2022-2025 The go-mail Authors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n" + }, + { + "name": "github.com/wneessen/go-mail/smtp", + "path": "github.com/wneessen/go-mail/smtp/LICENSE", + "licenseText": "Copyright (c) 2009 The Go Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." + }, { "name": "github.com/x448/float16", "path": "github.com/x448/float16/LICENSE", "licenseText": "MIT License\n\nCopyright (c) 2019 Montgomery Edwardsâ´â´â¸ and Faye Amacker\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n" }, - { - "name": "github.com/xanzy/go-gitlab", - "path": "github.com/xanzy/go-gitlab/LICENSE", - "licenseText": " Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"{}\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright {yyyy} {name of copyright owner}\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n" - }, { "name": "github.com/xanzy/ssh-agent", "path": "github.com/xanzy/ssh-agent/LICENSE", "licenseText": " Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"{}\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright {yyyy} {name of copyright owner}\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n\n" }, - { - "name": "github.com/xrash/smetrics", - "path": "github.com/xrash/smetrics/LICENSE", - "licenseText": "Copyright (C) 2016 Felipe da Cunha Gonçalves\nAll Rights Reserved.\n\nMIT LICENSE\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n" - }, { "name": "github.com/yohcop/openid-go", "path": "github.com/yohcop/openid-go/LICENSE", @@ -1169,21 +1139,16 @@ "path": "github.com/zeebo/blake3/LICENSE", "licenseText": "This work is released into the public domain with CC0 1.0.\n\n-------------------------------------------------------------------------------\n\nCreative Commons Legal Code\n\nCC0 1.0 Universal\n\n CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE\n LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN\n ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS\n INFORMATION ON AN \"AS-IS\" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES\n REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS\n PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM\n THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED\n HEREUNDER.\n\nStatement of Purpose\n\nThe laws of most jurisdictions throughout the world automatically confer\nexclusive Copyright and Related Rights (defined below) upon the creator\nand subsequent owner(s) (each and all, an \"owner\") of an original work of\nauthorship and/or a database (each, a \"Work\").\n\nCertain owners wish to permanently relinquish those rights to a Work for\nthe purpose of contributing to a commons of creative, cultural and\nscientific works (\"Commons\") that the public can reliably and without fear\nof later claims of infringement build upon, modify, incorporate in other\nworks, reuse and redistribute as freely as possible in any form whatsoever\nand for any purposes, including without limitation commercial purposes.\nThese owners may contribute to the Commons to promote the ideal of a free\nculture and the further production of creative, cultural and scientific\nworks, or to gain reputation or greater distribution for their Work in\npart through the use and efforts of others.\n\nFor these and/or other purposes and motivations, and without any\nexpectation of additional consideration or compensation, the person\nassociating CC0 with a Work (the \"Affirmer\"), to the extent that he or she\nis an owner of Copyright and Related Rights in the Work, voluntarily\nelects to apply CC0 to the Work and publicly distribute the Work under its\nterms, with knowledge of his or her Copyright and Related Rights in the\nWork and the meaning and intended legal effect of CC0 on those rights.\n\n1. Copyright and Related Rights. A Work made available under CC0 may be\nprotected by copyright and related or neighboring rights (\"Copyright and\nRelated Rights\"). Copyright and Related Rights include, but are not\nlimited to, the following:\n\n i. the right to reproduce, adapt, distribute, perform, display,\n communicate, and translate a Work;\n ii. moral rights retained by the original author(s) and/or performer(s);\niii. publicity and privacy rights pertaining to a person's image or\n likeness depicted in a Work;\n iv. rights protecting against unfair competition in regards to a Work,\n subject to the limitations in paragraph 4(a), below;\n v. rights protecting the extraction, dissemination, use and reuse of data\n in a Work;\n vi. database rights (such as those arising under Directive 96/9/EC of the\n European Parliament and of the Council of 11 March 1996 on the legal\n protection of databases, and under any national implementation\n thereof, including any amended or successor version of such\n directive); and\nvii. other similar, equivalent or corresponding rights throughout the\n world based on applicable law or treaty, and any national\n implementations thereof.\n\n2. Waiver. To the greatest extent permitted by, but not in contravention\nof, applicable law, Affirmer hereby overtly, fully, permanently,\nirrevocably and unconditionally waives, abandons, and surrenders all of\nAffirmer's Copyright and Related Rights and associated claims and causes\nof action, whether now known or unknown (including existing as well as\nfuture claims and causes of action), in the Work (i) in all territories\nworldwide, (ii) for the maximum duration provided by applicable law or\ntreaty (including future time extensions), (iii) in any current or future\nmedium and for any number of copies, and (iv) for any purpose whatsoever,\nincluding without limitation commercial, advertising or promotional\npurposes (the \"Waiver\"). Affirmer makes the Waiver for the benefit of each\nmember of the public at large and to the detriment of Affirmer's heirs and\nsuccessors, fully intending that such Waiver shall not be subject to\nrevocation, rescission, cancellation, termination, or any other legal or\nequitable action to disrupt the quiet enjoyment of the Work by the public\nas contemplated by Affirmer's express Statement of Purpose.\n\n3. Public License Fallback. Should any part of the Waiver for any reason\nbe judged legally invalid or ineffective under applicable law, then the\nWaiver shall be preserved to the maximum extent permitted taking into\naccount Affirmer's express Statement of Purpose. In addition, to the\nextent the Waiver is so judged Affirmer hereby grants to each affected\nperson a royalty-free, non transferable, non sublicensable, non exclusive,\nirrevocable and unconditional license to exercise Affirmer's Copyright and\nRelated Rights in the Work (i) in all territories worldwide, (ii) for the\nmaximum duration provided by applicable law or treaty (including future\ntime extensions), (iii) in any current or future medium and for any number\nof copies, and (iv) for any purpose whatsoever, including without\nlimitation commercial, advertising or promotional purposes (the\n\"License\"). The License shall be deemed effective as of the date CC0 was\napplied by Affirmer to the Work. Should any part of the License for any\nreason be judged legally invalid or ineffective under applicable law, such\npartial invalidity or ineffectiveness shall not invalidate the remainder\nof the License, and in such case Affirmer hereby affirms that he or she\nwill not (i) exercise any of his or her remaining Copyright and Related\nRights in the Work or (ii) assert any associated claims and causes of\naction with respect to the Work, in either case contrary to Affirmer's\nexpress Statement of Purpose.\n\n4. Limitations and Disclaimers.\n\n a. No trademark or patent rights held by Affirmer are waived, abandoned,\n surrendered, licensed or otherwise affected by this document.\n b. Affirmer offers the Work as-is and makes no representations or\n warranties of any kind concerning the Work, express, implied,\n statutory or otherwise, including without limitation warranties of\n title, merchantability, fitness for a particular purpose, non\n infringement, or the absence of latent or other defects, accuracy, or\n the present or absence of errors, whether or not discoverable, all to\n the greatest extent permissible under applicable law.\n c. Affirmer disclaims responsibility for clearing rights of other persons\n that may apply to the Work or any use thereof, including without\n limitation any person's Copyright and Related Rights in the Work.\n Further, Affirmer disclaims responsibility for obtaining any necessary\n consents, permissions or other rights required for any use of the\n Work.\n d. Affirmer understands and acknowledges that Creative Commons is not a\n party to this document and has no duty or obligation with respect to\n this CC0 or use of the Work.\n" }, + { + "name": "gitlab.com/gitlab-org/api/client-go", + "path": "gitlab.com/gitlab-org/api/client-go/LICENSE", + "licenseText": " Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"{}\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright {yyyy} {name of copyright owner}\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n" + }, { "name": "go.etcd.io/bbolt", "path": "go.etcd.io/bbolt/LICENSE", "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2013 Ben Johnson\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n" }, - { - "name": "go.opentelemetry.io/otel", - "path": "go.opentelemetry.io/otel/LICENSE", - "licenseText": " Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n" - }, - { - "name": "go.opentelemetry.io/otel/trace", - "path": "go.opentelemetry.io/otel/trace/LICENSE", - "licenseText": " Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n" - }, { "name": "go.uber.org/atomic", "path": "go.uber.org/atomic/LICENSE.txt", @@ -1199,6 +1164,11 @@ "path": "go.uber.org/zap/LICENSE", "licenseText": "Copyright (c) 2016-2017 Uber Technologies, Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n" }, + { + "name": "go.uber.org/zap/exp/zapslog", + "path": "go.uber.org/zap/exp/zapslog/LICENSE", + "licenseText": "Copyright (c) 2016-2024 Uber Technologies, Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n" + }, { "name": "golang.org/x/crypto", "path": "golang.org/x/crypto/LICENSE", @@ -1259,11 +1229,6 @@ "path": "google.golang.org/protobuf/LICENSE", "licenseText": "Copyright (c) 2018 The Go Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" }, - { - "name": "gopkg.in/gomail.v2", - "path": "gopkg.in/gomail.v2/LICENSE", - "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2014 Alexandre Cesaro\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n" - }, { "name": "gopkg.in/ini.v1", "path": "gopkg.in/ini.v1/LICENSE", diff --git a/build.go b/build.go index 234579b5149..e81ba54690b 100644 --- a/build.go +++ b/build.go @@ -5,19 +5,10 @@ package main -// Libraries that are included to vendor utilities used during build. +// Libraries that are included to vendor utilities used during Makefile build. // These libraries will not be included in a normal compilation. import ( - // for embed - _ "github.com/shurcooL/vfsgen" - - // for cover merge - _ "golang.org/x/tools/cover" - // for vet _ "code.gitea.io/gitea-vet" - - // for swagger - _ "github.com/go-swagger/go-swagger/cmd/swagger" ) diff --git a/build/generate-bindata.go b/build/generate-bindata.go index 2fcb7c2f2a0..2553770762f 100644 --- a/build/generate-bindata.go +++ b/build/generate-bindata.go @@ -6,87 +6,22 @@ package main import ( - "bytes" - "crypto/sha1" "fmt" - "log" - "net/http" "os" - "path/filepath" - "strconv" - "github.com/shurcooL/vfsgen" + "code.gitea.io/gitea/modules/assetfs" ) -func needsUpdate(dir, filename string) (bool, []byte) { - needRegen := false - _, err := os.Stat(filename) - if err != nil { - needRegen = true - } - - oldHash, err := os.ReadFile(filename + ".hash") - if err != nil { - oldHash = []byte{} - } - - hasher := sha1.New() - - err = filepath.WalkDir(dir, func(path string, d os.DirEntry, err error) error { - if err != nil { - return err - } - info, err := d.Info() - if err != nil { - return err - } - _, _ = hasher.Write([]byte(d.Name())) - _, _ = hasher.Write([]byte(info.ModTime().String())) - _, _ = hasher.Write([]byte(strconv.FormatInt(info.Size(), 16))) - return nil - }) - if err != nil { - return true, oldHash - } - - newHash := hasher.Sum([]byte{}) - - if bytes.Compare(oldHash, newHash) != 0 { - return true, newHash - } - - return needRegen, newHash -} - func main() { - if len(os.Args) < 4 { - log.Fatal("Insufficient number of arguments. Need: directory packageName filename") + if len(os.Args) != 3 { + fmt.Println("usage: ./generate-bindata {local-directory} {bindata-filename}") + os.Exit(1) } - dir, packageName, filename := os.Args[1], os.Args[2], os.Args[3] - var useGlobalModTime bool - if len(os.Args) == 5 { - useGlobalModTime, _ = strconv.ParseBool(os.Args[4]) + dir, filename := os.Args[1], os.Args[2] + fmt.Printf("generating bindata for %s to %s\n", dir, filename) + if err := assetfs.GenerateEmbedBindata(dir, filename); err != nil { + fmt.Printf("failed: %s\n", err.Error()) + os.Exit(1) } - - update, newHash := needsUpdate(dir, filename) - - if !update { - fmt.Printf("bindata for %s already up-to-date\n", packageName) - return - } - - fmt.Printf("generating bindata for %s\n", packageName) - var fsTemplates http.FileSystem = http.Dir(dir) - err := vfsgen.Generate(fsTemplates, vfsgen.Options{ - PackageName: packageName, - BuildTags: "bindata", - VariableName: "Assets", - Filename: filename, - UseGlobalModTime: useGlobalModTime, - }) - if err != nil { - log.Fatalf("%v\n", err) - } - _ = os.WriteFile(filename+".hash", newHash, 0o666) } diff --git a/build/generate-licenses.go b/build/generate-licenses.go deleted file mode 100644 index 66e1d377551..00000000000 --- a/build/generate-licenses.go +++ /dev/null @@ -1,176 +0,0 @@ -// Copyright 2017 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -//go:build ignore - -package main - -import ( - "archive/tar" - "compress/gzip" - "crypto/md5" - "encoding/hex" - "flag" - "fmt" - "io" - "log" - "net/http" - "os" - "path" - "path/filepath" - "strings" - - "code.gitea.io/gitea/build/license" - "code.gitea.io/gitea/modules/json" - "code.gitea.io/gitea/modules/util" -) - -func main() { - var ( - prefix = "gitea-licenses" - url = "https://api.github.com/repos/spdx/license-list-data/tarball" - githubApiToken = "" - githubUsername = "" - destination = "" - ) - - flag.StringVar(&destination, "dest", "options/license/", "destination for the licenses") - flag.StringVar(&githubUsername, "username", "", "github username") - flag.StringVar(&githubApiToken, "token", "", "github api token") - flag.Parse() - - file, err := os.CreateTemp(os.TempDir(), prefix) - if err != nil { - log.Fatalf("Failed to create temp file. %s", err) - } - - defer util.Remove(file.Name()) - - if err := os.RemoveAll(destination); err != nil { - log.Fatalf("Cannot clean destination folder: %v", err) - } - - if err := os.MkdirAll(destination, 0o755); err != nil { - log.Fatalf("Cannot create destination: %v", err) - } - - req, err := http.NewRequest("GET", url, nil) - if err != nil { - log.Fatalf("Failed to download archive. %s", err) - } - - if len(githubApiToken) > 0 && len(githubUsername) > 0 { - req.SetBasicAuth(githubUsername, githubApiToken) - } - - resp, err := http.DefaultClient.Do(req) - if err != nil { - log.Fatalf("Failed to download archive. %s", err) - } - - defer resp.Body.Close() - - if _, err := io.Copy(file, resp.Body); err != nil { - log.Fatalf("Failed to copy archive to file. %s", err) - } - - if _, err := file.Seek(0, 0); err != nil { - log.Fatalf("Failed to reset seek on archive. %s", err) - } - - gz, err := gzip.NewReader(file) - if err != nil { - log.Fatalf("Failed to gunzip the archive. %s", err) - } - - tr := tar.NewReader(gz) - aliasesFiles := make(map[string][]string) - for { - hdr, err := tr.Next() - - if err == io.EOF { - break - } - - if err != nil { - log.Fatalf("Failed to iterate archive. %s", err) - } - - if !strings.Contains(hdr.Name, "/text/") { - continue - } - - if filepath.Ext(hdr.Name) != ".txt" { - continue - } - - fileBaseName := filepath.Base(hdr.Name) - licenseName := strings.TrimSuffix(fileBaseName, ".txt") - - if strings.HasPrefix(fileBaseName, "README") { - continue - } - - if strings.HasPrefix(fileBaseName, "deprecated_") { - continue - } - out, err := os.Create(path.Join(destination, licenseName)) - if err != nil { - log.Fatalf("Failed to create new file. %s", err) - } - - defer out.Close() - - // some license files have same content, so we need to detect these files and create a convert map into a json file - // Later we use this convert map to avoid adding same license content with different license name - h := md5.New() - // calculate md5 and write file in the same time - r := io.TeeReader(tr, h) - if _, err := io.Copy(out, r); err != nil { - log.Fatalf("Failed to write new file. %s", err) - } else { - fmt.Printf("Written %s\n", out.Name()) - - md5 := hex.EncodeToString(h.Sum(nil)) - aliasesFiles[md5] = append(aliasesFiles[md5], licenseName) - } - } - - // generate convert license name map - licenseAliases := make(map[string]string) - for _, fileNames := range aliasesFiles { - if len(fileNames) > 1 { - licenseName := license.GetLicenseNameFromAliases(fileNames) - if licenseName == "" { - // license name should not be empty as expected - // if it is empty, we need to rewrite the logic of GetLicenseNameFromAliases - log.Fatalf("GetLicenseNameFromAliases: license name is empty") - } - for _, fileName := range fileNames { - licenseAliases[fileName] = licenseName - } - } - } - // save convert license name map to file - b, err := json.Marshal(licenseAliases) - if err != nil { - log.Fatalf("Failed to create json bytes. %s", err) - } - - licenseAliasesDestination := filepath.Join(destination, "etc", "license-aliases.json") - if err := os.MkdirAll(filepath.Dir(licenseAliasesDestination), 0o755); err != nil { - log.Fatalf("Failed to create directory for license aliases json file. %s", err) - } - - f, err := os.Create(licenseAliasesDestination) - if err != nil { - log.Fatalf("Failed to create license aliases json file. %s", err) - } - defer f.Close() - - if _, err = f.Write(b); err != nil { - log.Fatalf("Failed to write license aliases json file. %s", err) - } - - fmt.Println("Done") -} diff --git a/build/license/aliasgenerator.go b/build/license/aliasgenerator.go deleted file mode 100644 index 7de1e6fbd6b..00000000000 --- a/build/license/aliasgenerator.go +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright 2024 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package license - -import "strings" - -func GetLicenseNameFromAliases(fnl []string) string { - if len(fnl) == 0 { - return "" - } - - shortestItem := func(list []string) string { - s := list[0] - for _, l := range list[1:] { - if len(l) < len(s) { - s = l - } - } - return s - } - allHasPrefix := func(list []string, s string) bool { - for _, l := range list { - if !strings.HasPrefix(l, s) { - return false - } - } - return true - } - - sl := shortestItem(fnl) - slv := strings.Split(sl, "-") - var result string - for i := len(slv); i >= 0; i-- { - result = strings.Join(slv[:i], "-") - if allHasPrefix(fnl, result) { - return result - } - } - return "" -} diff --git a/build/license/aliasgenerator_test.go b/build/license/aliasgenerator_test.go deleted file mode 100644 index 239181b7365..00000000000 --- a/build/license/aliasgenerator_test.go +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright 2024 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package license - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestGetLicenseNameFromAliases(t *testing.T) { - tests := []struct { - target string - inputs []string - }{ - { - // real case which you can find in license-aliases.json - target: "AGPL-1.0", - inputs: []string{ - "AGPL-1.0-only", - "AGPL-1.0-or-late", - }, - }, - { - target: "", - inputs: []string{ - "APSL-1.0", - "AGPL-1.0-only", - "AGPL-1.0-or-late", - }, - }, - } - - for _, tt := range tests { - result := GetLicenseNameFromAliases(tt.inputs) - assert.Equal(t, result, tt.target) - } -} diff --git a/cmd/actions.go b/cmd/actions.go index f582c16c81c..2c51c6a1bcc 100644 --- a/cmd/actions.go +++ b/cmd/actions.go @@ -4,12 +4,13 @@ package cmd import ( + "context" "fmt" "code.gitea.io/gitea/modules/private" "code.gitea.io/gitea/modules/setting" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) var ( @@ -17,7 +18,7 @@ var ( CmdActions = &cli.Command{ Name: "actions", Usage: "Manage Gitea Actions", - Subcommands: []*cli.Command{ + Commands: []*cli.Command{ subcmdActionsGenRunnerToken, }, } @@ -38,10 +39,7 @@ var ( } ) -func runGenerateActionsRunnerToken(c *cli.Context) error { - ctx, cancel := installSignals() - defer cancel() - +func runGenerateActionsRunnerToken(ctx context.Context, c *cli.Command) error { setting.MustInstalled() scope := c.String("scope") diff --git a/cmd/admin.go b/cmd/admin.go index 6c9480e76eb..5c58a40ca27 100644 --- a/cmd/admin.go +++ b/cmd/admin.go @@ -15,7 +15,7 @@ import ( "code.gitea.io/gitea/modules/log" repo_module "code.gitea.io/gitea/modules/repository" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) var ( @@ -23,7 +23,7 @@ var ( CmdAdmin = &cli.Command{ Name: "admin", Usage: "Perform common administrative operations", - Subcommands: []*cli.Command{ + Commands: []*cli.Command{ subcmdUser, subcmdRepoSyncReleases, subcmdRegenerate, @@ -41,7 +41,7 @@ var ( subcmdRegenerate = &cli.Command{ Name: "regenerate", Usage: "Regenerate specific files", - Subcommands: []*cli.Command{ + Commands: []*cli.Command{ microcmdRegenHooks, microcmdRegenKeys, }, @@ -50,15 +50,15 @@ var ( subcmdAuth = &cli.Command{ Name: "auth", Usage: "Modify external auth providers", - Subcommands: []*cli.Command{ - microcmdAuthAddOauth, - microcmdAuthUpdateOauth, - microcmdAuthAddLdapBindDn, - microcmdAuthUpdateLdapBindDn, - microcmdAuthAddLdapSimpleAuth, - microcmdAuthUpdateLdapSimpleAuth, - microcmdAuthAddSMTP, - microcmdAuthUpdateSMTP, + Commands: []*cli.Command{ + microcmdAuthAddOauth(), + microcmdAuthUpdateOauth(), + microcmdAuthAddLdapBindDn(), + microcmdAuthUpdateLdapBindDn(), + microcmdAuthAddLdapSimpleAuth(), + microcmdAuthUpdateLdapSimpleAuth(), + microcmdAuthAddSMTP(), + microcmdAuthUpdateSMTP(), microcmdAuthList, microcmdAuthDelete, }, @@ -70,9 +70,9 @@ var ( Action: runSendMail, Flags: []cli.Flag{ &cli.StringFlag{ - Name: "title", - Usage: `a title of a message`, - Value: "", + Name: "title", + Usage: "a title of a message", + Required: true, }, &cli.StringFlag{ Name: "content", @@ -86,28 +86,27 @@ var ( }, }, } +) - idFlag = &cli.Int64Flag{ +func idFlag() *cli.Int64Flag { + return &cli.Int64Flag{ Name: "id", Usage: "ID of authentication source", } -) - -func runRepoSyncReleases(_ *cli.Context) error { - ctx, cancel := installSignals() - defer cancel() +} +func runRepoSyncReleases(ctx context.Context, _ *cli.Command) error { if err := initDB(ctx); err != nil { return err } - if err := git.InitSimple(ctx); err != nil { + if err := git.InitSimple(); err != nil { return err } log.Trace("Synchronizing repository releases (this may take a while)") for page := 1; ; page++ { - repos, count, err := repo_model.SearchRepositoryByName(ctx, &repo_model.SearchRepoOptions{ + repos, count, err := repo_model.SearchRepositoryByName(ctx, repo_model.SearchRepoOptions{ ListOptions: db.ListOptions{ PageSize: repo_model.RepositoryListDefaultPageSize, Page: page, diff --git a/cmd/admin_auth.go b/cmd/admin_auth.go index 4777a929086..1a093667229 100644 --- a/cmd/admin_auth.go +++ b/cmd/admin_auth.go @@ -4,6 +4,7 @@ package cmd import ( + "context" "errors" "fmt" "os" @@ -13,14 +14,14 @@ import ( "code.gitea.io/gitea/models/db" auth_service "code.gitea.io/gitea/services/auth" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) var ( microcmdAuthDelete = &cli.Command{ Name: "delete", Usage: "Delete specific auth source", - Flags: []cli.Flag{idFlag}, + Flags: []cli.Flag{idFlag()}, Action: runDeleteAuth, } microcmdAuthList = &cli.Command{ @@ -56,10 +57,7 @@ var ( } ) -func runListAuth(c *cli.Context) error { - ctx, cancel := installSignals() - defer cancel() - +func runListAuth(ctx context.Context, c *cli.Command) error { if err := initDB(ctx); err != nil { return err } @@ -90,14 +88,11 @@ func runListAuth(c *cli.Context) error { return nil } -func runDeleteAuth(c *cli.Context) error { +func runDeleteAuth(ctx context.Context, c *cli.Command) error { if !c.IsSet("id") { return errors.New("--id flag is missing") } - ctx, cancel := installSignals() - defer cancel() - if err := initDB(ctx); err != nil { return err } diff --git a/cmd/admin_auth_ldap.go b/cmd/admin_auth_ldap.go index aff2a128554..069ad6600c7 100644 --- a/cmd/admin_auth_ldap.go +++ b/cmd/admin_auth_ldap.go @@ -9,9 +9,10 @@ import ( "strings" "code.gitea.io/gitea/models/auth" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/services/auth/source/ldap" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) type ( @@ -23,8 +24,8 @@ type ( } ) -var ( - commonLdapCLIFlags = []cli.Flag{ +func commonLdapCLIFlags() []cli.Flag { + return []cli.Flag{ &cli.StringFlag{ Name: "name", Usage: "Authentication name.", @@ -102,8 +103,10 @@ var ( Usage: "The attribute of the user’s LDAP record containing the user’s avatar.", }, } +} - ldapBindDnCLIFlags = append(commonLdapCLIFlags, +func ldapBindDnCLIFlags() []cli.Flag { + return append(commonLdapCLIFlags(), &cli.StringFlag{ Name: "bind-dn", Usage: "The DN to bind to the LDAP server with when searching for the user.", @@ -127,50 +130,88 @@ var ( &cli.UintFlag{ Name: "page-size", Usage: "Search page size.", + }, + &cli.BoolFlag{ + Name: "enable-groups", + Usage: "Enable LDAP groups", + }, + &cli.StringFlag{ + Name: "group-search-base-dn", + Usage: "The LDAP base DN at which group accounts will be searched for", + }, + &cli.StringFlag{ + Name: "group-member-attribute", + Usage: "Group attribute containing list of users", + }, + &cli.StringFlag{ + Name: "group-user-attribute", + Usage: "User attribute listed in group", + }, + &cli.StringFlag{ + Name: "group-filter", + Usage: "Verify group membership in LDAP", + }, + &cli.StringFlag{ + Name: "group-team-map", + Usage: "Map LDAP groups to Organization teams", + }, + &cli.BoolFlag{ + Name: "group-team-map-removal", + Usage: "Remove users from synchronized teams if user does not belong to corresponding LDAP group", }) +} - ldapSimpleAuthCLIFlags = append(commonLdapCLIFlags, +func ldapSimpleAuthCLIFlags() []cli.Flag { + return append(commonLdapCLIFlags(), &cli.StringFlag{ Name: "user-dn", Usage: "The user's DN.", }) +} - microcmdAuthAddLdapBindDn = &cli.Command{ +func microcmdAuthAddLdapBindDn() *cli.Command { + return &cli.Command{ Name: "add-ldap", Usage: "Add new LDAP (via Bind DN) authentication source", - Action: func(c *cli.Context) error { - return newAuthService().addLdapBindDn(c) + Action: func(ctx context.Context, cmd *cli.Command) error { + return newAuthService().addLdapBindDn(ctx, cmd) }, - Flags: ldapBindDnCLIFlags, + Flags: ldapBindDnCLIFlags(), } +} - microcmdAuthUpdateLdapBindDn = &cli.Command{ +func microcmdAuthUpdateLdapBindDn() *cli.Command { + return &cli.Command{ Name: "update-ldap", Usage: "Update existing LDAP (via Bind DN) authentication source", - Action: func(c *cli.Context) error { - return newAuthService().updateLdapBindDn(c) + Action: func(ctx context.Context, cmd *cli.Command) error { + return newAuthService().updateLdapBindDn(ctx, cmd) }, - Flags: append([]cli.Flag{idFlag}, ldapBindDnCLIFlags...), + Flags: append([]cli.Flag{idFlag()}, ldapBindDnCLIFlags()...), } +} - microcmdAuthAddLdapSimpleAuth = &cli.Command{ +func microcmdAuthAddLdapSimpleAuth() *cli.Command { + return &cli.Command{ Name: "add-ldap-simple", Usage: "Add new LDAP (simple auth) authentication source", - Action: func(c *cli.Context) error { - return newAuthService().addLdapSimpleAuth(c) + Action: func(ctx context.Context, cmd *cli.Command) error { + return newAuthService().addLdapSimpleAuth(ctx, cmd) }, - Flags: ldapSimpleAuthCLIFlags, + Flags: ldapSimpleAuthCLIFlags(), } +} - microcmdAuthUpdateLdapSimpleAuth = &cli.Command{ +func microcmdAuthUpdateLdapSimpleAuth() *cli.Command { + return &cli.Command{ Name: "update-ldap-simple", Usage: "Update existing LDAP (simple auth) authentication source", - Action: func(c *cli.Context) error { - return newAuthService().updateLdapSimpleAuth(c) + Action: func(ctx context.Context, cmd *cli.Command) error { + return newAuthService().updateLdapSimpleAuth(ctx, cmd) }, - Flags: append([]cli.Flag{idFlag}, ldapSimpleAuthCLIFlags...), + Flags: append([]cli.Flag{idFlag()}, ldapSimpleAuthCLIFlags()...), } -) +} // newAuthService creates a service with default functions. func newAuthService() *authService { @@ -182,8 +223,8 @@ func newAuthService() *authService { } } -// parseAuthSource assigns values on authSource according to command line flags. -func parseAuthSource(c *cli.Context, authSource *auth.Source) { +// parseAuthSourceLdap assigns values on authSource according to command line flags. +func parseAuthSourceLdap(c *cli.Command, authSource *auth.Source) { if c.IsSet("name") { authSource.Name = c.String("name") } @@ -199,10 +240,11 @@ func parseAuthSource(c *cli.Context, authSource *auth.Source) { if c.IsSet("disable-synchronize-users") { authSource.IsSyncEnabled = !c.Bool("disable-synchronize-users") } + authSource.TwoFactorPolicy = util.Iif(c.Bool("skip-local-2fa"), "skip", "") } // parseLdapConfig assigns values on config according to command line flags. -func parseLdapConfig(c *cli.Context, config *ldap.Source) error { +func parseLdapConfig(c *cli.Command, config *ldap.Source) error { if c.IsSet("name") { config.Name = c.String("name") } @@ -215,7 +257,7 @@ func parseLdapConfig(c *cli.Context, config *ldap.Source) error { if c.IsSet("security-protocol") { p, ok := findLdapSecurityProtocolByName(c.String("security-protocol")) if !ok { - return fmt.Errorf("Unknown security protocol name: %s", c.String("security-protocol")) + return fmt.Errorf("unknown security protocol name: %s", c.String("security-protocol")) } config.SecurityProtocol = p } @@ -270,8 +312,26 @@ func parseLdapConfig(c *cli.Context, config *ldap.Source) error { if c.IsSet("allow-deactivate-all") { config.AllowDeactivateAll = c.Bool("allow-deactivate-all") } - if c.IsSet("skip-local-2fa") { - config.SkipLocalTwoFA = c.Bool("skip-local-2fa") + if c.IsSet("enable-groups") { + config.GroupsEnabled = c.Bool("enable-groups") + } + if c.IsSet("group-search-base-dn") { + config.GroupDN = c.String("group-search-base-dn") + } + if c.IsSet("group-member-attribute") { + config.GroupMemberUID = c.String("group-member-attribute") + } + if c.IsSet("group-user-attribute") { + config.UserUID = c.String("group-user-attribute") + } + if c.IsSet("group-filter") { + config.GroupFilter = c.String("group-filter") + } + if c.IsSet("group-team-map") { + config.GroupTeamMap = c.String("group-team-map") + } + if c.IsSet("group-team-map-removal") { + config.GroupTeamMapRemoval = c.Bool("group-team-map-removal") } return nil } @@ -289,32 +349,27 @@ func findLdapSecurityProtocolByName(name string) (ldap.SecurityProtocol, bool) { // getAuthSource gets the login source by its id defined in the command line flags. // It returns an error if the id is not set, does not match any source or if the source is not of expected type. -func (a *authService) getAuthSource(ctx context.Context, c *cli.Context, authType auth.Type) (*auth.Source, error) { +func (a *authService) getAuthSource(ctx context.Context, c *cli.Command, authType auth.Type) (*auth.Source, error) { if err := argsSet(c, "id"); err != nil { return nil, err } - authSource, err := a.getAuthSourceByID(ctx, c.Int64("id")) if err != nil { return nil, err } if authSource.Type != authType { - return nil, fmt.Errorf("Invalid authentication type. expected: %s, actual: %s", authType.String(), authSource.Type.String()) + return nil, fmt.Errorf("invalid authentication type. expected: %s, actual: %s", authType.String(), authSource.Type.String()) } return authSource, nil } // addLdapBindDn adds a new LDAP via Bind DN authentication source. -func (a *authService) addLdapBindDn(c *cli.Context) error { +func (a *authService) addLdapBindDn(ctx context.Context, c *cli.Command) error { if err := argsSet(c, "name", "security-protocol", "host", "port", "user-search-base", "user-filter", "email-attribute"); err != nil { return err } - - ctx, cancel := installSignals() - defer cancel() - if err := a.initDB(ctx); err != nil { return err } @@ -327,7 +382,7 @@ func (a *authService) addLdapBindDn(c *cli.Context) error { }, } - parseAuthSource(c, authSource) + parseAuthSourceLdap(c, authSource) if err := parseLdapConfig(c, authSource.Cfg.(*ldap.Source)); err != nil { return err } @@ -336,10 +391,7 @@ func (a *authService) addLdapBindDn(c *cli.Context) error { } // updateLdapBindDn updates a new LDAP via Bind DN authentication source. -func (a *authService) updateLdapBindDn(c *cli.Context) error { - ctx, cancel := installSignals() - defer cancel() - +func (a *authService) updateLdapBindDn(ctx context.Context, c *cli.Command) error { if err := a.initDB(ctx); err != nil { return err } @@ -349,7 +401,7 @@ func (a *authService) updateLdapBindDn(c *cli.Context) error { return err } - parseAuthSource(c, authSource) + parseAuthSourceLdap(c, authSource) if err := parseLdapConfig(c, authSource.Cfg.(*ldap.Source)); err != nil { return err } @@ -358,14 +410,11 @@ func (a *authService) updateLdapBindDn(c *cli.Context) error { } // addLdapSimpleAuth adds a new LDAP (simple auth) authentication source. -func (a *authService) addLdapSimpleAuth(c *cli.Context) error { +func (a *authService) addLdapSimpleAuth(ctx context.Context, c *cli.Command) error { if err := argsSet(c, "name", "security-protocol", "host", "port", "user-dn", "user-filter", "email-attribute"); err != nil { return err } - ctx, cancel := installSignals() - defer cancel() - if err := a.initDB(ctx); err != nil { return err } @@ -378,7 +427,7 @@ func (a *authService) addLdapSimpleAuth(c *cli.Context) error { }, } - parseAuthSource(c, authSource) + parseAuthSourceLdap(c, authSource) if err := parseLdapConfig(c, authSource.Cfg.(*ldap.Source)); err != nil { return err } @@ -387,10 +436,7 @@ func (a *authService) addLdapSimpleAuth(c *cli.Context) error { } // updateLdapSimpleAuth updates a new LDAP (simple auth) authentication source. -func (a *authService) updateLdapSimpleAuth(c *cli.Context) error { - ctx, cancel := installSignals() - defer cancel() - +func (a *authService) updateLdapSimpleAuth(ctx context.Context, c *cli.Command) error { if err := a.initDB(ctx); err != nil { return err } @@ -400,7 +446,7 @@ func (a *authService) updateLdapSimpleAuth(c *cli.Context) error { return err } - parseAuthSource(c, authSource) + parseAuthSourceLdap(c, authSource) if err := parseLdapConfig(c, authSource.Cfg.(*ldap.Source)); err != nil { return err } diff --git a/cmd/admin_auth_ldap_test.go b/cmd/admin_auth_ldap_test.go index 7791f3a9cc1..2da7ebc573b 100644 --- a/cmd/admin_auth_ldap_test.go +++ b/cmd/admin_auth_ldap_test.go @@ -8,17 +8,16 @@ import ( "testing" "code.gitea.io/gitea/models/auth" + "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/services/auth/source/ldap" "github.com/stretchr/testify/assert" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) func TestAddLdapBindDn(t *testing.T) { // Mock cli functions to do not exit on error - osExiter := cli.OsExiter - defer func() { cli.OsExiter = osExiter }() - cli.OsExiter = func(code int) {} + defer test.MockVariableValue(&cli.OsExiter, func(code int) {})() // Test cases cases := []struct { @@ -51,6 +50,13 @@ func TestAddLdapBindDn(t *testing.T) { "--attributes-in-bind", "--synchronize-users", "--page-size", "99", + "--enable-groups", + "--group-search-base-dn", "ou=group,dc=full-domain-bind,dc=org", + "--group-member-attribute", "memberUid", + "--group-user-attribute", "uid", + "--group-filter", "(|(cn=gitea_users)(cn=admins))", + "--group-team-map", `{"cn=my-group,cn=groups,dc=example,dc=org": {"MyGiteaOrganization": ["MyGiteaTeam1", "MyGiteaTeam2"]}}`, + "--group-team-map-removal", }, source: &auth.Source{ Type: auth.LDAP, @@ -78,6 +84,13 @@ func TestAddLdapBindDn(t *testing.T) { AdminFilter: "(memberOf=cn=admin-group,ou=example,dc=full-domain-bind,dc=org)", RestrictedFilter: "(memberOf=cn=restricted-group,ou=example,dc=full-domain-bind,dc=org)", Enabled: true, + GroupsEnabled: true, + GroupDN: "ou=group,dc=full-domain-bind,dc=org", + GroupMemberUID: "memberUid", + UserUID: "uid", + GroupFilter: "(|(cn=gitea_users)(cn=admins))", + GroupTeamMap: `{"cn=my-group,cn=groups,dc=example,dc=org": {"MyGiteaOrganization": ["MyGiteaTeam1", "MyGiteaTeam2"]}}`, + GroupTeamMapRemoval: true, }, }, }, @@ -121,7 +134,7 @@ func TestAddLdapBindDn(t *testing.T) { "--user-filter", "(memberOf=cn=user-group,ou=example,dc=domain,dc=org)", "--email-attribute", "mail", }, - errMsg: "Unknown security protocol name: zzzzz", + errMsg: "unknown security protocol name: zzzzz", }, // case 3 { @@ -215,22 +228,23 @@ func TestAddLdapBindDn(t *testing.T) { return nil }, updateAuthSource: func(ctx context.Context, authSource *auth.Source) error { - assert.FailNow(t, "case %d: should not call updateAuthSource", n) + assert.FailNow(t, "updateAuthSource called", "case %d: should not call updateAuthSource", n) return nil }, getAuthSourceByID: func(ctx context.Context, id int64) (*auth.Source, error) { - assert.FailNow(t, "case %d: should not call getAuthSourceByID", n) + assert.FailNow(t, "getAuthSourceByID called", "case %d: should not call getAuthSourceByID", n) return nil, nil }, } // Create a copy of command to test - app := cli.NewApp() - app.Flags = microcmdAuthAddLdapBindDn.Flags - app.Action = service.addLdapBindDn + app := cli.Command{ + Flags: microcmdAuthAddLdapBindDn().Flags, + Action: service.addLdapBindDn, + } // Run it - err := app.Run(c.args) + err := app.Run(t.Context(), c.args) if c.errMsg != "" { assert.EqualError(t, err, c.errMsg, "case %d: error should match", n) } else { @@ -242,9 +256,7 @@ func TestAddLdapBindDn(t *testing.T) { func TestAddLdapSimpleAuth(t *testing.T) { // Mock cli functions to do not exit on error - osExiter := cli.OsExiter - defer func() { cli.OsExiter = osExiter }() - cli.OsExiter = func(code int) {} + defer test.MockVariableValue(&cli.OsExiter, func(code int) {})() // Test cases cases := []struct { @@ -334,12 +346,12 @@ func TestAddLdapSimpleAuth(t *testing.T) { "--name", "ldap (simple auth) source", "--security-protocol", "zzzzz", "--host", "ldap-server", - "--port", "123", + "--port", "1234", "--user-filter", "(&(objectClass=posixAccount)(cn=%s))", "--email-attribute", "mail", "--user-dn", "cn=%s,ou=Users,dc=domain,dc=org", }, - errMsg: "Unknown security protocol name: zzzzz", + errMsg: "unknown security protocol name: zzzzz", }, // case 3 { @@ -446,22 +458,23 @@ func TestAddLdapSimpleAuth(t *testing.T) { return nil }, updateAuthSource: func(ctx context.Context, authSource *auth.Source) error { - assert.FailNow(t, "case %d: should not call updateAuthSource", n) + assert.FailNow(t, "updateAuthSource called", "case %d: should not call updateAuthSource", n) return nil }, getAuthSourceByID: func(ctx context.Context, id int64) (*auth.Source, error) { - assert.FailNow(t, "case %d: should not call getAuthSourceByID", n) + assert.FailNow(t, "getAuthSourceById called", "case %d: should not call getAuthSourceByID", n) return nil, nil }, } // Create a copy of command to test - app := cli.NewApp() - app.Flags = microcmdAuthAddLdapSimpleAuth.Flags - app.Action = service.addLdapSimpleAuth + app := &cli.Command{ + Flags: microcmdAuthAddLdapSimpleAuth().Flags, + Action: service.addLdapSimpleAuth, + } // Run it - err := app.Run(c.args) + err := app.Run(t.Context(), c.args) if c.errMsg != "" { assert.EqualError(t, err, c.errMsg, "case %d: error should match", n) } else { @@ -473,9 +486,7 @@ func TestAddLdapSimpleAuth(t *testing.T) { func TestUpdateLdapBindDn(t *testing.T) { // Mock cli functions to do not exit on error - osExiter := cli.OsExiter - defer func() { cli.OsExiter = osExiter }() - cli.OsExiter = func(code int) {} + defer test.MockVariableValue(&cli.OsExiter, func(code int) {})() // Test cases cases := []struct { @@ -510,6 +521,13 @@ func TestUpdateLdapBindDn(t *testing.T) { "--bind-password", "secret-bind-full", "--synchronize-users", "--page-size", "99", + "--enable-groups", + "--group-search-base-dn", "ou=group,dc=full-domain-bind,dc=org", + "--group-member-attribute", "memberUid", + "--group-user-attribute", "uid", + "--group-filter", "(|(cn=gitea_users)(cn=admins))", + "--group-team-map", `{"cn=my-group,cn=groups,dc=example,dc=org": {"MyGiteaOrganization": ["MyGiteaTeam1", "MyGiteaTeam2"]}}`, + "--group-team-map-removal", }, id: 23, existingAuthSource: &auth.Source{ @@ -545,6 +563,13 @@ func TestUpdateLdapBindDn(t *testing.T) { AdminFilter: "(memberOf=cn=admin-group,ou=example,dc=full-domain-bind,dc=org)", RestrictedFilter: "(memberOf=cn=restricted-group,ou=example,dc=full-domain-bind,dc=org)", Enabled: true, + GroupsEnabled: true, + GroupDN: "ou=group,dc=full-domain-bind,dc=org", + GroupMemberUID: "memberUid", + UserUID: "uid", + GroupFilter: "(|(cn=gitea_users)(cn=admins))", + GroupTeamMap: `{"cn=my-group,cn=groups,dc=example,dc=org": {"MyGiteaOrganization": ["MyGiteaTeam1", "MyGiteaTeam2"]}}`, + GroupTeamMapRemoval: true, }, }, }, @@ -836,7 +861,7 @@ func TestUpdateLdapBindDn(t *testing.T) { "--id", "1", "--security-protocol", "xxxxx", }, - errMsg: "Unknown security protocol name: xxxxx", + errMsg: "unknown security protocol name: xxxxx", }, // case 22 { @@ -855,7 +880,7 @@ func TestUpdateLdapBindDn(t *testing.T) { Type: auth.OAuth2, Cfg: &ldap.Source{}, }, - errMsg: "Invalid authentication type. expected: LDAP (via BindDN), actual: OAuth2", + errMsg: "invalid authentication type. expected: LDAP (via BindDN), actual: OAuth2", }, // case 24 { @@ -897,7 +922,7 @@ func TestUpdateLdapBindDn(t *testing.T) { return nil }, createAuthSource: func(ctx context.Context, authSource *auth.Source) error { - assert.FailNow(t, "case %d: should not call createAuthSource", n) + assert.FailNow(t, "createAuthSource called", "case %d: should not call createAuthSource", n) return nil }, updateAuthSource: func(ctx context.Context, authSource *auth.Source) error { @@ -919,12 +944,12 @@ func TestUpdateLdapBindDn(t *testing.T) { } // Create a copy of command to test - app := cli.NewApp() - app.Flags = microcmdAuthUpdateLdapBindDn.Flags - app.Action = service.updateLdapBindDn - + app := cli.Command{ + Flags: microcmdAuthUpdateLdapBindDn().Flags, + Action: service.updateLdapBindDn, + } // Run it - err := app.Run(c.args) + err := app.Run(t.Context(), c.args) if c.errMsg != "" { assert.EqualError(t, err, c.errMsg, "case %d: error should match", n) } else { @@ -936,9 +961,7 @@ func TestUpdateLdapBindDn(t *testing.T) { func TestUpdateLdapSimpleAuth(t *testing.T) { // Mock cli functions to do not exit on error - osExiter := cli.OsExiter - defer func() { cli.OsExiter = osExiter }() - cli.OsExiter = func(code int) {} + defer test.MockVariableValue(&cli.OsExiter, func(code int) {})() // Test cases cases := []struct { @@ -1229,7 +1252,7 @@ func TestUpdateLdapSimpleAuth(t *testing.T) { "--id", "1", "--security-protocol", "xxxxx", }, - errMsg: "Unknown security protocol name: xxxxx", + errMsg: "unknown security protocol name: xxxxx", }, // case 18 { @@ -1248,7 +1271,7 @@ func TestUpdateLdapSimpleAuth(t *testing.T) { Type: auth.PAM, Cfg: &ldap.Source{}, }, - errMsg: "Invalid authentication type. expected: LDAP (simple auth), actual: PAM", + errMsg: "invalid authentication type. expected: LDAP (simple auth), actual: PAM", }, // case 20 { @@ -1287,7 +1310,7 @@ func TestUpdateLdapSimpleAuth(t *testing.T) { return nil }, createAuthSource: func(ctx context.Context, authSource *auth.Source) error { - assert.FailNow(t, "case %d: should not call createAuthSource", n) + assert.FailNow(t, "createAuthSource called", "case %d: should not call createAuthSource", n) return nil }, updateAuthSource: func(ctx context.Context, authSource *auth.Source) error { @@ -1309,12 +1332,12 @@ func TestUpdateLdapSimpleAuth(t *testing.T) { } // Create a copy of command to test - app := cli.NewApp() - app.Flags = microcmdAuthUpdateLdapSimpleAuth.Flags - app.Action = service.updateLdapSimpleAuth - + app := cli.Command{ + Flags: microcmdAuthUpdateLdapSimpleAuth().Flags, + Action: service.updateLdapSimpleAuth, + } // Run it - err := app.Run(c.args) + err := app.Run(t.Context(), c.args) if c.errMsg != "" { assert.EqualError(t, err, c.errMsg, "case %d: error should match", n) } else { diff --git a/cmd/admin_auth_oauth.go b/cmd/admin_auth_oauth.go index 8e6239ac338..8848c94fc51 100644 --- a/cmd/admin_auth_oauth.go +++ b/cmd/admin_auth_oauth.go @@ -4,18 +4,20 @@ package cmd import ( + "context" "errors" "fmt" "net/url" auth_model "code.gitea.io/gitea/models/auth" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/services/auth/source/oauth2" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) -var ( - oauthCLIFlags = []cli.Flag{ +func oauthCLIFlags() []cli.Flag { + return []cli.Flag{ &cli.StringFlag{ Name: "name", Value: "", @@ -85,6 +87,14 @@ var ( Value: nil, Usage: "Scopes to request when to authenticate against this OAuth2 source", }, + &cli.StringFlag{ + Name: "ssh-public-key-claim-name", + Usage: "Claim name that provides SSH public keys", + }, + &cli.StringFlag{ + Name: "full-name-claim-name", + Usage: "Claim name that provides user's full name", + }, &cli.StringFlag{ Name: "required-claim-name", Value: "", @@ -120,23 +130,34 @@ var ( Usage: "Activate automatic team membership removal depending on groups", }, } +} - microcmdAuthAddOauth = &cli.Command{ - Name: "add-oauth", - Usage: "Add new Oauth authentication source", - Action: runAddOauth, - Flags: oauthCLIFlags, +func microcmdAuthAddOauth() *cli.Command { + return &cli.Command{ + Name: "add-oauth", + Usage: "Add new Oauth authentication source", + Action: func(ctx context.Context, cmd *cli.Command) error { + return newAuthService().runAddOauth(ctx, cmd) + }, + Flags: oauthCLIFlags(), } +} - microcmdAuthUpdateOauth = &cli.Command{ - Name: "update-oauth", - Usage: "Update existing Oauth authentication source", - Action: runUpdateOauth, - Flags: append(oauthCLIFlags[:1], append([]cli.Flag{idFlag}, oauthCLIFlags[1:]...)...), +func microcmdAuthUpdateOauth() *cli.Command { + return &cli.Command{ + Name: "update-oauth", + Usage: "Update existing Oauth authentication source", + Action: func(ctx context.Context, cmd *cli.Command) error { + return newAuthService().runUpdateOauth(ctx, cmd) + }, + Flags: append(oauthCLIFlags()[:1], append([]cli.Flag{&cli.Int64Flag{ + Name: "id", + Usage: "ID of authentication source", + }}, oauthCLIFlags()[1:]...)...), } -) +} -func parseOAuth2Config(c *cli.Context) *oauth2.Source { +func parseOAuth2Config(c *cli.Command) *oauth2.Source { var customURLMapping *oauth2.CustomURLMapping if c.IsSet("use-custom-urls") { customURLMapping = &oauth2.CustomURLMapping{ @@ -156,7 +177,6 @@ func parseOAuth2Config(c *cli.Context) *oauth2.Source { OpenIDConnectAutoDiscoveryURL: c.String("auto-discover-url"), CustomURLMapping: customURLMapping, IconURL: c.String("icon-url"), - SkipLocalTwoFA: c.Bool("skip-local-2fa"), Scopes: c.StringSlice("scopes"), RequiredClaimName: c.String("required-claim-name"), RequiredClaimValue: c.String("required-claim-value"), @@ -165,14 +185,13 @@ func parseOAuth2Config(c *cli.Context) *oauth2.Source { RestrictedGroup: c.String("restricted-group"), GroupTeamMap: c.String("group-team-map"), GroupTeamMapRemoval: c.Bool("group-team-map-removal"), + SSHPublicKeyClaimName: c.String("ssh-public-key-claim-name"), + FullNameClaimName: c.String("full-name-claim-name"), } } -func runAddOauth(c *cli.Context) error { - ctx, cancel := installSignals() - defer cancel() - - if err := initDB(ctx); err != nil { +func (a *authService) runAddOauth(ctx context.Context, c *cli.Command) error { + if err := a.initDB(ctx); err != nil { return err } @@ -184,27 +203,25 @@ func runAddOauth(c *cli.Context) error { } } - return auth_model.CreateSource(ctx, &auth_model.Source{ - Type: auth_model.OAuth2, - Name: c.String("name"), - IsActive: true, - Cfg: config, + return a.createAuthSource(ctx, &auth_model.Source{ + Type: auth_model.OAuth2, + Name: c.String("name"), + IsActive: true, + Cfg: config, + TwoFactorPolicy: util.Iif(c.Bool("skip-local-2fa"), "skip", ""), }) } -func runUpdateOauth(c *cli.Context) error { +func (a *authService) runUpdateOauth(ctx context.Context, c *cli.Command) error { if !c.IsSet("id") { return errors.New("--id flag is missing") } - ctx, cancel := installSignals() - defer cancel() - - if err := initDB(ctx); err != nil { + if err := a.initDB(ctx); err != nil { return err } - source, err := auth_model.GetSourceByID(ctx, c.Int64("id")) + source, err := a.getAuthSourceByID(ctx, c.Int64("id")) if err != nil { return err } @@ -261,6 +278,12 @@ func runUpdateOauth(c *cli.Context) error { if c.IsSet("group-team-map-removal") { oAuth2Config.GroupTeamMapRemoval = c.Bool("group-team-map-removal") } + if c.IsSet("ssh-public-key-claim-name") { + oAuth2Config.SSHPublicKeyClaimName = c.String("ssh-public-key-claim-name") + } + if c.IsSet("full-name-claim-name") { + oAuth2Config.FullNameClaimName = c.String("full-name-claim-name") + } // update custom URL mapping customURLMapping := &oauth2.CustomURLMapping{} @@ -294,6 +317,6 @@ func runUpdateOauth(c *cli.Context) error { oAuth2Config.CustomURLMapping = customURLMapping source.Cfg = oAuth2Config - - return auth_model.UpdateSource(ctx, source) + source.TwoFactorPolicy = util.Iif(c.Bool("skip-local-2fa"), "skip", "") + return a.updateAuthSource(ctx, source) } diff --git a/cmd/admin_auth_oauth_test.go b/cmd/admin_auth_oauth_test.go new file mode 100644 index 00000000000..bb9da667fd1 --- /dev/null +++ b/cmd/admin_auth_oauth_test.go @@ -0,0 +1,343 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package cmd + +import ( + "context" + "testing" + + auth_model "code.gitea.io/gitea/models/auth" + "code.gitea.io/gitea/services/auth/source/oauth2" + + "github.com/stretchr/testify/assert" + "github.com/urfave/cli/v3" +) + +func TestAddOauth(t *testing.T) { + testCases := []struct { + name string + args []string + source *auth_model.Source + errMsg string + }{ + { + name: "valid config", + args: []string{ + "--name", "test", + "--provider", "github", + "--key", "some_key", + "--secret", "some_secret", + }, + source: &auth_model.Source{ + Type: auth_model.OAuth2, + Name: "test", + IsActive: true, + Cfg: &oauth2.Source{ + Scopes: []string{}, + Provider: "github", + ClientID: "some_key", + ClientSecret: "some_secret", + }, + TwoFactorPolicy: "", + }, + }, + { + name: "valid config with openid connect", + args: []string{ + "--name", "test", + "--provider", "openidConnect", + "--key", "some_key", + "--secret", "some_secret", + "--auto-discover-url", "https://example.com", + }, + source: &auth_model.Source{ + Type: auth_model.OAuth2, + Name: "test", + IsActive: true, + Cfg: &oauth2.Source{ + Scopes: []string{}, + Provider: "openidConnect", + ClientID: "some_key", + ClientSecret: "some_secret", + OpenIDConnectAutoDiscoveryURL: "https://example.com", + }, + TwoFactorPolicy: "", + }, + }, + { + name: "valid config with options", + args: []string{ + "--name", "test", + "--provider", "gitlab", + "--key", "some_key", + "--secret", "some_secret", + "--use-custom-urls", "true", + "--custom-token-url", "https://example.com/token", + "--custom-auth-url", "https://example.com/auth", + "--custom-profile-url", "https://example.com/profile", + "--custom-email-url", "https://example.com/email", + "--custom-tenant-id", "some_tenant", + "--icon-url", "https://example.com/icon", + "--scopes", "scope1,scope2", + "--skip-local-2fa", "true", + "--required-claim-name", "claim_name", + "--required-claim-value", "claim_value", + "--group-claim-name", "group_name", + "--admin-group", "admin", + "--restricted-group", "restricted", + "--group-team-map", `{"group1": [1,2]}`, + "--group-team-map-removal=true", + "--ssh-public-key-claim-name", "attr_ssh_pub_key", + "--full-name-claim-name", "attr_full_name", + }, + source: &auth_model.Source{ + Type: auth_model.OAuth2, + Name: "test", + IsActive: true, + Cfg: &oauth2.Source{ + Provider: "gitlab", + ClientID: "some_key", + ClientSecret: "some_secret", + CustomURLMapping: &oauth2.CustomURLMapping{ + TokenURL: "https://example.com/token", + AuthURL: "https://example.com/auth", + ProfileURL: "https://example.com/profile", + EmailURL: "https://example.com/email", + Tenant: "some_tenant", + }, + IconURL: "https://example.com/icon", + Scopes: []string{"scope1", "scope2"}, + RequiredClaimName: "claim_name", + RequiredClaimValue: "claim_value", + GroupClaimName: "group_name", + AdminGroup: "admin", + RestrictedGroup: "restricted", + GroupTeamMap: `{"group1": [1,2]}`, + GroupTeamMapRemoval: true, + SSHPublicKeyClaimName: "attr_ssh_pub_key", + FullNameClaimName: "attr_full_name", + }, + TwoFactorPolicy: "skip", + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + var createdSource *auth_model.Source + a := &authService{ + initDB: func(ctx context.Context) error { + return nil + }, + createAuthSource: func(ctx context.Context, source *auth_model.Source) error { + createdSource = source + return nil + }, + } + + app := &cli.Command{ + Flags: microcmdAuthAddOauth().Flags, + Action: a.runAddOauth, + } + + args := []string{"oauth-test"} + args = append(args, tc.args...) + + err := app.Run(t.Context(), args) + + if tc.errMsg != "" { + assert.EqualError(t, err, tc.errMsg) + } else { + assert.NoError(t, err) + assert.Equal(t, tc.source, createdSource) + } + }) + } +} + +func TestUpdateOauth(t *testing.T) { + testCases := []struct { + name string + args []string + id int64 + existingAuthSource *auth_model.Source + authSource *auth_model.Source + errMsg string + }{ + { + name: "missing id", + args: []string{ + "--name", "test", + }, + errMsg: "--id flag is missing", + }, + { + name: "valid config", + id: 1, + existingAuthSource: &auth_model.Source{ + ID: 1, + Type: auth_model.OAuth2, + Name: "old name", + IsActive: true, + Cfg: &oauth2.Source{ + Provider: "github", + ClientID: "old_key", + ClientSecret: "old_secret", + }, + TwoFactorPolicy: "", + }, + args: []string{ + "--id", "1", + "--name", "test", + "--provider", "gitlab", + "--key", "new_key", + "--secret", "new_secret", + }, + authSource: &auth_model.Source{ + ID: 1, + Type: auth_model.OAuth2, + Name: "test", + IsActive: true, + Cfg: &oauth2.Source{ + Provider: "gitlab", + ClientID: "new_key", + ClientSecret: "new_secret", + CustomURLMapping: &oauth2.CustomURLMapping{}, + }, + TwoFactorPolicy: "", + }, + }, + { + name: "valid config with options", + id: 1, + existingAuthSource: &auth_model.Source{ + ID: 1, + Type: auth_model.OAuth2, + Name: "old name", + IsActive: true, + Cfg: &oauth2.Source{ + Provider: "gitlab", + ClientID: "old_key", + ClientSecret: "old_secret", + CustomURLMapping: &oauth2.CustomURLMapping{ + TokenURL: "https://old.example.com/token", + AuthURL: "https://old.example.com/auth", + ProfileURL: "https://old.example.com/profile", + EmailURL: "https://old.example.com/email", + Tenant: "old_tenant", + }, + IconURL: "https://old.example.com/icon", + Scopes: []string{"old_scope1", "old_scope2"}, + RequiredClaimName: "old_claim_name", + RequiredClaimValue: "old_claim_value", + GroupClaimName: "old_group_name", + AdminGroup: "old_admin", + RestrictedGroup: "old_restricted", + GroupTeamMap: `{"old_group1": [1,2]}`, + GroupTeamMapRemoval: true, + SSHPublicKeyClaimName: "old_ssh_pub_key", + FullNameClaimName: "old_full_name", + }, + TwoFactorPolicy: "", + }, + args: []string{ + "--id", "1", + "--name", "test", + "--provider", "github", + "--key", "new_key", + "--secret", "new_secret", + "--use-custom-urls", "true", + "--custom-token-url", "https://example.com/token", + "--custom-auth-url", "https://example.com/auth", + "--custom-profile-url", "https://example.com/profile", + "--custom-email-url", "https://example.com/email", + "--custom-tenant-id", "new_tenant", + "--icon-url", "https://example.com/icon", + "--scopes", "scope1,scope2", + "--skip-local-2fa=true", + "--required-claim-name", "claim_name", + "--required-claim-value", "claim_value", + "--group-claim-name", "group_name", + "--admin-group", "admin", + "--restricted-group", "restricted", + "--group-team-map", `{"group1": [1,2]}`, + "--group-team-map-removal=false", + "--ssh-public-key-claim-name", "new_ssh_pub_key", + "--full-name-claim-name", "new_full_name", + }, + authSource: &auth_model.Source{ + ID: 1, + Type: auth_model.OAuth2, + Name: "test", + IsActive: true, + Cfg: &oauth2.Source{ + Provider: "github", + ClientID: "new_key", + ClientSecret: "new_secret", + CustomURLMapping: &oauth2.CustomURLMapping{ + TokenURL: "https://example.com/token", + AuthURL: "https://example.com/auth", + ProfileURL: "https://example.com/profile", + EmailURL: "https://example.com/email", + Tenant: "new_tenant", + }, + IconURL: "https://example.com/icon", + Scopes: []string{"scope1", "scope2"}, + RequiredClaimName: "claim_name", + RequiredClaimValue: "claim_value", + GroupClaimName: "group_name", + AdminGroup: "admin", + RestrictedGroup: "restricted", + GroupTeamMap: `{"group1": [1,2]}`, + GroupTeamMapRemoval: false, + SSHPublicKeyClaimName: "new_ssh_pub_key", + FullNameClaimName: "new_full_name", + }, + TwoFactorPolicy: "skip", + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + a := &authService{ + initDB: func(ctx context.Context) error { + return nil + }, + getAuthSourceByID: func(ctx context.Context, id int64) (*auth_model.Source, error) { + return &auth_model.Source{ + ID: 1, + Type: auth_model.OAuth2, + Name: "test", + IsActive: true, + Cfg: &oauth2.Source{ + CustomURLMapping: &oauth2.CustomURLMapping{}, + }, + TwoFactorPolicy: "skip", + }, nil + }, + updateAuthSource: func(ctx context.Context, source *auth_model.Source) error { + assert.Equal(t, tc.authSource, source) + return nil + }, + } + + app := &cli.Command{ + Flags: microcmdAuthUpdateOauth().Flags, + Action: a.runUpdateOauth, + } + + args := []string{"oauth-test"} + args = append(args, tc.args...) + + err := app.Run(t.Context(), args) + + if tc.errMsg != "" { + assert.EqualError(t, err, tc.errMsg) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/cmd/admin_auth_stmp.go b/cmd/admin_auth_smtp.go similarity index 67% rename from cmd/admin_auth_stmp.go rename to cmd/admin_auth_smtp.go index d7247469052..93e0587fc3c 100644 --- a/cmd/admin_auth_stmp.go +++ b/cmd/admin_auth_smtp.go @@ -4,6 +4,7 @@ package cmd import ( + "context" "errors" "strings" @@ -11,11 +12,11 @@ import ( "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/services/auth/source/smtp" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) -var ( - smtpCLIFlags = []cli.Flag{ +func smtpCLIFlags() []cli.Flag { + return []cli.Flag{ &cli.StringFlag{ Name: "name", Value: "", @@ -38,12 +39,10 @@ var ( &cli.BoolFlag{ Name: "force-smtps", Usage: "SMTPS is always used on port 465. Set this to force SMTPS on other ports.", - Value: true, }, &cli.BoolFlag{ Name: "skip-verify", Usage: "Skip TLS verify.", - Value: true, }, &cli.StringFlag{ Name: "helo-hostname", @@ -53,7 +52,6 @@ var ( &cli.BoolFlag{ Name: "disable-helo", Usage: "Disable SMTP helo.", - Value: true, }, &cli.StringFlag{ Name: "allowed-domains", @@ -63,7 +61,6 @@ var ( &cli.BoolFlag{ Name: "skip-local-2fa", Usage: "Skip 2FA to log on.", - Value: true, }, &cli.BoolFlag{ Name: "active", @@ -71,23 +68,34 @@ var ( Value: true, }, } +} - microcmdAuthAddSMTP = &cli.Command{ - Name: "add-smtp", - Usage: "Add new SMTP authentication source", - Action: runAddSMTP, - Flags: smtpCLIFlags, +func microcmdAuthUpdateSMTP() *cli.Command { + return &cli.Command{ + Name: "update-smtp", + Usage: "Update existing SMTP authentication source", + Action: func(ctx context.Context, cmd *cli.Command) error { + return newAuthService().runUpdateSMTP(ctx, cmd) + }, + Flags: append(smtpCLIFlags()[:1], append([]cli.Flag{&cli.Int64Flag{ + Name: "id", + Usage: "ID of authentication source", + }}, smtpCLIFlags()[1:]...)...), } +} - microcmdAuthUpdateSMTP = &cli.Command{ - Name: "update-smtp", - Usage: "Update existing SMTP authentication source", - Action: runUpdateSMTP, - Flags: append(smtpCLIFlags[:1], append([]cli.Flag{idFlag}, smtpCLIFlags[1:]...)...), +func microcmdAuthAddSMTP() *cli.Command { + return &cli.Command{ + Name: "add-smtp", + Usage: "Add new SMTP authentication source", + Action: func(ctx context.Context, cmd *cli.Command) error { + return newAuthService().runAddSMTP(ctx, cmd) + }, + Flags: smtpCLIFlags(), } -) +} -func parseSMTPConfig(c *cli.Context, conf *smtp.Source) error { +func parseSMTPConfig(c *cli.Command, conf *smtp.Source) error { if c.IsSet("auth-type") { conf.Auth = c.String("auth-type") validAuthTypes := []string{"PLAIN", "LOGIN", "CRAM-MD5"} @@ -117,17 +125,11 @@ func parseSMTPConfig(c *cli.Context, conf *smtp.Source) error { if c.IsSet("disable-helo") { conf.DisableHelo = c.Bool("disable-helo") } - if c.IsSet("skip-local-2fa") { - conf.SkipLocalTwoFA = c.Bool("skip-local-2fa") - } return nil } -func runAddSMTP(c *cli.Context) error { - ctx, cancel := installSignals() - defer cancel() - - if err := initDB(ctx); err != nil { +func (a *authService) runAddSMTP(ctx context.Context, c *cli.Command) error { + if err := a.initDB(ctx); err != nil { return err } @@ -155,27 +157,25 @@ func runAddSMTP(c *cli.Context) error { smtpConfig.Auth = "PLAIN" } - return auth_model.CreateSource(ctx, &auth_model.Source{ - Type: auth_model.SMTP, - Name: c.String("name"), - IsActive: active, - Cfg: &smtpConfig, + return a.createAuthSource(ctx, &auth_model.Source{ + Type: auth_model.SMTP, + Name: c.String("name"), + IsActive: active, + Cfg: &smtpConfig, + TwoFactorPolicy: util.Iif(c.Bool("skip-local-2fa"), "skip", ""), }) } -func runUpdateSMTP(c *cli.Context) error { +func (a *authService) runUpdateSMTP(ctx context.Context, c *cli.Command) error { if !c.IsSet("id") { return errors.New("--id flag is missing") } - ctx, cancel := installSignals() - defer cancel() - - if err := initDB(ctx); err != nil { + if err := a.initDB(ctx); err != nil { return err } - source, err := auth_model.GetSourceByID(ctx, c.Int64("id")) + source, err := a.getAuthSourceByID(ctx, c.Int64("id")) if err != nil { return err } @@ -195,6 +195,6 @@ func runUpdateSMTP(c *cli.Context) error { } source.Cfg = smtpConfig - - return auth_model.UpdateSource(ctx, source) + source.TwoFactorPolicy = util.Iif(c.Bool("skip-local-2fa"), "skip", "") + return a.updateAuthSource(ctx, source) } diff --git a/cmd/admin_auth_smtp_test.go b/cmd/admin_auth_smtp_test.go new file mode 100644 index 00000000000..e54e01830c7 --- /dev/null +++ b/cmd/admin_auth_smtp_test.go @@ -0,0 +1,271 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package cmd + +import ( + "context" + "testing" + + auth_model "code.gitea.io/gitea/models/auth" + "code.gitea.io/gitea/services/auth/source/smtp" + + "github.com/stretchr/testify/assert" + "github.com/urfave/cli/v3" +) + +func TestAddSMTP(t *testing.T) { + testCases := []struct { + name string + args []string + source *auth_model.Source + errMsg string + }{ + { + name: "missing name", + args: []string{ + "--host", "localhost", + "--port", "25", + }, + errMsg: "name must be set", + }, + { + name: "missing host", + args: []string{ + "--name", "test", + "--port", "25", + }, + errMsg: "host must be set", + }, + { + name: "missing port", + args: []string{ + "--name", "test", + "--host", "localhost", + }, + errMsg: "port must be set", + }, + { + name: "valid config", + args: []string{ + "--name", "test", + "--host", "localhost", + "--port", "25", + }, + source: &auth_model.Source{ + Type: auth_model.SMTP, + Name: "test", + IsActive: true, + Cfg: &smtp.Source{ + Auth: "PLAIN", + Host: "localhost", + Port: 25, + }, + TwoFactorPolicy: "", + }, + }, + { + name: "valid config with options", + args: []string{ + "--name", "test", + "--host", "localhost", + "--port", "25", + "--auth-type", "LOGIN", + "--force-smtps", + "--skip-verify", + "--helo-hostname", "example.com", + "--disable-helo=true", + "--allowed-domains", "example.com,example.org", + "--skip-local-2fa", + "--active=false", + }, + source: &auth_model.Source{ + Type: auth_model.SMTP, + Name: "test", + IsActive: false, + Cfg: &smtp.Source{ + Auth: "LOGIN", + Host: "localhost", + Port: 25, + ForceSMTPS: true, + SkipVerify: true, + HeloHostname: "example.com", + DisableHelo: true, + AllowedDomains: "example.com,example.org", + }, + TwoFactorPolicy: "skip", + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + a := &authService{ + initDB: func(ctx context.Context) error { + return nil + }, + createAuthSource: func(ctx context.Context, source *auth_model.Source) error { + assert.Equal(t, tc.source, source) + return nil + }, + } + + cmd := &cli.Command{ + Flags: microcmdAuthAddSMTP().Flags, + Action: a.runAddSMTP, + } + + args := []string{"smtp-test"} + args = append(args, tc.args...) + + t.Log(args) + err := cmd.Run(t.Context(), args) + + if tc.errMsg != "" { + assert.EqualError(t, err, tc.errMsg) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestUpdateSMTP(t *testing.T) { + testCases := []struct { + name string + args []string + existingAuthSource *auth_model.Source + authSource *auth_model.Source + errMsg string + }{ + { + name: "missing id", + args: []string{ + "--name", "test", + "--host", "localhost", + "--port", "25", + }, + errMsg: "--id flag is missing", + }, + { + name: "valid config", + existingAuthSource: &auth_model.Source{ + ID: 1, + Type: auth_model.SMTP, + Name: "old name", + IsActive: true, + Cfg: &smtp.Source{ + Auth: "PLAIN", + Host: "old host", + Port: 26, + }, + }, + args: []string{ + "--id", "1", + "--name", "test", + "--host", "localhost", + "--port", "25", + }, + authSource: &auth_model.Source{ + ID: 1, + Type: auth_model.SMTP, + Name: "test", + IsActive: true, + Cfg: &smtp.Source{ + Auth: "PLAIN", + Host: "localhost", + Port: 25, + }, + }, + }, + { + name: "valid config with options", + existingAuthSource: &auth_model.Source{ + ID: 1, + Type: auth_model.SMTP, + Name: "old name", + IsActive: true, + Cfg: &smtp.Source{ + Auth: "PLAIN", + Host: "old host", + Port: 26, + HeloHostname: "old.example.com", + AllowedDomains: "old.example.com", + }, + TwoFactorPolicy: "", + }, + args: []string{ + "--id", "1", + "--name", "test", + "--host", "localhost", + "--port", "25", + "--auth-type", "LOGIN", + "--force-smtps", + "--skip-verify", + "--helo-hostname", "example.com", + "--disable-helo", + "--allowed-domains", "example.com,example.org", + "--skip-local-2fa", + "--active=false", + }, + authSource: &auth_model.Source{ + ID: 1, + Type: auth_model.SMTP, + Name: "test", + IsActive: false, + Cfg: &smtp.Source{ + Auth: "LOGIN", + Host: "localhost", + Port: 25, + ForceSMTPS: true, + SkipVerify: true, + HeloHostname: "example.com", + DisableHelo: true, + AllowedDomains: "example.com,example.org", + }, + TwoFactorPolicy: "skip", + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + a := &authService{ + initDB: func(ctx context.Context) error { + return nil + }, + getAuthSourceByID: func(ctx context.Context, id int64) (*auth_model.Source, error) { + return &auth_model.Source{ + ID: 1, + Type: auth_model.SMTP, + Name: "test", + IsActive: true, + Cfg: &smtp.Source{ + Auth: "PLAIN", + }, + }, nil + }, + + updateAuthSource: func(ctx context.Context, source *auth_model.Source) error { + assert.Equal(t, tc.authSource, source) + return nil + }, + } + + app := &cli.Command{ + Flags: microcmdAuthUpdateSMTP().Flags, + Action: a.runUpdateSMTP, + } + args := []string{"smtp-tests"} + args = append(args, tc.args...) + + err := app.Run(t.Context(), args) + + if tc.errMsg != "" { + assert.EqualError(t, err, tc.errMsg) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/cmd/admin_regenerate.go b/cmd/admin_regenerate.go index ab769f6d0c6..a5f1bd51059 100644 --- a/cmd/admin_regenerate.go +++ b/cmd/admin_regenerate.go @@ -4,11 +4,13 @@ package cmd import ( + "context" + "code.gitea.io/gitea/modules/graceful" asymkey_service "code.gitea.io/gitea/services/asymkey" repo_service "code.gitea.io/gitea/services/repository" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) var ( @@ -25,20 +27,14 @@ var ( } ) -func runRegenerateHooks(_ *cli.Context) error { - ctx, cancel := installSignals() - defer cancel() - +func runRegenerateHooks(ctx context.Context, _ *cli.Command) error { if err := initDB(ctx); err != nil { return err } return repo_service.SyncRepositoryHooks(graceful.GetManager().ShutdownContext()) } -func runRegenerateKeys(_ *cli.Context) error { - ctx, cancel := installSignals() - defer cancel() - +func runRegenerateKeys(ctx context.Context, _ *cli.Command) error { if err := initDB(ctx); err != nil { return err } diff --git a/cmd/admin_user.go b/cmd/admin_user.go index 967a6ed88a2..3a24c3e56f1 100644 --- a/cmd/admin_user.go +++ b/cmd/admin_user.go @@ -4,18 +4,18 @@ package cmd import ( - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) var subcmdUser = &cli.Command{ Name: "user", Usage: "Modify users", - Subcommands: []*cli.Command{ - microcmdUserCreate, + Commands: []*cli.Command{ + microcmdUserCreate(), microcmdUserList, - microcmdUserChangePassword, - microcmdUserDelete, + microcmdUserChangePassword(), + microcmdUserDelete(), microcmdUserGenerateAccessToken, - microcmdUserMustChangePassword, + microcmdUserMustChangePassword(), }, } diff --git a/cmd/admin_user_change_password.go b/cmd/admin_user_change_password.go index f1ed46e70b0..c27905b4db5 100644 --- a/cmd/admin_user_change_password.go +++ b/cmd/admin_user_change_password.go @@ -4,6 +4,7 @@ package cmd import ( + "context" "errors" "fmt" @@ -13,44 +14,41 @@ import ( "code.gitea.io/gitea/modules/setting" user_service "code.gitea.io/gitea/services/user" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) -var microcmdUserChangePassword = &cli.Command{ - Name: "change-password", - Usage: "Change a user's password", - Action: runChangePassword, - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "username", - Aliases: []string{"u"}, - Value: "", - Usage: "The user to change password for", +func microcmdUserChangePassword() *cli.Command { + return &cli.Command{ + Name: "change-password", + Usage: "Change a user's password", + Action: runChangePassword, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "username", + Aliases: []string{"u"}, + Usage: "The user to change password for", + Required: true, + }, + &cli.StringFlag{ + Name: "password", + Aliases: []string{"p"}, + Usage: "New password to set for user", + Required: true, + }, + &cli.BoolFlag{ + Name: "must-change-password", + Usage: "User must change password (can be disabled by --must-change-password=false)", + Value: true, + }, }, - &cli.StringFlag{ - Name: "password", - Aliases: []string{"p"}, - Value: "", - Usage: "New password to set for user", - }, - &cli.BoolFlag{ - Name: "must-change-password", - Usage: "User must change password (can be disabled by --must-change-password=false)", - Value: true, - }, - }, + } } -func runChangePassword(c *cli.Context) error { - if err := argsSet(c, "username", "password"); err != nil { - return err - } - - ctx, cancel := installSignals() - defer cancel() - - if err := initDB(ctx); err != nil { - return err +func runChangePassword(ctx context.Context, c *cli.Command) error { + if !setting.IsInTesting { + if err := initDB(ctx); err != nil { + return err + } } user, err := user_model.GetUserByName(ctx, c.String("username")) diff --git a/cmd/admin_user_change_password_test.go b/cmd/admin_user_change_password_test.go new file mode 100644 index 00000000000..902632f3e49 --- /dev/null +++ b/cmd/admin_user_change_password_test.go @@ -0,0 +1,91 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package cmd + +import ( + "testing" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestChangePasswordCommand(t *testing.T) { + ctx := t.Context() + + defer func() { + require.NoError(t, db.TruncateBeans(t.Context(), &user_model.User{})) + }() + + t.Run("change password successfully", func(t *testing.T) { + // defer func() { + // require.NoError(t, db.TruncateBeans(t.Context(), &user_model.User{})) + // }() + // Prepare test user + unittest.AssertNotExistsBean(t, &user_model.User{LowerName: "testuser"}) + err := microcmdUserCreate().Run(ctx, []string{"create", "--username", "testuser", "--email", "testuser@gitea.local", "--random-password"}) + require.NoError(t, err) + + // load test user + userBase := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"}) + + // Change the password + err = microcmdUserChangePassword().Run(ctx, []string{"change-password", "--username", "testuser", "--password", "newpassword"}) + require.NoError(t, err) + + // Verify the password has been changed + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"}) + assert.NotEqual(t, userBase.Passwd, user.Passwd) + assert.NotEqual(t, userBase.Salt, user.Salt) + + // Additional check for must-change-password flag + require.NoError(t, microcmdUserChangePassword().Run(ctx, []string{"change-password", "--username", "testuser", "--password", "anotherpassword", "--must-change-password=false"})) + user = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"}) + assert.False(t, user.MustChangePassword) + + require.NoError(t, microcmdUserChangePassword().Run(ctx, []string{"change-password", "--username", "testuser", "--password", "yetanotherpassword", "--must-change-password"})) + user = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"}) + assert.True(t, user.MustChangePassword) + }) + + t.Run("failure cases", func(t *testing.T) { + testCases := []struct { + name string + args []string + expectedErr string + }{ + { + name: "user does not exist", + args: []string{"change-password", "--username", "nonexistentuser", "--password", "newpassword"}, + expectedErr: "user does not exist", + }, + { + name: "missing username", + args: []string{"change-password", "--password", "newpassword"}, + expectedErr: `"username" not set`, + }, + { + name: "missing password", + args: []string{"change-password", "--username", "testuser"}, + expectedErr: `"password" not set`, + }, + { + name: "too short password", + args: []string{"change-password", "--username", "testuser", "--password", "1"}, + expectedErr: "password is not long enough", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := microcmdUserChangePassword().Run(ctx, tc.args) + require.Error(t, err) + require.Contains(t, err.Error(), tc.expectedErr) + }) + } + }) +} diff --git a/cmd/admin_user_create.go b/cmd/admin_user_create.go index 106d14b25a7..cbdb5f90e2e 100644 --- a/cmd/admin_user_create.go +++ b/cmd/admin_user_create.go @@ -7,6 +7,7 @@ import ( "context" "errors" "fmt" + "strings" auth_model "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/db" @@ -15,69 +16,110 @@ import ( "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) -var microcmdUserCreate = &cli.Command{ - Name: "create", - Usage: "Create a new user in database", - Action: runCreateUser, - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "name", - Usage: "Username. DEPRECATED: use username instead", +func microcmdUserCreate() *cli.Command { + return &cli.Command{ + Name: "create", + Usage: "Create a new user in database", + Action: runCreateUser, + MutuallyExclusiveFlags: []cli.MutuallyExclusiveFlags{ + { + Flags: [][]cli.Flag{ + { + &cli.StringFlag{ + Name: "name", + Usage: "Username. DEPRECATED: use username instead", + }, + &cli.StringFlag{ + Name: "username", + Usage: "Username", + }, + }, + }, + Required: true, + }, }, - &cli.StringFlag{ - Name: "username", - Usage: "Username", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "user-type", + Usage: "Set user's type: individual or bot", + Value: "individual", + }, + &cli.StringFlag{ + Name: "password", + Usage: "User password", + }, + &cli.StringFlag{ + Name: "email", + Usage: "User email address", + Required: true, + }, + &cli.BoolFlag{ + Name: "admin", + Usage: "User is an admin", + }, + &cli.BoolFlag{ + Name: "random-password", + Usage: "Generate a random password for the user", + }, + &cli.BoolFlag{ + Name: "must-change-password", + Usage: "User must change password after initial login, defaults to true for all users except the first one (can be disabled by --must-change-password=false)", + HideDefault: true, + }, + &cli.IntFlag{ + Name: "random-password-length", + Usage: "Length of the random password to be generated", + Value: 12, + }, + &cli.BoolFlag{ + Name: "access-token", + Usage: "Generate access token for the user", + }, + &cli.StringFlag{ + Name: "access-token-name", + Usage: `Name of the generated access token`, + Value: "gitea-admin", + }, + &cli.StringFlag{ + Name: "access-token-scopes", + Usage: `Scopes of the generated access token, comma separated. Examples: "all", "public-only,read:issue", "write:repository,write:user"`, + Value: "all", + }, + &cli.BoolFlag{ + Name: "restricted", + Usage: "Make a restricted user account", + }, + &cli.StringFlag{ + Name: "fullname", + Usage: `The full, human-readable name of the user`, + }, }, - &cli.StringFlag{ - Name: "password", - Usage: "User password", - }, - &cli.StringFlag{ - Name: "email", - Usage: "User email address", - }, - &cli.BoolFlag{ - Name: "admin", - Usage: "User is an admin", - }, - &cli.BoolFlag{ - Name: "random-password", - Usage: "Generate a random password for the user", - }, - &cli.BoolFlag{ - Name: "must-change-password", - Usage: "User must change password after initial login, defaults to true for all users except the first one (can be disabled by --must-change-password=false)", - DisableDefaultText: true, - }, - &cli.IntFlag{ - Name: "random-password-length", - Usage: "Length of the random password to be generated", - Value: 12, - }, - &cli.BoolFlag{ - Name: "access-token", - Usage: "Generate access token for the user", - }, - &cli.BoolFlag{ - Name: "restricted", - Usage: "Make a restricted user account", - }, - }, + } } -func runCreateUser(c *cli.Context) error { - if err := argsSet(c, "email"); err != nil { - return err - } +func runCreateUser(ctx context.Context, c *cli.Command) error { + // this command highly depends on the many setting options (create org, visibility, etc.), so it must have a full setting load first + // duplicate setting loading should be safe at the moment, but it should be refactored & improved in the future. + setting.LoadSettings() - if c.IsSet("name") && c.IsSet("username") { - return errors.New("cannot set both --name and --username flags") + userTypes := map[string]user_model.UserType{ + "individual": user_model.UserTypeIndividual, + "bot": user_model.UserTypeBot, } - if !c.IsSet("name") && !c.IsSet("username") { - return errors.New("one of --name or --username flags must be set") + userType, ok := userTypes[c.String("user-type")] + if !ok { + return fmt.Errorf("invalid user type: %s", c.String("user-type")) + } + if userType != user_model.UserTypeIndividual { + // Some other commands like "change-password" also only support individual users. + // It needs to clarify the "password" behavior for bot users in the future. + // At the moment, we do not allow setting password for bot users. + if c.IsSet("password") || c.IsSet("random-password") { + return errors.New("password can only be set for individual users") + } } if c.IsSet("password") && c.IsSet("random-password") { @@ -89,16 +131,12 @@ func runCreateUser(c *cli.Context) error { username = c.String("username") } else { username = c.String("name") - _, _ = fmt.Fprintf(c.App.ErrWriter, "--name flag is deprecated. Use --username instead.\n") + _, _ = fmt.Fprintf(c.ErrWriter, "--name flag is deprecated. Use --username instead.\n") } - ctx := c.Context if !setting.IsInTesting { - // FIXME: need to refactor the "installSignals/initDB" related code later + // FIXME: need to refactor the "initDB" related code later // it doesn't make sense to call it in (almost) every command action function - var cancel context.CancelFunc - ctx, cancel = installSignals() - defer cancel() if err := initDB(ctx); err != nil { return err } @@ -114,16 +152,19 @@ func runCreateUser(c *cli.Context) error { return err } fmt.Printf("generated random password is '%s'\n", password) - } else { + } else if userType == user_model.UserTypeIndividual { return errors.New("must set either password or random-password flag") } isAdmin := c.Bool("admin") mustChangePassword := true // always default to true if c.IsSet("must-change-password") { + if userType != user_model.UserTypeIndividual { + return errors.New("must-change-password flag can only be set for individual users") + } // if the flag is set, use the value provided by the user mustChangePassword = c.Bool("must-change-password") - } else { + } else if userType == user_model.UserTypeIndividual { // check whether there are users in the database hasUserRecord, err := db.IsTableNotEmpty(&user_model.User{}) if err != nil { @@ -147,10 +188,12 @@ func runCreateUser(c *cli.Context) error { u := &user_model.User{ Name: username, Email: c.String("email"), - Passwd: password, IsAdmin: isAdmin, + Type: userType, + Passwd: password, MustChangePassword: mustChangePassword, Visibility: visibility, + FullName: c.String("fullname"), } overwriteDefault := &user_model.CreateUserOverwriteOptions{ @@ -158,23 +201,40 @@ func runCreateUser(c *cli.Context) error { IsRestricted: restricted, } + var accessTokenName string + var accessTokenScope auth_model.AccessTokenScope + if c.IsSet("access-token") { + accessTokenName = strings.TrimSpace(c.String("access-token-name")) + if accessTokenName == "" { + return errors.New("access-token-name cannot be empty") + } + var err error + accessTokenScope, err = auth_model.AccessTokenScope(c.String("access-token-scopes")).Normalize() + if err != nil { + return fmt.Errorf("invalid access token scope provided: %w", err) + } + if !accessTokenScope.HasPermissionScope() { + return errors.New("access token does not have any permission") + } + } else if c.IsSet("access-token-name") || c.IsSet("access-token-scopes") { + return errors.New("access-token-name and access-token-scopes flags are only valid when access-token flag is set") + } + + // arguments should be prepared before creating the user & access token, in case there is anything wrong + + // create the user if err := user_model.CreateUser(ctx, u, &user_model.Meta{}, overwriteDefault); err != nil { return fmt.Errorf("CreateUser: %w", err) } + fmt.Printf("New user '%s' has been successfully created!\n", username) - if c.Bool("access-token") { - t := &auth_model.AccessToken{ - Name: "gitea-admin", - UID: u.ID, - } - + // create the access token + if accessTokenScope != "" { + t := &auth_model.AccessToken{Name: accessTokenName, UID: u.ID, Scope: accessTokenScope} if err := auth_model.NewAccessToken(ctx, t); err != nil { return err } - fmt.Printf("Access token was successfully created... %s\n", t.Token) } - - fmt.Printf("New user '%s' has been successfully created!\n", username) return nil } diff --git a/cmd/admin_user_create_test.go b/cmd/admin_user_create_test.go index 83754e97b10..dbe949ff8d2 100644 --- a/cmd/admin_user_create_test.go +++ b/cmd/admin_user_create_test.go @@ -8,37 +8,127 @@ import ( "strings" "testing" + auth_model "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestAdminUserCreate(t *testing.T) { - app := NewMainApp(AppVersion{}) - reset := func() { - assert.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.User{})) - assert.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.EmailAddress{})) + require.NoError(t, db.TruncateBeans(t.Context(), &user_model.User{})) + require.NoError(t, db.TruncateBeans(t.Context(), &user_model.EmailAddress{})) + require.NoError(t, db.TruncateBeans(t.Context(), &auth_model.AccessToken{})) } - type createCheck struct{ IsAdmin, MustChangePassword bool } - createUser := func(name, args string) createCheck { - assert.NoError(t, app.Run(strings.Fields(fmt.Sprintf("./gitea admin user create --username %s --email %s@gitea.local %s --password foobar", name, name, args)))) - u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: name}) - return createCheck{u.IsAdmin, u.MustChangePassword} + t.Run("MustChangePassword", func(t *testing.T) { + type check struct { + IsAdmin bool + MustChangePassword bool + } + + createCheck := func(name, args string) check { + require.NoError(t, microcmdUserCreate().Run(t.Context(), strings.Fields(fmt.Sprintf("create --username %s --email %s@gitea.local %s --password foobar", name, name, args)))) + u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: name}) + return check{IsAdmin: u.IsAdmin, MustChangePassword: u.MustChangePassword} + } + reset() + assert.Equal(t, check{IsAdmin: false, MustChangePassword: false}, createCheck("u", ""), "first non-admin user doesn't need to change password") + + reset() + assert.Equal(t, check{IsAdmin: true, MustChangePassword: false}, createCheck("u", "--admin"), "first admin user doesn't need to change password") + + reset() + assert.Equal(t, check{IsAdmin: true, MustChangePassword: true}, createCheck("u", "--admin --must-change-password")) + assert.Equal(t, check{IsAdmin: true, MustChangePassword: true}, createCheck("u2", "--admin")) + assert.Equal(t, check{IsAdmin: true, MustChangePassword: false}, createCheck("u3", "--admin --must-change-password=false")) + assert.Equal(t, check{IsAdmin: false, MustChangePassword: true}, createCheck("u4", "")) + assert.Equal(t, check{IsAdmin: false, MustChangePassword: false}, createCheck("u5", "--must-change-password=false")) + }) + + createUser := func(name string, args ...string) error { + return microcmdUserCreate().Run(t.Context(), append([]string{"create", "--username", name, "--email", name + "@gitea.local"}, args...)) } - reset() - assert.Equal(t, createCheck{IsAdmin: false, MustChangePassword: false}, createUser("u", ""), "first non-admin user doesn't need to change password") - reset() - assert.Equal(t, createCheck{IsAdmin: true, MustChangePassword: false}, createUser("u", "--admin"), "first admin user doesn't need to change password") + t.Run("UserType", func(t *testing.T) { + reset() + assert.ErrorContains(t, createUser("u", "--user-type", "invalid"), "invalid user type") + assert.ErrorContains(t, createUser("u", "--user-type", "bot", "--password", "123"), "can only be set for individual users") + assert.ErrorContains(t, createUser("u", "--user-type", "bot", "--must-change-password"), "can only be set for individual users") - reset() - assert.Equal(t, createCheck{IsAdmin: true, MustChangePassword: true}, createUser("u", "--admin --must-change-password")) - assert.Equal(t, createCheck{IsAdmin: true, MustChangePassword: true}, createUser("u2", "--admin")) - assert.Equal(t, createCheck{IsAdmin: true, MustChangePassword: false}, createUser("u3", "--admin --must-change-password=false")) - assert.Equal(t, createCheck{IsAdmin: false, MustChangePassword: true}, createUser("u4", "")) - assert.Equal(t, createCheck{IsAdmin: false, MustChangePassword: false}, createUser("u5", "--must-change-password=false")) + assert.NoError(t, createUser("u", "--user-type", "bot")) + u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "u"}) + assert.Equal(t, user_model.UserTypeBot, u.Type) + assert.Empty(t, u.Passwd) + }) + + t.Run("AccessToken", func(t *testing.T) { + // no generated access token + reset() + assert.NoError(t, createUser("u", "--random-password")) + assert.Equal(t, 1, unittest.GetCount(t, &user_model.User{})) + assert.Equal(t, 0, unittest.GetCount(t, &auth_model.AccessToken{})) + + // using "--access-token" only means "all" access + reset() + assert.NoError(t, createUser("u", "--random-password", "--access-token")) + assert.Equal(t, 1, unittest.GetCount(t, &user_model.User{})) + assert.Equal(t, 1, unittest.GetCount(t, &auth_model.AccessToken{})) + accessToken := unittest.AssertExistsAndLoadBean(t, &auth_model.AccessToken{Name: "gitea-admin"}) + hasScopes, err := accessToken.Scope.HasScope(auth_model.AccessTokenScopeWriteAdmin, auth_model.AccessTokenScopeWriteRepository) + assert.NoError(t, err) + assert.True(t, hasScopes) + + // using "--access-token" with name & scopes + reset() + assert.NoError(t, createUser("u", "--random-password", "--access-token", "--access-token-name", "new-token-name", "--access-token-scopes", "read:issue,read:user")) + assert.Equal(t, 1, unittest.GetCount(t, &user_model.User{})) + assert.Equal(t, 1, unittest.GetCount(t, &auth_model.AccessToken{})) + accessToken = unittest.AssertExistsAndLoadBean(t, &auth_model.AccessToken{Name: "new-token-name"}) + hasScopes, err = accessToken.Scope.HasScope(auth_model.AccessTokenScopeReadIssue, auth_model.AccessTokenScopeReadUser) + assert.NoError(t, err) + assert.True(t, hasScopes) + hasScopes, err = accessToken.Scope.HasScope(auth_model.AccessTokenScopeWriteAdmin, auth_model.AccessTokenScopeWriteRepository) + assert.NoError(t, err) + assert.False(t, hasScopes) + + // using "--access-token-name" without "--access-token" + reset() + err = createUser("u", "--random-password", "--access-token-name", "new-token-name") + assert.Equal(t, 0, unittest.GetCount(t, &user_model.User{})) + assert.Equal(t, 0, unittest.GetCount(t, &auth_model.AccessToken{})) + assert.ErrorContains(t, err, "access-token-name and access-token-scopes flags are only valid when access-token flag is set") + + // using "--access-token-scopes" without "--access-token" + reset() + err = createUser("u", "--random-password", "--access-token-scopes", "read:issue") + assert.Equal(t, 0, unittest.GetCount(t, &user_model.User{})) + assert.Equal(t, 0, unittest.GetCount(t, &auth_model.AccessToken{})) + assert.ErrorContains(t, err, "access-token-name and access-token-scopes flags are only valid when access-token flag is set") + + // empty permission + reset() + err = createUser("u", "--random-password", "--access-token", "--access-token-scopes", "public-only") + assert.Equal(t, 0, unittest.GetCount(t, &user_model.User{})) + assert.Equal(t, 0, unittest.GetCount(t, &auth_model.AccessToken{})) + assert.ErrorContains(t, err, "access token does not have any permission") + }) + + t.Run("UserFields", func(t *testing.T) { + reset() + assert.NoError(t, createUser("u-FullNameWithSpace", "--random-password", "--fullname", "First O'Middle Last")) + unittest.AssertExistsAndLoadBean(t, &user_model.User{ + Name: "u-FullNameWithSpace", + LowerName: "u-fullnamewithspace", + FullName: "First O'Middle Last", + Email: "u-FullNameWithSpace@gitea.local", + }) + + assert.NoError(t, createUser("u-FullNameEmpty", "--random-password", "--fullname", "")) + u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "u-fullnameempty"}) + assert.Empty(t, u.FullName) + }) } diff --git a/cmd/admin_user_delete.go b/cmd/admin_user_delete.go index 520557554a2..f91041577c3 100644 --- a/cmd/admin_user_delete.go +++ b/cmd/admin_user_delete.go @@ -4,53 +4,56 @@ package cmd import ( + "context" "errors" "fmt" "strings" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/storage" user_service "code.gitea.io/gitea/services/user" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) -var microcmdUserDelete = &cli.Command{ - Name: "delete", - Usage: "Delete specific user by id, name or email", - Flags: []cli.Flag{ - &cli.Int64Flag{ - Name: "id", - Usage: "ID of user of the user to delete", +func microcmdUserDelete() *cli.Command { + return &cli.Command{ + Name: "delete", + Usage: "Delete specific user by id, name or email", + Flags: []cli.Flag{ + &cli.Int64Flag{ + Name: "id", + Usage: "ID of user of the user to delete", + }, + &cli.StringFlag{ + Name: "username", + Aliases: []string{"u"}, + Usage: "Username of the user to delete", + }, + &cli.StringFlag{ + Name: "email", + Aliases: []string{"e"}, + Usage: "Email of the user to delete", + }, + &cli.BoolFlag{ + Name: "purge", + Usage: "Purge user, all their repositories, organizations and comments", + }, }, - &cli.StringFlag{ - Name: "username", - Aliases: []string{"u"}, - Usage: "Username of the user to delete", - }, - &cli.StringFlag{ - Name: "email", - Aliases: []string{"e"}, - Usage: "Email of the user to delete", - }, - &cli.BoolFlag{ - Name: "purge", - Usage: "Purge user, all their repositories, organizations and comments", - }, - }, - Action: runDeleteUser, + Action: runDeleteUser, + } } -func runDeleteUser(c *cli.Context) error { +func runDeleteUser(ctx context.Context, c *cli.Command) error { if !c.IsSet("id") && !c.IsSet("username") && !c.IsSet("email") { return errors.New("You must provide the id, username or email of a user to delete") } - ctx, cancel := installSignals() - defer cancel() - - if err := initDB(ctx); err != nil { - return err + if !setting.IsInTesting { + if err := initDB(ctx); err != nil { + return err + } } if err := storage.Init(); err != nil { @@ -70,11 +73,11 @@ func runDeleteUser(c *cli.Context) error { return err } if c.IsSet("username") && user.LowerName != strings.ToLower(strings.TrimSpace(c.String("username"))) { - return fmt.Errorf("The user %s who has email %s does not match the provided username %s", user.Name, c.String("email"), c.String("username")) + return fmt.Errorf("the user %s who has email %s does not match the provided username %s", user.Name, c.String("email"), c.String("username")) } if c.IsSet("id") && user.ID != c.Int64("id") { - return fmt.Errorf("The user %s does not match the provided id %d", user.Name, c.Int64("id")) + return fmt.Errorf("the user %s does not match the provided id %d", user.Name, c.Int64("id")) } return user_service.DeleteUser(ctx, user, c.Bool("purge")) diff --git a/cmd/admin_user_delete_test.go b/cmd/admin_user_delete_test.go new file mode 100644 index 00000000000..b68b358152a --- /dev/null +++ b/cmd/admin_user_delete_test.go @@ -0,0 +1,111 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package cmd + +import ( + "strconv" + "strings" + "testing" + + auth_model "code.gitea.io/gitea/models/auth" + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + + "github.com/stretchr/testify/require" +) + +func TestAdminUserDelete(t *testing.T) { + ctx := t.Context() + defer func() { + require.NoError(t, db.TruncateBeans(t.Context(), &user_model.User{})) + require.NoError(t, db.TruncateBeans(t.Context(), &user_model.EmailAddress{})) + require.NoError(t, db.TruncateBeans(t.Context(), &auth_model.AccessToken{})) + }() + + setupTestUser := func(t *testing.T) { + unittest.AssertNotExistsBean(t, &user_model.User{LowerName: "testuser"}) + err := microcmdUserCreate().Run(t.Context(), []string{"create", "--username", "testuser", "--email", "testuser@gitea.local", "--random-password"}) + require.NoError(t, err) + } + + t.Run("delete user by id", func(t *testing.T) { + setupTestUser(t) + + u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"}) + err := microcmdUserDelete().Run(ctx, []string{"delete-test", "--id", strconv.FormatInt(u.ID, 10)}) + require.NoError(t, err) + unittest.AssertNotExistsBean(t, &user_model.User{LowerName: "testuser"}) + }) + t.Run("delete user by username", func(t *testing.T) { + setupTestUser(t) + + err := microcmdUserDelete().Run(ctx, []string{"delete-test", "--username", "testuser"}) + require.NoError(t, err) + unittest.AssertNotExistsBean(t, &user_model.User{LowerName: "testuser"}) + }) + t.Run("delete user by email", func(t *testing.T) { + setupTestUser(t) + + err := microcmdUserDelete().Run(ctx, []string{"delete-test", "--email", "testuser@gitea.local"}) + require.NoError(t, err) + unittest.AssertNotExistsBean(t, &user_model.User{LowerName: "testuser"}) + }) + t.Run("delete user by all 3 attributes", func(t *testing.T) { + setupTestUser(t) + + u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"}) + err := microcmdUserDelete().Run(ctx, []string{"delete", "--id", strconv.FormatInt(u.ID, 10), "--username", "testuser", "--email", "testuser@gitea.local"}) + require.NoError(t, err) + unittest.AssertNotExistsBean(t, &user_model.User{LowerName: "testuser"}) + }) +} + +func TestAdminUserDeleteFailure(t *testing.T) { + testCases := []struct { + name string + args []string + expectedErr string + }{ + { + name: "no user to delete", + args: []string{"delete", "--username", "nonexistentuser"}, + expectedErr: "user does not exist", + }, + { + name: "user exists but provided username does not match", + args: []string{"delete", "--email", "testuser@gitea.local", "--username", "wrongusername"}, + expectedErr: "the user testuser who has email testuser@gitea.local does not match the provided username wrongusername", + }, + { + name: "user exists but provided id does not match", + args: []string{"delete", "--username", "testuser", "--id", "999"}, + expectedErr: "the user testuser does not match the provided id 999", + }, + { + name: "no required flags are provided", + args: []string{"delete"}, + expectedErr: "You must provide the id, username or email of a user to delete", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ctx := t.Context() + if strings.Contains(tc.name, "user exists") { + unittest.AssertNotExistsBean(t, &user_model.User{LowerName: "testuser"}) + err := microcmdUserCreate().Run(t.Context(), []string{"create", "--username", "testuser", "--email", "testuser@gitea.local", "--random-password"}) + require.NoError(t, err) + } + + err := microcmdUserDelete().Run(ctx, tc.args) + require.Error(t, err) + require.Contains(t, err.Error(), tc.expectedErr) + }) + + require.NoError(t, db.TruncateBeans(t.Context(), &user_model.User{})) + require.NoError(t, db.TruncateBeans(t.Context(), &user_model.EmailAddress{})) + require.NoError(t, db.TruncateBeans(t.Context(), &auth_model.AccessToken{})) + } +} diff --git a/cmd/admin_user_generate_access_token.go b/cmd/admin_user_generate_access_token.go index 6c2c10494ee..61064fdef4b 100644 --- a/cmd/admin_user_generate_access_token.go +++ b/cmd/admin_user_generate_access_token.go @@ -4,13 +4,14 @@ package cmd import ( + "context" "errors" "fmt" auth_model "code.gitea.io/gitea/models/auth" user_model "code.gitea.io/gitea/models/user" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) var microcmdUserGenerateAccessToken = &cli.Command{ @@ -34,21 +35,18 @@ var microcmdUserGenerateAccessToken = &cli.Command{ }, &cli.StringFlag{ Name: "scopes", - Value: "", - Usage: "Comma separated list of scopes to apply to access token", + Value: "all", + Usage: `Comma separated list of scopes to apply to access token, examples: "all", "public-only,read:issue", "write:repository,write:user"`, }, }, Action: runGenerateAccessToken, } -func runGenerateAccessToken(c *cli.Context) error { +func runGenerateAccessToken(ctx context.Context, c *cli.Command) error { if !c.IsSet("username") { - return errors.New("You must provide a username to generate a token for") + return errors.New("you must provide a username to generate a token for") } - ctx, cancel := installSignals() - defer cancel() - if err := initDB(ctx); err != nil { return err } @@ -77,6 +75,9 @@ func runGenerateAccessToken(c *cli.Context) error { if err != nil { return fmt.Errorf("invalid access token scope provided: %w", err) } + if !accessTokenScope.HasPermissionScope() { + return errors.New("access token does not have any permission") + } t.Scope = accessTokenScope // create the token diff --git a/cmd/admin_user_list.go b/cmd/admin_user_list.go index 4c2b26d1dfd..e3d345e2f24 100644 --- a/cmd/admin_user_list.go +++ b/cmd/admin_user_list.go @@ -4,13 +4,14 @@ package cmd import ( + "context" "fmt" "os" "text/tabwriter" user_model "code.gitea.io/gitea/models/user" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) var microcmdUserList = &cli.Command{ @@ -25,10 +26,7 @@ var microcmdUserList = &cli.Command{ }, } -func runListUsers(c *cli.Context) error { - ctx, cancel := installSignals() - defer cancel() - +func runListUsers(ctx context.Context, c *cli.Command) error { if err := initDB(ctx); err != nil { return err } diff --git a/cmd/admin_user_must_change_password.go b/cmd/admin_user_must_change_password.go index 2794414259a..8521853dc19 100644 --- a/cmd/admin_user_must_change_password.go +++ b/cmd/admin_user_must_change_password.go @@ -4,40 +4,41 @@ package cmd import ( + "context" "errors" "fmt" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/setting" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) -var microcmdUserMustChangePassword = &cli.Command{ - Name: "must-change-password", - Usage: "Set the must change password flag for the provided users or all users", - Action: runMustChangePassword, - Flags: []cli.Flag{ - &cli.BoolFlag{ - Name: "all", - Aliases: []string{"A"}, - Usage: "All users must change password, except those explicitly excluded with --exclude", +func microcmdUserMustChangePassword() *cli.Command { + return &cli.Command{ + Name: "must-change-password", + Usage: "Set the must change password flag for the provided users or all users", + Action: runMustChangePassword, + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "all", + Aliases: []string{"A"}, + Usage: "All users must change password, except those explicitly excluded with --exclude", + }, + &cli.StringSliceFlag{ + Name: "exclude", + Aliases: []string{"e"}, + Usage: "Do not change the must-change-password flag for these users", + }, + &cli.BoolFlag{ + Name: "unset", + Usage: "Instead of setting the must-change-password flag, unset it", + }, }, - &cli.StringSliceFlag{ - Name: "exclude", - Aliases: []string{"e"}, - Usage: "Do not change the must-change-password flag for these users", - }, - &cli.BoolFlag{ - Name: "unset", - Usage: "Instead of setting the must-change-password flag, unset it", - }, - }, + } } -func runMustChangePassword(c *cli.Context) error { - ctx, cancel := installSignals() - defer cancel() - +func runMustChangePassword(ctx context.Context, c *cli.Command) error { if c.NArg() == 0 && !c.IsSet("all") { return errors.New("either usernames or --all must be provided") } @@ -46,8 +47,10 @@ func runMustChangePassword(c *cli.Context) error { all := c.Bool("all") exclude := c.StringSlice("exclude") - if err := initDB(ctx); err != nil { - return err + if !setting.IsInTesting { + if err := initDB(ctx); err != nil { + return err + } } n, err := user_model.SetMustChangePassword(ctx, all, mustChangePassword, c.Args().Slice(), exclude) diff --git a/cmd/admin_user_must_change_password_test.go b/cmd/admin_user_must_change_password_test.go new file mode 100644 index 00000000000..efdbe3a9ee9 --- /dev/null +++ b/cmd/admin_user_must_change_password_test.go @@ -0,0 +1,78 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package cmd + +import ( + "testing" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestMustChangePassword(t *testing.T) { + defer func() { + require.NoError(t, db.TruncateBeans(t.Context(), &user_model.User{})) + }() + err := microcmdUserCreate().Run(t.Context(), []string{"create", "--username", "testuser", "--email", "testuser@gitea.local", "--random-password"}) + require.NoError(t, err) + err = microcmdUserCreate().Run(t.Context(), []string{"create", "--username", "testuserexclude", "--email", "testuserexclude@gitea.local", "--random-password"}) + require.NoError(t, err) + // Reset password change flag + err = microcmdUserMustChangePassword().Run(t.Context(), []string{"change-test", "--all", "--unset"}) + require.NoError(t, err) + + testUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"}) + assert.False(t, testUser.MustChangePassword) + testUserExclude := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuserexclude"}) + assert.False(t, testUserExclude.MustChangePassword) + + // Make all users change password + err = microcmdUserMustChangePassword().Run(t.Context(), []string{"change-test", "--all"}) + require.NoError(t, err) + + testUser = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"}) + assert.True(t, testUser.MustChangePassword) + testUserExclude = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuserexclude"}) + assert.True(t, testUserExclude.MustChangePassword) + + // Reset password change flag but exclude all tested users + err = microcmdUserMustChangePassword().Run(t.Context(), []string{"change-test", "--all", "--unset", "--exclude", "testuser,testuserexclude"}) + require.NoError(t, err) + + testUser = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"}) + assert.True(t, testUser.MustChangePassword) + testUserExclude = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuserexclude"}) + assert.True(t, testUserExclude.MustChangePassword) + + // Reset password change flag by listing multiple users + err = microcmdUserMustChangePassword().Run(t.Context(), []string{"change-test", "--unset", "testuser", "testuserexclude"}) + require.NoError(t, err) + + testUser = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"}) + assert.False(t, testUser.MustChangePassword) + testUserExclude = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuserexclude"}) + assert.False(t, testUserExclude.MustChangePassword) + + // Exclude a user from all user + err = microcmdUserMustChangePassword().Run(t.Context(), []string{"change-test", "--all", "--exclude", "testuserexclude"}) + require.NoError(t, err) + + testUser = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"}) + assert.True(t, testUser.MustChangePassword) + testUserExclude = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuserexclude"}) + assert.False(t, testUserExclude.MustChangePassword) + + // Unset a flag for single user + err = microcmdUserMustChangePassword().Run(t.Context(), []string{"change-test", "--unset", "testuser"}) + require.NoError(t, err) + + testUser = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"}) + assert.False(t, testUser.MustChangePassword) + testUserExclude = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuserexclude"}) + assert.False(t, testUserExclude.MustChangePassword) +} diff --git a/cmd/cert.go b/cmd/cert.go index 38241d71a33..53b4f9dcb4c 100644 --- a/cmd/cert.go +++ b/cmd/cert.go @@ -6,6 +6,7 @@ package cmd import ( + "context" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" @@ -13,6 +14,7 @@ import ( "crypto/x509" "crypto/x509/pkix" "encoding/pem" + "fmt" "log" "math/big" "net" @@ -20,47 +22,59 @@ import ( "strings" "time" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) -// CmdCert represents the available cert sub-command. -var CmdCert = &cli.Command{ - Name: "cert", - Usage: "Generate self-signed certificate", - Description: `Generate a self-signed X.509 certificate for a TLS server. +// cmdCert represents the available cert sub-command. +func cmdCert() *cli.Command { + return &cli.Command{ + Name: "cert", + Usage: "Generate self-signed certificate", + Description: `Generate a self-signed X.509 certificate for a TLS server. Outputs to 'cert.pem' and 'key.pem' and will overwrite existing files.`, - Action: runCert, - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "host", - Value: "", - Usage: "Comma-separated hostnames and IPs to generate a certificate for", + Action: runCert, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "host", + Usage: "Comma-separated hostnames and IPs to generate a certificate for", + Required: true, + }, + &cli.StringFlag{ + Name: "ecdsa-curve", + Value: "", + Usage: "ECDSA curve to use to generate a key. Valid values are P224, P256, P384, P521", + }, + &cli.IntFlag{ + Name: "rsa-bits", + Value: 3072, + Usage: "Size of RSA key to generate. Ignored if --ecdsa-curve is set", + }, + &cli.StringFlag{ + Name: "start-date", + Value: "", + Usage: "Creation date formatted as Jan 1 15:04:05 2011", + }, + &cli.DurationFlag{ + Name: "duration", + Value: 365 * 24 * time.Hour, + Usage: "Duration that certificate is valid for", + }, + &cli.BoolFlag{ + Name: "ca", + Usage: "whether this cert should be its own Certificate Authority", + }, + &cli.StringFlag{ + Name: "out", + Value: "cert.pem", + Usage: "Path to the file where there certificate will be saved", + }, + &cli.StringFlag{ + Name: "keyout", + Value: "key.pem", + Usage: "Path to the file where there certificate key will be saved", + }, }, - &cli.StringFlag{ - Name: "ecdsa-curve", - Value: "", - Usage: "ECDSA curve to use to generate a key. Valid values are P224, P256, P384, P521", - }, - &cli.IntFlag{ - Name: "rsa-bits", - Value: 3072, - Usage: "Size of RSA key to generate. Ignored if --ecdsa-curve is set", - }, - &cli.StringFlag{ - Name: "start-date", - Value: "", - Usage: "Creation date formatted as Jan 1 15:04:05 2011", - }, - &cli.DurationFlag{ - Name: "duration", - Value: 365 * 24 * time.Hour, - Usage: "Duration that certificate is valid for", - }, - &cli.BoolFlag{ - Name: "ca", - Usage: "whether this cert should be its own Certificate Authority", - }, - }, + } } func publicKey(priv any) any { @@ -89,11 +103,7 @@ func pemBlockForKey(priv any) *pem.Block { } } -func runCert(c *cli.Context) error { - if err := argsSet(c, "host"); err != nil { - return err - } - +func runCert(_ context.Context, c *cli.Command) error { var priv any var err error switch c.String("ecdsa-curve") { @@ -108,17 +118,17 @@ func runCert(c *cli.Context) error { case "P521": priv, err = ecdsa.GenerateKey(elliptic.P521(), rand.Reader) default: - log.Fatalf("Unrecognized elliptic curve: %q", c.String("ecdsa-curve")) + err = fmt.Errorf("unrecognized elliptic curve: %q", c.String("ecdsa-curve")) } if err != nil { - log.Fatalf("Failed to generate private key: %v", err) + return fmt.Errorf("failed to generate private key: %w", err) } var notBefore time.Time if startDate := c.String("start-date"); startDate != "" { notBefore, err = time.Parse("Jan 2 15:04:05 2006", startDate) if err != nil { - log.Fatalf("Failed to parse creation date: %v", err) + return fmt.Errorf("failed to parse creation date %w", err) } } else { notBefore = time.Now() @@ -129,7 +139,7 @@ func runCert(c *cli.Context) error { serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) if err != nil { - log.Fatalf("Failed to generate serial number: %v", err) + return fmt.Errorf("failed to generate serial number: %w", err) } template := x509.Certificate{ @@ -146,8 +156,8 @@ func runCert(c *cli.Context) error { BasicConstraintsValid: true, } - hosts := strings.Split(c.String("host"), ",") - for _, h := range hosts { + hosts := strings.SplitSeq(c.String("host"), ",") + for h := range hosts { if ip := net.ParseIP(h); ip != nil { template.IPAddresses = append(template.IPAddresses, ip) } else { @@ -162,35 +172,35 @@ func runCert(c *cli.Context) error { derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, publicKey(priv), priv) if err != nil { - log.Fatalf("Failed to create certificate: %v", err) + return fmt.Errorf("failed to create certificate: %w", err) } - certOut, err := os.Create("cert.pem") + certOut, err := os.Create(c.String("out")) if err != nil { - log.Fatalf("Failed to open cert.pem for writing: %v", err) + return fmt.Errorf("failed to open %s for writing: %w", c.String("keyout"), err) } err = pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) if err != nil { - log.Fatalf("Failed to encode certificate: %v", err) + return fmt.Errorf("failed to encode certificate: %w", err) } err = certOut.Close() if err != nil { - log.Fatalf("Failed to write cert: %v", err) + return fmt.Errorf("failed to write cert: %w", err) } - log.Println("Written cert.pem") + fmt.Fprintf(c.Writer, "Written cert to %s\n", c.String("out")) - keyOut, err := os.OpenFile("key.pem", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600) + keyOut, err := os.OpenFile(c.String("keyout"), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600) if err != nil { - log.Fatalf("Failed to open key.pem for writing: %v", err) + return fmt.Errorf("failed to open %s for writing: %w", c.String("keyout"), err) } err = pem.Encode(keyOut, pemBlockForKey(priv)) if err != nil { - log.Fatalf("Failed to encode key: %v", err) + return fmt.Errorf("failed to encode key: %w", err) } err = keyOut.Close() if err != nil { - log.Fatalf("Failed to write key: %v", err) + return fmt.Errorf("failed to write key: %w", err) } - log.Println("Written key.pem") + fmt.Fprintf(c.Writer, "Written key to %s\n", c.String("keyout")) return nil } diff --git a/cmd/cert_test.go b/cmd/cert_test.go new file mode 100644 index 00000000000..4242d8915b3 --- /dev/null +++ b/cmd/cert_test.go @@ -0,0 +1,123 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package cmd + +import ( + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCertCommand(t *testing.T) { + cases := []struct { + name string + args []string + }{ + { + name: "RSA cert generation", + args: []string{ + "cert-test", + "--host", "localhost", + "--rsa-bits", "2048", + "--duration", "1h", + "--start-date", "Jan 1 00:00:00 2024", + }, + }, + { + name: "ECDSA cert generation", + args: []string{ + "cert-test", + "--host", "localhost", + "--ecdsa-curve", "P256", + "--duration", "1h", + "--start-date", "Jan 1 00:00:00 2024", + }, + }, + { + name: "mixed host, certificate authority", + args: []string{ + "cert-test", + "--host", "localhost,127.0.0.1", + "--duration", "1h", + "--start-date", "Jan 1 00:00:00 2024", + }, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + app := cmdCert() + tempDir := t.TempDir() + + certFile := filepath.Join(tempDir, "cert.pem") + keyFile := filepath.Join(tempDir, "key.pem") + + err := app.Run(t.Context(), append(c.args, "--out", certFile, "--keyout", keyFile)) + require.NoError(t, err) + + assert.FileExists(t, certFile) + assert.FileExists(t, keyFile) + }) + } +} + +func TestCertCommandFailures(t *testing.T) { + cases := []struct { + name string + args []string + errMsg string + }{ + { + name: "Start Date Parsing failure", + args: []string{ + "cert-test", + "--host", "localhost", + "--start-date", "invalid-date", + }, + errMsg: "parsing time", + }, + { + name: "Unknown curve", + args: []string{ + "cert-test", + "--host", "localhost", + "--ecdsa-curve", "invalid-curve", + }, + errMsg: "unrecognized elliptic curve", + }, + { + name: "Key generation failure", + args: []string{ + "cert-test", + "--host", "localhost", + "--rsa-bits", "invalid-bits", + }, + }, + { + name: "Missing parameters", + args: []string{ + "cert-test", + }, + errMsg: `"host" not set`, + }, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + app := cmdCert() + tempDir := t.TempDir() + + certFile := filepath.Join(tempDir, "cert.pem") + keyFile := filepath.Join(tempDir, "key.pem") + err := app.Run(t.Context(), append(c.args, "--out", certFile, "--keyout", keyFile)) + require.Error(t, err) + if c.errMsg != "" { + assert.ErrorContains(t, err, c.errMsg) + } + assert.NoFileExists(t, certFile) + assert.NoFileExists(t, keyFile) + }) + } +} diff --git a/cmd/cmd.go b/cmd/cmd.go index 423dce26748..5b96bcbf9a9 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -18,20 +18,19 @@ import ( "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/util" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) // argsSet checks that all the required arguments are set. args is a list of // arguments that must be set in the passed Context. -func argsSet(c *cli.Context, args ...string) error { +func argsSet(c *cli.Command, args ...string) error { for _, a := range args { if !c.IsSet(a) { return errors.New(a + " is not set") } - if util.IsEmptyString(c.String(a)) { + if c.Value(a) == nil { return errors.New(a + " is required") } } @@ -109,7 +108,7 @@ func setupConsoleLogger(level log.Level, colorize bool, out io.Writer) { log.GetManager().GetLogger(log.DEFAULT).ReplaceAllWriters(writer) } -func globalBool(c *cli.Context, name string) bool { +func globalBool(c *cli.Command, name string) bool { for _, ctx := range c.Lineage() { if ctx.Bool(name) { return true @@ -120,8 +119,8 @@ func globalBool(c *cli.Context, name string) bool { // PrepareConsoleLoggerLevel by default, use INFO level for console logger, but some sub-commands (for git/ssh protocol) shouldn't output any log to stdout. // Any log appears in git stdout pipe will break the git protocol, eg: client can't push and hangs forever. -func PrepareConsoleLoggerLevel(defaultLevel log.Level) func(*cli.Context) error { - return func(c *cli.Context) error { +func PrepareConsoleLoggerLevel(defaultLevel log.Level) func(context.Context, *cli.Command) (context.Context, error) { + return func(ctx context.Context, c *cli.Command) (context.Context, error) { level := defaultLevel if globalBool(c, "quiet") { level = log.FATAL @@ -130,6 +129,16 @@ func PrepareConsoleLoggerLevel(defaultLevel log.Level) func(*cli.Context) error level = log.TRACE } log.SetConsoleLogger(log.DEFAULT, "console-default", level) - return nil + return ctx, nil } } + +func isValidDefaultSubCommand(cmd *cli.Command) (string, bool) { + // Dirty patch for urfave/cli's strange design. + // "./gitea bad-cmd" should not start the web server. + rootArgs := cmd.Root().Args().Slice() + if len(rootArgs) != 0 && rootArgs[0] != cmd.Name { + return rootArgs[0], false + } + return "", true +} diff --git a/cmd/cmd_test.go b/cmd/cmd_test.go new file mode 100644 index 00000000000..a36d05c76e0 --- /dev/null +++ b/cmd/cmd_test.go @@ -0,0 +1,38 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package cmd + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/urfave/cli/v3" +) + +func TestDefaultCommand(t *testing.T) { + test := func(t *testing.T, args []string, expectedRetName string, expectedRetValid bool) { + called := false + cmd := &cli.Command{ + DefaultCommand: "test", + Commands: []*cli.Command{ + { + Name: "test", + Action: func(ctx context.Context, command *cli.Command) error { + retName, retValid := isValidDefaultSubCommand(command) + assert.Equal(t, expectedRetName, retName) + assert.Equal(t, expectedRetValid, retValid) + called = true + return nil + }, + }, + }, + } + assert.NoError(t, cmd.Run(t.Context(), args)) + assert.True(t, called) + } + test(t, []string{"./gitea"}, "", true) + test(t, []string{"./gitea", "test"}, "", true) + test(t, []string{"./gitea", "other"}, "other", false) +} diff --git a/cmd/docs.go b/cmd/docs.go index 605d02e3efe..098c0e9a8a1 100644 --- a/cmd/docs.go +++ b/cmd/docs.go @@ -4,11 +4,13 @@ package cmd import ( + "context" "fmt" "os" "strings" - "github.com/urfave/cli/v2" + cli_docs "github.com/urfave/cli-docs/v3" + "github.com/urfave/cli/v3" ) // CmdDocs represents the available docs sub-command. @@ -30,16 +32,16 @@ var CmdDocs = &cli.Command{ }, } -func runDocs(ctx *cli.Context) error { - docs, err := ctx.App.ToMarkdown() - if ctx.Bool("man") { - docs, err = ctx.App.ToMan() +func runDocs(_ context.Context, cmd *cli.Command) error { + docs, err := cli_docs.ToMarkdown(cmd.Root()) + if cmd.Bool("man") { + docs, err = cli_docs.ToMan(cmd.Root()) } if err != nil { return err } - if !ctx.Bool("man") { + if !cmd.Bool("man") { // Clean up markdown. The following bug was fixed in v2, but is present in v1. // It affects markdown output (even though the issue is referring to man pages) // https://github.com/urfave/cli/issues/1040 @@ -51,8 +53,8 @@ func runDocs(ctx *cli.Context) error { } out := os.Stdout - if ctx.String("output") != "" { - fi, err := os.Create(ctx.String("output")) + if cmd.String("output") != "" { + fi, err := os.Create(cmd.String("output")) if err != nil { return err } diff --git a/cmd/doctor.go b/cmd/doctor.go index e433f4adc5d..596dd611786 100644 --- a/cmd/doctor.go +++ b/cmd/doctor.go @@ -4,6 +4,7 @@ package cmd import ( + "context" "fmt" golog "log" "os" @@ -19,7 +20,7 @@ import ( "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/services/doctor" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" "xorm.io/xorm" ) @@ -29,7 +30,7 @@ var CmdDoctor = &cli.Command{ Usage: "Diagnose and optionally fix problems, convert or re-create database tables", Description: "A command to diagnose problems with the current Gitea instance according to the given configuration. Some problems can optionally be fixed by modifying the database or data storage.", - Subcommands: []*cli.Command{ + Commands: []*cli.Command{ cmdDoctorCheck, cmdRecreateTable, cmdDoctorConvert, @@ -92,16 +93,13 @@ You should back-up your database before doing this and ensure that your database Action: runRecreateTable, } -func runRecreateTable(ctx *cli.Context) error { - stdCtx, cancel := installSignals() - defer cancel() - +func runRecreateTable(ctx context.Context, cmd *cli.Command) error { // Redirect the default golog to here golog.SetFlags(0) golog.SetPrefix("") golog.SetOutput(log.LoggerToWriter(log.GetLogger(log.DEFAULT).Info)) - debug := ctx.Bool("debug") + debug := cmd.Bool("debug") setting.MustInstalled() setting.LoadDBSetting() @@ -112,15 +110,15 @@ func runRecreateTable(ctx *cli.Context) error { } setting.Database.LogSQL = debug - if err := db.InitEngine(stdCtx); err != nil { + if err := db.InitEngine(ctx); err != nil { fmt.Println(err) fmt.Println("Check if you are using the right config file. You can use a --config directive to specify one.") return nil } - args := ctx.Args() - names := make([]string, 0, ctx.NArg()) - for i := 0; i < ctx.NArg(); i++ { + args := cmd.Args() + names := make([]string, 0, cmd.NArg()) + for i := 0; i < cmd.NArg(); i++ { names = append(names, args.Get(i)) } @@ -130,24 +128,25 @@ func runRecreateTable(ctx *cli.Context) error { } recreateTables := migrate_base.RecreateTables(beans...) - return db.InitEngineWithMigration(stdCtx, func(x *xorm.Engine) error { - if err := migrations.EnsureUpToDate(x); err != nil { + return db.InitEngineWithMigration(context.Background(), func(ctx context.Context, x *xorm.Engine) error { + if err := migrations.EnsureUpToDate(ctx, x); err != nil { return err } return recreateTables(x) }) } -func setupDoctorDefaultLogger(ctx *cli.Context, colorize bool) { +func setupDoctorDefaultLogger(cmd *cli.Command, colorize bool) { // Silence the default loggers setupConsoleLogger(log.FATAL, log.CanColorStderr, os.Stderr) - logFile := ctx.String("log-file") - if logFile == "" { + logFile := cmd.String("log-file") + switch logFile { + case "": return // if no doctor log-file is set, do not show any log from default logger - } else if logFile == "-" { + case "-": setupConsoleLogger(log.TRACE, colorize, os.Stdout) - } else { + default: logFile, _ = filepath.Abs(logFile) writeMode := log.WriterMode{Level: log.TRACE, WriterOption: log.WriterFileOption{FileName: logFile}} writer, err := log.NewEventWriter("console-to-file", "file", writeMode) @@ -159,23 +158,20 @@ func setupDoctorDefaultLogger(ctx *cli.Context, colorize bool) { } } -func runDoctorCheck(ctx *cli.Context) error { - stdCtx, cancel := installSignals() - defer cancel() - +func runDoctorCheck(ctx context.Context, cmd *cli.Command) error { colorize := log.CanColorStdout - if ctx.IsSet("color") { - colorize = ctx.Bool("color") + if cmd.IsSet("color") { + colorize = cmd.Bool("color") } - setupDoctorDefaultLogger(ctx, colorize) + setupDoctorDefaultLogger(cmd, colorize) // Finally redirect the default golang's log to here golog.SetFlags(0) golog.SetPrefix("") golog.SetOutput(log.LoggerToWriter(log.GetLogger(log.DEFAULT).Info)) - if ctx.IsSet("list") { + if cmd.IsSet("list") { w := tabwriter.NewWriter(os.Stdout, 0, 8, 1, '\t', 0) _, _ = w.Write([]byte("Default\tName\tTitle\n")) doctor.SortChecks(doctor.Checks) @@ -193,12 +189,12 @@ func runDoctorCheck(ctx *cli.Context) error { } var checks []*doctor.Check - if ctx.Bool("all") { + if cmd.Bool("all") { checks = make([]*doctor.Check, len(doctor.Checks)) copy(checks, doctor.Checks) - } else if ctx.IsSet("run") { - addDefault := ctx.Bool("default") - runNamesSet := container.SetOf(ctx.StringSlice("run")...) + } else if cmd.IsSet("run") { + addDefault := cmd.Bool("default") + runNamesSet := container.SetOf(cmd.StringSlice("run")...) for _, check := range doctor.Checks { if (addDefault && check.IsDefault) || runNamesSet.Contains(check.Name) { checks = append(checks, check) @@ -215,5 +211,5 @@ func runDoctorCheck(ctx *cli.Context) error { } } } - return doctor.RunChecks(stdCtx, colorize, ctx.Bool("fix"), checks) + return doctor.RunChecks(ctx, colorize, cmd.Bool("fix"), checks) } diff --git a/cmd/doctor_convert.go b/cmd/doctor_convert.go index 48c835ad0e2..8cb718d3839 100644 --- a/cmd/doctor_convert.go +++ b/cmd/doctor_convert.go @@ -4,13 +4,14 @@ package cmd import ( + "context" "fmt" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) // cmdDoctorConvert represents the available convert sub-command. @@ -21,11 +22,8 @@ var cmdDoctorConvert = &cli.Command{ Action: runDoctorConvert, } -func runDoctorConvert(ctx *cli.Context) error { - stdCtx, cancel := installSignals() - defer cancel() - - if err := initDB(stdCtx); err != nil { +func runDoctorConvert(ctx context.Context, cmd *cli.Command) error { + if err := initDB(ctx); err != nil { return err } diff --git a/cmd/doctor_test.go b/cmd/doctor_test.go index 3e1ff299c5a..da942b38b60 100644 --- a/cmd/doctor_test.go +++ b/cmd/doctor_test.go @@ -11,7 +11,7 @@ import ( "code.gitea.io/gitea/services/doctor" "github.com/stretchr/testify/assert" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) func TestDoctorRun(t *testing.T) { @@ -22,12 +22,13 @@ func TestDoctorRun(t *testing.T) { SkipDatabaseInitialization: true, }) - app := cli.NewApp() - app.Commands = []*cli.Command{cmdDoctorCheck} - err := app.Run([]string{"./gitea", "check", "--run", "test-check"}) + app := &cli.Command{ + Commands: []*cli.Command{cmdDoctorCheck}, + } + err := app.Run(t.Context(), []string{"./gitea", "check", "--run", "test-check"}) assert.NoError(t, err) - err = app.Run([]string{"./gitea", "check", "--run", "no-such"}) + err = app.Run(t.Context(), []string{"./gitea", "check", "--run", "no-such"}) assert.ErrorContains(t, err, `unknown checks: "no-such"`) - err = app.Run([]string{"./gitea", "check", "--run", "test-check,no-such"}) + err = app.Run(t.Context(), []string{"./gitea", "check", "--run", "test-check,no-such"}) assert.ErrorContains(t, err, `unknown checks: "no-such"`) } diff --git a/cmd/dump.go b/cmd/dump.go index ececc80f725..ed19e3d4bf0 100644 --- a/cmd/dump.go +++ b/cmd/dump.go @@ -5,7 +5,7 @@ package cmd import ( - "fmt" + "context" "os" "path" "path/filepath" @@ -21,7 +21,7 @@ import ( "gitea.com/go-chi/session" "github.com/mholt/archiver/v3" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) // CmdDump represents the available dump sub-command. @@ -93,7 +93,7 @@ var CmdDump = &cli.Command{ }, &cli.StringFlag{ Name: "type", - Usage: fmt.Sprintf(`Dump output format, default to "zip", supported types: %s`, strings.Join(dump.SupportedOutputTypes, ", ")), + Usage: `Dump output format, default to "zip", supported types: ` + strings.Join(dump.SupportedOutputTypes, ", "), }, }, } @@ -102,17 +102,17 @@ func fatal(format string, args ...any) { log.Fatal(format, args...) } -func runDump(ctx *cli.Context) error { +func runDump(ctx context.Context, cmd *cli.Command) error { setting.MustInstalled() - quite := ctx.Bool("quiet") - verbose := ctx.Bool("verbose") + quite := cmd.Bool("quiet") + verbose := cmd.Bool("verbose") if verbose && quite { fatal("Option --quiet and --verbose cannot both be set") } // outFileName is either "-" or a file name (will be made absolute) - outFileName, outType := dump.PrepareFileNameAndType(ctx.String("file"), ctx.String("type")) + outFileName, outType := dump.PrepareFileNameAndType(cmd.String("file"), cmd.String("type")) if outType == "" { fatal("Invalid output type") } @@ -137,10 +137,7 @@ func runDump(ctx *cli.Context) error { setting.DisableLoggerInit() setting.LoadSettings() // cannot access session settings otherwise - stdCtx, cancel := installSignals() - defer cancel() - - err := db.InitEngine(stdCtx) + err := db.InitEngine(ctx) if err != nil { return err } @@ -166,7 +163,7 @@ func runDump(ctx *cli.Context) error { } dumper.GlobalExcludeAbsPath(outFileName) - if ctx.IsSet("skip-repository") && ctx.Bool("skip-repository") { + if cmd.IsSet("skip-repository") && cmd.Bool("skip-repository") { log.Info("Skip dumping local repositories") } else { log.Info("Dumping local repositories... %s", setting.RepoRootPath) @@ -174,7 +171,7 @@ func runDump(ctx *cli.Context) error { fatal("Failed to include repositories: %v", err) } - if ctx.IsSet("skip-lfs-data") && ctx.Bool("skip-lfs-data") { + if cmd.IsSet("skip-lfs-data") && cmd.Bool("skip-lfs-data") { log.Info("Skip dumping LFS data") } else if !setting.LFS.StartServer { log.Info("LFS isn't enabled. Skip dumping LFS data") @@ -189,12 +186,12 @@ func runDump(ctx *cli.Context) error { } } - if ctx.Bool("skip-db") { + if cmd.Bool("skip-db") { // Ensure that we don't dump the database file that may reside in setting.AppDataPath or elsewhere. dumper.GlobalExcludeAbsPath(setting.Database.Path) log.Info("Skipping database") } else { - tmpDir := ctx.String("tempdir") + tmpDir := cmd.String("tempdir") if _, err := os.Stat(tmpDir); os.IsNotExist(err) { fatal("Path does not exist: %s", tmpDir) } @@ -210,7 +207,7 @@ func runDump(ctx *cli.Context) error { } }() - targetDBType := ctx.String("database") + targetDBType := cmd.String("database") if len(targetDBType) > 0 && targetDBType != setting.Database.Type.String() { log.Info("Dumping database %s => %s...", setting.Database.Type, targetDBType) } else { @@ -231,7 +228,7 @@ func runDump(ctx *cli.Context) error { fatal("Failed to include specified app.ini: %v", err) } - if ctx.IsSet("skip-custom-dir") && ctx.Bool("skip-custom-dir") { + if cmd.IsSet("skip-custom-dir") && cmd.Bool("skip-custom-dir") { log.Info("Skipping custom directory") } else { customDir, err := os.Stat(setting.CustomPath) @@ -264,7 +261,7 @@ func runDump(ctx *cli.Context) error { excludes = append(excludes, opts.ProviderConfig) } - if ctx.IsSet("skip-index") && ctx.Bool("skip-index") { + if cmd.IsSet("skip-index") && cmd.Bool("skip-index") { excludes = append(excludes, setting.Indexer.RepoPath) excludes = append(excludes, setting.Indexer.IssuePath) } @@ -279,7 +276,7 @@ func runDump(ctx *cli.Context) error { } } - if ctx.IsSet("skip-attachment-data") && ctx.Bool("skip-attachment-data") { + if cmd.IsSet("skip-attachment-data") && cmd.Bool("skip-attachment-data") { log.Info("Skip dumping attachment data") } else if err := storage.Attachments.IterateObjects("", func(objPath string, object storage.Object) error { info, err := object.Stat() @@ -291,7 +288,7 @@ func runDump(ctx *cli.Context) error { fatal("Failed to dump attachments: %v", err) } - if ctx.IsSet("skip-package-data") && ctx.Bool("skip-package-data") { + if cmd.IsSet("skip-package-data") && cmd.Bool("skip-package-data") { log.Info("Skip dumping package data") } else if !setting.Packages.Enabled { log.Info("Packages isn't enabled. Skip dumping package data") @@ -308,7 +305,7 @@ func runDump(ctx *cli.Context) error { // Doesn't check if LogRootPath exists before processing --skip-log intentionally, // ensuring that it's clear the dump is skipped whether the directory's initialized // yet or not. - if ctx.IsSet("skip-log") && ctx.Bool("skip-log") { + if cmd.IsSet("skip-log") && cmd.Bool("skip-log") { log.Info("Skip dumping log files") } else { isExist, err := util.IsExist(setting.Log.RootPath) diff --git a/cmd/dump_repo.go b/cmd/dump_repo.go index 3a24cf6c5f0..beda305c85f 100644 --- a/cmd/dump_repo.go +++ b/cmd/dump_repo.go @@ -19,7 +19,7 @@ import ( "code.gitea.io/gitea/services/convert" "code.gitea.io/gitea/services/migrations" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) // CmdDumpRepository represents the available dump repository sub-command. @@ -79,16 +79,18 @@ wiki, issues, labels, releases, release_assets, milestones, pull_requests, comme }, } -func runDumpRepository(ctx *cli.Context) error { - stdCtx, cancel := installSignals() - defer cancel() +func runDumpRepository(ctx context.Context, cmd *cli.Command) error { + setupConsoleLogger(log.INFO, log.CanColorStderr, os.Stderr) - if err := initDB(stdCtx); err != nil { + setting.DisableLoggerInit() + setting.LoadSettings() // cannot access skip_tls_verify settings otherwise + + if err := initDB(ctx); err != nil { return err } // migrations.GiteaLocalUploader depends on git module - if err := git.InitSimple(context.Background()); err != nil { + if err := git.InitSimple(); err != nil { return err } @@ -100,8 +102,8 @@ func runDumpRepository(ctx *cli.Context) error { var ( serviceType structs.GitServiceType - cloneAddr = ctx.String("clone_addr") - serviceStr = ctx.String("git_service") + cloneAddr = cmd.String("clone_addr") + serviceStr = cmd.String("git_service") ) if strings.HasPrefix(strings.ToLower(cloneAddr), "https://github.com/") { @@ -119,13 +121,13 @@ func runDumpRepository(ctx *cli.Context) error { opts := base.MigrateOptions{ GitServiceType: serviceType, CloneAddr: cloneAddr, - AuthUsername: ctx.String("auth_username"), - AuthPassword: ctx.String("auth_password"), - AuthToken: ctx.String("auth_token"), - RepoName: ctx.String("repo_name"), + AuthUsername: cmd.String("auth_username"), + AuthPassword: cmd.String("auth_password"), + AuthToken: cmd.String("auth_token"), + RepoName: cmd.String("repo_name"), } - if len(ctx.String("units")) == 0 { + if len(cmd.String("units")) == 0 { opts.Wiki = true opts.Issues = true opts.Milestones = true @@ -135,8 +137,8 @@ func runDumpRepository(ctx *cli.Context) error { opts.PullRequests = true opts.ReleaseAssets = true } else { - units := strings.Split(ctx.String("units"), ",") - for _, unit := range units { + units := strings.SplitSeq(cmd.String("units"), ",") + for unit := range units { switch strings.ToLower(strings.TrimSpace(unit)) { case "": continue @@ -164,7 +166,7 @@ func runDumpRepository(ctx *cli.Context) error { // the repo_dir will be removed if error occurs in DumpRepository // make sure the directory doesn't exist or is empty, prevent from deleting user files - repoDir := ctx.String("repo_dir") + repoDir := cmd.String("repo_dir") if exists, err := util.IsExist(repoDir); err != nil { return fmt.Errorf("unable to stat repo_dir %q: %w", repoDir, err) } else if exists { @@ -177,9 +179,9 @@ func runDumpRepository(ctx *cli.Context) error { } if err := migrations.DumpRepository( - context.Background(), + ctx, repoDir, - ctx.String("owner_name"), + cmd.String("owner_name"), opts, ); err != nil { log.Fatal("Failed to dump repository: %v", err) diff --git a/cmd/embedded.go b/cmd/embedded.go index 9f03f7be7c4..1908352453f 100644 --- a/cmd/embedded.go +++ b/cmd/embedded.go @@ -4,6 +4,7 @@ package cmd import ( + "context" "errors" "fmt" "os" @@ -19,7 +20,7 @@ import ( "code.gitea.io/gitea/modules/util" "github.com/gobwas/glob" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) // CmdEmbedded represents the available extract sub-command. @@ -28,7 +29,7 @@ var ( Name: "embedded", Usage: "Extract embedded resources", Description: "A command for extracting embedded resources, like templates and images", - Subcommands: []*cli.Command{ + Commands: []*cli.Command{ subcmdList, subcmdView, subcmdExtract, @@ -100,7 +101,7 @@ type assetFile struct { path string } -func initEmbeddedExtractor(c *cli.Context) error { +func initEmbeddedExtractor(c *cli.Command) error { setupConsoleLogger(log.ERROR, log.CanColorStderr, os.Stderr) patterns, err := compileCollectPatterns(c.Args().Slice()) @@ -115,31 +116,31 @@ func initEmbeddedExtractor(c *cli.Context) error { return nil } -func runList(c *cli.Context) error { +func runList(_ context.Context, c *cli.Command) error { if err := runListDo(c); err != nil { - fmt.Fprintf(os.Stderr, "%v\n", err) + _, _ = fmt.Fprintf(os.Stderr, "%v\n", err) return err } return nil } -func runView(c *cli.Context) error { +func runView(_ context.Context, c *cli.Command) error { if err := runViewDo(c); err != nil { - fmt.Fprintf(os.Stderr, "%v\n", err) + _, _ = fmt.Fprintf(os.Stderr, "%v\n", err) return err } return nil } -func runExtract(c *cli.Context) error { +func runExtract(_ context.Context, c *cli.Command) error { if err := runExtractDo(c); err != nil { - fmt.Fprintf(os.Stderr, "%v\n", err) + _, _ = fmt.Fprintf(os.Stderr, "%v\n", err) return err } return nil } -func runListDo(c *cli.Context) error { +func runListDo(c *cli.Command) error { if err := initEmbeddedExtractor(c); err != nil { return err } @@ -151,7 +152,7 @@ func runListDo(c *cli.Context) error { return nil } -func runViewDo(c *cli.Context) error { +func runViewDo(c *cli.Command) error { if err := initEmbeddedExtractor(c); err != nil { return err } @@ -174,7 +175,7 @@ func runViewDo(c *cli.Context) error { return nil } -func runExtractDo(c *cli.Context) error { +func runExtractDo(c *cli.Command) error { if err := initEmbeddedExtractor(c); err != nil { return err } @@ -216,7 +217,7 @@ func runExtractDo(c *cli.Context) error { for _, a := range matchedAssetFiles { if err := extractAsset(destdir, a, overwrite, rename); err != nil { // Non-fatal error - fmt.Fprintf(os.Stderr, "%s: %v", a.path, err) + _, _ = fmt.Fprintf(os.Stderr, "%s: %v\n", a.path, err) } } @@ -271,7 +272,7 @@ func extractAsset(d string, a assetFile, overwrite, rename bool) error { return nil } -func collectAssetFilesByPattern(c *cli.Context, globs []glob.Glob, path string, layer *assetfs.Layer) { +func collectAssetFilesByPattern(c *cli.Command, globs []glob.Glob, path string, layer *assetfs.Layer) { fs := assetfs.Layered(layer) files, err := fs.ListAllFiles(".", true) if err != nil { @@ -294,16 +295,14 @@ func collectAssetFilesByPattern(c *cli.Context, globs []glob.Glob, path string, } } -func compileCollectPatterns(args []string) ([]glob.Glob, error) { +func compileCollectPatterns(args []string) (_ []glob.Glob, err error) { if len(args) == 0 { args = []string{"**"} } pat := make([]glob.Glob, len(args)) for i := range args { - if g, err := glob.Compile(args[i], '/'); err != nil { - return nil, fmt.Errorf("'%s': Invalid glob pattern: %w", args[i], err) - } else { //nolint:revive - pat[i] = g + if pat[i], err = glob.Compile(args[i], '/'); err != nil { + return nil, fmt.Errorf("invalid glob patterh %q: %w", args[i], err) } } return pat, nil diff --git a/cmd/generate.go b/cmd/generate.go index 90b32ecaf0e..cf491604efa 100644 --- a/cmd/generate.go +++ b/cmd/generate.go @@ -5,13 +5,14 @@ package cmd import ( + "context" "fmt" "os" "code.gitea.io/gitea/modules/generate" "github.com/mattn/go-isatty" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) var ( @@ -19,7 +20,7 @@ var ( CmdGenerate = &cli.Command{ Name: "generate", Usage: "Generate Gitea's secrets/keys/tokens", - Subcommands: []*cli.Command{ + Commands: []*cli.Command{ subcmdSecret, }, } @@ -27,7 +28,7 @@ var ( subcmdSecret = &cli.Command{ Name: "secret", Usage: "Generate a secret token", - Subcommands: []*cli.Command{ + Commands: []*cli.Command{ microcmdGenerateInternalToken, microcmdGenerateLfsJwtSecret, microcmdGenerateSecretKey, @@ -54,7 +55,7 @@ var ( } ) -func runGenerateInternalToken(c *cli.Context) error { +func runGenerateInternalToken(_ context.Context, c *cli.Command) error { internalToken, err := generate.NewInternalToken() if err != nil { return err @@ -69,7 +70,7 @@ func runGenerateInternalToken(c *cli.Context) error { return nil } -func runGenerateLfsJwtSecret(c *cli.Context) error { +func runGenerateLfsJwtSecret(_ context.Context, c *cli.Command) error { _, jwtSecretBase64, err := generate.NewJwtSecretWithBase64() if err != nil { return err @@ -84,7 +85,7 @@ func runGenerateLfsJwtSecret(c *cli.Context) error { return nil } -func runGenerateSecretKey(c *cli.Context) error { +func runGenerateSecretKey(_ context.Context, c *cli.Command) error { secretKey, err := generate.NewSecretKey() if err != nil { return err diff --git a/cmd/hook.go b/cmd/hook.go index 578380ab40d..b741127ca3c 100644 --- a/cmd/hook.go +++ b/cmd/hook.go @@ -20,11 +20,11 @@ import ( repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) const ( - hookBatchSize = 30 + hookBatchSize = 500 ) var ( @@ -32,9 +32,10 @@ var ( CmdHook = &cli.Command{ Name: "hook", Usage: "(internal) Should only be called by Git", + Hidden: true, // internal commands shouldn't be visible Description: "Delegate commands to corresponding Git hooks", Before: PrepareConsoleLoggerLevel(log.FATAL), - Subcommands: []*cli.Command{ + Commands: []*cli.Command{ subcmdHookPreReceive, subcmdHookUpdate, subcmdHookPostReceive, @@ -161,12 +162,10 @@ func (n *nilWriter) WriteString(s string) (int, error) { return len(s), nil } -func runHookPreReceive(c *cli.Context) error { +func runHookPreReceive(ctx context.Context, c *cli.Command) error { if isInternal, _ := strconv.ParseBool(os.Getenv(repo_module.EnvIsInternal)); isInternal { return nil } - ctx, cancel := installSignals() - defer cancel() setup(ctx, c.Bool("debug")) @@ -292,7 +291,7 @@ Gitea or set your environment appropriately.`, "") // runHookUpdate avoid to do heavy operations on update hook because it will be // invoked for every ref update which does not like pre-receive and post-receive -func runHookUpdate(c *cli.Context) error { +func runHookUpdate(_ context.Context, c *cli.Command) error { if isInternal, _ := strconv.ParseBool(os.Getenv(repo_module.EnvIsInternal)); isInternal { return nil } @@ -309,15 +308,12 @@ func runHookUpdate(c *cli.Context) error { return nil } -func runHookPostReceive(c *cli.Context) error { - ctx, cancel := installSignals() - defer cancel() - +func runHookPostReceive(ctx context.Context, c *cli.Command) error { setup(ctx, c.Bool("debug")) // First of all run update-server-info no matter what - if _, _, err := git.NewCommand(ctx, "update-server-info").RunStdString(nil); err != nil { - return fmt.Errorf("Failed to call 'git update-server-info': %w", err) + if _, _, err := git.NewCommand("update-server-info").RunStdString(ctx, nil); err != nil { + return fmt.Errorf("failed to call 'git update-server-info': %w", err) } // Now if we're an internal don't do anything else @@ -485,7 +481,7 @@ func hookPrintResult(output, isCreate bool, branch, url string) { func pushOptions() map[string]string { opts := make(map[string]string) if pushCount, err := strconv.Atoi(os.Getenv(private.GitPushOptionCount)); err == nil { - for idx := 0; idx < pushCount; idx++ { + for idx := range pushCount { opt := os.Getenv(fmt.Sprintf("GIT_PUSH_OPTION_%d", idx)) kv := strings.SplitN(opt, "=", 2) if len(kv) == 2 { @@ -496,10 +492,7 @@ func pushOptions() map[string]string { return opts } -func runHookProcReceive(c *cli.Context) error { - ctx, cancel := installSignals() - defer cancel() - +func runHookProcReceive(ctx context.Context, c *cli.Command) error { setup(ctx, c.Bool("debug")) if len(os.Getenv("SSH_ORIGINAL_COMMAND")) == 0 { @@ -740,7 +733,7 @@ func readPktLine(ctx context.Context, in *bufio.Reader, requestType pktLineType) // read prefix lengthBytes := make([]byte, 4) - for i := 0; i < 4; i++ { + for i := range 4 { lengthBytes[i], err = in.ReadByte() if err != nil { return nil, fail(ctx, "Protocol: stdin error", "Pkt-Line: read stdin failed : %v", err) diff --git a/cmd/hook_test.go b/cmd/hook_test.go index 91f24ff2b44..86cd4834f20 100644 --- a/cmd/hook_test.go +++ b/cmd/hook_test.go @@ -6,7 +6,6 @@ package cmd import ( "bufio" "bytes" - "context" "strings" "testing" @@ -15,7 +14,7 @@ import ( func TestPktLine(t *testing.T) { // test read - ctx := context.Background() + ctx := t.Context() s := strings.NewReader("0000") r := bufio.NewReader(s) result, err := readPktLine(ctx, r, pktLineTypeFlush) diff --git a/cmd/keys.go b/cmd/keys.go index 7fdbe16119f..5ca3b91e15e 100644 --- a/cmd/keys.go +++ b/cmd/keys.go @@ -4,6 +4,7 @@ package cmd import ( + "context" "errors" "fmt" "strings" @@ -11,13 +12,14 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/private" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) // CmdKeys represents the available keys sub-command var CmdKeys = &cli.Command{ Name: "keys", Usage: "(internal) Should only be called by SSH server", + Hidden: true, // internal commands shouldn't not be visible Description: "Queries the Gitea database to get the authorized command for a given ssh key fingerprint", Before: PrepareConsoleLoggerLevel(log.FATAL), Action: runKeys, @@ -49,7 +51,7 @@ var CmdKeys = &cli.Command{ }, } -func runKeys(c *cli.Context) error { +func runKeys(ctx context.Context, c *cli.Command) error { if !c.IsSet("username") { return errors.New("No username provided") } @@ -68,9 +70,6 @@ func runKeys(c *cli.Context) error { return errors.New("No key type and content provided") } - ctx, cancel := installSignals() - defer cancel() - setup(ctx, c.Bool("debug")) authorizedString, extra := private.AuthorizedPublicKeyByContent(ctx, content) @@ -78,6 +77,6 @@ func runKeys(c *cli.Context) error { if extra.Error != nil { return extra.Error } - _, _ = fmt.Fprintln(c.App.Writer, strings.TrimSpace(authorizedString.Text)) + _, _ = fmt.Fprintln(c.Root().Writer, strings.TrimSpace(authorizedString.Text)) return nil } diff --git a/cmd/mailer.go b/cmd/mailer.go index 0c5f2c8c8d4..72bd8e56012 100644 --- a/cmd/mailer.go +++ b/cmd/mailer.go @@ -4,24 +4,18 @@ package cmd import ( + "context" "fmt" "code.gitea.io/gitea/modules/private" "code.gitea.io/gitea/modules/setting" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) -func runSendMail(c *cli.Context) error { - ctx, cancel := installSignals() - defer cancel() - +func runSendMail(ctx context.Context, c *cli.Command) error { setting.MustInstalled() - if err := argsSet(c, "title"); err != nil { - return err - } - subject := c.String("title") confirmSkiped := c.Bool("force") body := c.String("content") diff --git a/cmd/main.go b/cmd/main.go index fd648946efa..3fdaf48ed96 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -4,36 +4,40 @@ package cmd import ( + "context" "fmt" + "io" "os" "strings" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) -// cmdHelp is our own help subcommand with more information -// Keep in mind that the "./gitea help"(subcommand) is different from "./gitea --help"(flag), the flag doesn't parse the config or output "DEFAULT CONFIGURATION:" information -func cmdHelp() *cli.Command { - c := &cli.Command{ - Name: "help", - Aliases: []string{"h"}, - Usage: "Shows a list of commands or help for one command", - ArgsUsage: "[command]", - Action: func(c *cli.Context) (err error) { - lineage := c.Lineage() // The order is from child to parent: help, doctor, Gitea, {Command:nil} - targetCmdIdx := 0 - if c.Command.Name == "help" { - targetCmdIdx = 1 - } - if lineage[targetCmdIdx+1].Command != nil { - err = cli.ShowCommandHelp(lineage[targetCmdIdx+1], lineage[targetCmdIdx].Command.Name) - } else { - err = cli.ShowAppHelp(c) - } - _, _ = fmt.Fprintf(c.App.Writer, ` +var cliHelpPrinterOld = cli.HelpPrinter + +func init() { + cli.HelpPrinter = cliHelpPrinterNew +} + +// cliHelpPrinterNew helps to print "DEFAULT CONFIGURATION" for the following cases ( "-c" can apper in any position): +// * ./gitea -c /dev/null -h +// * ./gitea -c help /dev/null help +// * ./gitea help -c /dev/null +// * ./gitea help -c /dev/null web +// * ./gitea help web -c /dev/null +// * ./gitea web help -c /dev/null +// * ./gitea web -h -c /dev/null +func cliHelpPrinterNew(out io.Writer, templ string, data any) { + cmd, _ := data.(*cli.Command) + if cmd != nil { + prepareWorkPathAndCustomConf(cmd) + } + cliHelpPrinterOld(out, templ, data) + if setting.CustomConf != "" { + _, _ = fmt.Fprintf(out, ` DEFAULT CONFIGURATION: AppPath: %s WorkPath: %s @@ -41,94 +45,71 @@ DEFAULT CONFIGURATION: ConfigFile: %s `, setting.AppPath, setting.AppWorkPath, setting.CustomPath, setting.CustomConf) - return err - }, - } - return c -} - -func appGlobalFlags() []cli.Flag { - return []cli.Flag{ - // make the builtin flags at the top - cli.HelpFlag, - - // shared configuration flags, they are for global and for each sub-command at the same time - // eg: such command is valid: "./gitea --config /tmp/app.ini web --config /tmp/app.ini", while it's discouraged indeed - // keep in mind that the short flags like "-C", "-c" and "-w" are globally polluted, they can't be used for sub-commands anymore. - &cli.StringFlag{ - Name: "custom-path", - Aliases: []string{"C"}, - Usage: "Set custom path (defaults to '{WorkPath}/custom')", - }, - &cli.StringFlag{ - Name: "config", - Aliases: []string{"c"}, - Value: setting.CustomConf, - Usage: "Set custom config file (defaults to '{WorkPath}/custom/conf/app.ini')", - }, - &cli.StringFlag{ - Name: "work-path", - Aliases: []string{"w"}, - Usage: "Set Gitea's working path (defaults to the Gitea's binary directory)", - }, } } -func prepareSubcommandWithConfig(command *cli.Command, globalFlags []cli.Flag) { - command.Flags = append(append([]cli.Flag{}, globalFlags...), command.Flags...) - command.Action = prepareWorkPathAndCustomConf(command.Action) - command.HideHelp = true - if command.Name != "help" { - command.Subcommands = append(command.Subcommands, cmdHelp()) - } - for i := range command.Subcommands { - prepareSubcommandWithConfig(command.Subcommands[i], globalFlags) - } -} - -// prepareWorkPathAndCustomConf wraps the Action to prepare the work path and custom config -// It can't use "Before", because each level's sub-command's Before will be called one by one, so the "init" would be done multiple times -func prepareWorkPathAndCustomConf(action cli.ActionFunc) func(ctx *cli.Context) error { - return func(ctx *cli.Context) error { - var args setting.ArgWorkPathAndCustomConf - // from children to parent, check the global flags - for _, curCtx := range ctx.Lineage() { - if curCtx.IsSet("work-path") && args.WorkPath == "" { - args.WorkPath = curCtx.String("work-path") - } - if curCtx.IsSet("custom-path") && args.CustomPath == "" { - args.CustomPath = curCtx.String("custom-path") - } - if curCtx.IsSet("config") && args.CustomConf == "" { - args.CustomConf = curCtx.String("config") - } +func prepareSubcommandWithGlobalFlags(originCmd *cli.Command) { + originBefore := originCmd.Before + originCmd.Before = func(ctx context.Context, cmd *cli.Command) (context.Context, error) { + prepareWorkPathAndCustomConf(cmd) + if originBefore != nil { + return originBefore(ctx, cmd) } - setting.InitWorkPathAndCommonConfig(os.Getenv, args) - if ctx.Bool("help") || action == nil { - // the default behavior of "urfave/cli": "nil action" means "show help" - return cmdHelp().Action(ctx) - } - return action(ctx) + return ctx, nil } } +// prepareWorkPathAndCustomConf tries to prepare the work path, custom path and custom config from various inputs: +// command line flags, environment variables, config file +func prepareWorkPathAndCustomConf(cmd *cli.Command) { + var args setting.ArgWorkPathAndCustomConf + if cmd.IsSet("work-path") { + args.WorkPath = cmd.String("work-path") + } + if cmd.IsSet("custom-path") { + args.CustomPath = cmd.String("custom-path") + } + if cmd.IsSet("config") { + args.CustomConf = cmd.String("config") + } + setting.InitWorkPathAndCommonConfig(os.Getenv, args) +} + type AppVersion struct { Version string Extra string } -func NewMainApp(appVer AppVersion) *cli.App { - app := cli.NewApp() - app.Name = "Gitea" - app.HelpName = "gitea" +func NewMainApp(appVer AppVersion) *cli.Command { + app := &cli.Command{} + app.Name = "gitea" // must be lower-cased because it appears in the "USAGE" section like "gitea doctor [command [command options]]" app.Usage = "A painless self-hosted Git service" app.Description = `Gitea program contains "web" and other subcommands. If no subcommand is given, it starts the web server by default. Use "web" subcommand for more web server arguments, use other subcommands for other purposes.` app.Version = appVer.Version + appVer.Extra - app.EnableBashCompletion = true - - // these sub-commands need to use config file + app.EnableShellCompletion = true + app.Flags = []cli.Flag{ + &cli.StringFlag{ + Name: "work-path", + Aliases: []string{"w"}, + TakesFile: true, + Usage: "Set Gitea's working path (defaults to the Gitea's binary directory)", + }, + &cli.StringFlag{ + Name: "config", + Aliases: []string{"c"}, + TakesFile: true, + Value: setting.CustomConf, + Usage: "Set custom config file (defaults to '{WorkPath}/custom/conf/app.ini')", + }, + &cli.StringFlag{ + Name: "custom-path", + Aliases: []string{"C"}, + TakesFile: true, + Usage: "Set custom path (defaults to '{WorkPath}/custom')", + }, + } + // these sub-commands need to use a config file subCmdWithConfig := []*cli.Command{ - cmdHelp(), // the "help" sub-command was used to show the more information for "work path" and "custom config" CmdWeb, CmdServ, CmdHook, @@ -147,29 +128,30 @@ func NewMainApp(appVer AppVersion) *cli.App { // these sub-commands do not need the config file, and they do not depend on any path or environment variable. subCmdStandalone := []*cli.Command{ - CmdCert, + cmdCert(), CmdGenerate, CmdDocs, } + // TODO: we should eventually drop the default command, + // but not sure whether it would break Windows users who used to double-click the EXE to run. app.DefaultCommand = CmdWeb.Name - globalFlags := appGlobalFlags() - app.Flags = append(app.Flags, cli.VersionFlag) - app.Flags = append(app.Flags, globalFlags...) - app.HideHelp = true // use our own help action to show helps (with more information like default config) app.Before = PrepareConsoleLoggerLevel(log.INFO) for i := range subCmdWithConfig { - prepareSubcommandWithConfig(subCmdWithConfig[i], globalFlags) + prepareSubcommandWithGlobalFlags(subCmdWithConfig[i]) } app.Commands = append(app.Commands, subCmdWithConfig...) app.Commands = append(app.Commands, subCmdStandalone...) + setting.InitGiteaEnvVars() return app } -func RunMainApp(app *cli.App, args ...string) error { - err := app.Run(args) +func RunMainApp(app *cli.Command, args ...string) error { + ctx, cancel := installSignals() + defer cancel() + err := app.Run(ctx, args) if err == nil { return nil } diff --git a/cmd/main_test.go b/cmd/main_test.go index c182b440199..d49ebfd4df4 100644 --- a/cmd/main_test.go +++ b/cmd/main_test.go @@ -4,9 +4,10 @@ package cmd import ( + "context" + "errors" "fmt" "io" - "os" "path/filepath" "strings" "testing" @@ -16,7 +17,7 @@ import ( "code.gitea.io/gitea/modules/test" "github.com/stretchr/testify/assert" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) func TestMain(m *testing.M) { @@ -27,10 +28,10 @@ func makePathOutput(workPath, customPath, customConf string) string { return fmt.Sprintf("WorkPath=%s\nCustomPath=%s\nCustomConf=%s", workPath, customPath, customConf) } -func newTestApp(testCmdAction func(ctx *cli.Context) error) *cli.App { +func newTestApp(testCmdAction cli.ActionFunc) *cli.Command { app := NewMainApp(AppVersion{}) testCmd := &cli.Command{Name: "test-cmd", Action: testCmdAction} - prepareSubcommandWithConfig(testCmd, appGlobalFlags()) + prepareSubcommandWithGlobalFlags(testCmd) app.Commands = append(app.Commands, testCmd) app.DefaultCommand = testCmd.Name return app @@ -42,7 +43,7 @@ type runResult struct { ExitCode int } -func runTestApp(app *cli.App, args ...string) (runResult, error) { +func runTestApp(app *cli.Command, args ...string) (runResult, error) { outBuf := new(strings.Builder) errBuf := new(strings.Builder) app.Writer = outBuf @@ -65,7 +66,7 @@ func TestCliCmd(t *testing.T) { defaultCustomConf := filepath.Join(defaultCustomPath, "conf/app.ini") cli.CommandHelpTemplate = "(command help template)" - cli.AppHelpTemplate = "(app help template)" + cli.RootCommandHelpTemplate = "(app help template)" cli.SubcommandHelpTemplate = "(subcommand help template)" cases := []struct { @@ -73,12 +74,56 @@ func TestCliCmd(t *testing.T) { cmd string exp string }{ - // main command help + // help commands + { + cmd: "./gitea -h", + exp: "DEFAULT CONFIGURATION:", + }, { cmd: "./gitea help", exp: "DEFAULT CONFIGURATION:", }, + { + cmd: "./gitea -c /dev/null -h", + exp: "ConfigFile: /dev/null", + }, + + { + cmd: "./gitea -c /dev/null help", + exp: "ConfigFile: /dev/null", + }, + { + cmd: "./gitea help -c /dev/null", + exp: "ConfigFile: /dev/null", + }, + + { + cmd: "./gitea -c /dev/null test-cmd -h", + exp: "ConfigFile: /dev/null", + }, + { + cmd: "./gitea test-cmd -c /dev/null -h", + exp: "ConfigFile: /dev/null", + }, + { + cmd: "./gitea test-cmd -h -c /dev/null", + exp: "ConfigFile: /dev/null", + }, + + { + cmd: "./gitea -c /dev/null test-cmd help", + exp: "ConfigFile: /dev/null", + }, + { + cmd: "./gitea test-cmd -c /dev/null help", + exp: "ConfigFile: /dev/null", + }, + { + cmd: "./gitea test-cmd help -c /dev/null", + exp: "ConfigFile: /dev/null", + }, + // parse paths { cmd: "./gitea test-cmd", @@ -109,70 +154,50 @@ func TestCliCmd(t *testing.T) { }, } - app := newTestApp(func(ctx *cli.Context) error { - _, _ = fmt.Fprint(ctx.App.Writer, makePathOutput(setting.AppWorkPath, setting.CustomPath, setting.CustomConf)) - return nil - }) - var envBackup []string - for _, s := range os.Environ() { - if strings.HasPrefix(s, "GITEA_") && strings.Contains(s, "=") { - envBackup = append(envBackup, s) - } - } - clearGiteaEnv := func() { - for _, s := range os.Environ() { - if strings.HasPrefix(s, "GITEA_") { - _ = os.Unsetenv(s) - } - } - } - defer func() { - clearGiteaEnv() - for _, s := range envBackup { - k, v, _ := strings.Cut(s, "=") - _ = os.Setenv(k, v) - } - }() - for _, c := range cases { - clearGiteaEnv() - for k, v := range c.env { - _ = os.Setenv(k, v) - } - args := strings.Split(c.cmd, " ") // for test only, "split" is good enough - r, err := runTestApp(app, args...) - assert.NoError(t, err, c.cmd) - assert.NotEmpty(t, c.exp, c.cmd) - assert.Contains(t, r.Stdout, c.exp, c.cmd) + t.Run(c.cmd, func(t *testing.T) { + app := newTestApp(func(ctx context.Context, cmd *cli.Command) error { + _, _ = fmt.Fprint(cmd.Root().Writer, makePathOutput(setting.AppWorkPath, setting.CustomPath, setting.CustomConf)) + return nil + }) + for k, v := range c.env { + t.Setenv(k, v) + } + args := strings.Split(c.cmd, " ") // for test only, "split" is good enough + r, err := runTestApp(app, args...) + assert.NoError(t, err, c.cmd) + assert.NotEmpty(t, c.exp, c.cmd) + assert.Contains(t, r.Stdout, c.exp, c.cmd) + }) } } func TestCliCmdError(t *testing.T) { - app := newTestApp(func(ctx *cli.Context) error { return fmt.Errorf("normal error") }) + app := newTestApp(func(ctx context.Context, cmd *cli.Command) error { return errors.New("normal error") }) r, err := runTestApp(app, "./gitea", "test-cmd") assert.Error(t, err) assert.Equal(t, 1, r.ExitCode) - assert.Equal(t, "", r.Stdout) + assert.Empty(t, r.Stdout) assert.Equal(t, "Command error: normal error\n", r.Stderr) - app = newTestApp(func(ctx *cli.Context) error { return cli.Exit("exit error", 2) }) + app = newTestApp(func(ctx context.Context, cmd *cli.Command) error { return cli.Exit("exit error", 2) }) r, err = runTestApp(app, "./gitea", "test-cmd") assert.Error(t, err) assert.Equal(t, 2, r.ExitCode) - assert.Equal(t, "", r.Stdout) + assert.Empty(t, r.Stdout) assert.Equal(t, "exit error\n", r.Stderr) - app = newTestApp(func(ctx *cli.Context) error { return nil }) + app = newTestApp(func(ctx context.Context, cmd *cli.Command) error { return nil }) r, err = runTestApp(app, "./gitea", "test-cmd", "--no-such") assert.Error(t, err) assert.Equal(t, 1, r.ExitCode) - assert.Equal(t, "Incorrect Usage: flag provided but not defined: -no-such\n\n", r.Stdout) - assert.Equal(t, "", r.Stderr) // the cli package's strange behavior, the error message is not in stderr .... + assert.Empty(t, r.Stdout) + assert.Equal(t, "Incorrect Usage: flag provided but not defined: -no-such\n\n", r.Stderr) - app = newTestApp(func(ctx *cli.Context) error { return nil }) + app = newTestApp(func(ctx context.Context, cmd *cli.Command) error { return nil }) r, err = runTestApp(app, "./gitea", "test-cmd") assert.NoError(t, err) assert.Equal(t, -1, r.ExitCode) // the cli.OsExiter is not called - assert.Equal(t, "", r.Stdout) - assert.Equal(t, "", r.Stderr) + assert.Empty(t, r.Stdout) + assert.Empty(t, r.Stderr) } diff --git a/cmd/manager.go b/cmd/manager.go index bd2da8edc78..f0935ea0657 100644 --- a/cmd/manager.go +++ b/cmd/manager.go @@ -4,12 +4,13 @@ package cmd import ( + "context" "os" "time" "code.gitea.io/gitea/modules/private" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) var ( @@ -18,7 +19,7 @@ var ( Name: "manager", Usage: "Manage the running gitea process", Description: "This is a command for managing the running gitea process", - Subcommands: []*cli.Command{ + Commands: []*cli.Command{ subcmdShutdown, subcmdRestart, subcmdReloadTemplates, @@ -108,46 +109,31 @@ var ( } ) -func runShutdown(c *cli.Context) error { - ctx, cancel := installSignals() - defer cancel() - +func runShutdown(ctx context.Context, c *cli.Command) error { setup(ctx, c.Bool("debug")) extra := private.Shutdown(ctx) return handleCliResponseExtra(extra) } -func runRestart(c *cli.Context) error { - ctx, cancel := installSignals() - defer cancel() - +func runRestart(ctx context.Context, c *cli.Command) error { setup(ctx, c.Bool("debug")) extra := private.Restart(ctx) return handleCliResponseExtra(extra) } -func runReloadTemplates(c *cli.Context) error { - ctx, cancel := installSignals() - defer cancel() - +func runReloadTemplates(ctx context.Context, c *cli.Command) error { setup(ctx, c.Bool("debug")) extra := private.ReloadTemplates(ctx) return handleCliResponseExtra(extra) } -func runFlushQueues(c *cli.Context) error { - ctx, cancel := installSignals() - defer cancel() - +func runFlushQueues(ctx context.Context, c *cli.Command) error { setup(ctx, c.Bool("debug")) extra := private.FlushQueues(ctx, c.Duration("timeout"), c.Bool("non-blocking")) return handleCliResponseExtra(extra) } -func runProcesses(c *cli.Context) error { - ctx, cancel := installSignals() - defer cancel() - +func runProcesses(ctx context.Context, c *cli.Command) error { setup(ctx, c.Bool("debug")) extra := private.Processes(ctx, os.Stdout, c.Bool("flat"), c.Bool("no-system"), c.Bool("stacktraces"), c.Bool("json"), c.String("cancel")) return handleCliResponseExtra(extra) diff --git a/cmd/manager_logging.go b/cmd/manager_logging.go index c2ae25ec572..ac29e7d3e50 100644 --- a/cmd/manager_logging.go +++ b/cmd/manager_logging.go @@ -4,6 +4,7 @@ package cmd import ( + "context" "errors" "fmt" "os" @@ -11,7 +12,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/private" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) var ( @@ -60,7 +61,7 @@ var ( subcmdLogging = &cli.Command{ Name: "logging", Usage: "Adjust logging commands", - Subcommands: []*cli.Command{ + Commands: []*cli.Command{ { Name: "pause", Usage: "Pause logging (Gitea will buffer logs up to a certain point and will drop them after that point)", @@ -104,7 +105,7 @@ var ( }, { Name: "add", Usage: "Add a logger", - Subcommands: []*cli.Command{ + Commands: []*cli.Command{ { Name: "file", Usage: "Add a file logger", @@ -118,7 +119,6 @@ var ( Name: "rotate", Aliases: []string{"r"}, Usage: "Rotate logs", - Value: true, }, &cli.Int64Flag{ Name: "max-size", @@ -129,7 +129,6 @@ var ( Name: "daily", Aliases: []string{"d"}, Usage: "Rotate logs daily", - Value: true, }, &cli.IntFlag{ Name: "max-days", @@ -140,7 +139,6 @@ var ( Name: "compress", Aliases: []string{"z"}, Usage: "Compress rotated logs", - Value: true, }, &cli.IntFlag{ Name: "compression-level", @@ -195,10 +193,7 @@ var ( } ) -func runRemoveLogger(c *cli.Context) error { - ctx, cancel := installSignals() - defer cancel() - +func runRemoveLogger(ctx context.Context, c *cli.Command) error { setup(ctx, c.Bool("debug")) logger := c.String("logger") if len(logger) == 0 { @@ -210,10 +205,7 @@ func runRemoveLogger(c *cli.Context) error { return handleCliResponseExtra(extra) } -func runAddConnLogger(c *cli.Context) error { - ctx, cancel := installSignals() - defer cancel() - +func runAddConnLogger(ctx context.Context, c *cli.Command) error { setup(ctx, c.Bool("debug")) vals := map[string]any{} mode := "conn" @@ -237,13 +229,10 @@ func runAddConnLogger(c *cli.Context) error { if c.IsSet("reconnect-on-message") { vals["reconnectOnMsg"] = c.Bool("reconnect-on-message") } - return commonAddLogger(c, mode, vals) + return commonAddLogger(ctx, c, mode, vals) } -func runAddFileLogger(c *cli.Context) error { - ctx, cancel := installSignals() - defer cancel() - +func runAddFileLogger(ctx context.Context, c *cli.Command) error { setup(ctx, c.Bool("debug")) vals := map[string]any{} mode := "file" @@ -270,10 +259,10 @@ func runAddFileLogger(c *cli.Context) error { if c.IsSet("compression-level") { vals["compressionLevel"] = c.Int("compression-level") } - return commonAddLogger(c, mode, vals) + return commonAddLogger(ctx, c, mode, vals) } -func commonAddLogger(c *cli.Context, mode string, vals map[string]any) error { +func commonAddLogger(ctx context.Context, c *cli.Command, mode string, vals map[string]any) error { if len(c.String("level")) > 0 { vals["level"] = log.LevelFromString(c.String("level")).String() } @@ -300,46 +289,33 @@ func commonAddLogger(c *cli.Context, mode string, vals map[string]any) error { if c.IsSet("writer") { writer = c.String("writer") } - ctx, cancel := installSignals() - defer cancel() extra := private.AddLogger(ctx, logger, writer, mode, vals) return handleCliResponseExtra(extra) } -func runPauseLogging(c *cli.Context) error { - ctx, cancel := installSignals() - defer cancel() - +func runPauseLogging(ctx context.Context, c *cli.Command) error { setup(ctx, c.Bool("debug")) userMsg := private.PauseLogging(ctx) _, _ = fmt.Fprintln(os.Stdout, userMsg) return nil } -func runResumeLogging(c *cli.Context) error { - ctx, cancel := installSignals() - defer cancel() - +func runResumeLogging(ctx context.Context, c *cli.Command) error { setup(ctx, c.Bool("debug")) userMsg := private.ResumeLogging(ctx) _, _ = fmt.Fprintln(os.Stdout, userMsg) return nil } -func runReleaseReopenLogging(c *cli.Context) error { - ctx, cancel := installSignals() - defer cancel() - +func runReleaseReopenLogging(ctx context.Context, c *cli.Command) error { setup(ctx, c.Bool("debug")) userMsg := private.ReleaseReopenLogging(ctx) _, _ = fmt.Fprintln(os.Stdout, userMsg) return nil } -func runSetLogSQL(c *cli.Context) error { - ctx, cancel := installSignals() - defer cancel() +func runSetLogSQL(ctx context.Context, c *cli.Command) error { setup(ctx, c.Bool("debug")) extra := private.SetLogSQL(ctx, !c.Bool("off")) diff --git a/cmd/migrate.go b/cmd/migrate.go index 4e4dd45af3a..e24dc9e5720 100644 --- a/cmd/migrate.go +++ b/cmd/migrate.go @@ -7,26 +7,23 @@ import ( "context" "code.gitea.io/gitea/models/db" - "code.gitea.io/gitea/models/migrations" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/services/versioned_migration" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) // CmdMigrate represents the available migrate sub-command. var CmdMigrate = &cli.Command{ Name: "migrate", Usage: "Migrate the database", - Description: "This is a command for migrating the database, so that you can run gitea admin create-user before starting the server.", + Description: `This is a command for migrating the database, so that you can run "gitea admin create user" before starting the server.`, Action: runMigrate, } -func runMigrate(ctx *cli.Context) error { - stdCtx, cancel := installSignals() - defer cancel() - - if err := initDB(stdCtx); err != nil { +func runMigrate(ctx context.Context, c *cli.Command) error { + if err := initDB(ctx); err != nil { return err } @@ -36,7 +33,7 @@ func runMigrate(ctx *cli.Context) error { log.Info("Log path: %s", setting.Log.RootPath) log.Info("Configuration file: %s", setting.CustomConf) - if err := db.InitEngineWithMigration(context.Background(), migrations.Migrate); err != nil { + if err := db.InitEngineWithMigration(context.Background(), versioned_migration.Migrate); err != nil { log.Fatal("Failed to initialize ORM engine: %v", err) return err } diff --git a/cmd/migrate_storage.go b/cmd/migrate_storage.go index 6ece4bf661b..2c63e15f509 100644 --- a/cmd/migrate_storage.go +++ b/cmd/migrate_storage.go @@ -13,7 +13,6 @@ import ( actions_model "code.gitea.io/gitea/models/actions" "code.gitea.io/gitea/models/db" git_model "code.gitea.io/gitea/models/git" - "code.gitea.io/gitea/models/migrations" packages_model "code.gitea.io/gitea/models/packages" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" @@ -21,8 +20,9 @@ import ( packages_module "code.gitea.io/gitea/modules/packages" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/storage" + "code.gitea.io/gitea/services/versioned_migration" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) // CmdMigrateStorage represents the available migrate storage sub-command. @@ -196,7 +196,7 @@ func migrateActionsLog(ctx context.Context, dstStorage storage.ObjectStorage) er func migrateActionsArtifacts(ctx context.Context, dstStorage storage.ObjectStorage) error { return db.Iterate(ctx, nil, func(ctx context.Context, artifact *actions_model.ActionArtifact) error { - if artifact.Status == int64(actions_model.ArtifactStatusExpired) { + if artifact.Status == actions_model.ArtifactStatusExpired { return nil } @@ -213,11 +213,8 @@ func migrateActionsArtifacts(ctx context.Context, dstStorage storage.ObjectStora }) } -func runMigrateStorage(ctx *cli.Context) error { - stdCtx, cancel := installSignals() - defer cancel() - - if err := initDB(stdCtx); err != nil { +func runMigrateStorage(ctx context.Context, cmd *cli.Command) error { + if err := initDB(ctx); err != nil { return err } @@ -227,7 +224,7 @@ func runMigrateStorage(ctx *cli.Context) error { log.Info("Log path: %s", setting.Log.RootPath) log.Info("Configuration file: %s", setting.CustomConf) - if err := db.InitEngineWithMigration(context.Background(), migrations.Migrate); err != nil { + if err := db.InitEngineWithMigration(context.Background(), versioned_migration.Migrate); err != nil { log.Fatal("Failed to initialize ORM engine: %v", err) return err } @@ -238,51 +235,51 @@ func runMigrateStorage(ctx *cli.Context) error { var dstStorage storage.ObjectStorage var err error - switch strings.ToLower(ctx.String("storage")) { + switch strings.ToLower(cmd.String("storage")) { case "": fallthrough case string(setting.LocalStorageType): - p := ctx.String("path") + p := cmd.String("path") if p == "" { log.Fatal("Path must be given when storage is local") return nil } dstStorage, err = storage.NewLocalStorage( - stdCtx, + ctx, &setting.Storage{ Path: p, }) case string(setting.MinioStorageType): dstStorage, err = storage.NewMinioStorage( - stdCtx, + ctx, &setting.Storage{ MinioConfig: setting.MinioStorageConfig{ - Endpoint: ctx.String("minio-endpoint"), - AccessKeyID: ctx.String("minio-access-key-id"), - SecretAccessKey: ctx.String("minio-secret-access-key"), - Bucket: ctx.String("minio-bucket"), - Location: ctx.String("minio-location"), - BasePath: ctx.String("minio-base-path"), - UseSSL: ctx.Bool("minio-use-ssl"), - InsecureSkipVerify: ctx.Bool("minio-insecure-skip-verify"), - ChecksumAlgorithm: ctx.String("minio-checksum-algorithm"), - BucketLookUpType: ctx.String("minio-bucket-lookup-type"), + Endpoint: cmd.String("minio-endpoint"), + AccessKeyID: cmd.String("minio-access-key-id"), + SecretAccessKey: cmd.String("minio-secret-access-key"), + Bucket: cmd.String("minio-bucket"), + Location: cmd.String("minio-location"), + BasePath: cmd.String("minio-base-path"), + UseSSL: cmd.Bool("minio-use-ssl"), + InsecureSkipVerify: cmd.Bool("minio-insecure-skip-verify"), + ChecksumAlgorithm: cmd.String("minio-checksum-algorithm"), + BucketLookUpType: cmd.String("minio-bucket-lookup-type"), }, }) case string(setting.AzureBlobStorageType): dstStorage, err = storage.NewAzureBlobStorage( - stdCtx, + ctx, &setting.Storage{ AzureBlobConfig: setting.AzureBlobStorageConfig{ - Endpoint: ctx.String("azureblob-endpoint"), - AccountName: ctx.String("azureblob-account-name"), - AccountKey: ctx.String("azureblob-account-key"), - Container: ctx.String("azureblob-container"), - BasePath: ctx.String("azureblob-base-path"), + Endpoint: cmd.String("azureblob-endpoint"), + AccountName: cmd.String("azureblob-account-name"), + AccountKey: cmd.String("azureblob-account-key"), + Container: cmd.String("azureblob-container"), + BasePath: cmd.String("azureblob-base-path"), }, }) default: - return fmt.Errorf("unsupported storage type: %s", ctx.String("storage")) + return fmt.Errorf("unsupported storage type: %s", cmd.String("storage")) } if err != nil { return err @@ -299,14 +296,14 @@ func runMigrateStorage(ctx *cli.Context) error { "actions-artifacts": migrateActionsArtifacts, } - tp := strings.ToLower(ctx.String("type")) + tp := strings.ToLower(cmd.String("type")) if m, ok := migratedMethods[tp]; ok { - if err := m(stdCtx, dstStorage); err != nil { + if err := m(ctx, dstStorage); err != nil { return err } log.Info("%s files have successfully been copied to the new storage.", tp) return nil } - return fmt.Errorf("unsupported storage: %s", ctx.String("type")) + return fmt.Errorf("unsupported storage: %s", cmd.String("type")) } diff --git a/cmd/migrate_storage_test.go b/cmd/migrate_storage_test.go index 5d8c8679930..3ea193eb1ea 100644 --- a/cmd/migrate_storage_test.go +++ b/cmd/migrate_storage_test.go @@ -4,12 +4,10 @@ package cmd import ( - "context" "os" "strings" "testing" - "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/packages" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" @@ -31,7 +29,7 @@ func TestMigratePackages(t *testing.T) { assert.NoError(t, err) defer buf.Close() - v, f, err := packages_service.CreatePackageAndAddFile(db.DefaultContext, &packages_service.PackageCreationInfo{ + v, f, err := packages_service.CreatePackageAndAddFile(t.Context(), &packages_service.PackageCreationInfo{ PackageInfo: packages_service.PackageInfo{ Owner: creator, PackageType: packages.TypeGeneric, @@ -53,7 +51,7 @@ func TestMigratePackages(t *testing.T) { assert.NotNil(t, v) assert.NotNil(t, f) - ctx := context.Background() + ctx := t.Context() p := t.TempDir() @@ -70,6 +68,6 @@ func TestMigratePackages(t *testing.T) { entries, err := os.ReadDir(p) assert.NoError(t, err) assert.Len(t, entries, 2) - assert.EqualValues(t, "01", entries[0].Name()) - assert.EqualValues(t, "tmp", entries[1].Name()) + assert.Equal(t, "01", entries[0].Name()) + assert.Equal(t, "tmp", entries[1].Name()) } diff --git a/cmd/restore_repo.go b/cmd/restore_repo.go index 37b32aa3045..c61f5a582ef 100644 --- a/cmd/restore_repo.go +++ b/cmd/restore_repo.go @@ -4,12 +4,13 @@ package cmd import ( + "context" "strings" "code.gitea.io/gitea/modules/private" "code.gitea.io/gitea/modules/setting" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) // CmdRestoreRepository represents the available restore a repository sub-command. @@ -48,10 +49,7 @@ wiki, issues, labels, releases, release_assets, milestones, pull_requests, comme }, } -func runRestoreRepository(c *cli.Context) error { - ctx, cancel := installSignals() - defer cancel() - +func runRestoreRepository(ctx context.Context, c *cli.Command) error { setting.MustInstalled() var units []string if s := c.String("units"); s != "" { diff --git a/cmd/serv.go b/cmd/serv.go index d2271b68d29..089d0e3bb7c 100644 --- a/cmd/serv.go +++ b/cmd/serv.go @@ -11,7 +11,6 @@ import ( "os" "os/exec" "path/filepath" - "regexp" "strconv" "strings" "time" @@ -20,7 +19,7 @@ import ( asymkey_model "code.gitea.io/gitea/models/asymkey" git_model "code.gitea.io/gitea/models/git" "code.gitea.io/gitea/models/perm" - "code.gitea.io/gitea/modules/container" + "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/lfstransfer" @@ -34,15 +33,7 @@ import ( "github.com/golang-jwt/jwt/v5" "github.com/kballard/go-shellquote" - "github.com/urfave/cli/v2" -) - -const ( - verbUploadPack = "git-upload-pack" - verbUploadArchive = "git-upload-archive" - verbReceivePack = "git-receive-pack" - verbLfsAuthenticate = "git-lfs-authenticate" - verbLfsTransfer = "git-lfs-transfer" + "github.com/urfave/cli/v3" ) // CmdServ represents the available serv sub-command. @@ -50,6 +41,7 @@ var CmdServ = &cli.Command{ Name: "serv", Usage: "(internal) Should only be called by SSH shell", Description: "Serv provides access auth for repositories", + Hidden: true, // Internal commands shouldn't be visible in help Before: PrepareConsoleLoggerLevel(log.FATAL), Action: runServ, Flags: []cli.Flag{ @@ -73,27 +65,11 @@ func setup(ctx context.Context, debug bool) { _ = fail(ctx, "Unable to access repository path", "Unable to access repository path %q, err: %v", setting.RepoRootPath, err) return } - if err := git.InitSimple(context.Background()); err != nil { + if err := git.InitSimple(); err != nil { _ = fail(ctx, "Failed to init git", "Failed to init git, err: %v", err) } } -var ( - // keep getAccessMode() in sync - allowedCommands = container.SetOf( - verbUploadPack, - verbUploadArchive, - verbReceivePack, - verbLfsAuthenticate, - verbLfsTransfer, - ) - allowedCommandsLfs = container.SetOf( - verbLfsAuthenticate, - verbLfsTransfer, - ) - alphaDashDotPattern = regexp.MustCompile(`[^\w-\.]`) -) - // fail prints message to stdout, it's mainly used for git serv and git hook commands. // The output will be passed to git client and shown to user. func fail(ctx context.Context, userMessage, logMsgFmt string, args ...any) error { @@ -104,7 +80,10 @@ func fail(ctx context.Context, userMessage, logMsgFmt string, args ...any) error // There appears to be a chance to cause a zombie process and failure to read the Exit status // if nothing is outputted on stdout. _, _ = fmt.Fprintln(os.Stdout, "") - _, _ = fmt.Fprintln(os.Stderr, "Gitea:", userMessage) + // add extra empty lines to separate our message from other git errors to get more attention + _, _ = fmt.Fprintln(os.Stderr, "error:") + _, _ = fmt.Fprintln(os.Stderr, "error:", userMessage) + _, _ = fmt.Fprintln(os.Stderr, "error:") if logMsgFmt != "" { logMsg := fmt.Sprintf(logMsgFmt, args...) @@ -136,19 +115,20 @@ func handleCliResponseExtra(extra private.ResponseExtra) error { func getAccessMode(verb, lfsVerb string) perm.AccessMode { switch verb { - case verbUploadPack, verbUploadArchive: + case git.CmdVerbUploadPack, git.CmdVerbUploadArchive: return perm.AccessModeRead - case verbReceivePack: + case git.CmdVerbReceivePack: return perm.AccessModeWrite - case verbLfsAuthenticate, verbLfsTransfer: + case git.CmdVerbLfsAuthenticate, git.CmdVerbLfsTransfer: switch lfsVerb { - case "upload": + case git.CmdSubVerbLfsUpload: return perm.AccessModeWrite - case "download": + case git.CmdSubVerbLfsDownload: return perm.AccessModeRead } } // should be unreachable + setting.PanicInDevOrTesting("unknown verb: %s %s", verb, lfsVerb) return perm.AccessModeNone } @@ -170,13 +150,10 @@ func getLFSAuthToken(ctx context.Context, lfsVerb string, results *private.ServC if err != nil { return "", fail(ctx, "Failed to sign JWT Token", "Failed to sign JWT token: %v", err) } - return fmt.Sprintf("Bearer %s", tokenString), nil + return "Bearer " + tokenString, nil } -func runServ(c *cli.Context) error { - ctx, cancel := installSignals() - defer cancel() - +func runServ(ctx context.Context, c *cli.Command) error { // FIXME: This needs to internationalised setup(ctx, c.Bool("debug")) @@ -227,41 +204,37 @@ func runServ(c *cli.Context) error { log.Debug("SSH_ORIGINAL_COMMAND: %s", os.Getenv("SSH_ORIGINAL_COMMAND")) } - words, err := shellquote.Split(cmd) + sshCmdArgs, err := shellquote.Split(cmd) if err != nil { return fail(ctx, "Error parsing arguments", "Failed to parse arguments: %v", err) } - if len(words) < 2 { + if len(sshCmdArgs) < 2 { if git.DefaultFeatures().SupportProcReceive { // for AGit Flow if cmd == "ssh_info" { - fmt.Print(`{"type":"gitea","version":1}`) + fmt.Print(`{"type":"agit","version":1}`) return nil } } return fail(ctx, "Too few arguments", "Too few arguments in cmd: %s", cmd) } - verb := words[0] - repoPath := strings.TrimPrefix(words[1], "/") - - var lfsVerb string - - rr := strings.SplitN(repoPath, "/", 2) - if len(rr) != 2 { + repoPath := strings.TrimPrefix(sshCmdArgs[1], "/") + repoPathFields := strings.SplitN(repoPath, "/", 2) + if len(repoPathFields) != 2 { return fail(ctx, "Invalid repository path", "Invalid repository path: %v", repoPath) } - username := rr[0] - reponame := strings.TrimSuffix(rr[1], ".git") + username := repoPathFields[0] + reponame := strings.TrimSuffix(repoPathFields[1], ".git") // “the-repo-name" or "the-repo-name.wiki" // LowerCase and trim the repoPath as that's how they are stored. // This should be done after splitting the repoPath into username and reponame // so that username and reponame are not affected. repoPath = strings.ToLower(strings.TrimSpace(repoPath)) - if alphaDashDotPattern.MatchString(reponame) { + if !repo.IsValidSSHAccessRepoName(reponame) { return fail(ctx, "Invalid repo name", "Invalid repo name: %s", reponame) } @@ -283,22 +256,23 @@ func runServ(c *cli.Context) error { }() } - if allowedCommands.Contains(verb) { - if allowedCommandsLfs.Contains(verb) { - if !setting.LFS.StartServer { - return fail(ctx, "LFS Server is not enabled", "") - } - if verb == verbLfsTransfer && !setting.LFS.AllowPureSSH { - return fail(ctx, "LFS SSH transfer is not enabled", "") - } - if len(words) > 2 { - lfsVerb = words[2] - } - } - } else { + verb, lfsVerb := sshCmdArgs[0], "" + if !git.IsAllowedVerbForServe(verb) { return fail(ctx, "Unknown git command", "Unknown git command %s", verb) } + if git.IsAllowedVerbForServeLfs(verb) { + if !setting.LFS.StartServer { + return fail(ctx, "LFS Server is not enabled", "") + } + if verb == git.CmdVerbLfsTransfer && !setting.LFS.AllowPureSSH { + return fail(ctx, "LFS SSH transfer is not enabled", "") + } + if len(sshCmdArgs) > 2 { + lfsVerb = sshCmdArgs[2] + } + } + requestedMode := getAccessMode(verb, lfsVerb) results, extra := private.ServCommand(ctx, keyID, username, reponame, requestedMode, verb, lfsVerb) @@ -307,7 +281,7 @@ func runServ(c *cli.Context) error { } // LFS SSH protocol - if verb == verbLfsTransfer { + if verb == git.CmdVerbLfsTransfer { token, err := getLFSAuthToken(ctx, lfsVerb, results) if err != nil { return err @@ -316,7 +290,7 @@ func runServ(c *cli.Context) error { } // LFS token authentication - if verb == verbLfsAuthenticate { + if verb == git.CmdVerbLfsAuthenticate { url := fmt.Sprintf("%s%s/%s.git/info/lfs", setting.AppURL, url.PathEscape(results.OwnerName), url.PathEscape(results.RepoName)) token, err := getLFSAuthToken(ctx, lfsVerb, results) @@ -369,9 +343,9 @@ func runServ(c *cli.Context) error { repo_module.EnvPusherEmail+"="+results.UserEmail, repo_module.EnvPusherID+"="+strconv.FormatInt(results.UserID, 10), repo_module.EnvRepoID+"="+strconv.FormatInt(results.RepoID, 10), - repo_module.EnvPRID+"="+fmt.Sprintf("%d", 0), - repo_module.EnvDeployKeyID+"="+fmt.Sprintf("%d", results.DeployKeyID), - repo_module.EnvKeyID+"="+fmt.Sprintf("%d", results.KeyID), + repo_module.EnvPRID+"="+strconv.Itoa(0), + repo_module.EnvDeployKeyID+"="+strconv.FormatInt(results.DeployKeyID, 10), + repo_module.EnvKeyID+"="+strconv.FormatInt(results.KeyID, 10), repo_module.EnvAppURL+"="+setting.AppURL, ) // to avoid breaking, here only use the minimal environment variables for the "gitea serv" command. diff --git a/cmd/web.go b/cmd/web.go index ef8a7426c14..4723ddbbdd2 100644 --- a/cmd/web.go +++ b/cmd/web.go @@ -12,20 +12,23 @@ import ( "path/filepath" "strconv" "strings" + "time" _ "net/http/pprof" // Used for debugging if enabled and a web server is running "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/graceful" + "code.gitea.io/gitea/modules/gtprof" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/process" "code.gitea.io/gitea/modules/public" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/routers" "code.gitea.io/gitea/routers/install" "github.com/felixge/fgprof" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) // PIDFile could be set from build tag @@ -115,21 +118,31 @@ func showWebStartupMessage(msg string) { log.Info("* CustomPath: %s", setting.CustomPath) log.Info("* ConfigFile: %s", setting.CustomConf) log.Info("%s", msg) // show startup message + + if setting.CORSConfig.Enabled { + log.Info("CORS Service Enabled") + } + if setting.DefaultUILocation != time.Local { + log.Info("Default UI Location is %v", setting.DefaultUILocation.String()) + } + if setting.MailService != nil { + log.Info("Mail Service Enabled: RegisterEmailConfirm=%v, Service.EnableNotifyMail=%v", setting.Service.RegisterEmailConfirm, setting.Service.EnableNotifyMail) + } } -func serveInstall(ctx *cli.Context) error { +func serveInstall(cmd *cli.Command) error { showWebStartupMessage("Prepare to run install page") routers.InitWebInstallPage(graceful.GetManager().HammerContext()) // Flag for port number in case first time run conflict - if ctx.IsSet("port") { - if err := setPort(ctx.String("port")); err != nil { + if cmd.IsSet("port") { + if err := setPort(cmd.String("port")); err != nil { return err } } - if ctx.IsSet("install-port") { - if err := setPort(ctx.String("install-port")); err != nil { + if cmd.IsSet("install-port") { + if err := setPort(cmd.String("install-port")); err != nil { return err } } @@ -150,7 +163,7 @@ func serveInstall(ctx *cli.Context) error { return nil } -func serveInstalled(ctx *cli.Context) error { +func serveInstalled(c *cli.Command) error { setting.InitCfgProvider(setting.CustomConf) setting.LoadCommonSettings() setting.MustInstalled() @@ -200,13 +213,19 @@ func serveInstalled(ctx *cli.Context) error { log.Fatal("Can not find APP_DATA_PATH %q", setting.AppDataPath) } + // the AppDataTempDir is fully managed by us with a safe sub-path + // so it's safe to automatically remove the outdated files + setting.AppDataTempDir("").RemoveOutdated(3 * 24 * time.Hour) + // Override the provided port number within the configuration - if ctx.IsSet("port") { - if err := setPort(ctx.String("port")); err != nil { + if c.IsSet("port") { + if err := setPort(c.String("port")); err != nil { return err } } + gtprof.EnableBuiltinTracer(util.Iif(setting.IsProd, 2000*time.Millisecond, 100*time.Millisecond)) + // Set up Chi routes webRoutes := routers.NormalRoutes() err := listen(webRoutes, true) @@ -217,22 +236,27 @@ func serveInstalled(ctx *cli.Context) error { } func servePprof() { + // FIXME: it shouldn't use the global DefaultServeMux, and it should use a proper context http.DefaultServeMux.Handle("/debug/fgprof", fgprof.Handler()) - _, _, finished := process.GetManager().AddTypedContext(context.Background(), "Web: PProf Server", process.SystemProcessType, true) - // The pprof server is for debug purpose only, it shouldn't be exposed on public network. At the moment it's not worth to introduce a configurable option for it. + _, _, finished := process.GetManager().AddTypedContext(context.TODO(), "Web: PProf Server", process.SystemProcessType, true) + // The pprof server is for debug purpose only, it shouldn't be exposed on public network. At the moment, it's not worth introducing a configurable option for it. log.Info("Starting pprof server on localhost:6060") log.Info("Stopped pprof server: %v", http.ListenAndServe("localhost:6060", nil)) finished() } -func runWeb(ctx *cli.Context) error { +func runWeb(ctx context.Context, cmd *cli.Command) error { defer func() { if panicked := recover(); panicked != nil { log.Fatal("PANIC: %v\n%s", panicked, log.Stack(2)) } }() - managerCtx, cancel := context.WithCancel(context.Background()) + if subCmdName, valid := isValidDefaultSubCommand(cmd); !valid { + return fmt.Errorf("unknown command: %s", subCmdName) + } + + managerCtx, cancel := context.WithCancel(ctx) graceful.InitManager(managerCtx) defer cancel() @@ -243,12 +267,12 @@ func runWeb(ctx *cli.Context) error { } // Set pid file setting - if ctx.IsSet("pid") { - createPIDFile(ctx.String("pid")) + if cmd.IsSet("pid") { + createPIDFile(cmd.String("pid")) } if !setting.InstallLock { - if err := serveInstall(ctx); err != nil { + if err := serveInstall(cmd); err != nil { return err } } else { @@ -259,7 +283,7 @@ func runWeb(ctx *cli.Context) error { go servePprof() } - return serveInstalled(ctx) + return serveInstalled(cmd) } func setPort(port string) error { diff --git a/cmd/web_acme.go b/cmd/web_acme.go index 90e4a02764b..5f7a3083349 100644 --- a/cmd/web_acme.go +++ b/cmd/web_acme.go @@ -16,6 +16,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/process" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" "github.com/caddyserver/certmagic" ) @@ -54,8 +55,6 @@ func runACME(listenAddr string, m http.Handler) error { altTLSALPNPort = p } - magic := certmagic.NewDefault() - magic.Storage = &certmagic.FileStorage{Path: setting.AcmeLiveDirectory} // Try to use private CA root if provided, otherwise defaults to system's trust var certPool *x509.CertPool if setting.AcmeCARoot != "" { @@ -65,8 +64,20 @@ func runACME(listenAddr string, m http.Handler) error { log.Warn("Failed to parse CA Root certificate, using default CA trust: %v", err) } } - myACME := certmagic.NewACMEIssuer(magic, certmagic.ACMEIssuer{ - CA: setting.AcmeURL, + // FIXME: this path is not right, it uses "AppWorkPath" incorrectly, and writes the data into "AppWorkPath/https" + // Ideally it should migrate to AppDataPath write to "AppDataPath/https" + // And one more thing, no idea why we should set the global default variables here + // But it seems that the current ACME code needs these global variables to make renew work. + // Otherwise, "renew" will use incorrect storage path + oldDefaultACME := certmagic.DefaultACME + certmagic.Default.Storage = &certmagic.FileStorage{Path: setting.AcmeLiveDirectory} + certmagic.DefaultACME = certmagic.ACMEIssuer{ + // try to use the default values provided by DefaultACME + CA: util.IfZero(setting.AcmeURL, oldDefaultACME.CA), + TestCA: oldDefaultACME.TestCA, + Logger: oldDefaultACME.Logger, + HTTPProxy: oldDefaultACME.HTTPProxy, + TrustedRoots: certPool, Email: setting.AcmeEmail, Agreed: setting.AcmeTOS, @@ -75,8 +86,10 @@ func runACME(listenAddr string, m http.Handler) error { ListenHost: setting.HTTPAddr, AltTLSALPNPort: altTLSALPNPort, AltHTTPPort: altHTTPPort, - }) + } + magic := certmagic.NewDefault() + myACME := certmagic.NewACMEIssuer(magic, certmagic.DefaultACME) magic.Issuers = []certmagic.Issuer{myACME} // this obtains certificates or renews them if necessary @@ -123,7 +136,7 @@ func runACME(listenAddr string, m http.Handler) error { } func runLetsEncryptFallbackHandler(w http.ResponseWriter, r *http.Request) { - if r.Method != "GET" && r.Method != "HEAD" { + if r.Method != http.MethodGet && r.Method != http.MethodHead { http.Error(w, "Use HTTPS", http.StatusBadRequest) return } diff --git a/cmd/web_graceful.go b/cmd/web_graceful.go index 996537be3b5..5e06d2c2164 100644 --- a/cmd/web_graceful.go +++ b/cmd/web_graceful.go @@ -23,12 +23,6 @@ func NoHTTPRedirector() { graceful.GetManager().InformCleanup() } -// NoMainListener tells our cleanup routine that we will not be using a possibly provided listener -// for our main HTTP/HTTPS service -func NoMainListener() { - graceful.GetManager().InformCleanup() -} - // NoInstallListener tells our cleanup routine that we will not be using a possibly provided listener // for our install HTTP/HTTPS service func NoInstallListener() { diff --git a/contrib/autocompletion/README b/contrib/autocompletion/README deleted file mode 100644 index 1defd219d8a..00000000000 --- a/contrib/autocompletion/README +++ /dev/null @@ -1,17 +0,0 @@ -Bash and Zsh completion -======================= - -From within the gitea root run: - -```bash -source contrib/autocompletion/bash_autocomplete -``` - -or for zsh run: - -```bash -source contrib/autocompletion/zsh_autocomplete -``` - -These scripts will check if gitea is on the path and if so add autocompletion for `gitea`. Or if not autocompletion will work for `./gitea`. -If gitea has been installed as a different program pass in the `PROG` environment variable to set the correct program name. diff --git a/contrib/autocompletion/bash_autocomplete b/contrib/autocompletion/bash_autocomplete deleted file mode 100755 index 5cb62f26a71..00000000000 --- a/contrib/autocompletion/bash_autocomplete +++ /dev/null @@ -1,30 +0,0 @@ -#! /bin/bash -# Heavily inspired by https://github.com/urfave/cli - -_cli_bash_autocomplete() { - if [[ "${COMP_WORDS[0]}" != "source" ]]; then - local cur opts base - COMPREPLY=() - cur="${COMP_WORDS[COMP_CWORD]}" - if [[ "$cur" == "-"* ]]; then - opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} ${cur} --generate-bash-completion ) - else - opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} --generate-bash-completion ) - fi - COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) - return 0 - fi -} - -if [ -z "$PROG" ] && [ ! "$(command -v gitea &> /dev/null)" ] ; then - complete -o bashdefault -o default -o nospace -F _cli_bash_autocomplete gitea -elif [ -z "$PROG" ]; then - complete -o bashdefault -o default -o nospace -F _cli_bash_autocomplete ./gitea - complete -o bashdefault -o default -o nospace -F _cli_bash_autocomplete "$PWD/gitea" -else - complete -o bashdefault -o default -o nospace -F _cli_bash_autocomplete "$PROG" - unset PROG -fi - - - diff --git a/contrib/autocompletion/zsh_autocomplete b/contrib/autocompletion/zsh_autocomplete deleted file mode 100644 index b3b40df503f..00000000000 --- a/contrib/autocompletion/zsh_autocomplete +++ /dev/null @@ -1,30 +0,0 @@ -#compdef ${PROG:=gitea} - - -# Heavily inspired by https://github.com/urfave/cli - -_cli_zsh_autocomplete() { - - local -a opts - local cur - cur=${words[-1]} - if [[ "$cur" == "-"* ]]; then - opts=("${(@f)$(_CLI_ZSH_AUTOCOMPLETE_HACK=1 ${words[@]:0:#words[@]-1} ${cur} --generate-bash-completion)}") - else - opts=("${(@f)$(_CLI_ZSH_AUTOCOMPLETE_HACK=1 ${words[@]:0:#words[@]-1} --generate-bash-completion)}") - fi - - if [[ "${opts[1]}" != "" ]]; then - _describe 'values' opts - else - _files - fi - - return -} - -if [ -z $PROG ] ; then - compdef _cli_zsh_autocomplete gitea -else - compdef _cli_zsh_autocomplete $(basename $PROG) -fi diff --git a/contrib/backport/backport.go b/contrib/backport/backport.go index eb194374452..2052295fb14 100644 --- a/contrib/backport/backport.go +++ b/contrib/backport/backport.go @@ -1,31 +1,30 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -//nolint:forbidigo +//nolint:forbidigo // use of print functions is allowed in cli package main import ( "context" + "errors" "fmt" "log" "net/http" "os" "os/exec" - "os/signal" "path" "strconv" "strings" - "syscall" - "github.com/google/go-github/v61/github" - "github.com/urfave/cli/v2" + "github.com/google/go-github/v71/github" + "github.com/urfave/cli/v3" "gopkg.in/yaml.v3" ) const defaultVersion = "v1.18" // to backport to func main() { - app := cli.NewApp() + app := &cli.Command{} app.Name = "backport" app.Usage = "Backport provided PR-number on to the current or previous released version" app.Description = `Backport will look-up the PR in Gitea's git log and attempt to cherry-pick it on the current version` @@ -90,7 +89,7 @@ func main() { Usage: "Set this flag to continue from a git cherry-pick that has broken", }, } - cli.AppHelpTemplate = `NAME: + cli.RootCommandHelpTemplate = `NAME: {{.Name}} - {{.Usage}} USAGE: {{.HelpName}} {{if .VisibleFlags}}[options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}} @@ -104,16 +103,12 @@ OPTIONS: ` app.Action = runBackport - - if err := app.Run(os.Args); err != nil { + if err := app.Run(context.Background(), os.Args); err != nil { fmt.Fprintf(os.Stderr, "Unable to backport: %v\n", err) } } -func runBackport(c *cli.Context) error { - ctx, cancel := installSignals() - defer cancel() - +func runBackport(ctx context.Context, c *cli.Command) error { continuing := c.Bool("continue") var pr string @@ -158,7 +153,7 @@ func runBackport(c *cli.Context) error { args := c.Args().Slice() if len(args) == 0 && pr == "" { - return fmt.Errorf("no PR number provided\nProvide a PR number to backport") + return errors.New("no PR number provided\nProvide a PR number to backport") } else if len(args) != 1 && pr == "" { return fmt.Errorf("multiple PRs provided %v\nOnly a single PR can be backported at a time", args) } @@ -342,8 +337,8 @@ func determineRemote(ctx context.Context, forkUser string) (string, string, erro fmt.Fprintf(os.Stderr, "Unable to list git remotes:\n%s\n", string(out)) return "", "", fmt.Errorf("unable to determine forked remote: %w", err) } - lines := strings.Split(string(out), "\n") - for _, line := range lines { + lines := strings.SplitSeq(string(out), "\n") + for line := range lines { fields := strings.Split(line, "\t") name, remote := fields[0], fields[1] // only look at pushers @@ -361,12 +356,12 @@ func determineRemote(ctx context.Context, forkUser string) (string, string, erro if !strings.Contains(remote, forkUser) { continue } - if strings.HasPrefix(remote, "git@github.com:") { - forkUser = strings.TrimPrefix(remote, "git@github.com:") - } else if strings.HasPrefix(remote, "https://github.com/") { - forkUser = strings.TrimPrefix(remote, "https://github.com/") - } else if strings.HasPrefix(remote, "https://www.github.com/") { - forkUser = strings.TrimPrefix(remote, "https://www.github.com/") + if after, ok := strings.CutPrefix(remote, "git@github.com:"); ok { + forkUser = after + } else if after, ok := strings.CutPrefix(remote, "https://github.com/"); ok { + forkUser = after + } else if after, ok := strings.CutPrefix(remote, "https://www.github.com/"); ok { + forkUser = after } else if forkUser == "" { return "", "", fmt.Errorf("unable to extract forkUser from remote %s: %s", name, remote) } @@ -459,25 +454,3 @@ func determineSHAforPR(ctx context.Context, prStr, accessToken string) (string, return "", nil } - -func installSignals() (context.Context, context.CancelFunc) { - ctx, cancel := context.WithCancel(context.Background()) - go func() { - // install notify - signalChannel := make(chan os.Signal, 1) - - signal.Notify( - signalChannel, - syscall.SIGINT, - syscall.SIGTERM, - ) - select { - case <-signalChannel: - case <-ctx.Done(): - } - cancel() - signal.Reset() - }() - - return ctx, cancel -} diff --git a/contrib/environment-to-ini/environment-to-ini.go b/contrib/environment-to-ini/environment-to-ini.go index a7d7a6d293d..5eb576c6fea 100644 --- a/contrib/environment-to-ini/environment-to-ini.go +++ b/contrib/environment-to-ini/environment-to-ini.go @@ -4,16 +4,17 @@ package main import ( + "context" "os" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) func main() { - app := cli.NewApp() + app := cli.Command{} app.Name = "environment-to-ini" app.Usage = "Use provided environment to update configuration ini" app.Description = `As a helper to allow docker users to update the gitea configuration @@ -72,13 +73,13 @@ func main() { }, } app.Action = runEnvironmentToIni - err := app.Run(os.Args) + err := app.Run(context.Background(), os.Args) if err != nil { log.Fatal("Failed to run app with %s: %v", os.Args, err) } } -func runEnvironmentToIni(c *cli.Context) error { +func runEnvironmentToIni(_ context.Context, c *cli.Command) error { // the config system may change the environment variables, so get a copy first, to be used later env := append([]string{}, os.Environ()...) setting.InitWorkPathAndCfgProvider(os.Getenv, setting.ArgWorkPathAndCustomConf{ diff --git a/contrib/fixtures/fixture_generation.go b/contrib/fixtures/fixture_generation.go deleted file mode 100644 index 31797cc800f..00000000000 --- a/contrib/fixtures/fixture_generation.go +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright 2020 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -//nolint:forbidigo -package main - -import ( - "context" - "fmt" - "os" - "path/filepath" - - "code.gitea.io/gitea/models" - "code.gitea.io/gitea/models/unittest" -) - -// To generate derivative fixtures, execute the following from Gitea's repository base dir: -// go run -tags 'sqlite sqlite_unlock_notify' contrib/fixtures/fixture_generation.go [fixture...] - -var ( - generators = []struct { - gen func(ctx context.Context) (string, error) - name string - }{ - { - models.GetYamlFixturesAccess, "access", - }, - } - fixturesDir string -) - -func main() { - pathToGiteaRoot := "." - fixturesDir = filepath.Join(pathToGiteaRoot, "models", "fixtures") - if err := unittest.CreateTestEngine(unittest.FixturesOptions{ - Dir: fixturesDir, - }); err != nil { - fmt.Printf("CreateTestEngine: %+v", err) - os.Exit(1) - } - if err := unittest.PrepareTestDatabase(); err != nil { - fmt.Printf("PrepareTestDatabase: %+v\n", err) - os.Exit(1) - } - ctx := context.Background() - if len(os.Args) == 0 { - for _, r := range os.Args { - if err := generate(ctx, r); err != nil { - fmt.Printf("generate '%s': %+v\n", r, err) - os.Exit(1) - } - } - } else { - for _, g := range generators { - if err := generate(ctx, g.name); err != nil { - fmt.Printf("generate '%s': %+v\n", g.name, err) - os.Exit(1) - } - } - } -} - -func generate(ctx context.Context, name string) error { - for _, g := range generators { - if g.name == name { - data, err := g.gen(ctx) - if err != nil { - return err - } - path := filepath.Join(fixturesDir, name+".yml") - if err := os.WriteFile(path, []byte(data), 0o644); err != nil { - return fmt.Errorf("%s: %+v", path, err) - } - fmt.Printf("%s created.\n", path) - return nil - } - } - - return fmt.Errorf("generator not found") -} diff --git a/contrib/upgrade.sh b/contrib/upgrade.sh index 4b166a02a0d..e5e296ea8b2 100755 --- a/contrib/upgrade.sh +++ b/contrib/upgrade.sh @@ -85,7 +85,7 @@ fi # confirm update echo "Checking currently installed version..." current=$(giteacmd --version | cut -d ' ' -f 3) -[[ "$current" == "$giteaversion" ]] && echo "$current is already installed, stopping." && exit 1 +[[ "$current" == "$giteaversion" ]] && echo "$current is already installed, stopping." && exit 0 if [[ -z "${no_confirm:-}" ]]; then echo "Make sure to read the changelog first: https://github.com/go-gitea/gitea/blob/main/CHANGELOG.md" echo "Are you ready to update Gitea from ${current} to ${giteaversion}? (y/N)" diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index c3b78e60bb4..aa2fcee7655 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -59,27 +59,22 @@ RUN_USER = ; git ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; -;; The protocol the server listens on. One of 'http', 'https', 'http+unix', 'fcgi' or 'fcgi+unix'. Defaults to 'http' -;; Note: Value must be lowercase. +;; The protocol the server listens on. One of "http", "https", "http+unix", "fcgi" or "fcgi+unix". ;PROTOCOL = http ;; -;; Expect PROXY protocol headers on connections -;USE_PROXY_PROTOCOL = false -;; -;; Use PROXY protocol in TLS Bridging mode -;PROXY_PROTOCOL_TLS_BRIDGING = false -;; -; Timeout to wait for PROXY protocol header (set to 0 to have no timeout) -;PROXY_PROTOCOL_HEADER_TIMEOUT=5s -;; -; Accept PROXY protocol headers with UNKNOWN type -;PROXY_PROTOCOL_ACCEPT_UNKNOWN=false -;; -;; Set the domain for the server +;; Set the domain for the server. ;DOMAIN = localhost ;; -;; Overwrite the automatically generated public URL. Necessary for proxies and docker. -;ROOT_URL = %(PROTOCOL)s://%(DOMAIN)s:%(HTTP_PORT)s/ +;; The AppURL is used to generate public URL links, defaults to "{PROTOCOL}://{DOMAIN}:{HTTP_PORT}/". +;; Most users should set it to the real website URL of their Gitea instance when there is a reverse proxy. +;ROOT_URL = +;; +;; Controls how to detect the public URL. +;; Although it defaults to "legacy" (to avoid breaking existing users), most instances should use the "auto" behavior, +;; especially when the Gitea instance needs to be accessed in a container network. +;; * legacy: detect the public URL from "Host" header if "X-Forwarded-Proto" header exists, otherwise use "ROOT_URL". +;; * auto: always use "Host" header, and also use "X-Forwarded-Proto" header if it exists. If no "Host" header, use "ROOT_URL". +;PUBLIC_URL_DETECTION = legacy ;; ;; For development purpose only. It makes Gitea handle sub-path ("/sub-path/owner/repo/...") directly when debugging without a reverse proxy. ;; DO NOT USE IT IN PRODUCTION!!! @@ -89,13 +84,25 @@ RUN_USER = ; git ;STATIC_URL_PREFIX = ;; ;; The address to listen on. Either a IPv4/IPv6 address or the path to a unix socket. -;; If PROTOCOL is set to `http+unix` or `fcgi+unix`, this should be the name of the Unix socket file to use. +;; If PROTOCOL is set to "http+unix" or "fcgi+unix", this should be the name of the Unix socket file to use. ;; Relative paths will be made absolute against the _`AppWorkPath`_. ;HTTP_ADDR = 0.0.0.0 ;; -;; The port to listen on. Leave empty when using a unix socket. +;; The port to listen on for "http" or "https" protocol. Leave empty when using a unix socket. ;HTTP_PORT = 3000 ;; +;; Expect PROXY protocol headers on connections +;USE_PROXY_PROTOCOL = false +;; +;; Use PROXY protocol in TLS Bridging mode +;PROXY_PROTOCOL_TLS_BRIDGING = false +;; +;; Timeout to wait for PROXY protocol header (set to 0 to have no timeout) +;PROXY_PROTOCOL_HEADER_TIMEOUT = 5s +;; +;; Accept PROXY protocol headers with UNKNOWN type +;PROXY_PROTOCOL_ACCEPT_UNKNOWN = false +;; ;; If REDIRECT_OTHER_PORT is true, and PROTOCOL is set to https an http server ;; will be started on PORT_TO_REDIRECT and it will redirect plain, non-secure http requests to the main ;; ROOT_URL. Defaults are false for REDIRECT_OTHER_PORT and 80 for @@ -103,8 +110,8 @@ RUN_USER = ; git ;REDIRECT_OTHER_PORT = false ;PORT_TO_REDIRECT = 80 ;; -;; expect PROXY protocol header on connections to https redirector. -;REDIRECTOR_USE_PROXY_PROTOCOL = %(USE_PROXY_PROTOCOL)s +;; expect PROXY protocol header on connections to https redirector, defaults to USE_PROXY_PROTOCOL +;REDIRECTOR_USE_PROXY_PROTOCOL = ;; Minimum and maximum supported TLS versions ;SSL_MIN_VERSION=TLSv1.2 ;SSL_MAX_VERSION= @@ -128,13 +135,14 @@ RUN_USER = ; git ;; most cases you do not need to change the default value. Alter it only if ;; your SSH server node is not the same as HTTP node. For different protocol, the default ;; values are different. If `PROTOCOL` is `http+unix`, the default value is `http://unix/`. -;; If `PROTOCOL` is `fcgi` or `fcgi+unix`, the default value is `%(PROTOCOL)s://%(HTTP_ADDR)s:%(HTTP_PORT)s/`. -;; If listen on `0.0.0.0`, the default value is `%(PROTOCOL)s://localhost:%(HTTP_PORT)s/`, Otherwise the default -;; value is `%(PROTOCOL)s://%(HTTP_ADDR)s:%(HTTP_PORT)s/`. -;LOCAL_ROOT_URL = %(PROTOCOL)s://%(HTTP_ADDR)s:%(HTTP_PORT)s/ +;; If `PROTOCOL` is `fcgi` or `fcgi+unix`, the default value is `{PROTOCOL}://{HTTP_ADDR}:{HTTP_PORT}/`. +;; If listen on `0.0.0.0`, the default value is `{PROTOCOL}://localhost:{HTTP_PORT}/`. +;; Otherwise the default value is `{PROTOCOL}://{HTTP_ADDR}:{HTTP_PORT}/`. +;; Most users don't need (and shouldn't) set this value. +;LOCAL_ROOT_URL = ;; -;; When making local connections pass the PROXY protocol header. -;LOCAL_USE_PROXY_PROTOCOL = %(USE_PROXY_PROTOCOL)s +;; When making local connections pass the PROXY protocol header, defaults to USE_PROXY_PROTOCOL +;LOCAL_USE_PROXY_PROTOCOL = ;; ;; Disable SSH feature when not available ;DISABLE_SSH = false @@ -146,13 +154,17 @@ RUN_USER = ; git ;SSH_SERVER_USE_PROXY_PROTOCOL = false ;; ;; Username to use for the builtin SSH server. If blank, then it is the value of RUN_USER. -;BUILTIN_SSH_SERVER_USER = %(RUN_USER)s +;BUILTIN_SSH_SERVER_USER = ;; -;; Domain name to be exposed in clone URL -;SSH_DOMAIN = %(DOMAIN)s +;; Domain name to be exposed in clone URL, defaults to DOMAIN or the domain part of ROOT_URL +;SSH_DOMAIN = ;; -;; SSH username displayed in clone URLs. -;SSH_USER = %(BUILTIN_SSH_SERVER_USER)s +;; SSH username displayed in clone URLs. It defaults to BUILTIN_SSH_SERVER_USER or RUN_USER. +;; If it is set to "(DOER_USERNAME)", it will use current signed-in user's username. +;; This option is only for some advanced users who have configured their SSH reverse-proxy +;; and need to use different usernames for git SSH clone. +;; Most users should just leave it blank. +;SSH_USER = ;; ;; The network interface the builtin SSH server should listen on ;SSH_LISTEN_HOST = @@ -160,8 +172,8 @@ RUN_USER = ; git ;; Port number to be exposed in clone URL ;SSH_PORT = 22 ;; -;; The port number the builtin SSH server should listen on -;SSH_LISTEN_PORT = %(SSH_PORT)s +;; The port number the builtin SSH server should listen on, defaults to SSH_PORT +;SSH_LISTEN_PORT = ;; ;; Root path of SSH directory, default is '~/.ssh', but you have to use '/home/git/.ssh'. ;SSH_ROOT_PATH = @@ -174,30 +186,19 @@ RUN_USER = ; git ;; If you intend to use the AuthorizedPrincipalsCommand functionality then you should turn this off. ;SSH_CREATE_AUTHORIZED_PRINCIPALS_FILE = true ;; -;; For the built-in SSH server, choose the ciphers to support for SSH connections, -;; for system SSH this setting has no effect -;SSH_SERVER_CIPHERS = chacha20-poly1305@openssh.com, aes128-ctr, aes192-ctr, aes256-ctr, aes128-gcm@openssh.com, aes256-gcm@openssh.com -;; -;; For the built-in SSH server, choose the key exchange algorithms to support for SSH connections, -;; for system SSH this setting has no effect -;SSH_SERVER_KEY_EXCHANGES = curve25519-sha256, ecdh-sha2-nistp256, ecdh-sha2-nistp384, ecdh-sha2-nistp521, diffie-hellman-group14-sha256, diffie-hellman-group14-sha1 -;; -;; For the built-in SSH server, choose the MACs to support for SSH connections, -;; for system SSH this setting has no effect -;SSH_SERVER_MACS = hmac-sha2-256-etm@openssh.com, hmac-sha2-256, hmac-sha1 +;; For the builtin SSH server, choose the supported ciphers/key-exchange-algorithms/MACs for SSH connections. +;; The supported names are listed in https://github.com/golang/crypto/blob/master/ssh/common.go. +;; Leave them empty to use the Golang crypto's recommended default values. +;; For system SSH (non-builtin SSH server), this setting has no effect. +;SSH_SERVER_CIPHERS = +;SSH_SERVER_KEY_EXCHANGES = +;SSH_SERVER_MACS = ;; ;; For the built-in SSH server, choose the keypair to offer as the host key ;; The private key should be at SSH_SERVER_HOST_KEY and the public SSH_SERVER_HOST_KEY.pub -;; relative paths are made absolute relative to the %(APP_DATA_PATH)s +;; relative paths are made absolute relative to the APP_DATA_PATH ;SSH_SERVER_HOST_KEYS=ssh/gitea.rsa, ssh/gogs.rsa ;; -;; Directory to create temporary files in when testing public keys using ssh-keygen, -;; default is the system temporary directory. -;SSH_KEY_TEST_PATH = -;; -;; Use `ssh-keygen` to parse public SSH keys. The value is passed to the shell. By default, Gitea does the parsing itself. -;SSH_KEYGEN_PATH = -;; ;; Enable SSH Authorized Key Backup when rewriting all keys, default is false ;SSH_AUTHORIZED_KEYS_BACKUP = false ;; @@ -288,6 +289,9 @@ RUN_USER = ; git ;; Default path for App data ;APP_DATA_PATH = data ; relative paths will be made absolute with _`AppWorkPath`_ ;; +;; Base path for App's temp files, leave empty to use the managed tmp directory in APP_DATA_PATH +;APP_TEMP_PATH = +;; ;; Enable gzip compression for runtime-generated content, static resources excluded ;ENABLE_GZIP = false ;; @@ -516,6 +520,10 @@ INTERNAL_TOKEN = ;; ;; On user registration, record the IP address and user agent of the user to help identify potential abuse. ;; RECORD_USER_SIGNUP_METADATA = false +;; +;; Set the two-factor auth behavior. +;; Set to "enforced", to force users to enroll into Two-Factor Authentication, users without 2FA have no access to repositories via API or web. +;TWO_FACTOR_AUTH = ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -582,7 +590,7 @@ ENABLED = true [log] ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; Root path for the log files - defaults to %(GITEA_WORK_DIR)/log +;; Root path for the log files - defaults to "{AppWorkPath}/log" ;ROOT_PATH = ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -682,8 +690,8 @@ LEVEL = Info ;; The path of git executable. If empty, Gitea searches through the PATH environment. ;PATH = ;; -;; The HOME directory for Git -;HOME_PATH = %(APP_DATA_PATH)s/home +;; The HOME directory for Git, defaults to "{APP_DATA_PATH}/home" +;HOME_PATH = ;; ;; Disables highlight of added and removed changes ;DISABLE_DIFF_HIGHLIGHT = false @@ -774,6 +782,9 @@ LEVEL = Info ;ALLOW_ONLY_EXTERNAL_REGISTRATION = false ;; ;; User must sign in to view anything. +;; It could be set to "expensive" to block anonymous users accessing some pages which consume a lot of resources, +;; for example: block anonymous AI crawlers from accessing repo code pages. +;; The "expensive" mode is experimental and subject to change. ;REQUIRE_SIGNIN_VIEW = false ;; ;; Mail notification @@ -784,6 +795,13 @@ LEVEL = Info ;; Please note that setting this to false will not disable OAuth Basic or Basic authentication using a token ;ENABLE_BASIC_AUTHENTICATION = true ;; +;; Show the password sign-in form (for password-based login), otherwise, only show OAuth2 or passkey login methods if they are enabled. +;; If you set it to false, maybe it also needs to set ENABLE_BASIC_AUTHENTICATION to false to completely disable password-based authentication. +;ENABLE_PASSWORD_SIGNIN_FORM = true +;; +;; Allow users to sign-in with a passkey +;ENABLE_PASSKEY_AUTHENTICATION = true +;; ;; More detail: https://github.com/gogits/gogs/issues/165 ;ENABLE_REVERSE_PROXY_AUTHENTICATION = false ; Enable this to allow reverse proxy authentication for API requests, the reverse proxy is responsible for ensuring that no CSRF is possible. @@ -928,7 +946,29 @@ LEVEL = Info ;; ;; Disable the code explore page. ;DISABLE_CODE_PAGE = false + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;[qos] +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; +;; Enable request quality of service and overload protection. +; ENABLED = false +;; +;; The maximum number of concurrent requests that the server will +;; process before enqueueing new requests. Default is "CpuNum * 4". +; MAX_INFLIGHT = +;; +;; The maximum number of requests that can be enqueued before new +;; requests will be dropped. +; MAX_WAITING = 100 +;; +;; Target maximum wait time a request may be enqueued for. Requests +;; that are enqueued for less than this amount of time will not be +;; dropped. When wait times exceed this amount, a portion of requests +;; will be dropped until wait times have decreased below this amount. +; TARGET_WAIT_TIME = 250ms ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -942,8 +982,8 @@ LEVEL = Info ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;[repository] ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; Root path for storing all repository data. By default, it is set to %(APP_DATA_PATH)s/gitea-repositories. -;; A relative path is interpreted as _`AppWorkPath`_/%(ROOT)s +;; Root path for storing all repository data. By default, it is set to "{APP_DATA_PATH}/gitea-repositories". +;; A relative path is interpreted as "{AppWorkPath}/{ROOT}" (use AppWorkPath as base path). ;ROOT = ;; ;; The script type this server supports. Usually this is `bash`, but some users report that only `sh` is available. @@ -1036,9 +1076,13 @@ LEVEL = Info ;; Don't allow download source archive files from UI ;DISABLE_DOWNLOAD_SOURCE_ARCHIVES = false -;; Allow fork repositories without maximum number limit +;; Allow to fork repositories without maximum number limit ;ALLOW_FORK_WITHOUT_MAXIMUM_LIMIT = true +;; Allow to fork repositories into the same owner (user or organization) +;; This feature is experimental, not fully tested, and may be changed in the future +;ALLOW_FORK_INTO_SAME_OWNER = false + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;[repository.editor] @@ -1049,15 +1093,6 @@ LEVEL = Info ;; Separate extensions with a comma. To line wrap files without an extension, just put a comma ;LINE_WRAP_EXTENSIONS = .txt,.md,.markdown,.mdown,.mkd,.livemd, -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;[repository.local] -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; -;; Path for local repository copy. Defaults to `tmp/local-repo` (content gets deleted on gitea restart) -;LOCAL_COPY_PATH = tmp/local-repo - ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;[repository.upload] @@ -1067,9 +1102,6 @@ LEVEL = Info ;; Whether repository file uploads are enabled. Defaults to `true` ;ENABLED = true ;; -;; Path for uploads. Defaults to `data/tmp/uploads` (content gets deleted on gitea restart) -;TEMP_PATH = data/tmp/uploads -;; ;; Comma-separated list of allowed file extensions (`.zip`), mime types (`text/plain`) or wildcard type (`image/*`, `audio/*`, `video/*`). Empty value or `*/*` allows all types. ;ALLOWED_TYPES = ;; @@ -1112,6 +1144,9 @@ LEVEL = Info ;; In default merge messages only include approvers who are official ;DEFAULT_MERGE_MESSAGE_OFFICIAL_APPROVERS_ONLY = true ;; +;; In default squash-merge messages include the commit message of all commits comprising the pull request. +;POPULATE_SQUASH_COMMENT_WITH_COMMIT_MESSAGES = false +;; ;; Add co-authored-by and co-committed-by trailers if committer does not match author ;ADD_CO_COMMITTER_TRAILERS = true ;; @@ -1120,6 +1155,10 @@ LEVEL = Info ;; ;; Retarget child pull requests to the parent pull request branch target on merge of parent pull request. It only works on merged PRs where the head and base branch target the same repo. ;RETARGET_CHILDREN_ON_MERGE = true +;; +;; Delay mergeable check until page view or API access, for pull requests that have not been updated in the specified days when their base branches get updated. +;; Use "-1" to always check all pull requests (old behavior). Use "0" to always delay the checks. +;DELAY_CHECK_FOR_INACTIVE_DAYS = 7 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -1147,17 +1186,24 @@ LEVEL = Info ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; -;; GPG key to use to sign commits, Defaults to the default - that is the value of git config --get user.signingkey +;; GPG or SSH key to use to sign commits, Defaults to the default - that is the value of git config --get user.signingkey +;; Depending on the value of SIGNING_FORMAT this is either: +;; - openpgp: the GPG key ID +;; - ssh: the path to the ssh public key "/path/to/key.pub": where "/path/to/key" is the private key, use ssh-keygen -t ed25519 to generate a new key pair without password ;; run in the context of the RUN_USER ;; Switch to none to stop signing completely ;SIGNING_KEY = default ;; -;; If a SIGNING_KEY ID is provided and is not set to default, use the provided Name and Email address as the signer. +;; If a SIGNING_KEY ID is provided and is not set to default, use the provided Name and Email address as the signer and the signing format. ;; These should match a publicized name and email address for the key. (When SIGNING_KEY is default these are set to -;; the results of git config --get user.name and git config --get user.email respectively and can only be overridden +;; the results of git config --get user.name, git config --get user.email and git config --default openpgp --get gpg.format respectively and can only be overridden ;; by setting the SIGNING_KEY ID to the correct ID.) ;SIGNING_NAME = ;SIGNING_EMAIL = +;; SIGNING_FORMAT can be one of: +;; - openpgp (default): use GPG to sign commits +;; - ssh: use SSH to sign commits +;SIGNING_FORMAT = openpgp ;; ;; Sets the default trust model for repositories. Options are: collaborator, committer, collaboratorcommitter ;DEFAULT_TRUST_MODEL = collaborator @@ -1184,6 +1230,13 @@ LEVEL = Info ;; - commitssigned: require that all the commits in the head branch are signed. ;; - approved: only sign when merging an approved pr to a protected branch ;MERGES = pubkey, twofa, basesigned, commitssigned +;; +;; Determines which additional ssh keys are trusted for all signed commits regardless of the user +;; This is useful for ssh signing key rotation. +;; Exposes the provided SIGNING_NAME and SIGNING_EMAIL as the signer, regardless of the SIGNING_FORMAT value. +;; Multiple keys should be comma separated. +;; E.g."ssh- ". or "ssh- , ssh- ". +;TRUSTED_SSH_KEYS = ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -1274,6 +1327,9 @@ LEVEL = Info ;; Leave it empty to allow users to select any theme from "{CustomPath}/public/assets/css/theme-*.css" ;THEMES = ;; +;; The icons for file list (basic/material), this is a temporary option which will be replaced by a user setting in the future. +;FILE_ICON_THEME = material +;; ;; All available reactions users can choose on issues/prs and comments. ;; Values can be emoji alias (:smile:) or a unicode emoji. ;; For custom reactions, add a tightly cropped square image to public/assets/img/emoji/reaction_name.png @@ -1331,6 +1387,9 @@ LEVEL = Info ;; Number of repos that are displayed on one page ;REPO_PAGING_NUM = 15 +;; Number of orgs that are displayed on profile page +;ORG_PAGING_NUM = 15 + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;[ui.meta] @@ -1384,14 +1443,14 @@ LEVEL = Info ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; -;; Render soft line breaks as hard line breaks, which means a single newline character between -;; paragraphs will cause a line break and adding trailing whitespace to paragraphs is not -;; necessary to force a line break. -;; Render soft line breaks as hard line breaks for comments -;ENABLE_HARD_LINE_BREAK_IN_COMMENTS = true -;; -;; Render soft line breaks as hard line breaks for markdown documents -;ENABLE_HARD_LINE_BREAK_IN_DOCUMENTS = false +;; Customize render options for different contexts. Set to "none" to disable the defaults, or use comma separated list: +;; * short-issue-pattern: recognized "#123" issue reference and render it as a link to the issue +;; * new-line-hard-break: render soft line breaks as hard line breaks, which means a single newline character between +;; paragraphs will cause a line break and adding trailing whitespace to paragraphs is not +;; necessary to force a line break. +;RENDER_OPTIONS_COMMENT = short-issue-pattern, new-line-hard-break +;RENDER_OPTIONS_WIKI = short-issue-pattern +;RENDER_OPTIONS_REPO_FILE = ;; ;; Comma separated list of custom URL-Schemes that are allowed as links when rendering Markdown ;; for example git,magnet,ftp (more at https://en.wikipedia.org/wiki/List_of_URI_schemes) @@ -1405,6 +1464,11 @@ LEVEL = Info ;; ;; Enables math inline and block detection ;ENABLE_MATH = true +;; +;; Enable delimiters for math code block detection. Set to "none" to disable all, +;; or use comma separated list: inline-dollar, inline-parentheses, block-dollar, block-square-brackets +;; Defaults to "inline-dollar,block-dollar" to follow GitHub's behavior. +;MATH_CODE_BLOCK_DETECTION = ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -1474,6 +1538,10 @@ LEVEL = Info ;REPO_INDEXER_EXCLUDE = ;; ;MAX_FILE_SIZE = 1048576 +;; +;; Bleve engine has performance problems with fuzzy search, so we limit the fuzziness to 0 by default to disable it. +;; If you'd like to enable it, you can set it to a value between 0 and 2. +;TYPE_BLEVE_MAX_FUZZINESS = 0 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -1491,7 +1559,8 @@ LEVEL = Info ;TYPE = persistable-channel ;; ;; data-dir for storing persistable queues and level queues, individual queues will default to `queues/common` meaning the queue is shared. -;DATADIR = queues/ ; Relative paths will be made absolute against `%(APP_DATA_PATH)s`. +;; Relative paths will be made absolute against "APP_DATA_PATH" +;DATADIR = queues/ ;; ;; Default queue length before a channel queue will block ;LENGTH = 100000 @@ -1739,6 +1808,9 @@ LEVEL = Info ;; ;; convert \r\n to \n for Sendmail ;SENDMAIL_CONVERT_CRLF = true +;; +;; convert links of attached images to inline images. Only for images hosted in this gitea instance. +;EMBED_ATTACHMENT_IMAGES = false ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -1944,13 +2016,13 @@ LEVEL = Info ;; Minio secretAccessKey to connect only available when STORAGE_TYPE is `minio` ;MINIO_SECRET_ACCESS_KEY = ;; -;; Preferred IAM Endpoint to override Minio's default IAM Endpoint resolution only available when STORAGE_TYPE is `minio`. -;; If not provided and STORAGE_TYPE is `minio`, will search for and derive endpoint from known environment variables -;; (AWS_CONTAINER_AUTHORIZATION_TOKEN, AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE, AWS_CONTAINER_CREDENTIALS_RELATIVE_URI, -;; AWS_CONTAINER_CREDENTIALS_FULL_URI, AWS_WEB_IDENTITY_TOKEN_FILE, AWS_ROLE_ARN, AWS_ROLE_SESSION_NAME, AWS_REGION), +;; Preferred IAM Endpoint to override Minio's default IAM Endpoint resolution only available when STORAGE_TYPE is `minio`. +;; If not provided and STORAGE_TYPE is `minio`, will search for and derive endpoint from known environment variables +;; (AWS_CONTAINER_AUTHORIZATION_TOKEN, AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE, AWS_CONTAINER_CREDENTIALS_RELATIVE_URI, +;; AWS_CONTAINER_CREDENTIALS_FULL_URI, AWS_WEB_IDENTITY_TOKEN_FILE, AWS_ROLE_ARN, AWS_ROLE_SESSION_NAME, AWS_REGION), ;; or the DefaultIAMRoleEndpoint if not provided otherwise. ;MINIO_IAM_ENDPOINT = -;; +;; ;; Minio bucket to store the attachments only available when STORAGE_TYPE is `minio` ;MINIO_BUCKET = gitea ;; @@ -2390,6 +2462,8 @@ LEVEL = Info ;DEFAULT_GIT_TREES_PER_PAGE = 1000 ;; Default max size of a blob returned by the blobs API (default is 10MiB) ;DEFAULT_MAX_BLOB_SIZE = 10485760 +;; Default max combined size of all blobs returned by the files API (default is 100MiB) +;DEFAULT_MAX_RESPONSE_SIZE = 104857600 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -2430,7 +2504,7 @@ LEVEL = Info ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Set the maximum number of characters in a mermaid source. (Set to -1 to disable limits) -;MERMAID_MAX_SOURCE_CHARACTERS = 5000 +;MERMAID_MAX_SOURCE_CHARACTERS = 50000 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -2551,9 +2625,6 @@ LEVEL = Info ;; Currently, only `minio` and `azureblob` is supported. ;SERVE_DIRECT = false ;; -;; Path for chunked uploads. Defaults to APP_DATA_PATH + `tmp/package-upload` -;CHUNKED_UPLOAD_PATH = tmp/package-upload -;; ;; Maximum count of package versions a single owner can have (`-1` means no limits) ;LIMIT_TOTAL_OWNER_COUNT = -1 ;; Maximum size of packages a single owner can use (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`) @@ -2695,10 +2766,10 @@ LEVEL = Info ;; Minio secretAccessKey to connect only available when STORAGE_TYPE is `minio` ;MINIO_SECRET_ACCESS_KEY = ;; -;; Preferred IAM Endpoint to override Minio's default IAM Endpoint resolution only available when STORAGE_TYPE is `minio`. -;; If not provided and STORAGE_TYPE is `minio`, will search for and derive endpoint from known environment variables -;; (AWS_CONTAINER_AUTHORIZATION_TOKEN, AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE, AWS_CONTAINER_CREDENTIALS_RELATIVE_URI, -;; AWS_CONTAINER_CREDENTIALS_FULL_URI, AWS_WEB_IDENTITY_TOKEN_FILE, AWS_ROLE_ARN, AWS_ROLE_SESSION_NAME, AWS_REGION), +;; Preferred IAM Endpoint to override Minio's default IAM Endpoint resolution only available when STORAGE_TYPE is `minio`. +;; If not provided and STORAGE_TYPE is `minio`, will search for and derive endpoint from known environment variables +;; (AWS_CONTAINER_AUTHORIZATION_TOKEN, AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE, AWS_CONTAINER_CREDENTIALS_RELATIVE_URI, +;; AWS_CONTAINER_CREDENTIALS_FULL_URI, AWS_WEB_IDENTITY_TOKEN_FILE, AWS_ROLE_ARN, AWS_ROLE_SESSION_NAME, AWS_REGION), ;; or the DefaultIAMRoleEndpoint if not provided otherwise. ;MINIO_IAM_ENDPOINT = ;; diff --git a/docker/manifest.rootless.tmpl b/docker/manifest.rootless.tmpl index 1ebf5b73c84..3fa94ab0ec3 100644 --- a/docker/manifest.rootless.tmpl +++ b/docker/manifest.rootless.tmpl @@ -22,3 +22,8 @@ manifests: architecture: arm64 os: linux variant: v8 + - + image: gitea/gitea:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}{{#if (hasPrefix "refs/heads/release/v" build.ref)}}{{trimPrefix "refs/heads/release/v" build.ref}}-{{/if}}nightly{{/if}}-linux-riscv64-rootless + platform: + architecture: riscv64 + os: linux diff --git a/docker/manifest.tmpl b/docker/manifest.tmpl index 08ccf61b578..c68ca46dd86 100644 --- a/docker/manifest.tmpl +++ b/docker/manifest.tmpl @@ -22,3 +22,8 @@ manifests: architecture: arm64 os: linux variant: v8 + - + image: gitea/gitea:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}{{#if (hasPrefix "refs/heads/release/v" build.ref)}}{{trimPrefix "refs/heads/release/v" build.ref}}-{{/if}}nightly{{/if}}-linux-riscv64 + platform: + architecture: riscv64 + os: linux diff --git a/docker/root/etc/s6/openssh/setup b/docker/root/etc/s6/openssh/setup index dbb3bafd352..48e7d4b2117 100755 --- a/docker/root/etc/s6/openssh/setup +++ b/docker/root/etc/s6/openssh/setup @@ -31,6 +31,21 @@ if [ -e /data/ssh/ssh_host_ecdsa_cert ]; then SSH_ECDSA_CERT=${SSH_ECDSA_CERT:-"/data/ssh/ssh_host_ecdsa_cert"} fi +# In case someone wants to sign the `{keyname}.pub` key by `ssh-keygen -s ca -I identity ...` to +# make use of the ssh-key certificate authority feature (see ssh-keygen CERTIFICATES section), +# the generated key file name is `{keyname}-cert.pub` +if [ -e /data/ssh/ssh_host_ed25519_key-cert.pub ]; then + SSH_ED25519_CERT=${SSH_ED25519_CERT:-"/data/ssh/ssh_host_ed25519_key-cert.pub"} +fi + +if [ -e /data/ssh/ssh_host_rsa_key-cert.pub ]; then + SSH_RSA_CERT=${SSH_RSA_CERT:-"/data/ssh/ssh_host_rsa_key-cert.pub"} +fi + +if [ -e /data/ssh/ssh_host_ecdsa_key-cert.pub ]; then + SSH_ECDSA_CERT=${SSH_ECDSA_CERT:-"/data/ssh/ssh_host_ecdsa_key-cert.pub"} +fi + if [ -d /etc/ssh ]; then SSH_PORT=${SSH_PORT:-"22"} \ SSH_LISTEN_PORT=${SSH_LISTEN_PORT:-"${SSH_PORT}"} \ diff --git a/docker/root/usr/bin/entrypoint b/docker/root/usr/bin/entrypoint index d9dbb3ebe0b..08587fc4f4d 100755 --- a/docker/root/usr/bin/entrypoint +++ b/docker/root/usr/bin/entrypoint @@ -37,5 +37,5 @@ done if [ $# -gt 0 ]; then exec "$@" else - exec /bin/s6-svscan /etc/s6 + exec /usr/bin/s6-svscan /etc/s6 fi diff --git a/flake.lock b/flake.lock index 1890b82dcfa..16a487ba133 100644 --- a/flake.lock +++ b/flake.lock @@ -5,11 +5,11 @@ "systems": "systems" }, "locked": { - "lastModified": 1726560853, - "narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=", + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", "owner": "numtide", "repo": "flake-utils", - "rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", "type": "github" }, "original": { @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1731139594, - "narHash": "sha256-IigrKK3vYRpUu+HEjPL/phrfh7Ox881er1UEsZvw9Q4=", + "lastModified": 1755186698, + "narHash": "sha256-wNO3+Ks2jZJ4nTHMuks+cxAiVBGNuEBXsT29Bz6HASo=", "owner": "nixos", "repo": "nixpkgs", - "rev": "76612b17c0ce71689921ca12d9ffdc9c23ce40b2", + "rev": "fbcf476f790d8a217c3eab4e12033dc4a0f6d23c", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index e3655b627eb..9008e1f8391 100644 --- a/flake.nix +++ b/flake.nix @@ -11,28 +11,58 @@ pkgs = nixpkgs.legacyPackages.${system}; in { - devShells.default = pkgs.mkShell { - buildInputs = with pkgs; [ - # generic - git - git-lfs - gnumake - gnused - gnutar - gzip + devShells.default = + with pkgs; + let + # only bump toolchain versions here + go = go_1_25; + nodejs = nodejs_24; + python3 = python312; - # frontend - nodejs_22 + # Platform-specific dependencies + linuxOnlyInputs = lib.optionals pkgs.stdenv.isLinux [ + glibc.static + ]; - # linting - python312 - poetry + linuxOnlyEnv = lib.optionalAttrs pkgs.stdenv.isLinux { + CFLAGS = "-I${glibc.static.dev}/include"; + LDFLAGS = "-L ${glibc.static}/lib"; + }; + in + pkgs.mkShell ( + { + buildInputs = [ + # generic + git + git-lfs + gnumake + gnused + gnutar + gzip + zip - # backend - gofumpt - sqlite - ]; - }; + # frontend + nodejs + + # linting + python3 + uv + + # backend + go + gofumpt + sqlite + ] + ++ linuxOnlyInputs; + + GO = "${go}/bin/go"; + GOROOT = "${go}/share/go"; + + TAGS = "sqlite sqlite_unlock_notify"; + STATIC = "true"; + } + // linuxOnlyEnv + ); } ); } diff --git a/go.mod b/go.mod index bbd81868684..164702c90e5 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module code.gitea.io/gitea -go 1.23 +go 1.24.6 // rfc5280 said: "The serial number is an integer assigned by the CA to each certificate." // But some CAs use negative serial number, just relax the check. related: @@ -8,11 +8,11 @@ go 1.23 godebug x509negativeserial=1 require ( - code.gitea.io/actions-proto-go v0.4.0 + code.gitea.io/actions-proto-go v0.4.1 code.gitea.io/gitea-vet v0.2.3 - code.gitea.io/sdk/gitea v0.19.0 + code.gitea.io/sdk/gitea v0.21.0 codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570 - connectrpc.com/connect v1.17.0 + connectrpc.com/connect v1.18.1 gitea.com/go-chi/binding v0.0.0-20240430071103-39a851e106ed gitea.com/go-chi/cache v0.2.1 gitea.com/go-chi/captcha v0.0.0-20240315150714-fb487f629098 @@ -21,20 +21,20 @@ require ( gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4 github.com/42wim/httpsig v1.2.2 github.com/42wim/sshsig v0.0.0-20240818000253-e3a6333df815 - github.com/Azure/azure-sdk-for-go/sdk/azcore v1.16.0 - github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.4.1 + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 + github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.0 github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 - github.com/ProtonMail/go-crypto v1.0.0 - github.com/PuerkitoBio/goquery v1.10.0 + github.com/ProtonMail/go-crypto v1.2.0 + github.com/PuerkitoBio/goquery v1.10.3 github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.7.3 - github.com/alecthomas/chroma/v2 v2.14.0 - github.com/aws/aws-sdk-go v1.55.5 - github.com/aws/aws-sdk-go-v2/credentials v1.17.42 - github.com/aws/aws-sdk-go-v2/service/codecommit v1.27.3 + github.com/alecthomas/chroma/v2 v2.20.0 + github.com/aws/aws-sdk-go-v2/credentials v1.17.67 + github.com/aws/aws-sdk-go-v2/service/codecommit v1.28.2 github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb - github.com/blevesearch/bleve/v2 v2.4.2 - github.com/buildkite/terminal-to-html/v3 v3.16.3 - github.com/caddyserver/certmagic v0.21.4 + github.com/blevesearch/bleve/v2 v2.5.0 + github.com/bohde/codel v0.2.0 + github.com/buildkite/terminal-to-html/v3 v3.16.8 + github.com/caddyserver/certmagic v0.23.0 github.com/charmbracelet/git-lfs-transfer v0.2.0 github.com/chi-middleware/proxy v1.1.1 github.com/dimiro1/reply v0.0.0-20200315094148-d0136a4c9e21 @@ -42,38 +42,35 @@ require ( github.com/djherbis/nio/v3 v3.0.1 github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 github.com/dustin/go-humanize v1.0.1 - github.com/editorconfig/editorconfig-core-go/v2 v2.6.2 + github.com/editorconfig/editorconfig-core-go/v2 v2.6.3 github.com/emersion/go-imap v1.2.1 github.com/emirpasic/gods v1.18.1 github.com/ethantkoenig/rupture v1.0.1 github.com/felixge/fgprof v0.9.5 - github.com/fsnotify/fsnotify v1.7.0 - github.com/gliderlabs/ssh v0.3.7 - github.com/go-ap/activitypub v0.0.0-20240910141749-b4b8c8aa484c + github.com/fsnotify/fsnotify v1.9.0 + github.com/gliderlabs/ssh v0.3.8 + github.com/go-ap/activitypub v0.0.0-20250409143848-7113328b1f3d github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73 - github.com/go-chi/chi/v5 v5.1.0 + github.com/go-chi/chi/v5 v5.2.2 github.com/go-chi/cors v1.2.1 github.com/go-co-op/gocron v1.37.0 - github.com/go-enry/go-enry/v2 v2.9.1 - github.com/go-git/go-billy/v5 v5.6.0 - github.com/go-git/go-git/v5 v5.12.0 - github.com/go-ldap/ldap/v3 v3.4.8 + github.com/go-enry/go-enry/v2 v2.9.2 + github.com/go-git/go-billy/v5 v5.6.2 + github.com/go-git/go-git/v5 v5.16.0 + github.com/go-ldap/ldap/v3 v3.4.11 github.com/go-redsync/redsync/v4 v4.13.0 - github.com/go-sql-driver/mysql v1.8.1 - github.com/go-swagger/go-swagger v0.31.0 - github.com/go-testfixtures/testfixtures/v3 v3.11.0 - github.com/go-webauthn/webauthn v0.11.2 + github.com/go-sql-driver/mysql v1.9.2 + github.com/go-webauthn/webauthn v0.12.3 github.com/gobwas/glob v0.2.3 github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f github.com/gogs/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85 - github.com/golang-jwt/jwt/v5 v5.2.1 - github.com/google/go-github/v61 v61.0.0 + github.com/golang-jwt/jwt/v5 v5.2.2 + github.com/google/go-github/v71 v71.0.0 github.com/google/licenseclassifier/v2 v2.0.0 - github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db + github.com/google/pprof v0.0.0-20250422154841-e1f9c1950416 github.com/google/uuid v1.6.0 github.com/gorilla/feeds v1.2.0 github.com/gorilla/sessions v1.4.0 - github.com/h2non/gock v1.2.0 github.com/hashicorp/go-version v1.7.0 github.com/hashicorp/golang-lru/v2 v2.0.7 github.com/huandu/xstrings v1.5.0 @@ -81,223 +78,179 @@ require ( github.com/jhillyerd/enmime v1.3.0 github.com/json-iterator/go v1.1.12 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 - github.com/keybase/go-crypto v0.0.0-20200123153347-de78d2cb44f4 - github.com/klauspost/compress v1.17.11 - github.com/klauspost/cpuid/v2 v2.2.8 + github.com/klauspost/compress v1.18.0 + github.com/klauspost/cpuid/v2 v2.2.10 github.com/lib/pq v1.10.9 - github.com/markbates/goth v1.80.0 + github.com/markbates/goth v1.81.0 github.com/mattn/go-isatty v0.0.20 - github.com/mattn/go-sqlite3 v1.14.24 - github.com/meilisearch/meilisearch-go v0.29.1-0.20241106140435-0bf60fad690a + github.com/mattn/go-sqlite3 v1.14.28 + github.com/meilisearch/meilisearch-go v0.31.0 github.com/mholt/archiver/v3 v3.5.1 github.com/microcosm-cc/bluemonday v1.0.27 - github.com/microsoft/go-mssqldb v1.7.2 - github.com/minio/minio-go/v7 v7.0.80 + github.com/microsoft/go-mssqldb v1.8.0 + github.com/minio/minio-go/v7 v7.0.91 github.com/msteinert/pam v1.2.0 github.com/nektos/act v0.2.63 - github.com/niklasfasching/go-org v1.7.0 + github.com/niklasfasching/go-org v1.8.0 github.com/olivere/elastic/v7 v7.0.32 github.com/opencontainers/go-digest v1.0.0 - github.com/opencontainers/image-spec v1.1.0 + github.com/opencontainers/image-spec v1.1.1 github.com/pkg/errors v0.9.1 github.com/pquerna/otp v1.4.0 - github.com/prometheus/client_golang v1.20.5 + github.com/prometheus/client_golang v1.22.0 github.com/quasoft/websspi v1.1.2 - github.com/redis/go-redis/v9 v9.7.0 + github.com/redis/go-redis/v9 v9.7.3 github.com/robfig/cron/v3 v3.0.1 github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 github.com/sassoftware/go-rpmutils v0.4.0 github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 - github.com/shurcooL/vfsgen v0.0.0-20230704071429-0000e147ea92 - github.com/stretchr/testify v1.9.0 + github.com/stretchr/testify v1.10.0 github.com/syndtr/goleveldb v1.0.0 github.com/tstranex/u2f v1.0.0 - github.com/ulikunitz/xz v0.5.12 - github.com/urfave/cli/v2 v2.27.5 - github.com/xanzy/go-gitlab v0.112.0 + github.com/ulikunitz/xz v0.5.15 + github.com/urfave/cli-docs/v3 v3.0.0-alpha6 + github.com/urfave/cli/v3 v3.3.3 + github.com/wneessen/go-mail v0.6.2 github.com/xeipuuv/gojsonschema v1.2.0 github.com/yohcop/openid-go v1.0.1 - github.com/yuin/goldmark v1.7.8 + github.com/yuin/goldmark v1.7.10 github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc github.com/yuin/goldmark-meta v1.1.0 - golang.org/x/crypto v0.28.0 - golang.org/x/image v0.21.0 - golang.org/x/net v0.30.0 - golang.org/x/oauth2 v0.23.0 - golang.org/x/sync v0.8.0 - golang.org/x/sys v0.26.0 - golang.org/x/text v0.19.0 - golang.org/x/tools v0.26.0 - google.golang.org/grpc v1.67.1 - google.golang.org/protobuf v1.35.1 - gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df + gitlab.com/gitlab-org/api/client-go v0.127.0 + golang.org/x/crypto v0.39.0 + golang.org/x/image v0.26.0 + golang.org/x/net v0.40.0 + golang.org/x/oauth2 v0.29.0 + golang.org/x/sync v0.15.0 + golang.org/x/sys v0.33.0 + golang.org/x/text v0.26.0 + google.golang.org/grpc v1.72.0 + google.golang.org/protobuf v1.36.6 gopkg.in/ini.v1 v1.67.0 gopkg.in/yaml.v3 v3.0.1 - mvdan.cc/xurls/v2 v2.5.0 + mvdan.cc/xurls/v2 v2.6.0 strk.kbt.io/projects/go/libravatar v0.0.0-20191008002943-06d1c002b251 xorm.io/builder v0.3.13 - xorm.io/xorm v1.3.9 + xorm.io/xorm v1.3.10 ) require ( - cloud.google.com/go/compute/metadata v0.5.2 // indirect + cloud.google.com/go/compute/metadata v0.6.0 // indirect dario.cat/mergo v1.0.1 // indirect filippo.io/edwards25519 v1.1.0 // indirect git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078 // indirect - github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect - github.com/ClickHouse/ch-go v0.63.1 // indirect - github.com/ClickHouse/clickhouse-go/v2 v2.24.0 // indirect - github.com/DataDog/zstd v1.5.6 // indirect - github.com/Masterminds/goutils v1.1.1 // indirect - github.com/Masterminds/semver/v3 v3.3.0 // indirect - github.com/Masterminds/sprig/v3 v3.3.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 // indirect + github.com/DataDog/zstd v1.5.7 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect - github.com/RoaringBitmap/roaring v1.9.4 // indirect + github.com/RoaringBitmap/roaring/v2 v2.4.5 // indirect github.com/andybalholm/brotli v1.1.1 // indirect - github.com/andybalholm/cascadia v1.3.2 // indirect + github.com/andybalholm/cascadia v1.3.3 // indirect github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect - github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect - github.com/aws/aws-sdk-go-v2 v1.32.3 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.22 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.22 // indirect - github.com/aws/smithy-go v1.22.0 // indirect + github.com/aws/aws-sdk-go-v2 v1.36.3 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 // indirect + github.com/aws/smithy-go v1.22.3 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/beorn7/perks v1.0.1 // indirect - github.com/bits-and-blooms/bitset v1.14.3 // indirect - github.com/blevesearch/bleve_index_api v1.1.12 // indirect - github.com/blevesearch/geo v0.1.20 // indirect - github.com/blevesearch/go-faiss v1.0.23 // indirect + github.com/bits-and-blooms/bitset v1.22.0 // indirect + github.com/blevesearch/bleve_index_api v1.2.8 // indirect + github.com/blevesearch/geo v0.2.0 // indirect + github.com/blevesearch/go-faiss v1.0.25 // indirect github.com/blevesearch/go-porterstemmer v1.0.3 // indirect github.com/blevesearch/gtreap v0.1.1 // indirect github.com/blevesearch/mmap-go v1.0.4 // indirect - github.com/blevesearch/scorch_segment_api/v2 v2.2.16 // indirect + github.com/blevesearch/scorch_segment_api/v2 v2.3.10 // indirect github.com/blevesearch/segment v0.9.1 // indirect github.com/blevesearch/snowballstem v0.9.0 // indirect github.com/blevesearch/upsidedown_store_api v1.0.2 // indirect - github.com/blevesearch/vellum v1.0.10 // indirect - github.com/blevesearch/zapx/v11 v11.3.10 // indirect - github.com/blevesearch/zapx/v12 v12.3.10 // indirect - github.com/blevesearch/zapx/v13 v13.3.10 // indirect - github.com/blevesearch/zapx/v14 v14.3.10 // indirect - github.com/blevesearch/zapx/v15 v15.3.16 // indirect - github.com/blevesearch/zapx/v16 v16.1.7 // indirect + github.com/blevesearch/vellum v1.1.0 // indirect + github.com/blevesearch/zapx/v11 v11.4.1 // indirect + github.com/blevesearch/zapx/v12 v12.4.1 // indirect + github.com/blevesearch/zapx/v13 v13.4.1 // indirect + github.com/blevesearch/zapx/v14 v14.4.1 // indirect + github.com/blevesearch/zapx/v15 v15.4.1 // indirect + github.com/blevesearch/zapx/v16 v16.2.3 // indirect + github.com/bmatcuk/doublestar/v4 v4.8.1 // indirect github.com/boombuler/barcode v1.0.2 // indirect - github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 // indirect + github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf // indirect github.com/caddyserver/zerossl v0.1.3 // indirect github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/cloudflare/circl v1.5.0 // indirect + github.com/cloudflare/circl v1.6.1 // indirect github.com/couchbase/go-couchbase v0.1.1 // indirect - github.com/couchbase/gomemcached v0.3.2 // indirect + github.com/couchbase/gomemcached v0.3.3 // indirect github.com/couchbase/goutils v0.1.2 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect - github.com/cyphar/filepath-securejoin v0.3.4 // indirect + github.com/cyphar/filepath-securejoin v0.4.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/davidmz/go-pageant v1.0.2 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect - github.com/dlclark/regexp2 v1.11.4 // indirect + github.com/dlclark/regexp2 v1.11.5 // indirect github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 // indirect github.com/fatih/color v1.18.0 // indirect - github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/fxamacker/cbor/v2 v2.8.0 // indirect github.com/git-lfs/pktline v0.0.0-20230103162542-ca444d533ef1 // indirect - github.com/go-ap/errors v0.0.0-20240910140019-1e9d33cc1568 // indirect - github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect + github.com/go-ap/errors v0.0.0-20250409143711-5686c11ae650 // indirect + github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect github.com/go-enry/go-oniguruma v1.2.1 // indirect - github.com/go-faster/city v1.0.1 // indirect - github.com/go-faster/errors v0.7.1 // indirect github.com/go-fed/httpsig v1.1.1-0.20201223112313-55836744818e // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-ini/ini v1.67.0 // indirect - github.com/go-openapi/analysis v0.23.0 // indirect - github.com/go-openapi/errors v0.22.0 // indirect - github.com/go-openapi/inflect v0.21.0 // indirect - github.com/go-openapi/jsonpointer v0.21.0 // indirect - github.com/go-openapi/jsonreference v0.21.0 // indirect - github.com/go-openapi/loads v0.22.0 // indirect - github.com/go-openapi/runtime v0.28.0 // indirect - github.com/go-openapi/spec v0.21.0 // indirect - github.com/go-openapi/strfmt v0.23.0 // indirect - github.com/go-openapi/swag v0.23.0 // indirect - github.com/go-openapi/validate v0.24.0 // indirect - github.com/go-webauthn/x v0.1.15 // indirect - github.com/goccy/go-json v0.10.3 // indirect - github.com/golang-jwt/jwt/v4 v4.5.1 // indirect + github.com/go-webauthn/x v0.1.20 // indirect + github.com/goccy/go-json v0.10.5 // indirect + github.com/golang-jwt/jwt/v4 v4.5.2 // indirect github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect github.com/golang-sql/sqlexp v0.1.0 // indirect - github.com/golang/geo v0.0.0-20230421003525-6adc56603217 // indirect - github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/golang/protobuf v1.5.4 // indirect - github.com/golang/snappy v0.0.4 // indirect + github.com/golang/snappy v1.0.0 // indirect github.com/google/btree v1.1.3 // indirect + github.com/google/flatbuffers v25.2.10+incompatible // indirect github.com/google/go-querystring v1.1.0 // indirect - github.com/google/go-tpm v0.9.1 // indirect + github.com/google/go-tpm v0.9.3 // indirect github.com/gorilla/css v1.0.1 // indirect - github.com/gorilla/handlers v1.5.2 // indirect github.com/gorilla/mux v1.8.1 // indirect github.com/gorilla/securecookie v1.1.2 // indirect - github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-retryablehttp v0.7.7 // indirect - github.com/hashicorp/hcl v1.0.0 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect - github.com/jessevdk/go-flags v1.6.1 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/klauspost/pgzip v1.2.6 // indirect - github.com/kr/pretty v0.3.1 // indirect - github.com/kr/text v0.2.0 // indirect - github.com/libdns/libdns v0.2.2 // indirect - github.com/magiconair/properties v1.8.7 // indirect - github.com/mailru/easyjson v0.7.7 // indirect + github.com/libdns/libdns v1.0.0-beta.1 // indirect + github.com/mailru/easyjson v0.9.0 // indirect github.com/markbates/going v1.0.3 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect - github.com/mholt/acmez/v2 v2.0.3 // indirect - github.com/miekg/dns v1.1.62 // indirect + github.com/mattn/go-shellwords v1.0.12 // indirect + github.com/mholt/acmez/v3 v3.1.2 // indirect + github.com/miekg/dns v1.1.65 // indirect + github.com/minio/crc64nvme v1.0.1 // indirect github.com/minio/md5-simd v1.1.2 // indirect - github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect - github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/mrjones/oauth v0.0.0-20190623134757-126b35219450 // indirect github.com/mschoch/smat v0.2.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/nwaples/rardecode v1.1.3 // indirect - github.com/oklog/ulid v1.3.1 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/onsi/ginkgo v1.16.5 // indirect - github.com/paulmach/orb v0.11.1 // indirect - github.com/pelletier/go-toml/v2 v2.2.3 // indirect - github.com/pierrec/lz4/v4 v4.1.21 // indirect - github.com/pjbgf/sha1cd v0.3.0 // indirect + github.com/pierrec/lz4/v4 v4.1.22 // indirect + github.com/pjbgf/sha1cd v0.3.2 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.60.1 // indirect - github.com/prometheus/procfs v0.15.1 // indirect - github.com/rhysd/actionlint v1.7.3 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.63.0 // indirect + github.com/prometheus/procfs v0.16.1 // indirect + github.com/rhysd/actionlint v1.7.7 // indirect github.com/rivo/uniseg v0.4.7 // indirect - github.com/rogpeppe/go-internal v1.13.1 // indirect github.com/rs/xid v1.6.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect - github.com/sagikazarmark/locafero v0.6.0 // indirect - github.com/sagikazarmark/slog-shim v0.1.0 // indirect - github.com/segmentio/asm v1.2.0 // indirect - github.com/shopspring/decimal v1.4.0 // indirect - github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c // indirect github.com/sirupsen/logrus v1.9.3 // indirect - github.com/skeema/knownhosts v1.3.0 // indirect - github.com/sourcegraph/conc v0.3.0 // indirect - github.com/spf13/afero v1.11.0 // indirect - github.com/spf13/cast v1.7.0 // indirect - github.com/spf13/pflag v1.0.5 // indirect - github.com/spf13/viper v1.19.0 // indirect + github.com/skeema/knownhosts v1.3.1 // indirect github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect - github.com/subosito/gotenv v1.6.0 // indirect - github.com/toqueteos/webbrowser v1.2.0 // indirect github.com/unknwon/com v1.0.1 // indirect github.com/valyala/fastjson v1.6.4 // indirect github.com/x448/float16 v0.8.4 // indirect @@ -305,30 +258,25 @@ require ( github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect - github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect github.com/zeebo/assert v1.3.0 // indirect github.com/zeebo/blake3 v0.2.4 // indirect - go.etcd.io/bbolt v1.3.11 // indirect - go.mongodb.org/mongo-driver v1.17.1 // indirect - go.opentelemetry.io/otel v1.31.0 // indirect - go.opentelemetry.io/otel/trace v1.31.0 // indirect + go.etcd.io/bbolt v1.4.0 // indirect go.uber.org/atomic v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect - golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect - golang.org/x/mod v0.21.0 // indirect - golang.org/x/time v0.7.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38 // indirect - gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect + go.uber.org/zap/exp v0.3.0 // indirect + golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect + golang.org/x/mod v0.25.0 // indirect + golang.org/x/time v0.11.0 // indirect + golang.org/x/tools v0.33.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250422160041-2d3770c4ea7f // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) replace github.com/hashicorp/go-version => github.com/6543/go-version v1.3.1 -replace github.com/shurcooL/vfsgen => github.com/lunny/vfsgen v0.0.0-20220105142115-2c99e1ffdfa0 - -replace github.com/nektos/act => gitea.com/gitea/act v0.261.3 +replace github.com/nektos/act => gitea.com/gitea/act v0.261.6 // TODO: the only difference is in `PutObject`: the fork doesn't use `NewVerifyingReader(r, sha256.New(), oid, expectedSize)`, need to figure out why replace github.com/charmbracelet/git-lfs-transfer => gitea.com/gitea/git-lfs-transfer v0.2.0 @@ -336,6 +284,8 @@ replace github.com/charmbracelet/git-lfs-transfer => gitea.com/gitea/git-lfs-tra // TODO: This could be removed after https://github.com/mholt/archiver/pull/396 merged replace github.com/mholt/archiver/v3 => github.com/anchore/archiver/v3 v3.5.2 +replace git.sr.ht/~mariusor/go-xsd-duration => gitea.com/gitea/go-xsd-duration v0.0.0-20220703122237-02e73435a078 + exclude github.com/gofrs/uuid v3.2.0+incompatible exclude github.com/gofrs/uuid v4.0.0+incompatible diff --git a/go.sum b/go.sum index df3b7d899cf..d30acd79e1b 100644 --- a/go.sum +++ b/go.sum @@ -1,25 +1,25 @@ -cloud.google.com/go/compute/metadata v0.5.2 h1:UxK4uu/Tn+I3p2dYWTfiX4wva7aYlKixAHn3fyqngqo= -cloud.google.com/go/compute/metadata v0.5.2/go.mod h1:C66sj2AluDcIqakBq/M8lw8/ybHgOZqin2obFxa/E5k= -code.gitea.io/actions-proto-go v0.4.0 h1:OsPBPhodXuQnsspG1sQ4eRE1PeoZyofd7+i73zCwnsU= -code.gitea.io/actions-proto-go v0.4.0/go.mod h1:mn7Wkqz6JbnTOHQpot3yDeHx+O5C9EGhMEE+htvHBas= +cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= +cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= +code.gitea.io/actions-proto-go v0.4.1 h1:l0EYhjsgpUe/1VABo2eK7zcoNX2W44WOnb0MSLrKfls= +code.gitea.io/actions-proto-go v0.4.1/go.mod h1:mn7Wkqz6JbnTOHQpot3yDeHx+O5C9EGhMEE+htvHBas= code.gitea.io/gitea-vet v0.2.3 h1:gdFmm6WOTM65rE8FUBTRzeQZYzXePKSSB1+r574hWwI= code.gitea.io/gitea-vet v0.2.3/go.mod h1:zcNbT/aJEmivCAhfmkHOlT645KNOf9W2KnkLgFjGGfE= -code.gitea.io/sdk/gitea v0.19.0 h1:8I6s1s4RHgzxiPHhOQdgim1RWIRcr0LVMbHBjBFXq4Y= -code.gitea.io/sdk/gitea v0.19.0/go.mod h1:IG9xZJoltDNeDSW0qiF2Vqx5orMWa7OhVWrjvrd5NpI= +code.gitea.io/sdk/gitea v0.21.0 h1:69n6oz6kEVHRo1+APQQyizkhrZrLsTLXey9142pfkD4= +code.gitea.io/sdk/gitea v0.21.0/go.mod h1:tnBjVhuKJCn8ibdyyhvUyxrR1Ca2KHEoTWoukNhXQPA= codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570 h1:TXbikPqa7YRtfU9vS6QJBg77pUvbEb6StRdZO8t1bEY= codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570/go.mod h1:IIAjsijsd8q1isWX8MACefDEgTQslQ4stk2AeeTt3kM= -connectrpc.com/connect v1.17.0 h1:W0ZqMhtVzn9Zhn2yATuUokDLO5N+gIuBWMOnsQrfmZk= -connectrpc.com/connect v1.17.0/go.mod h1:0292hj1rnx8oFrStN7cB4jjVBeqs+Yx5yDIC2prWDO8= +connectrpc.com/connect v1.18.1 h1:PAg7CjSAGvscaf6YZKUefjoih5Z/qYkyaTrBW8xvYPw= +connectrpc.com/connect v1.18.1/go.mod h1:0292hj1rnx8oFrStN7cB4jjVBeqs+Yx5yDIC2prWDO8= dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= -git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078 h1:cliQ4HHsCo6xi2oWZYKWW4bly/Ory9FuTpFPRxj/mAg= -git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078/go.mod h1:g/V2Hjas6Z1UHUp4yIx6bATpNzJ7DYtD0FG3+xARWxs= -gitea.com/gitea/act v0.261.3 h1:BhiYpGJQKGq0XMYYICCYAN4KnsEWHyLbA6dxhZwFcV4= -gitea.com/gitea/act v0.261.3/go.mod h1:Pg5C9kQY1CEA3QjthjhlrqOC/QOT5NyWNjOjRHw23Ok= +gitea.com/gitea/act v0.261.6 h1:CjZwKOyejonNFDmsXOw3wGm5Vet573hHM6VMLsxtvPY= +gitea.com/gitea/act v0.261.6/go.mod h1:Pg5C9kQY1CEA3QjthjhlrqOC/QOT5NyWNjOjRHw23Ok= gitea.com/gitea/git-lfs-transfer v0.2.0 h1:baHaNoBSRaeq/xKayEXwiDQtlIjps4Ac/Ll4KqLMB40= gitea.com/gitea/git-lfs-transfer v0.2.0/go.mod h1:UrXUCm3xLQkq15fu7qlXHUMlrhdlXHoi13KH2Dfiits= +gitea.com/gitea/go-xsd-duration v0.0.0-20220703122237-02e73435a078 h1:BAFmdZpRW7zMQZQDClaCWobRj9uL1MR3MzpCVJvc5s4= +gitea.com/gitea/go-xsd-duration v0.0.0-20220703122237-02e73435a078/go.mod h1:g/V2Hjas6Z1UHUp4yIx6bATpNzJ7DYtD0FG3+xARWxs= gitea.com/go-chi/binding v0.0.0-20240430071103-39a851e106ed h1:EZZBtilMLSZNWtHHcgq2mt6NSGhJSZBuduAlinMEmso= gitea.com/go-chi/binding v0.0.0-20240430071103-39a851e106ed/go.mod h1:E3i3cgB04dDx0v3CytCgRTTn9Z/9x891aet3r456RVw= gitea.com/go-chi/cache v0.2.1 h1:bfAPkvXlbcZxPCpcmDVCWoHgiBSBmZN/QosnZvEC0+g= @@ -40,59 +40,49 @@ github.com/42wim/sshsig v0.0.0-20240818000253-e3a6333df815 h1:5EoemV++kUK2Sw98yW github.com/42wim/sshsig v0.0.0-20240818000253-e3a6333df815/go.mod h1:zjsWZdDLrcDojDIfpQg7A6J4YZLT0cbwuAD26AppDBo= github.com/6543/go-version v1.3.1 h1:HvOp+Telns7HWJ2Xo/05YXQSB2bE0WmVgbHqwMPZT4U= github.com/6543/go-version v1.3.1/go.mod h1:oqFAHCwtLVUTLdhQmVZWYvaHXTdsbB4SY85at64SQEo= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.16.0 h1:JZg6HRh6W6U4OLl6lk7BZ7BLisIzM9dG1R50zUk9C/M= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.16.0/go.mod h1:YL1xnZ6QejvQHWJrX/AvhFl4WW4rqHVoKspWNVwFk0M= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0 h1:tfLQ34V6F7tVSwoTf/4lH5sE0o6eCJuNDTmH09nDpbc= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 h1:Gt0j3wceWMwPmiazCa8MzMA0MfhmPIz0Qp0FJ6qcM0U= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2 h1:F0gBpfdPLGsw+nsgk6aqqkZS1jiixa5WwFe3fk/T3Ys= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2/go.mod h1:SqINnQ9lVVdRlyC8cd1lCI0SdX4n2paeABd2K8ggfnE= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 h1:FPKJS1T+clwv+OLGt13a8UjqeRuh0O4SJ3lUriThc+4= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1/go.mod h1:j2chePtV91HrC22tGoRX3sGY42uF13WzmmV80/OdVAA= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.6.0 h1:PiSrjRPpkQNjrM8H0WwKMnZUdu1RGMtd/LdGKUrOo+c= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.6.0/go.mod h1:oDrbWx4ewMylP7xHivfgixbfGBT6APAwsSoHRKotnIc= github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1 h1:MyVTgWR8qd/Jw1Le0NZebGBUCLbtak3bJ3z1OlqZBpw= github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1/go.mod h1:GpPjLhVR9dnUoJMyHWSPy71xY9/lcmpzIPZXmF0FCVY= github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0 h1:D3occbWoio4EBLkbkevetNMAVX197GkzbUMtqjGWn80= github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0/go.mod h1:bTSOgj05NGRuHHhQwAdPnYr9TOdNmKlZTgGLL6nyAdI= -github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.4.1 h1:cf+OIKbkmMHBaC3u78AXomweqM0oxQSgBXRZf3WH4yM= -github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.4.1/go.mod h1:ap1dmS6vQKJxSMNiGJcq4QuUQkOynyD93gLw6MDF7ek= +github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.0 h1:UXT0o77lXQrikd1kgwIPQOUect7EoR/+sbP4wQKdzxM= +github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.0/go.mod h1:cTvi54pg19DoT07ekoeMgE/taAwNtCShVeZqA+Iv2xI= github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8= github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= -github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU= -github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= +github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs= +github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/ClickHouse/ch-go v0.63.1 h1:s2JyZvWLTCSAGdtjMBBmAgQQHMco6pawLJMOXi0FODM= -github.com/ClickHouse/ch-go v0.63.1/go.mod h1:I1kJJCL3WJcBMGe1m+HVK0+nREaG+JOYYBWjrDrF3R0= -github.com/ClickHouse/clickhouse-go/v2 v2.24.0 h1:L/n/pVVpk95KtkHOiKuSnO7cu2ckeW4gICbbOh5qs74= -github.com/ClickHouse/clickhouse-go/v2 v2.24.0/go.mod h1:iDTViXk2Fgvf1jn2dbJd1ys+fBkdD1UMRnXlwmhijhQ= -github.com/DataDog/zstd v1.5.6 h1:LbEglqepa/ipmmQJUDnSsfvA8e8IStVcGaFWDuxvGOY= -github.com/DataDog/zstd v1.5.6/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= +github.com/DataDog/zstd v1.5.7 h1:ybO8RBeh29qrxIhCA9E8gKY6xfONU9T6G6aP9DTKfLE= +github.com/DataDog/zstd v1.5.7/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= github.com/Julusian/godocdown v0.0.0-20170816220326-6d19f8ff2df8/go.mod h1:INZr5t32rG59/5xeltqoCJoNY7e5x/3xoY9WSWVWg74= -github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= -github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= -github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0= -github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= -github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= -github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= -github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78= -github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= -github.com/PuerkitoBio/goquery v1.10.0 h1:6fiXdLuUvYs2OJSvNRqlNPoBm6YABE226xrbavY5Wv4= -github.com/PuerkitoBio/goquery v1.10.0/go.mod h1:TjZZl68Q3eGHNBA8CWaxAN7rOU1EbDz3CWuolcO5Yu4= +github.com/ProtonMail/go-crypto v1.2.0 h1:+PhXXn4SPGd+qk76TlEePBfOfivE0zkWFenhGhFLzWs= +github.com/ProtonMail/go-crypto v1.2.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE= +github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo= +github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y= github.com/RoaringBitmap/roaring v0.4.23/go.mod h1:D0gp8kJQgE1A4LQ5wFLggQEyvDi06Mq5mKs52e1TwOo= github.com/RoaringBitmap/roaring v0.7.1/go.mod h1:jdT9ykXwHFNdJbEtxePexlFYH9LXucApeS0/+/g+p1I= -github.com/RoaringBitmap/roaring v1.9.4 h1:yhEIoH4YezLYT04s1nHehNO64EKFTop/wBhxv2QzDdQ= -github.com/RoaringBitmap/roaring v1.9.4/go.mod h1:6AXUsoIEzDTFFQCe1RbGA6uFONMhvejWj5rqITANK90= +github.com/RoaringBitmap/roaring/v2 v2.4.5 h1:uGrrMreGjvAtTBobc0g5IrW1D5ldxDQYe2JW2gggRdg= +github.com/RoaringBitmap/roaring/v2 v2.4.5/go.mod h1:FiJcsfkGje/nZBZgCu0ZxCPOKD/hVXDS2dXi7/eUFE0= github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.7.3 h1:BP0HiyNT3AQEYi+if3wkRcIdQFHtsw6xX3Kx0glckgA= github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.7.3/go.mod h1:hMNtySovKkn2gdDuLqnqveP+mfhUSaBdoBcr2I7Zt0E= -github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE= -github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= +github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= +github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs= -github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E= -github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I= +github.com/alecthomas/chroma/v2 v2.20.0 h1:sfIHpxPyR07/Oylvmcai3X/exDlE8+FA820NTz+9sGw= +github.com/alecthomas/chroma/v2 v2.20.0/go.mod h1:e7tViK0xh/Nf4BYHl00ycY6rV7b8iXBksI9E359yNmA= github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8= -github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= -github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +github.com/alecthomas/repr v0.5.1 h1:E3G4t2QbHTSNpPKBgMTln5KLkZHLOcU7r37J4pXBuIg= +github.com/alecthomas/repr v0.5.1/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI= github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= github.com/anchore/archiver/v3 v3.5.2 h1:Bjemm2NzuRhmHy3m0lRe5tNoClB9A4zYyDV58PaB6aA= @@ -100,29 +90,25 @@ github.com/anchore/archiver/v3 v3.5.2/go.mod h1:e3dqJ7H78uzsRSEACH1joayhuSyhnons github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= -github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss= -github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU= +github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM= +github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= -github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= -github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= -github.com/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU= -github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= -github.com/aws/aws-sdk-go-v2 v1.32.3 h1:T0dRlFBKcdaUPGNtkBSwHZxrtis8CQU17UpNBZYd0wk= -github.com/aws/aws-sdk-go-v2 v1.32.3/go.mod h1:2SK5n0a2karNTv5tbP1SjsX0uhttou00v/HpXKM1ZUo= -github.com/aws/aws-sdk-go-v2/credentials v1.17.42 h1:sBP0RPjBU4neGpIYyx8mkU2QqLPl5u9cmdTWVzIpHkM= -github.com/aws/aws-sdk-go-v2/credentials v1.17.42/go.mod h1:FwZBfU530dJ26rv9saAbxa9Ej3eF/AK0OAY86k13n4M= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.22 h1:Jw50LwEkVjuVzE1NzkhNKkBf9cRN7MtE1F/b2cOKTUM= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.22/go.mod h1:Y/SmAyPcOTmpeVaWSzSKiILfXTVJwrGmYZhcRbhWuEY= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.22 h1:981MHwBaRZM7+9QSR6XamDzF/o7ouUGxFzr+nVSIhrs= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.22/go.mod h1:1RA1+aBEfn+CAB/Mh0MB6LsdCYCnjZm7tKXtnk499ZQ= -github.com/aws/aws-sdk-go-v2/service/codecommit v1.27.3 h1:NbAYtQnTXzv4u5uesdDhXGWDTsEtTyFXlzfXELtI6cc= -github.com/aws/aws-sdk-go-v2/service/codecommit v1.27.3/go.mod h1:ZhgiuB7I3d3UU6PnCWwb0IYFgGzJz0VT+DK3Xv1xtF8= -github.com/aws/smithy-go v1.22.0 h1:uunKnWlcoL3zO7q+gG2Pk53joueEOsnNB28QdMsmiMM= -github.com/aws/smithy-go v1.22.0/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= +github.com/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38yqWM= +github.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg= +github.com/aws/aws-sdk-go-v2/credentials v1.17.67 h1:9KxtdcIA/5xPNQyZRgUSpYOE6j9Bc4+D7nZua0KGYOM= +github.com/aws/aws-sdk-go-v2/credentials v1.17.67/go.mod h1:p3C44m+cfnbv763s52gCqrjaqyPikj9Sg47kUVaNZQQ= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 h1:ZK5jHhnrioRkUNOc+hOgQKlUL5JeC3S6JgLxtQ+Rm0Q= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34/go.mod h1:p4VfIceZokChbA9FzMbRGz5OV+lekcVtHlPKEO0gSZY= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 h1:SZwFm17ZUNNg5Np0ioo/gq8Mn6u9w19Mri8DnJ15Jf0= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34/go.mod h1:dFZsC0BLo346mvKQLWmoJxT+Sjp+qcVR1tRVHQGOH9Q= +github.com/aws/aws-sdk-go-v2/service/codecommit v1.28.2 h1:enL75gIdaPAoBztv/GDuMgOocEUpO2jYc45qp2Uweqs= +github.com/aws/aws-sdk-go-v2/service/codecommit v1.28.2/go.mod h1:JsdLne5QNlqJdCQFm2DbHLNmNfEWSU7HnTuvi8SIl+E= +github.com/aws/smithy-go v1.22.3 h1:Z//5NuZCSW6R4PhQ93hShNbyBbn8BWCmCVCt+Q8Io5k= +github.com/aws/smithy-go v1.22.3/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -130,20 +116,20 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r github.com/bits-and-blooms/bitset v1.1.10/go.mod h1:w0XsmFg8qg6cmpTtJ0z3pKgjTDBMMnI/+I2syrE6XBE= github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA= github.com/bits-and-blooms/bitset v1.12.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= -github.com/bits-and-blooms/bitset v1.14.3 h1:Gd2c8lSNf9pKXom5JtD7AaKO8o7fGQ2LtFj1436qilA= -github.com/bits-and-blooms/bitset v1.14.3/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/bits-and-blooms/bitset v1.22.0 h1:Tquv9S8+SGaS3EhyA+up3FXzmkhxPGjQQCkcs2uw7w4= +github.com/bits-and-blooms/bitset v1.22.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb h1:m935MPodAbYS46DG4pJSv7WO+VECIWUQ7OJYSoTrMh4= github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb/go.mod h1:PkYb9DJNAwrSvRx5DYA+gUcOIgTGVMNkfSCbZM8cWpI= github.com/blevesearch/bleve/v2 v2.0.5/go.mod h1:ZjWibgnbRX33c+vBRgla9QhPb4QOjD6fdVJ+R1Bk8LM= -github.com/blevesearch/bleve/v2 v2.4.2 h1:NooYP1mb3c0StkiY9/xviiq2LGSaE8BQBCc/pirMx0U= -github.com/blevesearch/bleve/v2 v2.4.2/go.mod h1:ATNKj7Yl2oJv/lGuF4kx39bST2dveX6w0th2FFYLkc8= +github.com/blevesearch/bleve/v2 v2.5.0 h1:HzYqBy/5/M9Ul9ESEmXzN/3Jl7YpmWBdHM/+zzv/3k4= +github.com/blevesearch/bleve/v2 v2.5.0/go.mod h1:PcJzTPnEynO15dCf9isxOga7YFRa/cMSsbnRwnszXUk= github.com/blevesearch/bleve_index_api v1.0.0/go.mod h1:fiwKS0xLEm+gBRgv5mumf0dhgFr2mDgZah1pqv1c1M4= -github.com/blevesearch/bleve_index_api v1.1.12 h1:P4bw9/G/5rulOF7SJ9l4FsDoo7UFJ+5kexNy1RXfegY= -github.com/blevesearch/bleve_index_api v1.1.12/go.mod h1:PbcwjIcRmjhGbkS/lJCpfgVSMROV6TRubGGAODaK1W8= -github.com/blevesearch/geo v0.1.20 h1:paaSpu2Ewh/tn5DKn/FB5SzvH0EWupxHEIwbCk/QPqM= -github.com/blevesearch/geo v0.1.20/go.mod h1:DVG2QjwHNMFmjo+ZgzrIq2sfCh6rIHzy9d9d0B59I6w= -github.com/blevesearch/go-faiss v1.0.23 h1:Wmc5AFwDLKGl2L6mjLX1Da3vCL0EKa2uHHSorcIS1Uc= -github.com/blevesearch/go-faiss v1.0.23/go.mod h1:OMGQwOaRRYxrmeNdMrXJPvVx8gBnvE5RYrr0BahNnkk= +github.com/blevesearch/bleve_index_api v1.2.8 h1:Y98Pu5/MdlkRyLM0qDHostYo7i+Vv1cDNhqTeR4Sy6Y= +github.com/blevesearch/bleve_index_api v1.2.8/go.mod h1:rKQDl4u51uwafZxFrPD1R7xFOwKnzZW7s/LSeK4lgo0= +github.com/blevesearch/geo v0.2.0 h1:f+IE3/C3mGeXDyhtMbWel6BgqBqaOUz43GtWg26GlB0= +github.com/blevesearch/geo v0.2.0/go.mod h1:k8Hyfz12kM8QmeWLhgX7VMMCoVFmttBnr62V5zniXak= +github.com/blevesearch/go-faiss v1.0.25 h1:lel1rkOUGbT1CJ0YgzKwC7k+XH0XVBHnCVWahdCXk4U= +github.com/blevesearch/go-faiss v1.0.25/go.mod h1:OMGQwOaRRYxrmeNdMrXJPvVx8gBnvE5RYrr0BahNnkk= github.com/blevesearch/go-porterstemmer v1.0.3 h1:GtmsqID0aZdCSNiY8SkuPJ12pD4jI+DdXTAn4YRcHCo= github.com/blevesearch/go-porterstemmer v1.0.3/go.mod h1:angGc5Ht+k2xhJdZi511LtmxuEf0OVpvUUNrwmM1P7M= github.com/blevesearch/gtreap v0.1.1 h1:2JWigFrzDMR+42WGIN/V2p0cUvn4UP3C4Q5nmaZGW8Y= @@ -152,8 +138,8 @@ github.com/blevesearch/mmap-go v1.0.2/go.mod h1:ol2qBqYaOUsGdm7aRMRrYGgPvnwLe6Y+ github.com/blevesearch/mmap-go v1.0.4 h1:OVhDhT5B/M1HNPpYPBKIEJaD0F3Si+CrEKULGCDPWmc= github.com/blevesearch/mmap-go v1.0.4/go.mod h1:EWmEAOmdAS9z/pi/+Toxu99DnsbhG1TIxUoRmJw/pSs= github.com/blevesearch/scorch_segment_api/v2 v2.0.1/go.mod h1:lq7yK2jQy1yQjtjTfU931aVqz7pYxEudHaDwOt1tXfU= -github.com/blevesearch/scorch_segment_api/v2 v2.2.16 h1:uGvKVvG7zvSxCwcm4/ehBa9cCEuZVE+/zvrSl57QUVY= -github.com/blevesearch/scorch_segment_api/v2 v2.2.16/go.mod h1:VF5oHVbIFTu+znY1v30GjSpT5+9YFs9dV2hjvuh34F0= +github.com/blevesearch/scorch_segment_api/v2 v2.3.10 h1:Yqk0XD1mE0fDZAJXTjawJ8If/85JxnLd8v5vG/jWE/s= +github.com/blevesearch/scorch_segment_api/v2 v2.3.10/go.mod h1:Z3e6ChN3qyN35yaQpl00MfI5s8AxUJbpTR/DL8QOQ+8= github.com/blevesearch/segment v0.9.0/go.mod h1:9PfHYUdQCgHktBgvtUOF4x+pc4/l8rdH0u5spnW85UQ= github.com/blevesearch/segment v0.9.1 h1:+dThDy+Lvgj5JMxhmOVlgFfkUtZV2kw49xax4+jTfSU= github.com/blevesearch/segment v0.9.1/go.mod h1:zN21iLm7+GnBHWTao9I+Au/7MBiL8pPFtJBJTsk6kQw= @@ -164,39 +150,43 @@ github.com/blevesearch/upsidedown_store_api v1.0.2 h1:U53Q6YoWEARVLd1OYNc9kvhBMG github.com/blevesearch/upsidedown_store_api v1.0.2/go.mod h1:M01mh3Gpfy56Ps/UXHjEO/knbqyQ1Oamg8If49gRwrQ= github.com/blevesearch/vellum v1.0.3/go.mod h1:2u5ax02KeDuNWu4/C+hVQMD6uLN4txH1JbtpaDNLJRo= github.com/blevesearch/vellum v1.0.4/go.mod h1:cMhywHI0de50f7Nj42YgvyD6bFJ2WkNRvNBlNMrEVgY= -github.com/blevesearch/vellum v1.0.10 h1:HGPJDT2bTva12hrHepVT3rOyIKFFF4t7Gf6yMxyMIPI= -github.com/blevesearch/vellum v1.0.10/go.mod h1:ul1oT0FhSMDIExNjIxHqJoGpVrBpKCdgDQNxfqgJt7k= +github.com/blevesearch/vellum v1.1.0 h1:CinkGyIsgVlYf8Y2LUQHvdelgXr6PYuvoDIajq6yR9w= +github.com/blevesearch/vellum v1.1.0/go.mod h1:QgwWryE8ThtNPxtgWJof5ndPfx0/YMBh+W2weHKPw8Y= github.com/blevesearch/zapx/v11 v11.2.0/go.mod h1:gN/a0alGw1FZt/YGTo1G6Z6XpDkeOfujX5exY9sCQQM= -github.com/blevesearch/zapx/v11 v11.3.10 h1:hvjgj9tZ9DeIqBCxKhi70TtSZYMdcFn7gDb71Xo/fvk= -github.com/blevesearch/zapx/v11 v11.3.10/go.mod h1:0+gW+FaE48fNxoVtMY5ugtNHHof/PxCqh7CnhYdnMzQ= +github.com/blevesearch/zapx/v11 v11.4.1 h1:qFCPlFbsEdwbbckJkysptSQOsHn4s6ZOHL5GMAIAVHA= +github.com/blevesearch/zapx/v11 v11.4.1/go.mod h1:qNOGxIqdPC1MXauJCD9HBG487PxviTUUbmChFOAosGs= github.com/blevesearch/zapx/v12 v12.2.0/go.mod h1:fdjwvCwWWwJW/EYTYGtAp3gBA0geCYGLcVTtJEZnY6A= -github.com/blevesearch/zapx/v12 v12.3.10 h1:yHfj3vXLSYmmsBleJFROXuO08mS3L1qDCdDK81jDl8s= -github.com/blevesearch/zapx/v12 v12.3.10/go.mod h1:0yeZg6JhaGxITlsS5co73aqPtM04+ycnI6D1v0mhbCs= +github.com/blevesearch/zapx/v12 v12.4.1 h1:K77bhypII60a4v8mwvav7r4IxWA8qxhNjgF9xGdb9eQ= +github.com/blevesearch/zapx/v12 v12.4.1/go.mod h1:QRPrlPOzAxBNMI0MkgdD+xsTqx65zbuPr3Ko4Re49II= github.com/blevesearch/zapx/v13 v13.2.0/go.mod h1:o5rAy/lRS5JpAbITdrOHBS/TugWYbkcYZTz6VfEinAQ= -github.com/blevesearch/zapx/v13 v13.3.10 h1:0KY9tuxg06rXxOZHg3DwPJBjniSlqEgVpxIqMGahDE8= -github.com/blevesearch/zapx/v13 v13.3.10/go.mod h1:w2wjSDQ/WBVeEIvP0fvMJZAzDwqwIEzVPnCPrz93yAk= +github.com/blevesearch/zapx/v13 v13.4.1 h1:EnkEMZFUK0lsW/jOJJF2xOcp+W8TjEsyeN5BeAZEYYE= +github.com/blevesearch/zapx/v13 v13.4.1/go.mod h1:e6duBMlCvgbH9rkzNMnUa9hRI9F7ri2BRcHfphcmGn8= github.com/blevesearch/zapx/v14 v14.2.0/go.mod h1:GNgZusc1p4ot040cBQMRGEZobvwjCquiEKYh1xLFK9g= -github.com/blevesearch/zapx/v14 v14.3.10 h1:SG6xlsL+W6YjhX5N3aEiL/2tcWh3DO75Bnz77pSwwKU= -github.com/blevesearch/zapx/v14 v14.3.10/go.mod h1:qqyuR0u230jN1yMmE4FIAuCxmahRQEOehF78m6oTgns= +github.com/blevesearch/zapx/v14 v14.4.1 h1:G47kGCshknBZzZAtjcnIAMn3oNx8XBLxp8DMq18ogyE= +github.com/blevesearch/zapx/v14 v14.4.1/go.mod h1:O7sDxiaL2r2PnCXbhh1Bvm7b4sP+jp4unE9DDPWGoms= github.com/blevesearch/zapx/v15 v15.2.0/go.mod h1:MmQceLpWfME4n1WrBFIwplhWmaQbQqLQARpaKUEOs/A= -github.com/blevesearch/zapx/v15 v15.3.16 h1:Ct3rv7FUJPfPk99TI/OofdC+Kpb4IdyfdMH48sb+FmE= -github.com/blevesearch/zapx/v15 v15.3.16/go.mod h1:Turk/TNRKj9es7ZpKK95PS7f6D44Y7fAFy8F4LXQtGg= -github.com/blevesearch/zapx/v16 v16.1.7 h1:I07qV6l1rPda19zyof9Q2J9E8cjZ57pQhNY0+ePI5vM= -github.com/blevesearch/zapx/v16 v16.1.7/go.mod h1:JqQlOqlRVaYDkpLIl3JnKql8u4zKTNlVEa3nLsi0Gn8= +github.com/blevesearch/zapx/v15 v15.4.1 h1:B5IoTMUCEzFdc9FSQbhVOxAY+BO17c05866fNruiI7g= +github.com/blevesearch/zapx/v15 v15.4.1/go.mod h1:b/MreHjYeQoLjyY2+UaM0hGZZUajEbE0xhnr1A2/Q6Y= +github.com/blevesearch/zapx/v16 v16.2.3 h1:7Y0r+a3diEvlazsncexq1qoFOcBd64xwMS7aDm4lo1s= +github.com/blevesearch/zapx/v16 v16.2.3/go.mod h1:wVJ+GtURAaRG9KQAMNYyklq0egV+XJlGcXNCE0OFjjA= +github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR5wKP38= +github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= +github.com/bmizerany/perks v0.0.0-20141205001514-d9a9656a3a4b/go.mod h1:ac9efd0D1fsDb3EJvhqgXRbFx7bs2wqZ10HQPeU8U/Q= +github.com/bohde/codel v0.2.0 h1:fzF7ibgKmCfQbOzQCblmQcwzDRmV7WO7VMLm/hDvD3E= +github.com/bohde/codel v0.2.0/go.mod h1:Idb1IRvTdwkRjIjguLIo+FXhIBhcpGl94o7xra6ggWk= github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/boombuler/barcode v1.0.2 h1:79yrbttoZrLGkL/oOI8hBrUKucwOL0oOjUgEguGMcJ4= github.com/boombuler/barcode v1.0.2/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= -github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 h1:N7oVaKyGp8bttX0bfZGmcGkjz7DLQXhAn3DNd3T0ous= -github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874/go.mod h1:r5xuitiExdLAJ09PR7vBVENGvp4ZuTBeWTGtxuX3K+c= +github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf h1:TqhNAT4zKbTdLa62d2HDBFdvgSbIGB3eJE8HqhgiL9I= +github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf/go.mod h1:r5xuitiExdLAJ09PR7vBVENGvp4ZuTBeWTGtxuX3K+c= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= -github.com/buildkite/terminal-to-html/v3 v3.16.3 h1:IGuJjboHjuMLWOGsKZKNxbbn41emOLiHzXPmQZk31fk= -github.com/buildkite/terminal-to-html/v3 v3.16.3/go.mod h1:r/J7cC9c3EzBzP3/wDz0RJLPwv5PUAMp+KF2w+ntMc0= -github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= -github.com/caddyserver/certmagic v0.21.4 h1:e7VobB8rffHv8ZZpSiZtEwnLDHUwLVYLWzWSa1FfKI0= -github.com/caddyserver/certmagic v0.21.4/go.mod h1:swUXjQ1T9ZtMv95qj7/InJvWLXURU85r+CfG0T+ZbDE= +github.com/buildkite/terminal-to-html/v3 v3.16.8 h1:QN/daUob6cmK8GcdKnwn9+YTlPr1vNj+oeAIiJK6fPc= +github.com/buildkite/terminal-to-html/v3 v3.16.8/go.mod h1:+k1KVKROZocrTLsEQ9PEf9A+8+X8uaVV5iO1ZIOwKYM= +github.com/caddyserver/certmagic v0.23.0 h1:CfpZ/50jMfG4+1J/u2LV6piJq4HOfO6ppOnOf7DkFEU= +github.com/caddyserver/certmagic v0.23.0/go.mod h1:9mEZIWqqWoI+Gf+4Trh04MOVPD0tGSxtqsxg87hAIH4= github.com/caddyserver/zerossl v0.1.3 h1:onS+pxp3M8HnHpN5MMbOMyNjmTheJyWRaZYwn+YTAyA= github.com/caddyserver/zerossl v0.1.3/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4= github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a h1:MISbI8sU/PSK/ztvmWKFcI7UGb5/HQT7B+i3a2myKgI= @@ -211,17 +201,16 @@ github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moA github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= -github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= -github.com/cloudflare/circl v1.5.0 h1:hxIWksrX6XN5a1L2TI/h53AGPhNHoUBo+TD1ms9+pys= -github.com/cloudflare/circl v1.5.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= +github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= +github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/couchbase/ghistogram v0.1.0/go.mod h1:s1Jhy76zqfEecpNWJfWUiKZookAFaiGOEoyzgHt9i7k= github.com/couchbase/go-couchbase v0.1.1 h1:ClFXELcKj/ojyoTYbsY34QUrrYCBi/1G749sXSCkdhk= github.com/couchbase/go-couchbase v0.1.1/go.mod h1:+/bddYDxXsf9qt0xpDUtRR47A2GjaXmGGAqQ/k3GJ8A= -github.com/couchbase/gomemcached v0.3.2 h1:08rxiOoNcv0x5LTxgcYhnx1aPvV7iEtfeyUgqsJyPk0= -github.com/couchbase/gomemcached v0.3.2/go.mod h1:mxliKQxOv84gQ0bJWbI+w9Wxdpt9HjDvgW9MjCym5Vo= +github.com/couchbase/gomemcached v0.3.3 h1:D7qqXLO8wNa4pn5oE65lT3pA3IeStn4joT7/JgGXzKc= +github.com/couchbase/gomemcached v0.3.3/go.mod h1:pISAjweI42vljCumsJIo7CVhqIMIIP9g3Wfhl1JJw68= github.com/couchbase/goutils v0.1.2 h1:gWr8B6XNWPIhfalHNog3qQKfGiYyh4K4VhO3P2o9BCs= github.com/couchbase/goutils v0.1.2/go.mod h1:h89Ek/tiOxxqjz30nPPlwZdQbdB8BwgnuBxeoUe/ViE= github.com/couchbase/moss v0.1.0/go.mod h1:9MaHIaRuy9pvLPUJxB8sh8OrLfyDczECVL37grCIubs= @@ -229,16 +218,14 @@ github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwc github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/cyphar/filepath-securejoin v0.3.4 h1:VBWugsJh2ZxJmLFSM06/0qzQyiQX2Qs0ViKrUAcqdZ8= -github.com/cyphar/filepath-securejoin v0.3.4/go.mod h1:8s/MCNJREmFK0H02MF6Ihv1nakJe4L/w3WZLHNkvlYM= +github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= +github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davidmz/go-pageant v1.0.2 h1:bPblRCh5jGU+Uptpz6LgMZGD5hJoOt7otgT454WvHn0= github.com/davidmz/go-pageant v1.0.2/go.mod h1:P2EDDnMqIwG5Rrp05dTRITj9z2zpGcD9efWSkTNKLIE= -github.com/denisenkom/go-mssqldb v0.12.3 h1:pBSGx9Tq67pBOTLmxNuirNTeB8Vjmf886Kx+8Y+8shw= -github.com/denisenkom/go-mssqldb v0.12.3/go.mod h1:k0mtMFOnU+AihqFxPMiF05rtiDrorD1Vrm1KEz5hxDo= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dimiro1/reply v0.0.0-20200315094148-d0136a4c9e21 h1:PdsjTl0Cg+ZJgOx/CFV5NNgO1ThTreqdgKYiDCMHJwA= @@ -251,19 +238,19 @@ github.com/djherbis/nio/v3 v3.0.1/go.mod h1:Ng4h80pbZFMla1yKzm61cF0tqqilXZYrogmW github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= -github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo= -github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= +github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 h1:iFaUwBSo5Svw6L7HYpRu/0lE3e0BaElwnNO1qkNQxBY= github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s= github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/dvyukov/go-fuzz v0.0.0-20210429054444-fca39067bc72/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw= -github.com/editorconfig/editorconfig-core-go/v2 v2.6.2 h1:dKG8sc7n321deIVRcQtwlMNoBEra7j0qQ8RwxO8RN0w= -github.com/editorconfig/editorconfig-core-go/v2 v2.6.2/go.mod h1:7dvD3GCm7eBw53xZ/lsiq72LqobdMg3ITbMBxnmJmqY= +github.com/editorconfig/editorconfig-core-go/v2 v2.6.3 h1:XVUp6qW3BIkmM3/1EkrHpa6bL56APOynfXcZEmIgOhs= +github.com/editorconfig/editorconfig-core-go/v2 v2.6.3/go.mod h1:ThHVc+hqbUsmE1wmK/MASpQEhCleWu1JDJDNhUOMy0c= github.com/elazarl/go-bindata-assetfs v1.0.1/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4= -github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU= -github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= +github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= +github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= github.com/emersion/go-imap v1.2.1 h1:+s9ZjMEjOB8NzZMVTM3cCenz2JrQIGGo5j1df19WjTA= github.com/emersion/go-imap v1.2.1/go.mod h1:Qlx1FSx2FTxjnjWpIlVNEuX+ylerZQNFE5NsmKFSejY= github.com/emersion/go-message v0.15.0/go.mod h1:wQUEfE+38+7EW8p8aZ96ptg6bAb1iwdgej19uXASlE4= @@ -279,84 +266,53 @@ github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/felixge/fgprof v0.9.5 h1:8+vR6yu2vvSKn08urWyEuxx75NWPEvybbkBirEpsbVY= github.com/felixge/fgprof v0.9.5/go.mod h1:yKl+ERSa++RYOs32d8K6WEXCB4uXdLls4ZaZPpayhMM= -github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= -github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= -github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= -github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= -github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= -github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= -github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/fxamacker/cbor/v2 v2.8.0 h1:fFtUGXUzXPHTIUdne5+zzMPTfffl3RD5qYnkY40vtxU= +github.com/fxamacker/cbor/v2 v2.8.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/git-lfs/pktline v0.0.0-20230103162542-ca444d533ef1 h1:mtDjlmloH7ytdblogrMz1/8Hqua1y8B4ID+bh3rvod0= github.com/git-lfs/pktline v0.0.0-20230103162542-ca444d533ef1/go.mod h1:fenKRzpXDjNpsIBhuhUzvjCKlDjKam0boRAenTE0Q6A= -github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE= -github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8= +github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= +github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= github.com/glycerine/go-unsnap-stream v0.0.0-20181221182339-f9677308dec2/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE= github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24= -github.com/go-ap/activitypub v0.0.0-20240910141749-b4b8c8aa484c h1:82lzmsy5Nr6JA6HcLRVxGfbdSoWfW45C6jnY3zFS7Ks= -github.com/go-ap/activitypub v0.0.0-20240910141749-b4b8c8aa484c/go.mod h1:rpIPGre4qtTgSpVT0zz3hycAMuLtUt7BNngVNpyXhL8= -github.com/go-ap/errors v0.0.0-20240910140019-1e9d33cc1568 h1:eQEXAzWEijFbwtm/Hr2EtFbM0LvATRd1ltpDb+mfjQk= -github.com/go-ap/errors v0.0.0-20240910140019-1e9d33cc1568/go.mod h1:Vkh+Z3f24K8nMsJKXo1FHn5ebPsXvB/WDH5JRtYqdNo= +github.com/go-ap/activitypub v0.0.0-20250409143848-7113328b1f3d h1:IWrWGnmKzpHqginJ18ljKkty/X8glxM8Mg3pk6bkb8g= +github.com/go-ap/activitypub v0.0.0-20250409143848-7113328b1f3d/go.mod h1:EUtZuXtHo4yKkTJmcbAZYW+X1G2poeT8icmBh24eq7o= +github.com/go-ap/errors v0.0.0-20250409143711-5686c11ae650 h1:tlwla5IQUea0CuktkBd2FLDwVzts4OeTWPPkhQPSK5Q= +github.com/go-ap/errors v0.0.0-20250409143711-5686c11ae650/go.mod h1:Vkh+Z3f24K8nMsJKXo1FHn5ebPsXvB/WDH5JRtYqdNo= github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73 h1:GMKIYXyXPGIp+hYiWOhfqK4A023HdgisDT4YGgf99mw= github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73/go.mod h1:jyveZeGw5LaADntW+UEsMjl3IlIwk+DxlYNsbofQkGA= -github.com/go-asn1-ber/asn1-ber v1.5.5/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= -github.com/go-asn1-ber/asn1-ber v1.5.7 h1:DTX+lbVTWaTw1hQ+PbZPlnDZPEIs0SS/GCZAl535dDk= -github.com/go-asn1-ber/asn1-ber v1.5.7/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= +github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo= +github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-chi/chi/v5 v5.0.1/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= -github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= -github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618= +github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= github.com/go-co-op/gocron v1.37.0 h1:ZYDJGtQ4OMhTLKOKMIch+/CY70Brbb1dGdooLEhh7b0= github.com/go-co-op/gocron v1.37.0/go.mod h1:3L/n6BkO7ABj+TrfSVXLRzsP26zmikL4ISkLQ0O8iNY= -github.com/go-enry/go-enry/v2 v2.9.1 h1:G9iDteJ/Mc0F4Di5NeQknf83R2OkRbwY9cAYmcqVG6U= -github.com/go-enry/go-enry/v2 v2.9.1/go.mod h1:9yrj4ES1YrbNb1Wb7/PWYr2bpaCXUGRt0uafN0ISyG8= +github.com/go-enry/go-enry/v2 v2.9.2 h1:giOQAtCgBX08kosrX818DCQJTCNtKwoPBGu0qb6nKTY= +github.com/go-enry/go-enry/v2 v2.9.2/go.mod h1:9yrj4ES1YrbNb1Wb7/PWYr2bpaCXUGRt0uafN0ISyG8= github.com/go-enry/go-oniguruma v1.2.1 h1:k8aAMuJfMrqm/56SG2lV9Cfti6tC4x8673aHCcBk+eo= github.com/go-enry/go-oniguruma v1.2.1/go.mod h1:bWDhYP+S6xZQgiRL7wlTScFYBe023B6ilRZbCAD5Hf4= -github.com/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw= -github.com/go-faster/city v1.0.1/go.mod h1:jKcUJId49qdW3L1qKHH/3wPeUstCVpVSXTM6vO3VcTw= -github.com/go-faster/errors v0.7.1 h1:MkJTnDoEdi9pDabt1dpWf7AA8/BaSYZqibYyhZ20AYg= -github.com/go-faster/errors v0.7.1/go.mod h1:5ySTjWFiphBs07IKuiL69nxdfd5+fzh1u7FPGZP2quo= github.com/go-fed/httpsig v1.1.1-0.20201223112313-55836744818e h1:oRq/fiirun5HqlEWMLIcDmLpIELlG4iGbd0s8iqgPi8= github.com/go-fed/httpsig v1.1.1-0.20201223112313-55836744818e/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= -github.com/go-git/go-billy/v5 v5.6.0 h1:w2hPNtoehvJIxR00Vb4xX94qHQi/ApZfX+nBE2Cjio8= -github.com/go-git/go-billy/v5 v5.6.0/go.mod h1:sFDq7xD3fn3E0GOwUSZqHo9lrkmx8xJhA0ZrfvjBRGM= +github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM= +github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= -github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys= -github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY= +github.com/go-git/go-git/v5 v5.16.0 h1:k3kuOEpkc0DeY7xlL6NaaNg39xdgQbtH5mwCafHO9AQ= +github.com/go-git/go-git/v5 v5.16.0/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8= github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= -github.com/go-ldap/ldap/v3 v3.4.8 h1:loKJyspcRezt2Q3ZRMq2p/0v8iOurlmeXDPw6fikSvQ= -github.com/go-ldap/ldap/v3 v3.4.8/go.mod h1:qS3Sjlu76eHfHGpUdWkAXQTw4beih+cHsco2jXlIXrk= -github.com/go-openapi/analysis v0.23.0 h1:aGday7OWupfMs+LbmLZG4k0MYXIANxcuBTYUC03zFCU= -github.com/go-openapi/analysis v0.23.0/go.mod h1:9mz9ZWaSlV8TvjQHLl2mUW2PbZtemkE8yA5v22ohupo= -github.com/go-openapi/errors v0.22.0 h1:c4xY/OLxUBSTiepAg3j/MHuAv5mJhnf53LLMWFB+u/w= -github.com/go-openapi/errors v0.22.0/go.mod h1:J3DmZScxCDufmIMsdOuDHxJbdOGC0xtUynjIx092vXE= -github.com/go-openapi/inflect v0.21.0 h1:FoBjBTQEcbg2cJUWX6uwL9OyIW8eqc9k4KhN4lfbeYk= -github.com/go-openapi/inflect v0.21.0/go.mod h1:INezMuUu7SJQc2AyR3WO0DqqYUJSj8Kb4hBd7WtjlAw= -github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= -github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= -github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= -github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= -github.com/go-openapi/loads v0.22.0 h1:ECPGd4jX1U6NApCGG1We+uEozOAvXvJSF4nnwHZ8Aco= -github.com/go-openapi/loads v0.22.0/go.mod h1:yLsaTCS92mnSAZX5WWoxszLj0u+Ojl+Zs5Stn1oF+rs= -github.com/go-openapi/runtime v0.28.0 h1:gpPPmWSNGo214l6n8hzdXYhPuJcGtziTOgUpvsFWGIQ= -github.com/go-openapi/runtime v0.28.0/go.mod h1:QN7OzcS+XuYmkQLw05akXk0jRH/eZ3kb18+1KwW9gyc= -github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY= -github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk= -github.com/go-openapi/strfmt v0.23.0 h1:nlUS6BCqcnAk0pyhi9Y+kdDVZdZMHfEKQiS4HaMgO/c= -github.com/go-openapi/strfmt v0.23.0/go.mod h1:NrtIpfKtWIygRkKVsxh7XQMDQW5HKQl6S5ik2elW+K4= -github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= -github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= -github.com/go-openapi/validate v0.24.0 h1:LdfDKwNbpB6Vn40xhTdNZAnfLECL81w+VX3BumrGD58= -github.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ= +github.com/go-ldap/ldap/v3 v3.4.11 h1:4k0Yxweg+a3OyBLjdYn5OKglv18JNvfDykSoI8bW0gU= +github.com/go-ldap/ldap/v3 v3.4.11/go.mod h1:bY7t0FLK8OAVpp/vV6sSlpz3EQDGcQwc8pF0ujLgKvM= github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg= github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= github.com/go-redis/redis/v7 v7.4.1 h1:PASvf36gyUpr2zdOUS/9Zqc80GbM+9BDyiJSJDDOrTI= @@ -365,43 +321,37 @@ github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= github.com/go-redsync/redsync/v4 v4.13.0 h1:49X6GJfnbLGaIpBBREM/zA4uIMDXKAh1NDkvQ1EkZKA= github.com/go-redsync/redsync/v4 v4.13.0/go.mod h1:HMW4Q224GZQz6x1Xc7040Yfgacukdzu7ifTDAKiyErQ= -github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= -github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= -github.com/go-swagger/go-swagger v0.31.0 h1:H8eOYQnY2u7vNKWDNykv2xJP3pBhRG/R+SOCAmKrLlc= -github.com/go-swagger/go-swagger v0.31.0/go.mod h1:WSigRRWEig8zV6t6Sm8Y+EmUjlzA/HoaZJ5edupq7po= +github.com/go-sql-driver/mysql v1.9.2 h1:4cNKDYQ1I84SXslGddlsrMhc8k4LeDVj6Ad6WRjiHuU= +github.com/go-sql-driver/mysql v1.9.2/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg= github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= -github.com/go-testfixtures/testfixtures/v3 v3.11.0 h1:XxQr8AnPORcZkyNd7go5UNLPD3dULN8ixYISlzrlfEQ= -github.com/go-testfixtures/testfixtures/v3 v3.11.0/go.mod h1:THmudHF1Ixq++J2/UodcJpxUphfyEd77m83TvDtryqE= -github.com/go-webauthn/webauthn v0.11.2 h1:Fgx0/wlmkClTKlnOsdOQ+K5HcHDsDcYIvtYmfhEOSUc= -github.com/go-webauthn/webauthn v0.11.2/go.mod h1:aOtudaF94pM71g3jRwTYYwQTG1KyTILTcZqN1srkmD0= -github.com/go-webauthn/x v0.1.15 h1:eG1OhggBJTkDE8gUeOlGRbRe8E/PSVG26YG4AyFbwkU= -github.com/go-webauthn/x v0.1.15/go.mod h1:pf7VI23raFLHPO9VVIs9/u1etqwAOP0S2KoHGL6WbZ8= +github.com/go-webauthn/webauthn v0.12.3 h1:hHQl1xkUuabUU9uS+ISNCMLs9z50p9mDUZI/FmkayNE= +github.com/go-webauthn/webauthn v0.12.3/go.mod h1:4JRe8Z3W7HIw8NGEWn2fnUwecoDzkkeach/NnvhkqGY= +github.com/go-webauthn/x v0.1.20 h1:brEBDqfiPtNNCdS/peu8gARtq8fIPsHz0VzpPjGvgiw= +github.com/go-webauthn/x v0.1.20/go.mod h1:n/gAc8ssZJGATM0qThE+W+vfgXiMedsWi3wf/C4lld0= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= -github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= -github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f h1:3BSP1Tbs2djlpprl7wCLuiqMaUh5SJkkzI2gDs+FgLs= github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14= github.com/gogs/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85 h1:UjoPNDAQ5JPCjlxoJd6K8ALZqSDDhk2ymieAZOVaDg0= github.com/gogs/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85/go.mod h1:fR6z1Ie6rtF7kl/vBYMfgD5/G5B1blui7z426/sj2DU= -github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo= github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= -github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= -github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= +github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= +github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= -github.com/golang/geo v0.0.0-20230421003525-6adc56603217 h1:HKlyj6in2JV6wVkmQ4XmG/EIm+SCYlPZ+V4GWit7Z+I= -github.com/golang/geo v0.0.0-20230421003525-6adc56603217/go.mod h1:8wI0hitZ3a1IxZfeH3/5I97CI8i5cLGsYe7xNhQGs9U= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= @@ -410,39 +360,42 @@ github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrU github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= -github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= +github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/gomodule/redigo v1.8.9 h1:Sl3u+2BI/kk+VEatbj0scLdrFhjPmbxOc1myhDP41ws= github.com/gomodule/redigo v1.8.9/go.mod h1:7ArFNvsTjH8GMMzB4uy1snslv2BwmginuMs06a1uzZE= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/flatbuffers v24.3.25+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q= +github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-github/v61 v61.0.0 h1:VwQCBwhyE9JclCI+22/7mLB1PuU9eowCXKY5pNlu1go= -github.com/google/go-github/v61 v61.0.0/go.mod h1:0WR+KmsWX75G2EbpyGsGmradjo3IiciuI4BmdVCobQY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-github/v71 v71.0.0 h1:Zi16OymGKZZMm8ZliffVVJ/Q9YZreDKONCr+WUd0Z30= +github.com/google/go-github/v71 v71.0.0/go.mod h1:URZXObp2BLlMjwu0O8g4y6VBneUj2bCHgnI8FfgZ51M= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= -github.com/google/go-tpm v0.9.1 h1:0pGc4X//bAlmZzMKf8iz6IsDo1nYTbYJ6FZN/rg4zdM= -github.com/google/go-tpm v0.9.1/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= +github.com/google/go-tpm v0.9.3 h1:+yx0/anQuGzi+ssRqeD6WpXjW2L/V0dItUayO0i9sRc= +github.com/google/go-tpm v0.9.3/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/licenseclassifier/v2 v2.0.0 h1:1Y57HHILNf4m0ABuMVb6xk4vAJYEUO0gDxNpog0pyeA= github.com/google/licenseclassifier/v2 v2.0.0/go.mod h1:cOjbdH0kyC9R22sdQbYsFkto4NGCAc+ZSwbeThazEtM= github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= -github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= -github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/pprof v0.0.0-20250422154841-e1f9c1950416 h1:1/qwHx8P72glDXdyCKesJ+/c40x71SY4q2avOxJ2iYQ= +github.com/google/pprof v0.0.0-20250422154841-e1f9c1950416/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA= github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -455,8 +408,6 @@ github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/gorilla/feeds v1.2.0 h1:O6pBiXJ5JHhPvqy53NsjKOThq+dNFm8+DFrxBEdzSCc= github.com/gorilla/feeds v1.2.0/go.mod h1:WMib8uJP3BbY+X8Szd1rA5Pzhdfh+HCCAYT2z7Fza6Y= -github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE= -github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/pat v0.0.0-20180118222023-199c85a7f6d1 h1:LqbZZ9sNMWVjeXS4NN5oVvhMjDyLhmA1LG86oSo+IqY= @@ -465,13 +416,8 @@ github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+ github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= -github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ= github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik= -github.com/h2non/gock v1.2.0 h1:K6ol8rfrRkUOefooBC8elXoaNGYkpp7y2qcxGG6BzUE= -github.com/h2non/gock v1.2.0/go.mod h1:tNhoxHYW2W42cYkYb1WqzdbYIieALC99kpYr7rH/BQk= -github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= -github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -483,12 +429,10 @@ github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+l github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= -github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= -github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= @@ -497,22 +441,6 @@ github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= -github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= -github.com/jackc/pgconn v1.14.3 h1:bVoTr12EGANZz66nZPkMInAV/KHD2TxH9npjXXgiB3w= -github.com/jackc/pgconn v1.14.3/go.mod h1:RZbme4uasqzybK2RK5c65VsHxoyaml09lx3tXOcO/VM= -github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= -github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= -github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= -github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= -github.com/jackc/pgproto3/v2 v2.3.3 h1:1HLSx5H+tXR9pW3in3zaztoEwQYRC9SQaYUHjTSUOag= -github.com/jackc/pgproto3/v2 v2.3.3/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= -github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= -github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgtype v1.14.0 h1:y+xUdabmyMkJLyApYuPj38mW+aAIqCe5uuBB51rH3Vw= -github.com/jackc/pgtype v1.14.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= -github.com/jackc/pgx/v4 v4.18.3 h1:dE2/TrEsGX3RBprb3qryqSV9Y60iZN1C6i8IrmW9/BA= -github.com/jackc/pgx/v4 v4.18.3/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw= github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 h1:iCHtR9CQyktQ5+f3dMVZfwD2KWJUgm7M0gdL9NGr8KA= github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= @@ -529,14 +457,8 @@ github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh6 github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= -github.com/jessevdk/go-flags v1.6.1 h1:Cvu5U8UGrLay1rZfv/zP7iLpSHGUZ/Ou68T0iX1bBK4= -github.com/jessevdk/go-flags v1.6.1/go.mod h1:Mk8T1hIAWpOiJiHa9rJASDK2UGWji0EuPGBnNLMooyc= github.com/jhillyerd/enmime v1.3.0 h1:LV5kzfLidiOr8qRGIpYYmUZCnhrPbcFAnAFUnWn99rw= github.com/jhillyerd/enmime v1.3.0/go.mod h1:6c6jg5HdRRV2FtvVL69LjiX1M8oE0xDX9VEhV3oy4gs= -github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= -github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= -github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= -github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= @@ -548,19 +470,14 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNU github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= -github.com/keybase/go-crypto v0.0.0-20200123153347-de78d2cb44f4 h1:cTxwSmnaqLoo+4tLukHoB9iqHOu3LmLhRmgUxZo6Vp4= -github.com/keybase/go-crypto v0.0.0-20200123153347-de78d2cb44f4/go.mod h1:ghbZscTyKdM07+Fw3KSi0hcJm+AlEUWj8QLlPtijN/M= -github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= -github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= -github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= -github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= -github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= +github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= @@ -579,57 +496,52 @@ github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+ github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/libdns/libdns v0.2.2 h1:O6ws7bAfRPaBsgAYt8MDe2HcNBGC29hkZ9MX2eUSX3s= -github.com/libdns/libdns v0.2.2/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ= -github.com/lunny/vfsgen v0.0.0-20220105142115-2c99e1ffdfa0 h1:F/3FfGmKdiKFa8kL3YrpZ7pe9H4l4AzA1pbaOUnRvPI= -github.com/lunny/vfsgen v0.0.0-20220105142115-2c99e1ffdfa0/go.mod h1:JEfTc3+2DF9Z4PXhLLvXL42zexJyh8rIq3OzUj/0rAk= +github.com/libdns/libdns v1.0.0-beta.1 h1:KIf4wLfsrEpXpZ3vmc/poM8zCATXT2klbdPe6hyOBjQ= +github.com/libdns/libdns v1.0.0-beta.1/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= -github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= -github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= +github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/markbates/going v1.0.3 h1:mY45T5TvW+Xz5A6jY7lf4+NLg9D8+iuStIHyR7M8qsE= github.com/markbates/going v1.0.3/go.mod h1:fQiT6v6yQar9UD6bd/D4Z5Afbk9J6BBVBtLiyY4gp2o= -github.com/markbates/goth v1.80.0 h1:NnvatczZDzOs1hn9Ug+dVYf2Viwwkp/ZDX5K+GLjan8= -github.com/markbates/goth v1.80.0/go.mod h1:4/GYHo+W6NWisrMPZnq0Yr2Q70UntNLn7KXEFhrIdAY= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/markbates/goth v1.81.0 h1:XVcCkeGWokynPV7MXvgb8pd2s3r7DS40P7931w6kdnE= +github.com/markbates/goth v1.81.0/go.mod h1:+6z31QyUms84EHmuBY7iuqYSxyoN3njIgg9iCF/lR1k= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= -github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= -github.com/meilisearch/meilisearch-go v0.29.1-0.20241106140435-0bf60fad690a h1:F0y+3QtCG00mr4KueQWuHv1tlIQeNXhH+XAKYLhb3X4= -github.com/meilisearch/meilisearch-go v0.29.1-0.20241106140435-0bf60fad690a/go.mod h1:NYOgjEGt/+oExD+NixreBMqxtIB0kCndXOOgpGhoqEs= -github.com/mholt/acmez/v2 v2.0.3 h1:CgDBlEwg3QBp6s45tPQmFIBrkRIkBT4rW4orMM6p4sw= -github.com/mholt/acmez/v2 v2.0.3/go.mod h1:pQ1ysaDeGrIMvJ9dfJMk5kJNkn7L2sb3UhyrX6Q91cw= +github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk= +github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= +github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A= +github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/meilisearch/meilisearch-go v0.31.0 h1:yZRhY1qJqdH8h6GFZALGtkDLyj8f9v5aJpsNMyrUmnY= +github.com/meilisearch/meilisearch-go v0.31.0/go.mod h1:aNtyuwurDg/ggxQIcKqWH6G9g2ptc8GyY7PLY4zMn/g= +github.com/mholt/acmez/v3 v3.1.2 h1:auob8J/0FhmdClQicvJvuDavgd5ezwLBfKuYmynhYzc= +github.com/mholt/acmez/v3 v3.1.2/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ= github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= -github.com/microsoft/go-mssqldb v1.7.2 h1:CHkFJiObW7ItKTJfHo1QX7QBBD1iV+mn1eOyRP3b/PA= -github.com/microsoft/go-mssqldb v1.7.2/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA= -github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ= -github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ= +github.com/microsoft/go-mssqldb v1.8.0 h1:7cyZ/AT7ycDsEoWPIXibd+aVKFtteUNhDGf3aobP+tw= +github.com/microsoft/go-mssqldb v1.8.0/go.mod h1:6znkekS3T2vp0waiMhen4GPU1BiAsrP+iXHcE7a7rFo= +github.com/miekg/dns v1.1.65 h1:0+tIPHzUW0GCge7IiK3guGP57VAw7hoPDfApjkMD1Fc= +github.com/miekg/dns v1.1.65/go.mod h1:Dzw9769uoKVaLuODMDZz9M6ynFU6Em65csPuoi8G0ck= +github.com/minio/crc64nvme v1.0.1 h1:DHQPrYPdqK7jQG/Ls5CTBZWeex/2FMS3G5XGkycuFrY= +github.com/minio/crc64nvme v1.0.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg= github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= -github.com/minio/minio-go/v7 v7.0.80 h1:2mdUHXEykRdY/BigLt3Iuu1otL0JTogT0Nmltg0wujk= -github.com/minio/minio-go/v7 v7.0.80/go.mod h1:84gmIilaX4zcvAWWzJ5Z1WI5axN+hAbM5w25xf8xvC0= -github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= -github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/minio/minio-go/v7 v7.0.91 h1:tWLZnEfo3OZl5PoXQwcwTAPNNrjyWwOh6cbZitW5JQc= +github.com/minio/minio-go/v7 v7.0.91/go.mod h1:uvMUcGrpgeSAAI6+sD3818508nUyMULw94j2Nxku/Go= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= -github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/mrjones/oauth v0.0.0-20190623134757-126b35219450 h1:j2kD3MT1z4PXCiUllUJF9mWUESr9TWKS7iEKsQ/IipM= github.com/mrjones/oauth v0.0.0-20190623134757-126b35219450/go.mod h1:skjdDftzkFALcuGzYSklqYd8gvat6F1gZJ4YPVbkZpM= github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg= @@ -639,18 +551,14 @@ github.com/msteinert/pam v1.2.0 h1:mYfjlvN2KYs2Pb9G6nb/1f/nPfAttT/Jee5Sq9r3bGE= github.com/msteinert/pam v1.2.0/go.mod h1:d2n0DCUK8rGecChV3JzvmsDjOY4R7AYbsNxAT+ftQl0= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4= -github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms= -github.com/niklasfasching/go-org v1.7.0 h1:vyMdcMWWTe/XmANk19F4k8XGBYg0GQ/gJGMimOjGMek= -github.com/niklasfasching/go-org v1.7.0/go.mod h1:WuVm4d45oePiE0eX25GqTDQIt/qPW1T9DGkRscqLW5o= +github.com/niklasfasching/go-org v1.8.0 h1:WyGLaajLLp8JbQzkmapZ1y0MOzKuKV47HkZRloi+HGY= +github.com/niklasfasching/go-org v1.8.0/go.mod h1:e2A9zJs7cdONrEGs3gvxCcaAEpwwPNPG7csDpXckMNg= github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0= github.com/nwaples/rardecode v1.1.3 h1:cWCaZwfM5H7nAD6PyEdcVnczzV8i/JtotnyW/dD9lEc= github.com/nwaples/rardecode v1.1.3/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= -github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= -github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/olivere/elastic/v7 v7.0.32 h1:R7CXvbu8Eq+WlsLgxmKVKPox0oOwAE/2T9Si5BnvK6E= @@ -667,21 +575,16 @@ github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= -github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= -github.com/paulmach/orb v0.11.1 h1:3koVegMC4X/WeiXYz9iswopaTwMem53NzTJuTF20JzU= -github.com/paulmach/orb v0.11.1/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU= -github.com/paulmach/protoscan v0.2.1/go.mod h1:SpcSwydNLrxUGSDvXvO0P7g7AuhJ7lcKfDlhJCDw2gY= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= -github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= -github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU= github.com/pierrec/lz4/v4 v4.1.2/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= -github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= -github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= -github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= -github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= +github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= +github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4= +github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= @@ -692,25 +595,25 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg= github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= -github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= -github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= -github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= -github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= -github.com/prometheus/common v0.60.1 h1:FUas6GcOw66yB/73KC+BOZoFJmbo/1pojoILArPAaSc= -github.com/prometheus/common v0.60.1/go.mod h1:h0LYf1R1deLSKtD4Vdg8gy4RuOvENW2J/h19V5NADQw= -github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= -github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= +github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k= +github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/quasoft/websspi v1.1.2 h1:/mA4w0LxWlE3novvsoEL6BBA1WnjJATbjkh1kFrTidw= github.com/quasoft/websspi v1.1.2/go.mod h1:HmVdl939dQ0WIXZhyik+ARdI03M6bQzaSEKcgpFmewk= github.com/rcrowley/go-metrics v0.0.0-20190826022208-cac0b30c2563/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= -github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E= -github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw= +github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM= +github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA= github.com/redis/rueidis v1.0.19 h1:s65oWtotzlIFN8eMPhyYwxlwLR1lUdhza2KtWprKYSo= github.com/redis/rueidis v1.0.19/go.mod h1:8B+r5wdnjwK3lTFml5VtxjzGOQAC+5UmujoD12pDrEo= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= -github.com/rhysd/actionlint v1.7.3 h1:WD919WuLYrSCwY8VGBqJBEuzyVEIL5viXmXqRRcKOVs= -github.com/rhysd/actionlint v1.7.3/go.mod h1:rl+8ZoX1rqnbcMWKaTyOHmw08mmb/zlmG/Zu1fY47F4= +github.com/rhysd/actionlint v1.7.7 h1:0KgkoNTrYY7vmOCs9BW2AHxLvvpoY9nEUzgBHiPUr0k= +github.com/rhysd/actionlint v1.7.7/go.mod h1:AE6I6vJEkNaIfWqC2GNE5spIJNhxf8NCtLEKU4NnUXg= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= @@ -719,59 +622,38 @@ github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= -github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= -github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sagikazarmark/locafero v0.6.0 h1:ON7AQg37yzcRPU69mt7gwhFEBwxI6P9T4Qu3N51bwOk= -github.com/sagikazarmark/locafero v0.6.0/go.mod h1:77OmuIc6VTraTXKXIs/uvUxKGUXjE1GbemJYHqdNjX0= -github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= -github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4= github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY= github.com/sassoftware/go-rpmutils v0.4.0 h1:ojND82NYBxgwrV+mX1CWsd5QJvvEZTKddtCdFLPWhpg= github.com/sassoftware/go-rpmutils v0.4.0/go.mod h1:3goNWi7PGAT3/dlql2lv3+MSN5jNYPjT5mVcQcIsYzI= -github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= -github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= github.com/serenize/snaker v0.0.0-20171204205717-a683aaf2d516/go.mod h1:Yow6lPLSAXx2ifx470yD/nUe22Dv5vBvxK/UK9UUTVs= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= -github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= -github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= -github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c h1:aqg5Vm5dwtvL+YgDpBcK1ITf3o96N/K7/wsRXQnUTEs= -github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c/go.mod h1:owqhoLW1qZoYLZzLnBw+QkPP9WZnjlSWihhxAJC1+/M= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/skeema/knownhosts v1.3.0 h1:AM+y0rI04VksttfwjkSTNQorvGqmwATnvnAHpSgc0LY= -github.com/skeema/knownhosts v1.3.0/go.mod h1:sPINvnADmT/qYH1kfv+ePMmOBTH6Tbl7b5LvTDjFK7M= +github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= +github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/assertions v1.1.1 h1:T/YLemO5Yp7KPzS+lVtu+WsHn8yoSwTfItdAd1r3cck= github.com/smartystreets/assertions v1.1.1/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s= github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337 h1:WN9BUFbdyOsSH/XohnWpXOlq9NBD5sGAB2FciQMUEe8= github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= -github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= -github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= -github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= -github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= -github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= -github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= -github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo= github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM= github.com/stephens2424/writerset v1.0.2/go.mod h1:aS2JhsMn6eA7e82oNmW4rfsgAOp9COBTTl8mzkwADnc= @@ -779,51 +661,46 @@ github.com/steveyen/gtreap v0.1.0/go.mod h1:kl/5J7XbrOmlIbYIXdRHDDE5QxHqpk0cmkT7 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stvp/tempredis v0.0.0-20181119212430-b82af8480203 h1:QVqDTf3h2WHt08YuiTGPZLls0Wq99X9bWd0Q5ZSBesM= github.com/stvp/tempredis v0.0.0-20181119212430-b82af8480203/go.mod h1:oqN97ltKNihBbwlX8dLpwxCl3+HnXKV/R0e+sRLd9C8= -github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= -github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE= github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= -github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tinylib/msgp v1.1.0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= -github.com/toqueteos/webbrowser v1.2.0 h1:tVP/gpK69Fx+qMJKsLE7TD8LuGWPnEV71wBN9rrstGQ= -github.com/toqueteos/webbrowser v1.2.0/go.mod h1:XWoZq4cyp9WeUeak7w7LXRUQf1F1ATJMir8RTqb4ayM= github.com/tstranex/u2f v1.0.0 h1:HhJkSzDDlVSVIVt7pDJwCHQj67k7A5EeBgPmeD+pVsQ= github.com/tstranex/u2f v1.0.0/go.mod h1:eahSLaqAS0zsIEv80+vXT7WanXs7MQQDg3j3wGBSayo= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= -github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc= -github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY= +github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/unknwon/com v1.0.1 h1:3d1LTxD+Lnf3soQiD4Cp/0BRB+Rsa/+RTvz8GMMzIXs= github.com/unknwon/com v1.0.1/go.mod h1:tOOxU81rwgoCLoOVVPHb6T/wt8HZygqH5id+GNnlCXM= -github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= -github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= +github.com/urfave/cli-docs/v3 v3.0.0-alpha6 h1:w/l/N0xw1rO/aHRIGXJ0lDwwYFOzilup1qGvIytP3BI= +github.com/urfave/cli-docs/v3 v3.0.0-alpha6/go.mod h1:p7Z4lg8FSTrPB9GTaNyTrK3ygffHZcK3w0cU2VE+mzU= +github.com/urfave/cli/v3 v3.3.3 h1:byCBaVdIXuLPIDm5CYZRVG6NvT7tv1ECqdU4YzlEa3I= +github.com/urfave/cli/v3 v3.3.3/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo= github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ= github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY= github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= +github.com/wneessen/go-mail v0.6.2 h1:c6V7c8D2mz868z9WJ+8zDKtUyLfZ1++uAZmo2GRFji8= +github.com/wneessen/go-mail v0.6.2/go.mod h1:L/PYjPK3/2ZlNb2/FjEBIn9n1rUWjW+Toy531oVmeb4= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= -github.com/xanzy/go-gitlab v0.112.0 h1:6Z0cqEooCvBMfBIHw+CgO4AKGRV8na/9781xOb0+DKw= -github.com/xanzy/go-gitlab v0.112.0/go.mod h1:wKNKh3GkYDMOsGmnfuX+ITCmDuSDWFO0G+C4AygL9RY= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= -github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= -github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g= -github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= @@ -834,20 +711,16 @@ github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQ github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo= github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= -github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= -github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yohcop/openid-go v1.0.1 h1:DPRd3iPO5F6O5zX2e62XpVAbPT6wV51cuucH0z9g3js= github.com/yohcop/openid-go v1.0.1/go.mod h1:b/AvD03P0KHj4yuihb+VtLD6bYYgsy0zqBzPCRjkCNs= -github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic= -github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= +github.com/yuin/goldmark v1.7.10 h1:S+LrtBjRmqMac2UdtB6yyCEJm+UILZ2fefI4p7o0QpI= +github.com/yuin/goldmark v1.7.10/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ= github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I= github.com/yuin/goldmark-meta v1.1.0 h1:pWw+JLHGZe8Rk0EGsMVssiNb/AaPMHfSRszZeUeiOUc= @@ -858,16 +731,11 @@ github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI= github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE= github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo= github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4= +gitlab.com/gitlab-org/api/client-go v0.127.0 h1:8xnxcNKGF2gDazEoMs+hOZfOspSSw8D0vAoWhQk9U+U= +gitlab.com/gitlab-org/api/client-go v0.127.0/go.mod h1:bYC6fPORKSmtuPRyD9Z2rtbAjE7UeNatu2VWHRf4/LE= go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= -go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0= -go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I= -go.mongodb.org/mongo-driver v1.11.4/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g= -go.mongodb.org/mongo-driver v1.17.1 h1:Wic5cJIwJgSpBhe3lx3+/RybR5PiYRMpVFgO7cOHyIM= -go.mongodb.org/mongo-driver v1.17.1/go.mod h1:wwWm/+BuOddhcq3n68LKRmgk2wXzmF6s0SFOa0GINL4= -go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY= -go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE= -go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys= -go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A= +go.etcd.io/bbolt v1.4.0 h1:TU77id3TnN/zKr7CO/uk+fBCwF2jGcMuw2B/FMAzYIk= +go.etcd.io/bbolt v1.4.0/go.mod h1:AsD+OCi/qPN1giOX1aiLAha3o1U8rAz65bvN4j0sRuk= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= @@ -877,6 +745,8 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U= +go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -884,29 +754,32 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= -golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= -golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= -golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= -golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= -golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY= -golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8= -golang.org/x/image v0.21.0 h1:c5qV36ajHpdj4Qi0GnE0jUc/yuo33OLFaa0d+crTD5s= -golang.org/x/image v0.21.0/go.mod h1:vUbsLavqK/W303ZroQQVKQ+Af3Yl6Uz1Ppu5J/cLz78= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= +golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= +golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= +golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= +golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= +golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= +golang.org/x/image v0.26.0 h1:4XjIFEZWQmCZi6Wv8BoxsDhRU3RVnLX04dToTDAEPlY= +golang.org/x/image v0.26.0/go.mod h1:lcxbMFAovzpnJxzXS3nyL83K27tmqtKzIJpctK8YO5c= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= -golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= +golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= @@ -914,28 +787,32 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= -golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= 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.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= -golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= -golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= -golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= +golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= +golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= +golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98= +golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= -golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= +golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181221143128-b4a75ba826a6/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -959,73 +836,73 @@ golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= -golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= -golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= -golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= -golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= +golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= +golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= +golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= +golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= -golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= -golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= -golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= +golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= +golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200325010219-a49f79bcc224/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200928182047-19e03678916f/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= -golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= +golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38 h1:zciRKQ4kBpFgpfC5QQCVtnnNAcLIqweL7plyZRQHVpI= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= -google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= -google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250422160041-2d3770c4ea7f h1:N/PrbTw4kdkqNRzVfWPrBekzLuarFREcbFOiOLkXon4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250422160041-2d3770c4ea7f/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/grpc v1.72.0 h1:S7UkcVa60b5AAQTaO6ZKamFp1zMZSU0fGDK2WZLbBnM= +google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= -google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= -gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= -gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -1033,8 +910,6 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE= -gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= @@ -1049,6 +924,7 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI= @@ -1071,11 +947,13 @@ modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY= modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw= modernc.org/token v1.0.1 h1:A3qvTqOwexpfZZeyI0FeGPDlSWX5pjZu9hF4lU+EKWg= modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= -mvdan.cc/xurls/v2 v2.5.0 h1:lyBNOm8Wo71UknhUs4QTFUNNMyxy2JEIaKKo0RWOh+8= -mvdan.cc/xurls/v2 v2.5.0/go.mod h1:yQgaGQ1rFtJUzkmKiHYSSfuQxqfYmd//X6PxvholpeE= +mvdan.cc/xurls/v2 v2.6.0 h1:3NTZpeTxYVWNSokW3MKeyVkz/j7uYXYiMtXRUfmjbgI= +mvdan.cc/xurls/v2 v2.6.0/go.mod h1:bCvEZ1XvdA6wDnxY7jPPjEmigDtvtvPXAD/Exa9IMSk= +pgregory.net/rapid v0.4.2 h1:lsi9jhvZTYvzVpeG93WWgimPRmiJQfGFRNTEZh1dtY0= +pgregory.net/rapid v0.4.2/go.mod h1:UYpPVyjFHzYBGHIxLFoupi8vwk6rXNzRY9OMvVxFIOU= strk.kbt.io/projects/go/libravatar v0.0.0-20191008002943-06d1c002b251 h1:mUcz5b3FJbP5Cvdq7Khzn6J9OCUQJaBwgBkCR+MOwSs= strk.kbt.io/projects/go/libravatar v0.0.0-20191008002943-06d1c002b251/go.mod h1:FJGmPh3vz9jSos1L/F91iAgnC/aejc0wIIrF2ZwJxdY= xorm.io/builder v0.3.13 h1:a3jmiVVL19psGeXx8GIurTp7p0IIgqeDmwhcR6BAOAo= xorm.io/builder v0.3.13/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE= -xorm.io/xorm v1.3.9 h1:TUovzS0ko+IQ1XnNLfs5dqK1cJl1H5uHpWbWqAQ04nU= -xorm.io/xorm v1.3.9/go.mod h1:LsCCffeeYp63ssk0pKumP6l96WZcHix7ChpurcLNuMw= +xorm.io/xorm v1.3.10 h1:yR83hTT4mKIPyA/lvWFTzS35xjLwkiYnwdw0Qupeh0o= +xorm.io/xorm v1.3.10/go.mod h1:Lo7hmsFF0F0GbDE7ubX5ZKa+eCf0eCuiJAUG3oI5cxQ= diff --git a/main.go b/main.go index 756c3e0f9ba..2c25bac4e3d 100644 --- a/main.go +++ b/main.go @@ -21,7 +21,7 @@ import ( _ "code.gitea.io/gitea/modules/markup/markdown" _ "code.gitea.io/gitea/modules/markup/orgmode" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) // these flags will be set by the build flags diff --git a/main_timezones.go b/main_timezones.go new file mode 100644 index 00000000000..e1233007c62 --- /dev/null +++ b/main_timezones.go @@ -0,0 +1,16 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +//go:build windows + +package main + +// Golang has the ability to load OS's timezone data from most UNIX systems (https://github.com/golang/go/blob/master/src/time/zoneinfo_unix.go) +// Even if the timezone data is missing, users could install the related packages to get it. +// But on Windows, although `zoneinfo_windows.go` tries to load the timezone data from Windows registry, +// some users still suffer from the issue that the timezone data is missing: https://github.com/go-gitea/gitea/issues/33235 +// So we import the tzdata package to make sure the timezone data is included in the binary. +// +// For non-Windows package builders, they could still use the "TAGS=timetzdata" to include the tzdata package in the binary. +// If we decided to add the tzdata for other platforms, modify the "go:build" directive above. +import _ "time/tzdata" diff --git a/models/actions/artifact.go b/models/actions/artifact.go index 0bc66ba24e8..757bd13acd7 100644 --- a/models/actions/artifact.go +++ b/models/actions/artifact.go @@ -30,6 +30,25 @@ const ( ArtifactStatusDeleted // 6, ArtifactStatusDeleted is the status of an artifact that is deleted ) +func (status ArtifactStatus) ToString() string { + switch status { + case ArtifactStatusUploadPending: + return "upload is not yet completed" + case ArtifactStatusUploadConfirmed: + return "upload is completed" + case ArtifactStatusUploadError: + return "upload failed" + case ArtifactStatusExpired: + return "expired" + case ArtifactStatusPendingDeletion: + return "pending deletion" + case ArtifactStatusDeleted: + return "deleted" + default: + return "unknown" + } +} + func init() { db.RegisterModel(new(ActionArtifact)) } @@ -48,7 +67,7 @@ type ActionArtifact struct { ContentEncoding string // The content encoding of the artifact ArtifactPath string `xorm:"index unique(runid_name_path)"` // The path to the artifact when runner uploads it ArtifactName string `xorm:"index unique(runid_name_path)"` // The name of the artifact when runner uploads it - Status int64 `xorm:"index"` // The status of the artifact, uploading, expired or need-delete + Status ArtifactStatus `xorm:"index"` // The status of the artifact, uploading, expired or need-delete CreatedUnix timeutil.TimeStamp `xorm:"created"` UpdatedUnix timeutil.TimeStamp `xorm:"updated index"` ExpiredUnix timeutil.TimeStamp `xorm:"index"` // The time when the artifact will be expired @@ -68,7 +87,7 @@ func CreateArtifact(ctx context.Context, t *ActionTask, artifactName, artifactPa RepoID: t.RepoID, OwnerID: t.OwnerID, CommitSHA: t.CommitSHA, - Status: int64(ArtifactStatusUploadPending), + Status: ArtifactStatusUploadPending, ExpiredUnix: timeutil.TimeStamp(time.Now().Unix() + timeutil.Day*expiredDays), } if _, err := db.GetEngine(ctx).Insert(artifact); err != nil { @@ -108,12 +127,19 @@ func UpdateArtifactByID(ctx context.Context, id int64, art *ActionArtifact) erro type FindArtifactsOptions struct { db.ListOptions - RepoID int64 - RunID int64 - ArtifactName string - Status int + RepoID int64 + RunID int64 + ArtifactName string + Status int + FinalizedArtifactsV4 bool } +func (opts FindArtifactsOptions) ToOrders() string { + return "id" +} + +var _ db.FindOptionsOrder = (*FindArtifactsOptions)(nil) + func (opts FindArtifactsOptions) ToConds() builder.Cond { cond := builder.NewCond() if opts.RepoID > 0 { @@ -128,11 +154,15 @@ func (opts FindArtifactsOptions) ToConds() builder.Cond { if opts.Status > 0 { cond = cond.And(builder.Eq{"status": opts.Status}) } + if opts.FinalizedArtifactsV4 { + cond = cond.And(builder.Eq{"status": ArtifactStatusUploadConfirmed}.Or(builder.Eq{"status": ArtifactStatusExpired})) + cond = cond.And(builder.Eq{"content_encoding": "application/zip"}) + } return cond } -// ActionArtifactMeta is the meta data of an artifact +// ActionArtifactMeta is the meta-data of an artifact type ActionArtifactMeta struct { ArtifactName string FileSize int64 @@ -166,18 +196,18 @@ func ListPendingDeleteArtifacts(ctx context.Context, limit int) ([]*ActionArtifa // SetArtifactExpired sets an artifact to expired func SetArtifactExpired(ctx context.Context, artifactID int64) error { - _, err := db.GetEngine(ctx).Where("id=? AND status = ?", artifactID, ArtifactStatusUploadConfirmed).Cols("status").Update(&ActionArtifact{Status: int64(ArtifactStatusExpired)}) + _, err := db.GetEngine(ctx).Where("id=? AND status = ?", artifactID, ArtifactStatusUploadConfirmed).Cols("status").Update(&ActionArtifact{Status: ArtifactStatusExpired}) return err } // SetArtifactNeedDelete sets an artifact to need-delete, cron job will delete it func SetArtifactNeedDelete(ctx context.Context, runID int64, name string) error { - _, err := db.GetEngine(ctx).Where("run_id=? AND artifact_name=? AND status = ?", runID, name, ArtifactStatusUploadConfirmed).Cols("status").Update(&ActionArtifact{Status: int64(ArtifactStatusPendingDeletion)}) + _, err := db.GetEngine(ctx).Where("run_id=? AND artifact_name=? AND status = ?", runID, name, ArtifactStatusUploadConfirmed).Cols("status").Update(&ActionArtifact{Status: ArtifactStatusPendingDeletion}) return err } // SetArtifactDeleted sets an artifact to deleted func SetArtifactDeleted(ctx context.Context, artifactID int64) error { - _, err := db.GetEngine(ctx).ID(artifactID).Cols("status").Update(&ActionArtifact{Status: int64(ArtifactStatusDeleted)}) + _, err := db.GetEngine(ctx).ID(artifactID).Cols("status").Update(&ActionArtifact{Status: ArtifactStatusDeleted}) return err } diff --git a/models/actions/run.go b/models/actions/run.go index 732fb48bb9a..f5ccba06c22 100644 --- a/models/actions/run.go +++ b/models/actions/run.go @@ -5,6 +5,7 @@ package actions import ( "context" + "errors" "fmt" "slices" "strings" @@ -15,6 +16,7 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" @@ -37,6 +39,7 @@ type ActionRun struct { TriggerUser *user_model.User `xorm:"-"` ScheduleID int64 Ref string `xorm:"index"` // the commit/tag/… that caused the run + IsRefDeleted bool `xorm:"-"` CommitSHA string IsForkPullRequest bool // If this is triggered by a PR from a forked repository or an untrusted user, we need to check if it is approved and limit permissions when running the workflow. NeedApproval bool // may need approval if it's a fork pull request @@ -87,7 +90,7 @@ func (run *ActionRun) RefLink() string { if refName.IsPull() { return run.Repo.Link() + "/pulls/" + refName.ShortName() } - return git.RefURL(run.Repo.Link(), run.Ref) + return run.Repo.Link() + "/src/" + refName.RefWebLinkPath() } // PrettyRef return #id for pull ref or ShortName for others @@ -153,7 +156,7 @@ func (run *ActionRun) GetPushEventPayload() (*api.PushPayload, error) { } func (run *ActionRun) GetPullRequestEventPayload() (*api.PullRequestPayload, error) { - if run.Event == webhook_module.HookEventPullRequest || run.Event == webhook_module.HookEventPullRequestSync { + if run.Event.IsPullRequest() { var payload api.PullRequestPayload if err := json.Unmarshal([]byte(run.EventPayload), &payload); err != nil { return nil, err @@ -163,12 +166,24 @@ func (run *ActionRun) GetPullRequestEventPayload() (*api.PullRequestPayload, err return nil, fmt.Errorf("event %s is not a pull request event", run.Event) } +func (run *ActionRun) GetWorkflowRunEventPayload() (*api.WorkflowRunPayload, error) { + if run.Event == webhook_module.HookEventWorkflowRun { + var payload api.WorkflowRunPayload + if err := json.Unmarshal([]byte(run.EventPayload), &payload); err != nil { + return nil, err + } + return &payload, nil + } + return nil, fmt.Errorf("event %s is not a workflow run event", run.Event) +} + func (run *ActionRun) IsSchedule() bool { return run.ScheduleID > 0 } func updateRepoRunsNumbers(ctx context.Context, repo *repo_model.Repository) error { _, err := db.GetEngine(ctx).ID(repo.ID). + NoAutoTime(). SetExpr("num_action_runs", builder.Select("count(*)").From("action_run"). Where(builder.Eq{"repo_id": repo.ID}), @@ -193,7 +208,7 @@ func updateRepoRunsNumbers(ctx context.Context, repo *repo_model.Repository) err // CancelPreviousJobs cancels all previous jobs of the same repository, reference, workflow, and event. // It's useful when a new run is triggered, and all previous runs needn't be continued anymore. -func CancelPreviousJobs(ctx context.Context, repoID int64, ref, workflowID string, event webhook_module.HookEventType) error { +func CancelPreviousJobs(ctx context.Context, repoID int64, ref, workflowID string, event webhook_module.HookEventType) ([]*ActionRunJob, error) { // Find all runs in the specified repository, reference, and workflow with non-final status runs, total, err := db.FindAndCount[ActionRun](ctx, FindRunOptions{ RepoID: repoID, @@ -203,14 +218,16 @@ func CancelPreviousJobs(ctx context.Context, repoID int64, ref, workflowID strin Status: []Status{StatusRunning, StatusWaiting, StatusBlocked}, }) if err != nil { - return err + return nil, err } // If there are no runs found, there's no need to proceed with cancellation, so return nil. if total == 0 { - return nil + return nil, nil } + cancelledJobs := make([]*ActionRunJob, 0, total) + // Iterate over each found run and cancel its associated jobs. for _, run := range runs { // Find all jobs associated with the current run. @@ -218,7 +235,7 @@ func CancelPreviousJobs(ctx context.Context, repoID int64, ref, workflowID strin RunID: run.ID, }) if err != nil { - return err + return cancelledJobs, err } // Iterate over each job and attempt to cancel it. @@ -237,112 +254,109 @@ func CancelPreviousJobs(ctx context.Context, repoID int64, ref, workflowID strin // Update the job's status and stopped time in the database. n, err := UpdateRunJob(ctx, job, builder.Eq{"task_id": 0}, "status", "stopped") if err != nil { - return err + return cancelledJobs, err } // If the update affected 0 rows, it means the job has changed in the meantime, so we need to try again. if n == 0 { - return fmt.Errorf("job has changed, try again") + return cancelledJobs, errors.New("job has changed, try again") } + cancelledJobs = append(cancelledJobs, job) // Continue with the next job. continue } // If the job has an associated task, try to stop the task, effectively cancelling the job. if err := StopTask(ctx, job.TaskID, StatusCancelled); err != nil { - return err + return cancelledJobs, err } + cancelledJobs = append(cancelledJobs, job) } } // Return nil to indicate successful cancellation of all running and waiting jobs. - return nil + return cancelledJobs, nil } // InsertRun inserts a run // The title will be cut off at 255 characters if it's longer than 255 characters. func InsertRun(ctx context.Context, run *ActionRun, jobs []*jobparser.SingleWorkflow) error { - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return err - } - defer committer.Close() - - index, err := db.GetNextResourceIndex(ctx, "action_run_index", run.RepoID) - if err != nil { - return err - } - run.Index = index - run.Title, _ = util.SplitStringAtByteN(run.Title, 255) - - if err := db.Insert(ctx, run); err != nil { - return err - } - - if run.Repo == nil { - repo, err := repo_model.GetRepositoryByID(ctx, run.RepoID) + return db.WithTx(ctx, func(ctx context.Context) error { + index, err := db.GetNextResourceIndex(ctx, "action_run_index", run.RepoID) if err != nil { return err } - run.Repo = repo - } + run.Index = index + run.Title = util.EllipsisDisplayString(run.Title, 255) - if err := updateRepoRunsNumbers(ctx, run.Repo); err != nil { - return err - } - - runJobs := make([]*ActionRunJob, 0, len(jobs)) - var hasWaiting bool - for _, v := range jobs { - id, job := v.Job() - needs := job.Needs() - if err := v.SetJob(id, job.EraseNeeds()); err != nil { + if err := db.Insert(ctx, run); err != nil { return err } - payload, _ := v.Marshal() - status := StatusWaiting - if len(needs) > 0 || run.NeedApproval { - status = StatusBlocked - } else { - hasWaiting = true - } - job.Name, _ = util.SplitStringAtByteN(job.Name, 255) - runJobs = append(runJobs, &ActionRunJob{ - RunID: run.ID, - RepoID: run.RepoID, - OwnerID: run.OwnerID, - CommitSHA: run.CommitSHA, - IsForkPullRequest: run.IsForkPullRequest, - Name: job.Name, - WorkflowPayload: payload, - JobID: id, - Needs: needs, - RunsOn: job.RunsOn(), - Status: status, - }) - } - if err := db.Insert(ctx, runJobs); err != nil { - return err - } - // if there is a job in the waiting status, increase tasks version. - if hasWaiting { - if err := IncreaseTaskVersion(ctx, run.OwnerID, run.RepoID); err != nil { + if run.Repo == nil { + repo, err := repo_model.GetRepositoryByID(ctx, run.RepoID) + if err != nil { + return err + } + run.Repo = repo + } + + if err := updateRepoRunsNumbers(ctx, run.Repo); err != nil { return err } - } - return committer.Commit() + runJobs := make([]*ActionRunJob, 0, len(jobs)) + var hasWaiting bool + for _, v := range jobs { + id, job := v.Job() + needs := job.Needs() + if err := v.SetJob(id, job.EraseNeeds()); err != nil { + return err + } + payload, _ := v.Marshal() + status := StatusWaiting + if len(needs) > 0 || run.NeedApproval { + status = StatusBlocked + } else { + hasWaiting = true + } + job.Name = util.EllipsisDisplayString(job.Name, 255) + runJobs = append(runJobs, &ActionRunJob{ + RunID: run.ID, + RepoID: run.RepoID, + OwnerID: run.OwnerID, + CommitSHA: run.CommitSHA, + IsForkPullRequest: run.IsForkPullRequest, + Name: job.Name, + WorkflowPayload: payload, + JobID: id, + Needs: needs, + RunsOn: job.RunsOn(), + Status: status, + }) + } + if err := db.Insert(ctx, runJobs); err != nil { + return err + } + + // if there is a job in the waiting status, increase tasks version. + if hasWaiting { + if err := IncreaseTaskVersion(ctx, run.OwnerID, run.RepoID); err != nil { + return err + } + } + return nil + }) } -func GetRunByID(ctx context.Context, id int64) (*ActionRun, error) { +func GetRunByRepoAndID(ctx context.Context, repoID, runID int64) (*ActionRun, error) { var run ActionRun - has, err := db.GetEngine(ctx).Where("id=?", id).Get(&run) + has, err := db.GetEngine(ctx).Where("id=? AND repo_id=?", runID, repoID).Get(&run) if err != nil { return nil, err } else if !has { - return nil, fmt.Errorf("run with id %d: %w", id, util.ErrNotExist) + return nil, fmt.Errorf("run with id %d: %w", runID, util.ErrNotExist) } return &run, nil @@ -401,29 +415,22 @@ func UpdateRun(ctx context.Context, run *ActionRun, cols ...string) error { if len(cols) > 0 { sess.Cols(cols...) } - run.Title, _ = util.SplitStringAtByteN(run.Title, 255) + run.Title = util.EllipsisDisplayString(run.Title, 255) affected, err := sess.Update(run) if err != nil { return err } if affected == 0 { - return fmt.Errorf("run has changed") + return errors.New("run has changed") // It's impossible that the run is not found, since Gitea never deletes runs. } if run.Status != 0 || slices.Contains(cols, "status") { if run.RepoID == 0 { - run, err = GetRunByID(ctx, run.ID) - if err != nil { - return err - } + setting.PanicInDevOrTesting("RepoID should not be 0") } - if run.Repo == nil { - repo, err := repo_model.GetRepositoryByID(ctx, run.RepoID) - if err != nil { - return err - } - run.Repo = repo + if err = run.LoadRepo(ctx); err != nil { + return err } if err := updateRepoRunsNumbers(ctx, run.Repo); err != nil { return err diff --git a/models/actions/run_job.go b/models/actions/run_job.go index 4b8664077dc..e7fa21270c1 100644 --- a/models/actions/run_job.go +++ b/models/actions/run_job.go @@ -10,6 +10,7 @@ import ( "time" "code.gitea.io/gitea/models/db" + repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" @@ -19,11 +20,12 @@ import ( // ActionRunJob represents a job of a run type ActionRunJob struct { ID int64 - RunID int64 `xorm:"index"` - Run *ActionRun `xorm:"-"` - RepoID int64 `xorm:"index"` - OwnerID int64 `xorm:"index"` - CommitSHA string `xorm:"index"` + RunID int64 `xorm:"index"` + Run *ActionRun `xorm:"-"` + RepoID int64 `xorm:"index"` + Repo *repo_model.Repository `xorm:"-"` + OwnerID int64 `xorm:"index"` + CommitSHA string `xorm:"index"` IsForkPullRequest bool Name string `xorm:"VARCHAR(255)"` Attempt int64 @@ -49,7 +51,7 @@ func (job *ActionRunJob) Duration() time.Duration { func (job *ActionRunJob) LoadRun(ctx context.Context) error { if job.Run == nil { - run, err := GetRunByID(ctx, job.RunID) + run, err := GetRunByRepoAndID(ctx, job.RepoID, job.RunID) if err != nil { return err } @@ -58,6 +60,17 @@ func (job *ActionRunJob) LoadRun(ctx context.Context) error { return nil } +func (job *ActionRunJob) LoadRepo(ctx context.Context) error { + if job.Repo == nil { + repo, err := repo_model.GetRepositoryByID(ctx, job.RepoID) + if err != nil { + return err + } + job.Repo = repo + } + return nil +} + // LoadAttributes load Run if not loaded func (job *ActionRunJob) LoadAttributes(ctx context.Context) error { if job == nil { @@ -83,7 +96,7 @@ func GetRunJobByID(ctx context.Context, id int64) (*ActionRunJob, error) { return &job, nil } -func GetRunJobsByRunID(ctx context.Context, runID int64) ([]*ActionRunJob, error) { +func GetRunJobsByRunID(ctx context.Context, runID int64) (ActionJobList, error) { var jobs []*ActionRunJob if err := db.GetEngine(ctx).Where("run_id=?", runID).OrderBy("id").Find(&jobs); err != nil { return nil, err @@ -129,7 +142,7 @@ func UpdateRunJob(ctx context.Context, job *ActionRunJob, cond builder.Cond, col { // Other goroutines may aggregate the status of the run and update it too. // So we need load the run and its jobs before updating the run. - run, err := GetRunByID(ctx, job.RunID) + run, err := GetRunByRepoAndID(ctx, job.RepoID, job.RunID) if err != nil { return 0, err } @@ -137,7 +150,7 @@ func UpdateRunJob(ctx context.Context, job *ActionRunJob, cond builder.Cond, col if err != nil { return 0, err } - run.Status = aggregateJobStatus(jobs) + run.Status = AggregateJobStatus(jobs) if run.Started.IsZero() && run.Status.IsRunning() { run.Started = timeutil.TimeStampNow() } @@ -152,29 +165,35 @@ func UpdateRunJob(ctx context.Context, job *ActionRunJob, cond builder.Cond, col return affected, nil } -func aggregateJobStatus(jobs []*ActionRunJob) Status { - allDone := true - allWaiting := true - hasFailure := false +func AggregateJobStatus(jobs []*ActionRunJob) Status { + allSuccessOrSkipped := len(jobs) != 0 + allSkipped := len(jobs) != 0 + var hasFailure, hasCancelled, hasWaiting, hasRunning, hasBlocked bool for _, job := range jobs { - if !job.Status.IsDone() { - allDone = false - } - if job.Status != StatusWaiting && !job.Status.IsDone() { - allWaiting = false - } - if job.Status == StatusFailure || job.Status == StatusCancelled { - hasFailure = true - } + allSuccessOrSkipped = allSuccessOrSkipped && (job.Status == StatusSuccess || job.Status == StatusSkipped) + allSkipped = allSkipped && job.Status == StatusSkipped + hasFailure = hasFailure || job.Status == StatusFailure + hasCancelled = hasCancelled || job.Status == StatusCancelled + hasWaiting = hasWaiting || job.Status == StatusWaiting + hasRunning = hasRunning || job.Status == StatusRunning + hasBlocked = hasBlocked || job.Status == StatusBlocked } - if allDone { - if hasFailure { - return StatusFailure - } + switch { + case allSkipped: + return StatusSkipped + case allSuccessOrSkipped: return StatusSuccess - } - if allWaiting { + case hasCancelled: + return StatusCancelled + case hasRunning: + return StatusRunning + case hasWaiting: return StatusWaiting + case hasFailure: + return StatusFailure + case hasBlocked: + return StatusBlocked + default: + return StatusUnknown // it shouldn't happen } - return StatusRunning } diff --git a/models/actions/run_job_list.go b/models/actions/run_job_list.go index 6c5d3b3252e..5f7bb62878a 100644 --- a/models/actions/run_job_list.go +++ b/models/actions/run_job_list.go @@ -7,6 +7,7 @@ import ( "context" "code.gitea.io/gitea/models/db" + repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/timeutil" @@ -21,7 +22,33 @@ func (jobs ActionJobList) GetRunIDs() []int64 { }) } +func (jobs ActionJobList) LoadRepos(ctx context.Context) error { + repoIDs := container.FilterSlice(jobs, func(j *ActionRunJob) (int64, bool) { + return j.RepoID, j.RepoID != 0 && j.Repo == nil + }) + if len(repoIDs) == 0 { + return nil + } + + repos := make(map[int64]*repo_model.Repository, len(repoIDs)) + if err := db.GetEngine(ctx).In("id", repoIDs).Find(&repos); err != nil { + return err + } + for _, j := range jobs { + if j.RepoID > 0 && j.Repo == nil { + j.Repo = repos[j.RepoID] + } + } + return nil +} + func (jobs ActionJobList) LoadRuns(ctx context.Context, withRepo bool) error { + if withRepo { + if err := jobs.LoadRepos(ctx); err != nil { + return err + } + } + runIDs := jobs.GetRunIDs() runs := make(map[int64]*ActionRun, len(runIDs)) if err := db.GetEngine(ctx).In("id", runIDs).Find(&runs); err != nil { @@ -30,15 +57,9 @@ func (jobs ActionJobList) LoadRuns(ctx context.Context, withRepo bool) error { for _, j := range jobs { if j.RunID > 0 && j.Run == nil { j.Run = runs[j.RunID] + j.Run.Repo = j.Repo } } - if withRepo { - var runsList RunList = make([]*ActionRun, 0, len(runs)) - for _, r := range runs { - runsList = append(runsList, r) - } - return runsList.LoadRepos(ctx) - } return nil } @@ -59,22 +80,31 @@ type FindRunJobOptions struct { func (opts FindRunJobOptions) ToConds() builder.Cond { cond := builder.NewCond() if opts.RunID > 0 { - cond = cond.And(builder.Eq{"run_id": opts.RunID}) + cond = cond.And(builder.Eq{"`action_run_job`.run_id": opts.RunID}) } if opts.RepoID > 0 { - cond = cond.And(builder.Eq{"repo_id": opts.RepoID}) - } - if opts.OwnerID > 0 { - cond = cond.And(builder.Eq{"owner_id": opts.OwnerID}) + cond = cond.And(builder.Eq{"`action_run_job`.repo_id": opts.RepoID}) } if opts.CommitSHA != "" { - cond = cond.And(builder.Eq{"commit_sha": opts.CommitSHA}) + cond = cond.And(builder.Eq{"`action_run_job`.commit_sha": opts.CommitSHA}) } if len(opts.Statuses) > 0 { - cond = cond.And(builder.In("status", opts.Statuses)) + cond = cond.And(builder.In("`action_run_job`.status", opts.Statuses)) } if opts.UpdatedBefore > 0 { - cond = cond.And(builder.Lt{"updated": opts.UpdatedBefore}) + cond = cond.And(builder.Lt{"`action_run_job`.updated": opts.UpdatedBefore}) } return cond } + +func (opts FindRunJobOptions) ToJoins() []db.JoinFunc { + if opts.OwnerID > 0 { + return []db.JoinFunc{ + func(sess db.Engine) error { + sess.Join("INNER", "repository", "repository.id = repo_id AND repository.owner_id = ?", opts.OwnerID) + return nil + }, + } + } + return nil +} diff --git a/models/actions/run_job_status_test.go b/models/actions/run_job_status_test.go new file mode 100644 index 00000000000..b9ae9f34bfd --- /dev/null +++ b/models/actions/run_job_status_test.go @@ -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, usually fail fast, but "running" wins to match GitHub's behavior + // another reason that we can't make "failure" wins over "running": it would cause a weird behavior that user cannot cancel a workflow or get current running workflows correctly by filter after a job fail. + {[]Status{StatusFailure}, StatusFailure}, + {[]Status{StatusFailure, StatusSuccess}, StatusFailure}, + {[]Status{StatusFailure, StatusSkipped}, StatusFailure}, + {[]Status{StatusFailure, StatusCancelled}, StatusCancelled}, + {[]Status{StatusFailure, StatusWaiting}, StatusWaiting}, + {[]Status{StatusFailure, StatusRunning}, StatusRunning}, + {[]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) + } +} diff --git a/models/actions/run_list.go b/models/actions/run_list.go index 4046c7d3694..12c55e538e7 100644 --- a/models/actions/run_list.go +++ b/models/actions/run_list.go @@ -10,6 +10,7 @@ import ( repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/container" + "code.gitea.io/gitea/modules/translation" webhook_module "code.gitea.io/gitea/modules/webhook" "xorm.io/builder" @@ -71,39 +72,50 @@ type FindRunOptions struct { TriggerEvent webhook_module.HookEventType Approved bool // not util.OptionalBool, it works only when it's true Status []Status + CommitSHA string } func (opts FindRunOptions) ToConds() builder.Cond { cond := builder.NewCond() if opts.RepoID > 0 { - cond = cond.And(builder.Eq{"repo_id": opts.RepoID}) - } - if opts.OwnerID > 0 { - cond = cond.And(builder.Eq{"owner_id": opts.OwnerID}) + cond = cond.And(builder.Eq{"`action_run`.repo_id": opts.RepoID}) } if opts.WorkflowID != "" { - cond = cond.And(builder.Eq{"workflow_id": opts.WorkflowID}) + cond = cond.And(builder.Eq{"`action_run`.workflow_id": opts.WorkflowID}) } if opts.TriggerUserID > 0 { - cond = cond.And(builder.Eq{"trigger_user_id": opts.TriggerUserID}) + cond = cond.And(builder.Eq{"`action_run`.trigger_user_id": opts.TriggerUserID}) } if opts.Approved { - cond = cond.And(builder.Gt{"approved_by": 0}) + cond = cond.And(builder.Gt{"`action_run`.approved_by": 0}) } if len(opts.Status) > 0 { - cond = cond.And(builder.In("status", opts.Status)) + cond = cond.And(builder.In("`action_run`.status", opts.Status)) } if opts.Ref != "" { - cond = cond.And(builder.Eq{"ref": opts.Ref}) + cond = cond.And(builder.Eq{"`action_run`.ref": opts.Ref}) } if opts.TriggerEvent != "" { - cond = cond.And(builder.Eq{"trigger_event": opts.TriggerEvent}) + cond = cond.And(builder.Eq{"`action_run`.trigger_event": opts.TriggerEvent}) + } + if opts.CommitSHA != "" { + cond = cond.And(builder.Eq{"`action_run`.commit_sha": opts.CommitSHA}) } return cond } +func (opts FindRunOptions) ToJoins() []db.JoinFunc { + if opts.OwnerID > 0 { + return []db.JoinFunc{func(sess db.Engine) error { + sess.Join("INNER", "repository", "repository.id = repo_id AND repository.owner_id = ?", opts.OwnerID) + return nil + }} + } + return nil +} + func (opts FindRunOptions) ToOrders() string { - return "`id` DESC" + return "`action_run`.`id` DESC" } type StatusInfo struct { @@ -112,14 +124,14 @@ type StatusInfo struct { } // GetStatusInfoList returns a slice of StatusInfo -func GetStatusInfoList(ctx context.Context) []StatusInfo { +func GetStatusInfoList(ctx context.Context, lang translation.Locale) []StatusInfo { // same as those in aggregateJobStatus allStatus := []Status{StatusSuccess, StatusFailure, StatusWaiting, StatusRunning} statusInfoList := make([]StatusInfo, 0, 4) for _, s := range allStatus { statusInfoList = append(statusInfoList, StatusInfo{ Status: int(s), - DisplayedStatus: s.String(), + DisplayedStatus: s.LocaleString(lang), }) } return statusInfoList diff --git a/models/actions/runner.go b/models/actions/runner.go index b35a76680c2..81d4249ae0b 100644 --- a/models/actions/runner.go +++ b/models/actions/runner.go @@ -5,6 +5,7 @@ package actions import ( "context" + "errors" "fmt" "strings" "time" @@ -14,6 +15,7 @@ import ( "code.gitea.io/gitea/models/shared/types" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/optional" + "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/translation" "code.gitea.io/gitea/modules/util" @@ -57,6 +59,8 @@ type ActionRunner struct { // Store labels defined in state file (default: .runner file) of `act_runner` AgentLabels []string `xorm:"TEXT"` + // Store if this is a runner that only ever get one single job assigned + Ephemeral bool `xorm:"ephemeral NOT NULL DEFAULT false"` Created timeutil.TimeStamp `xorm:"created"` Updated timeutil.TimeStamp `xorm:"updated"` @@ -84,9 +88,10 @@ func (r *ActionRunner) BelongsToOwnerType() types.OwnerType { return types.OwnerTypeRepository } if r.OwnerID != 0 { - if r.Owner.Type == user_model.UserTypeOrganization { + switch r.Owner.Type { + case user_model.UserTypeOrganization: return types.OwnerTypeOrganization - } else if r.Owner.Type == user_model.UserTypeIndividual { + case user_model.UserTypeIndividual: return types.OwnerTypeIndividual } } @@ -120,8 +125,15 @@ func (r *ActionRunner) IsOnline() bool { return false } -// Editable checks if the runner is editable by the user -func (r *ActionRunner) Editable(ownerID, repoID int64) bool { +// EditableInContext checks if the runner is editable by the "context" owner/repo +// ownerID == 0 and repoID == 0 means "admin" context, any runner including global runners could be edited +// ownerID == 0 and repoID != 0 means "repo" context, any runner belonging to the given repo could be edited +// ownerID != 0 and repoID == 0 means "owner(org/user)" context, any runner belonging to the given user/org could be edited +// ownerID != 0 and repoID != 0 means "owner" OR "repo" context, legacy behavior, but we should forbid using it +func (r *ActionRunner) EditableInContext(ownerID, repoID int64) bool { + if ownerID != 0 && repoID != 0 { + setting.PanicInDevOrTesting("ownerID and repoID should not be both set") + } if ownerID == 0 && repoID == 0 { return true } @@ -165,8 +177,15 @@ func init() { db.RegisterModel(&ActionRunner{}) } +// FindRunnerOptions +// ownerID == 0 and repoID == 0 means any runner including global runners +// repoID != 0 and WithAvailable == false means any runner for the given repo +// repoID != 0 and WithAvailable == true means any runner for the given repo, parent user/org, and global runners +// ownerID != 0 and repoID == 0 and WithAvailable == false means any runner for the given user/org +// ownerID != 0 and repoID == 0 and WithAvailable == true means any runner for the given user/org and global runners type FindRunnerOptions struct { db.ListOptions + IDs []int64 RepoID int64 OwnerID int64 // it will be ignored if RepoID is set Sort string @@ -178,6 +197,14 @@ type FindRunnerOptions struct { func (opts FindRunnerOptions) ToConds() builder.Cond { cond := builder.NewCond() + if len(opts.IDs) > 0 { + if len(opts.IDs) == 1 { + cond = cond.And(builder.Eq{"id": opts.IDs[0]}) + } else { + cond = cond.And(builder.In("id", opts.IDs)) + } + } + if opts.RepoID > 0 { c := builder.NewCond().And(builder.Eq{"repo_id": opts.RepoID}) if opts.WithAvailable { @@ -252,7 +279,7 @@ func GetRunnerByID(ctx context.Context, id int64) (*ActionRunner, error) { // UpdateRunner updates runner's information. func UpdateRunner(ctx context.Context, r *ActionRunner, cols ...string) error { e := db.GetEngine(ctx) - r.Name, _ = util.SplitStringAtByteN(r.Name, 255) + r.Name = util.EllipsisDisplayString(r.Name, 255) var err error if len(cols) == 0 { _, err = e.ID(r.ID).AllCols().Update(r) @@ -272,6 +299,23 @@ func DeleteRunner(ctx context.Context, id int64) error { return err } +// DeleteEphemeralRunner deletes a ephemeral runner by given ID. +func DeleteEphemeralRunner(ctx context.Context, id int64) error { + runner, err := GetRunnerByID(ctx, id) + if err != nil { + if errors.Is(err, util.ErrNotExist) { + return nil + } + return err + } + if !runner.Ephemeral { + return nil + } + + _, err = db.DeleteByID[ActionRunner](ctx, id) + return err +} + // CreateRunner creates new runner. func CreateRunner(ctx context.Context, t *ActionRunner) error { if t.OwnerID != 0 && t.RepoID != 0 { @@ -279,7 +323,7 @@ func CreateRunner(ctx context.Context, t *ActionRunner) error { // Remove OwnerID to avoid confusion; it's not worth returning an error here. t.OwnerID = 0 } - t.Name, _ = util.SplitStringAtByteN(t.Name, 255) + t.Name = util.EllipsisDisplayString(t.Name, 255) return db.Insert(ctx, t) } @@ -328,3 +372,17 @@ func FixRunnersWithoutBelongingRepo(ctx context.Context) (int64, error) { } return res.RowsAffected() } + +func CountWrongRepoLevelRunners(ctx context.Context) (int64, error) { + var result int64 + _, err := db.GetEngine(ctx).SQL("SELECT count(`id`) FROM `action_runner` WHERE `repo_id` > 0 AND `owner_id` > 0").Get(&result) + return result, err +} + +func UpdateWrongRepoLevelRunners(ctx context.Context) (int64, error) { + result, err := db.GetEngine(ctx).Exec("UPDATE `action_runner` SET `owner_id` = 0 WHERE `repo_id` > 0 AND `owner_id` > 0") + if err != nil { + return 0, err + } + return result.RowsAffected() +} diff --git a/models/actions/runner_token.go b/models/actions/runner_token.go index fd6ba7ecadb..bbd2af73b65 100644 --- a/models/actions/runner_token.go +++ b/models/actions/runner_token.go @@ -51,7 +51,7 @@ func GetRunnerToken(ctx context.Context, token string) (*ActionRunnerToken, erro if err != nil { return nil, err } else if !has { - return nil, fmt.Errorf("runner token %q: %w", token, util.ErrNotExist) + return nil, fmt.Errorf(`runner token "%s...": %w`, util.TruncateRunes(token, 3), util.ErrNotExist) } return &runnerToken, nil } @@ -68,19 +68,15 @@ func UpdateRunnerToken(ctx context.Context, r *ActionRunnerToken, cols ...string return err } -// NewRunnerToken creates a new active runner token and invalidate all old tokens +// NewRunnerTokenWithValue creates a new active runner token and invalidate all old tokens // ownerID will be ignored and treated as 0 if repoID is non-zero. -func NewRunnerToken(ctx context.Context, ownerID, repoID int64) (*ActionRunnerToken, error) { +func NewRunnerTokenWithValue(ctx context.Context, ownerID, repoID int64, token string) (*ActionRunnerToken, error) { if ownerID != 0 && repoID != 0 { // It's trying to create a runner token that belongs to a repository, but OwnerID has been set accidentally. // Remove OwnerID to avoid confusion; it's not worth returning an error here. ownerID = 0 } - token, err := util.CryptoRandomString(40) - if err != nil { - return nil, err - } runnerToken := &ActionRunnerToken{ OwnerID: ownerID, RepoID: repoID, @@ -95,11 +91,19 @@ func NewRunnerToken(ctx context.Context, ownerID, repoID int64) (*ActionRunnerTo return err } - _, err = db.GetEngine(ctx).Insert(runnerToken) + _, err := db.GetEngine(ctx).Insert(runnerToken) return err }) } +func NewRunnerToken(ctx context.Context, ownerID, repoID int64) (*ActionRunnerToken, error) { + token, err := util.CryptoRandomString(40) + if err != nil { + return nil, err + } + return NewRunnerTokenWithValue(ctx, ownerID, repoID, token) +} + // GetLatestRunnerToken returns the latest runner token func GetLatestRunnerToken(ctx context.Context, ownerID, repoID int64) (*ActionRunnerToken, error) { if ownerID != 0 && repoID != 0 { diff --git a/models/actions/runner_token_test.go b/models/actions/runner_token_test.go index e85e99abe53..243d6716a00 100644 --- a/models/actions/runner_token_test.go +++ b/models/actions/runner_token_test.go @@ -6,7 +6,6 @@ package actions import ( "testing" - "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/unittest" "github.com/stretchr/testify/assert" @@ -15,26 +14,26 @@ import ( func TestGetLatestRunnerToken(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) token := unittest.AssertExistsAndLoadBean(t, &ActionRunnerToken{ID: 3}) - expectedToken, err := GetLatestRunnerToken(db.DefaultContext, 1, 0) + expectedToken, err := GetLatestRunnerToken(t.Context(), 1, 0) assert.NoError(t, err) - assert.EqualValues(t, token, expectedToken) + assert.Equal(t, expectedToken, token) } func TestNewRunnerToken(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - token, err := NewRunnerToken(db.DefaultContext, 1, 0) + token, err := NewRunnerToken(t.Context(), 1, 0) assert.NoError(t, err) - expectedToken, err := GetLatestRunnerToken(db.DefaultContext, 1, 0) + expectedToken, err := GetLatestRunnerToken(t.Context(), 1, 0) assert.NoError(t, err) - assert.EqualValues(t, token, expectedToken) + assert.Equal(t, expectedToken, token) } func TestUpdateRunnerToken(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) token := unittest.AssertExistsAndLoadBean(t, &ActionRunnerToken{ID: 3}) token.IsActive = true - assert.NoError(t, UpdateRunnerToken(db.DefaultContext, token)) - expectedToken, err := GetLatestRunnerToken(db.DefaultContext, 1, 0) + assert.NoError(t, UpdateRunnerToken(t.Context(), token)) + expectedToken, err := GetLatestRunnerToken(t.Context(), 1, 0) assert.NoError(t, err) - assert.EqualValues(t, token, expectedToken) + assert.Equal(t, expectedToken, token) } diff --git a/models/actions/schedule.go b/models/actions/schedule.go index 961ffd0851c..ffde5092e0f 100644 --- a/models/actions/schedule.go +++ b/models/actions/schedule.go @@ -43,15 +43,12 @@ func init() { // GetSchedulesMapByIDs returns the schedules by given id slice. func GetSchedulesMapByIDs(ctx context.Context, ids []int64) (map[int64]*ActionSchedule, error) { schedules := make(map[int64]*ActionSchedule, len(ids)) + if len(ids) == 0 { + return schedules, nil + } return schedules, db.GetEngine(ctx).In("id", ids).Find(&schedules) } -// GetReposMapByIDs returns the repos by given id slice. -func GetReposMapByIDs(ctx context.Context, ids []int64) (map[int64]*repo_model.Repository, error) { - repos := make(map[int64]*repo_model.Repository, len(ids)) - return repos, db.GetEngine(ctx).In("id", ids).Find(&repos) -} - // CreateScheduleTask creates new schedule task. func CreateScheduleTask(ctx context.Context, rows []*ActionSchedule) error { // Return early if there are no rows to insert @@ -59,82 +56,72 @@ func CreateScheduleTask(ctx context.Context, rows []*ActionSchedule) error { return nil } - // Begin transaction - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return err - } - defer committer.Close() - - // Loop through each schedule row - for _, row := range rows { - row.Title, _ = util.SplitStringAtByteN(row.Title, 255) - // Create new schedule row - if err = db.Insert(ctx, row); err != nil { - return err - } - - // Loop through each schedule spec and create a new spec row - now := time.Now() - - for _, spec := range row.Specs { - specRow := &ActionScheduleSpec{ - RepoID: row.RepoID, - ScheduleID: row.ID, - Spec: spec, - } - // Parse the spec and check for errors - schedule, err := specRow.Parse() - if err != nil { - continue // skip to the next spec if there's an error - } - - specRow.Next = timeutil.TimeStamp(schedule.Next(now).Unix()) - - // Insert the new schedule spec row - if err = db.Insert(ctx, specRow); err != nil { + return db.WithTx(ctx, func(ctx context.Context) error { + // Loop through each schedule row + for _, row := range rows { + row.Title = util.EllipsisDisplayString(row.Title, 255) + // Create new schedule row + if err := db.Insert(ctx, row); err != nil { return err } - } - } - // Commit transaction - return committer.Commit() + // Loop through each schedule spec and create a new spec row + now := time.Now() + + for _, spec := range row.Specs { + specRow := &ActionScheduleSpec{ + RepoID: row.RepoID, + ScheduleID: row.ID, + Spec: spec, + } + // Parse the spec and check for errors + schedule, err := specRow.Parse() + if err != nil { + continue // skip to the next spec if there's an error + } + + specRow.Next = timeutil.TimeStamp(schedule.Next(now).Unix()) + + // Insert the new schedule spec row + if err = db.Insert(ctx, specRow); err != nil { + return err + } + } + } + return nil + }) } func DeleteScheduleTaskByRepo(ctx context.Context, id int64) error { - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return err - } - defer committer.Close() + return db.WithTx(ctx, func(ctx context.Context) error { + if _, err := db.GetEngine(ctx).Delete(&ActionSchedule{RepoID: id}); err != nil { + return err + } - if _, err := db.GetEngine(ctx).Delete(&ActionSchedule{RepoID: id}); err != nil { - return err - } + if _, err := db.GetEngine(ctx).Delete(&ActionScheduleSpec{RepoID: id}); err != nil { + return err + } - if _, err := db.GetEngine(ctx).Delete(&ActionScheduleSpec{RepoID: id}); err != nil { - return err - } - - return committer.Commit() + return nil + }) } -func CleanRepoScheduleTasks(ctx context.Context, repo *repo_model.Repository) error { +func CleanRepoScheduleTasks(ctx context.Context, repo *repo_model.Repository) ([]*ActionRunJob, error) { // If actions disabled when there is schedule task, this will remove the outdated schedule tasks // There is no other place we can do this because the app.ini will be changed manually if err := DeleteScheduleTaskByRepo(ctx, repo.ID); err != nil { - return fmt.Errorf("DeleteCronTaskByRepo: %v", err) + return nil, fmt.Errorf("DeleteCronTaskByRepo: %v", err) } // cancel running cron jobs of this repository and delete old schedules - if err := CancelPreviousJobs( + jobs, err := CancelPreviousJobs( ctx, repo.ID, repo.DefaultBranch, "", webhook_module.HookEventSchedule, - ); err != nil { - return fmt.Errorf("CancelPreviousJobs: %v", err) + ) + if err != nil { + return jobs, fmt.Errorf("CancelPreviousJobs: %v", err) } - return nil + return jobs, nil } diff --git a/models/actions/schedule_spec_list.go b/models/actions/schedule_spec_list.go index f7dac72f8b3..e26b2c11206 100644 --- a/models/actions/schedule_spec_list.go +++ b/models/actions/schedule_spec_list.go @@ -32,7 +32,7 @@ func (specs SpecList) LoadSchedules(ctx context.Context) error { } repoIDs := specs.GetRepoIDs() - repos, err := GetReposMapByIDs(ctx, repoIDs) + repos, err := repo_model.GetRepositoriesMapByIDs(ctx, repoIDs) if err != nil { return err } diff --git a/models/actions/status.go b/models/actions/status.go index eda22341378..2b1d70613c7 100644 --- a/models/actions/status.go +++ b/models/actions/status.go @@ -4,6 +4,8 @@ package actions import ( + "slices" + "code.gitea.io/gitea/modules/translation" runnerv1 "code.gitea.io/actions-proto-go/runner/v1" @@ -88,12 +90,7 @@ func (s Status) IsBlocked() bool { // In returns whether s is one of the given statuses func (s Status) In(statuses ...Status) bool { - for _, v := range statuses { - if s == v { - return true - } - } - return false + return slices.Contains(statuses, s) } func (s Status) AsResult() runnerv1.Result { diff --git a/models/actions/task.go b/models/actions/task.go index af74faf937e..c1306a84182 100644 --- a/models/actions/task.go +++ b/models/actions/task.go @@ -6,6 +6,7 @@ package actions import ( "context" "crypto/subtle" + "errors" "fmt" "time" @@ -277,14 +278,13 @@ func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask return nil, false, err } - var workflowJob *jobparser.Job - if gots, err := jobparser.Parse(job.WorkflowPayload); err != nil { + parsedWorkflows, err := jobparser.Parse(job.WorkflowPayload) + if err != nil { return nil, false, fmt.Errorf("parse workflow of job %d: %w", job.ID, err) - } else if len(gots) != 1 { + } else if len(parsedWorkflows) != 1 { return nil, false, fmt.Errorf("workflow of job %d: not single workflow", job.ID) - } else { //nolint:revive - _, workflowJob = gots[0].Job() } + _, workflowJob := parsedWorkflows[0].Job() if _, err := e.Insert(task); err != nil { return nil, false, err @@ -298,7 +298,7 @@ func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask if len(workflowJob.Steps) > 0 { steps := make([]*ActionTaskStep, len(workflowJob.Steps)) for i, v := range workflowJob.Steps { - name, _ := util.SplitStringAtByteN(v.String(), 255) + name := util.EllipsisDisplayString(v.String(), 255) steps[i] = &ActionTaskStep{ Name: name, TaskID: task.ID, @@ -335,6 +335,11 @@ func UpdateTask(ctx context.Context, task *ActionTask, cols ...string) error { sess.Cols(cols...) } _, err := sess.Update(task) + + // Automatically delete the ephemeral runner if the task is done + if err == nil && task.Status.IsDone() && util.SliceContainsString(cols, "status") { + return DeleteEphemeralRunner(ctx, task.RunnerID) + } return err } @@ -347,78 +352,70 @@ func UpdateTaskByState(ctx context.Context, runnerID int64, state *runnerv1.Task stepStates[v.Id] = v } - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return nil, err - } - defer committer.Close() + return db.WithTx2(ctx, func(ctx context.Context) (*ActionTask, error) { + e := db.GetEngine(ctx) - e := db.GetEngine(ctx) + task := &ActionTask{} + if has, err := e.ID(state.Id).Get(task); err != nil { + return nil, err + } else if !has { + return nil, util.ErrNotExist + } else if runnerID != task.RunnerID { + return nil, errors.New("invalid runner for task") + } - task := &ActionTask{} - if has, err := e.ID(state.Id).Get(task); err != nil { - return nil, err - } else if !has { - return nil, util.ErrNotExist - } else if runnerID != task.RunnerID { - return nil, fmt.Errorf("invalid runner for task") - } + if task.Status.IsDone() { + // the state is final, do nothing + return task, nil + } + + // state.Result is not unspecified means the task is finished + if state.Result != runnerv1.Result_RESULT_UNSPECIFIED { + task.Status = Status(state.Result) + task.Stopped = timeutil.TimeStamp(state.StoppedAt.AsTime().Unix()) + if err := UpdateTask(ctx, task, "status", "stopped"); err != nil { + return nil, err + } + if _, err := UpdateRunJob(ctx, &ActionRunJob{ + ID: task.JobID, + Status: task.Status, + Stopped: task.Stopped, + }, nil); err != nil { + return nil, err + } + } else { + // Force update ActionTask.Updated to avoid the task being judged as a zombie task + task.Updated = timeutil.TimeStampNow() + if err := UpdateTask(ctx, task, "updated"); err != nil { + return nil, err + } + } + + if err := task.LoadAttributes(ctx); err != nil { + return nil, err + } + + for _, step := range task.Steps { + var result runnerv1.Result + if v, ok := stepStates[step.Index]; ok { + result = v.Result + step.LogIndex = v.LogIndex + step.LogLength = v.LogLength + step.Started = convertTimestamp(v.StartedAt) + step.Stopped = convertTimestamp(v.StoppedAt) + } + if result != runnerv1.Result_RESULT_UNSPECIFIED { + step.Status = Status(result) + } else if step.Started != 0 { + step.Status = StatusRunning + } + if _, err := e.ID(step.ID).Update(step); err != nil { + return nil, err + } + } - if task.Status.IsDone() { - // the state is final, do nothing return task, nil - } - - // state.Result is not unspecified means the task is finished - if state.Result != runnerv1.Result_RESULT_UNSPECIFIED { - task.Status = Status(state.Result) - task.Stopped = timeutil.TimeStamp(state.StoppedAt.AsTime().Unix()) - if err := UpdateTask(ctx, task, "status", "stopped"); err != nil { - return nil, err - } - if _, err := UpdateRunJob(ctx, &ActionRunJob{ - ID: task.JobID, - Status: task.Status, - Stopped: task.Stopped, - }, nil); err != nil { - return nil, err - } - } else { - // Force update ActionTask.Updated to avoid the task being judged as a zombie task - task.Updated = timeutil.TimeStampNow() - if err := UpdateTask(ctx, task, "updated"); err != nil { - return nil, err - } - } - - if err := task.LoadAttributes(ctx); err != nil { - return nil, err - } - - for _, step := range task.Steps { - var result runnerv1.Result - if v, ok := stepStates[step.Index]; ok { - result = v.Result - step.LogIndex = v.LogIndex - step.LogLength = v.LogLength - step.Started = convertTimestamp(v.StartedAt) - step.Stopped = convertTimestamp(v.StoppedAt) - } - if result != runnerv1.Result_RESULT_UNSPECIFIED { - step.Status = Status(result) - } else if step.Started != 0 { - step.Status = StatusRunning - } - if _, err := e.ID(step.ID).Update(step); err != nil { - return nil, err - } - } - - if err := committer.Commit(); err != nil { - return nil, err - } - - return task, nil + }) } func StopTask(ctx context.Context, taskID int64, status Status) error { diff --git a/models/actions/task_list.go b/models/actions/task_list.go index df4b43c5ef3..0c803978994 100644 --- a/models/actions/task_list.go +++ b/models/actions/task_list.go @@ -48,6 +48,7 @@ func (tasks TaskList) LoadAttributes(ctx context.Context) error { type FindTaskOptions struct { db.ListOptions RepoID int64 + JobID int64 OwnerID int64 CommitSHA string Status Status @@ -61,6 +62,9 @@ func (opts FindTaskOptions) ToConds() builder.Cond { if opts.RepoID > 0 { cond = cond.And(builder.Eq{"repo_id": opts.RepoID}) } + if opts.JobID > 0 { + cond = cond.And(builder.Eq{"job_id": opts.JobID}) + } if opts.OwnerID > 0 { cond = cond.And(builder.Eq{"owner_id": opts.OwnerID}) } diff --git a/models/actions/tasks_version.go b/models/actions/tasks_version.go index 96c5468c1a4..b686ce24431 100644 --- a/models/actions/tasks_version.go +++ b/models/actions/tasks_version.go @@ -73,33 +73,29 @@ func increaseTasksVersionByScope(ctx context.Context, ownerID, repoID int64) err } func IncreaseTaskVersion(ctx context.Context, ownerID, repoID int64) error { - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return err - } - defer committer.Close() - - // 1. increase global - if err := increaseTasksVersionByScope(ctx, 0, 0); err != nil { - log.Error("IncreaseTasksVersionByScope(Global): %v", err) - return err - } - - // 2. increase owner - if ownerID > 0 { - if err := increaseTasksVersionByScope(ctx, ownerID, 0); err != nil { - log.Error("IncreaseTasksVersionByScope(Owner): %v", err) + return db.WithTx(ctx, func(ctx context.Context) error { + // 1. increase global + if err := increaseTasksVersionByScope(ctx, 0, 0); err != nil { + log.Error("IncreaseTasksVersionByScope(Global): %v", err) return err } - } - // 3. increase repo - if repoID > 0 { - if err := increaseTasksVersionByScope(ctx, 0, repoID); err != nil { - log.Error("IncreaseTasksVersionByScope(Repo): %v", err) - return err + // 2. increase owner + if ownerID > 0 { + if err := increaseTasksVersionByScope(ctx, ownerID, 0); err != nil { + log.Error("IncreaseTasksVersionByScope(Owner): %v", err) + return err + } } - } - return committer.Commit() + // 3. increase repo + if repoID > 0 { + if err := increaseTasksVersionByScope(ctx, 0, repoID); err != nil { + log.Error("IncreaseTasksVersionByScope(Repo): %v", err) + return err + } + } + + return nil + }) } diff --git a/models/actions/utils.go b/models/actions/utils.go index 12657942fc2..f6ba661ae30 100644 --- a/models/actions/utils.go +++ b/models/actions/utils.go @@ -82,3 +82,22 @@ func calculateDuration(started, stopped timeutil.TimeStamp, status Status) time. } return timeSince(s).Truncate(time.Second) } + +// best effort function to convert an action schedule to action run, to be used in GenerateGiteaContext +func (s *ActionSchedule) ToActionRun() *ActionRun { + return &ActionRun{ + Title: s.Title, + RepoID: s.RepoID, + Repo: s.Repo, + OwnerID: s.OwnerID, + WorkflowID: s.WorkflowID, + TriggerUserID: s.TriggerUserID, + TriggerUser: s.TriggerUser, + Ref: s.Ref, + CommitSHA: s.CommitSHA, + Event: s.Event, + EventPayload: s.EventPayload, + Created: s.Created, + Updated: s.Updated, + } +} diff --git a/models/actions/variable.go b/models/actions/variable.go index d0f917d9232..7154843c170 100644 --- a/models/actions/variable.go +++ b/models/actions/variable.go @@ -6,10 +6,12 @@ package actions import ( "context" "strings" + "unicode/utf8" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/modules/util" "xorm.io/builder" ) @@ -32,32 +34,46 @@ type ActionVariable struct { RepoID int64 `xorm:"INDEX UNIQUE(owner_repo_name)"` Name string `xorm:"UNIQUE(owner_repo_name) NOT NULL"` Data string `xorm:"LONGTEXT NOT NULL"` + Description string `xorm:"TEXT"` CreatedUnix timeutil.TimeStamp `xorm:"created NOT NULL"` UpdatedUnix timeutil.TimeStamp `xorm:"updated"` } +const ( + VariableDataMaxLength = 65536 + VariableDescriptionMaxLength = 4096 +) + func init() { db.RegisterModel(new(ActionVariable)) } -func InsertVariable(ctx context.Context, ownerID, repoID int64, name, data string) (*ActionVariable, error) { +func InsertVariable(ctx context.Context, ownerID, repoID int64, name, data, description string) (*ActionVariable, error) { if ownerID != 0 && repoID != 0 { // It's trying to create a variable that belongs to a repository, but OwnerID has been set accidentally. // Remove OwnerID to avoid confusion; it's not worth returning an error here. ownerID = 0 } + if utf8.RuneCountInString(data) > VariableDataMaxLength { + return nil, util.NewInvalidArgumentErrorf("data too long") + } + + description = util.TruncateRunes(description, VariableDescriptionMaxLength) + variable := &ActionVariable{ - OwnerID: ownerID, - RepoID: repoID, - Name: strings.ToUpper(name), - Data: data, + OwnerID: ownerID, + RepoID: repoID, + Name: strings.ToUpper(name), + Data: data, + Description: description, } return variable, db.Insert(ctx, variable) } type FindVariablesOpts struct { db.ListOptions + IDs []int64 RepoID int64 OwnerID int64 // it will be ignored if RepoID is set Name string @@ -65,6 +81,15 @@ type FindVariablesOpts struct { func (opts FindVariablesOpts) ToConds() builder.Cond { cond := builder.NewCond() + + if len(opts.IDs) > 0 { + if len(opts.IDs) == 1 { + cond = cond.And(builder.Eq{"id": opts.IDs[0]}) + } else { + cond = cond.And(builder.In("id", opts.IDs)) + } + } + // Since we now support instance-level variables, // there is no need to check for null values for `owner_id` and `repo_id` cond = cond.And(builder.Eq{"repo_id": opts.RepoID}) @@ -85,12 +110,18 @@ func FindVariables(ctx context.Context, opts FindVariablesOpts) ([]*ActionVariab return db.Find[ActionVariable](ctx, opts) } -func UpdateVariable(ctx context.Context, variable *ActionVariable) (bool, error) { - count, err := db.GetEngine(ctx).ID(variable.ID).Cols("name", "data"). - Update(&ActionVariable{ - Name: variable.Name, - Data: variable.Data, - }) +func UpdateVariableCols(ctx context.Context, variable *ActionVariable, cols ...string) (bool, error) { + if utf8.RuneCountInString(variable.Data) > VariableDataMaxLength { + return false, util.NewInvalidArgumentErrorf("data too long") + } + + variable.Description = util.TruncateRunes(variable.Description, VariableDescriptionMaxLength) + + variable.Name = strings.ToUpper(variable.Name) + count, err := db.GetEngine(ctx). + ID(variable.ID). + Cols(cols...). + Update(variable) return count != 0, err } @@ -137,3 +168,17 @@ func GetVariablesOfRun(ctx context.Context, run *ActionRun) (map[string]string, return variables, nil } + +func CountWrongRepoLevelVariables(ctx context.Context) (int64, error) { + var result int64 + _, err := db.GetEngine(ctx).SQL("SELECT count(`id`) FROM `action_variable` WHERE `repo_id` > 0 AND `owner_id` > 0").Get(&result) + return result, err +} + +func UpdateWrongRepoLevelVariables(ctx context.Context) (int64, error) { + result, err := db.GetEngine(ctx).Exec("UPDATE `action_variable` SET `owner_id` = 0 WHERE `repo_id` > 0 AND `owner_id` > 0") + if err != nil { + return 0, err + } + return result.RowsAffected() +} diff --git a/models/activities/action.go b/models/activities/action.go index 65d95fbe667..8e589eda88d 100644 --- a/models/activities/action.go +++ b/models/activities/action.go @@ -9,6 +9,7 @@ import ( "fmt" "net/url" "path" + "slices" "strconv" "strings" "time" @@ -16,16 +17,14 @@ import ( "code.gitea.io/gitea/models/db" issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/organization" - access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" - "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/modules/util" "xorm.io/builder" "xorm.io/xorm/schemas" @@ -72,9 +71,9 @@ func (at ActionType) String() string { case ActionRenameRepo: return "rename_repo" case ActionStarRepo: - return "star_repo" + return "star_repo" // will not displayed in feeds.tmpl case ActionWatchRepo: - return "watch_repo" + return "watch_repo" // will not displayed in feeds.tmpl case ActionCommitRepo: return "commit_repo" case ActionCreateIssue: @@ -127,12 +126,7 @@ func (at ActionType) String() string { } func (at ActionType) InActions(actions ...string) bool { - for _, action := range actions { - if action == at.String() { - return true - } - } - return false + return slices.Contains(actions, at.String()) } // Action represents user operation type and other information to @@ -174,7 +168,10 @@ func (a *Action) TableIndices() []*schemas.Index { cuIndex := schemas.NewIndex("c_u", schemas.IndexType) cuIndex.AddColumn("user_id", "is_deleted") - indices := []*schemas.Index{actUserIndex, repoIndex, cudIndex, cuIndex} + actUserUserIndex := schemas.NewIndex("au_c_u", schemas.IndexType) + actUserUserIndex.AddColumn("act_user_id", "created_unix", "user_id") + + indices := []*schemas.Index{actUserIndex, repoIndex, cudIndex, cuIndex, actUserUserIndex} return indices } @@ -190,7 +187,7 @@ func (a *Action) LoadActUser(ctx context.Context) { return } var err error - a.ActUser, err = user_model.GetUserByID(ctx, a.ActUserID) + a.ActUser, err = user_model.GetPossibleUserByID(ctx, a.ActUserID) if err == nil { return } else if user_model.IsErrUserNotExist(err) { @@ -200,15 +197,13 @@ func (a *Action) LoadActUser(ctx context.Context) { } } -func (a *Action) LoadRepo(ctx context.Context) { +func (a *Action) LoadRepo(ctx context.Context) error { if a.Repo != nil { - return + return nil } var err error a.Repo, err = repo_model.GetRepositoryByID(ctx, a.RepoID) - if err != nil { - log.Error("repo_model.GetRepositoryByID(%d): %v", a.RepoID, err) - } + return err } // GetActFullName gets the action's user full name. @@ -226,7 +221,7 @@ func (a *Action) GetActUserName(ctx context.Context) string { // ShortActUserName gets the action's user name trimmed to max 20 // chars. func (a *Action) ShortActUserName(ctx context.Context) string { - return base.EllipsisString(a.GetActUserName(ctx), 20) + return util.EllipsisDisplayString(a.GetActUserName(ctx), 20) } // GetActDisplayName gets the action's display name based on DEFAULT_SHOW_FULL_NAME, or falls back to the username if it is blank. @@ -250,7 +245,7 @@ func (a *Action) GetActDisplayNameTitle(ctx context.Context) string { // GetRepoUserName returns the name of the action repository owner. func (a *Action) GetRepoUserName(ctx context.Context) string { - a.LoadRepo(ctx) + _ = a.LoadRepo(ctx) if a.Repo == nil { return "(non-existing-repo)" } @@ -260,12 +255,12 @@ func (a *Action) GetRepoUserName(ctx context.Context) string { // ShortRepoUserName returns the name of the action repository owner // trimmed to max 20 chars. func (a *Action) ShortRepoUserName(ctx context.Context) string { - return base.EllipsisString(a.GetRepoUserName(ctx), 20) + return util.EllipsisDisplayString(a.GetRepoUserName(ctx), 20) } // GetRepoName returns the name of the action repository. func (a *Action) GetRepoName(ctx context.Context) string { - a.LoadRepo(ctx) + _ = a.LoadRepo(ctx) if a.Repo == nil { return "(non-existing-repo)" } @@ -275,7 +270,7 @@ func (a *Action) GetRepoName(ctx context.Context) string { // ShortRepoName returns the name of the action repository // trimmed to max 33 chars. func (a *Action) ShortRepoName(ctx context.Context) string { - return base.EllipsisString(a.GetRepoName(ctx), 33) + return util.EllipsisDisplayString(a.GetRepoName(ctx), 33) } // GetRepoPath returns the virtual path to the action repository. @@ -325,7 +320,7 @@ func (a *Action) GetCommentHTMLURL(ctx context.Context) string { return "#" } - return a.Issue.HTMLURL() + return a.Issue.HTMLURL(ctx) } // GetCommentLink returns link to action comment. @@ -355,7 +350,7 @@ func (a *Action) GetBranch() string { // GetRefLink returns the action's ref link. func (a *Action) GetRefLink(ctx context.Context) string { - return git.RefURL(a.GetRepoLink(ctx), a.RefName) + return a.GetRepoLink(ctx) + "/src/" + git.RefName(a.RefName).RefWebLinkPath() } // GetTag returns the action's repository tag. @@ -446,6 +441,7 @@ type GetFeedsOptions struct { OnlyPerformedBy bool // only actions performed by requested user IncludeDeleted bool // include deleted actions Date string // the day we want activity for: YYYY-MM-DD + DontCount bool // do counting in GetFeeds } // ActivityReadable return whether doer can read activities of user @@ -454,6 +450,24 @@ func ActivityReadable(user, doer *user_model.User) bool { doer != nil && (doer.IsAdmin || user.ID == doer.ID) } +func FeedDateCond(opts GetFeedsOptions) builder.Cond { + cond := builder.NewCond() + if opts.Date == "" { + return cond + } + + dateLow, err := time.ParseInLocation("2006-01-02", opts.Date, setting.DefaultUILocation) + if err != nil { + log.Warn("Unable to parse %s, filter not applied: %v", opts.Date, err) + } else { + dateHigh := dateLow.Add(86399000000000) // 23h59m59s + + cond = cond.And(builder.Gte{"`action`.created_unix": dateLow.Unix()}) + cond = cond.And(builder.Lte{"`action`.created_unix": dateHigh.Unix()}) + } + return cond +} + func ActivityQueryCondition(ctx context.Context, opts GetFeedsOptions) (builder.Cond, error) { cond := builder.NewCond() @@ -511,8 +525,8 @@ func ActivityQueryCondition(ctx context.Context, opts GetFeedsOptions) (builder. } if opts.RequestedTeam != nil { - env := organization.OrgFromUser(opts.RequestedUser).AccessibleTeamReposEnv(ctx, opts.RequestedTeam) - teamRepoIDs, err := env.RepoIDs(1, opts.RequestedUser.NumRepos) + env := repo_model.AccessibleTeamReposEnv(organization.OrgFromUser(opts.RequestedUser), opts.RequestedTeam) + teamRepoIDs, err := env.RepoIDs(ctx) if err != nil { return nil, fmt.Errorf("GetTeamRepositories: %w", err) } @@ -534,17 +548,7 @@ func ActivityQueryCondition(ctx context.Context, opts GetFeedsOptions) (builder. cond = cond.And(builder.Eq{"is_deleted": false}) } - if opts.Date != "" { - dateLow, err := time.ParseInLocation("2006-01-02", opts.Date, setting.DefaultUILocation) - if err != nil { - log.Warn("Unable to parse %s, filter not applied: %v", opts.Date, err) - } else { - dateHigh := dateLow.Add(86399000000000) // 23h59m59s - - cond = cond.And(builder.Gte{"`action`.created_unix": dateLow.Unix()}) - cond = cond.And(builder.Lte{"`action`.created_unix": dateHigh.Unix()}) - } - } + cond = cond.And(FeedDateCond(opts)) return cond, nil } @@ -559,130 +563,6 @@ func DeleteOldActions(ctx context.Context, olderThan time.Duration) (err error) return err } -// NotifyWatchers creates batch of actions for every watcher. -// It could insert duplicate actions for a repository action, like this: -// * Original action: UserID=1 (the real actor), ActUserID=1 -// * Organization action: UserID=100 (the repo's org), ActUserID=1 -// * Watcher action: UserID=20 (a user who is watching a repo), ActUserID=1 -func NotifyWatchers(ctx context.Context, actions ...*Action) error { - var watchers []*repo_model.Watch - var repo *repo_model.Repository - var err error - var permCode []bool - var permIssue []bool - var permPR []bool - - e := db.GetEngine(ctx) - - for _, act := range actions { - repoChanged := repo == nil || repo.ID != act.RepoID - - if repoChanged { - // Add feeds for user self and all watchers. - watchers, err = repo_model.GetWatchers(ctx, act.RepoID) - if err != nil { - return fmt.Errorf("get watchers: %w", err) - } - } - - // Add feed for actioner. - act.UserID = act.ActUserID - if _, err = e.Insert(act); err != nil { - return fmt.Errorf("insert new actioner: %w", err) - } - - if repoChanged { - act.LoadRepo(ctx) - repo = act.Repo - - // check repo owner exist. - if err := act.Repo.LoadOwner(ctx); err != nil { - return fmt.Errorf("can't get repo owner: %w", err) - } - } else if act.Repo == nil { - act.Repo = repo - } - - // Add feed for organization - if act.Repo.Owner.IsOrganization() && act.ActUserID != act.Repo.Owner.ID { - act.ID = 0 - act.UserID = act.Repo.Owner.ID - if err = db.Insert(ctx, act); err != nil { - return fmt.Errorf("insert new actioner: %w", err) - } - } - - if repoChanged { - permCode = make([]bool, len(watchers)) - permIssue = make([]bool, len(watchers)) - permPR = make([]bool, len(watchers)) - for i, watcher := range watchers { - user, err := user_model.GetUserByID(ctx, watcher.UserID) - if err != nil { - permCode[i] = false - permIssue[i] = false - permPR[i] = false - continue - } - perm, err := access_model.GetUserRepoPermission(ctx, repo, user) - if err != nil { - permCode[i] = false - permIssue[i] = false - permPR[i] = false - continue - } - permCode[i] = perm.CanRead(unit.TypeCode) - permIssue[i] = perm.CanRead(unit.TypeIssues) - permPR[i] = perm.CanRead(unit.TypePullRequests) - } - } - - for i, watcher := range watchers { - if act.ActUserID == watcher.UserID { - continue - } - act.ID = 0 - act.UserID = watcher.UserID - act.Repo.Units = nil - - switch act.OpType { - case ActionCommitRepo, ActionPushTag, ActionDeleteTag, ActionPublishRelease, ActionDeleteBranch: - if !permCode[i] { - continue - } - case ActionCreateIssue, ActionCommentIssue, ActionCloseIssue, ActionReopenIssue: - if !permIssue[i] { - continue - } - case ActionCreatePullRequest, ActionCommentPull, ActionMergePullRequest, ActionClosePullRequest, ActionReopenPullRequest, ActionAutoMergePullRequest: - if !permPR[i] { - continue - } - } - - if err = db.Insert(ctx, act); err != nil { - return fmt.Errorf("insert new action: %w", err) - } - } - } - return nil -} - -// NotifyWatchersActions creates batch of actions for every watcher. -func NotifyWatchersActions(ctx context.Context, acts []*Action) error { - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return err - } - defer committer.Close() - for _, act := range acts { - if err := NotifyWatchers(ctx, act); err != nil { - return err - } - } - return committer.Commit() -} - // DeleteIssueActions delete all actions related with issueID func DeleteIssueActions(ctx context.Context, repoID, issueID, issueIndex int64) error { // delete actions assigned to this issue diff --git a/models/activities/action_list.go b/models/activities/action_list.go index 5f9acb8f2aa..b52cf7ee497 100644 --- a/models/activities/action_list.go +++ b/models/activities/action_list.go @@ -5,6 +5,7 @@ package activities import ( "context" + "errors" "fmt" "strconv" @@ -205,12 +206,34 @@ func (actions ActionList) LoadIssues(ctx context.Context) error { // GetFeeds returns actions according to the provided options func GetFeeds(ctx context.Context, opts GetFeedsOptions) (ActionList, int64, error) { if opts.RequestedUser == nil && opts.RequestedTeam == nil && opts.RequestedRepo == nil { - return nil, 0, fmt.Errorf("need at least one of these filters: RequestedUser, RequestedTeam, RequestedRepo") + return nil, 0, errors.New("need at least one of these filters: RequestedUser, RequestedTeam, RequestedRepo") } - cond, err := ActivityQueryCondition(ctx, opts) - if err != nil { - return nil, 0, err + var err error + var cond builder.Cond + // if the actor is the requested user or is an administrator, we can skip the ActivityQueryCondition + if opts.Actor != nil && opts.RequestedUser != nil && (opts.Actor.IsAdmin || opts.Actor.ID == opts.RequestedUser.ID) { + cond = builder.Eq{ + "user_id": opts.RequestedUser.ID, + }.And( + FeedDateCond(opts), + ) + + if !opts.IncludeDeleted { + cond = cond.And(builder.Eq{"is_deleted": false}) + } + + if !opts.IncludePrivate { + cond = cond.And(builder.Eq{"is_private": false}) + } + if opts.OnlyPerformedBy { + cond = cond.And(builder.Eq{"act_user_id": opts.RequestedUser.ID}) + } + } else { + cond, err = ActivityQueryCondition(ctx, opts) + if err != nil { + return nil, 0, err + } } actions := make([]*Action, 0, opts.PageSize) @@ -221,7 +244,11 @@ func GetFeeds(ctx context.Context, opts GetFeedsOptions) (ActionList, int64, err sess := db.GetEngine(ctx).Where(cond) sess = db.SetSessionPagination(sess, &opts) - count, err = sess.Desc("`action`.created_unix").FindAndCount(&actions) + if opts.DontCount { + err = sess.Desc("`action`.created_unix").Find(&actions) + } else { + count, err = sess.Desc("`action`.created_unix").FindAndCount(&actions) + } if err != nil { return nil, 0, fmt.Errorf("FindAndCount: %w", err) } @@ -235,11 +262,13 @@ func GetFeeds(ctx context.Context, opts GetFeedsOptions) (ActionList, int64, err return nil, 0, fmt.Errorf("Find(actionsIDs): %w", err) } - count, err = db.GetEngine(ctx).Where(cond). - Table("action"). - Cols("`action`.id").Count() - if err != nil { - return nil, 0, fmt.Errorf("Count: %w", err) + if !opts.DontCount { + count, err = db.GetEngine(ctx).Where(cond). + Table("action"). + Cols("`action`.id").Count() + if err != nil { + return nil, 0, fmt.Errorf("Count: %w", err) + } } if err := db.GetEngine(ctx).In("`action`.id", actionIDs).Desc("`action`.created_unix").Find(&actions); err != nil { @@ -253,3 +282,9 @@ func GetFeeds(ctx context.Context, opts GetFeedsOptions) (ActionList, int64, err return actions, count, nil } + +func CountUserFeeds(ctx context.Context, userID int64) (int64, error) { + return db.GetEngine(ctx).Where("user_id = ?", userID). + And("is_deleted = ?", false). + Count(&Action{}) +} diff --git a/models/activities/action_test.go b/models/activities/action_test.go index 9cfe9816568..9447f39d62a 100644 --- a/models/activities/action_test.go +++ b/models/activities/action_test.go @@ -25,7 +25,7 @@ func TestAction_GetRepoPath(t *testing.T) { repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) action := &activities_model.Action{RepoID: repo.ID} - assert.Equal(t, path.Join(owner.Name, repo.Name), action.GetRepoPath(db.DefaultContext)) + assert.Equal(t, path.Join(owner.Name, repo.Name), action.GetRepoPath(t.Context())) } func TestAction_GetRepoLink(t *testing.T) { @@ -37,9 +37,9 @@ func TestAction_GetRepoLink(t *testing.T) { defer test.MockVariableValue(&setting.AppURL, "https://try.gitea.io/suburl/")() defer test.MockVariableValue(&setting.AppSubURL, "/suburl")() expected := path.Join(setting.AppSubURL, owner.Name, repo.Name) - assert.Equal(t, expected, action.GetRepoLink(db.DefaultContext)) - assert.Equal(t, repo.HTMLURL(), action.GetRepoAbsoluteLink(db.DefaultContext)) - assert.Equal(t, comment.HTMLURL(db.DefaultContext), action.GetCommentHTMLURL(db.DefaultContext)) + assert.Equal(t, expected, action.GetRepoLink(t.Context())) + assert.Equal(t, repo.HTMLURL(), action.GetRepoAbsoluteLink(t.Context())) + assert.Equal(t, comment.HTMLURL(t.Context()), action.GetCommentHTMLURL(t.Context())) } func TestActivityReadable(t *testing.T) { @@ -82,43 +82,6 @@ func TestActivityReadable(t *testing.T) { } } -func TestNotifyWatchers(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - action := &activities_model.Action{ - ActUserID: 8, - RepoID: 1, - OpType: activities_model.ActionStarRepo, - } - assert.NoError(t, activities_model.NotifyWatchers(db.DefaultContext, action)) - - // One watchers are inactive, thus action is only created for user 8, 1, 4, 11 - unittest.AssertExistsAndLoadBean(t, &activities_model.Action{ - ActUserID: action.ActUserID, - UserID: 8, - RepoID: action.RepoID, - OpType: action.OpType, - }) - unittest.AssertExistsAndLoadBean(t, &activities_model.Action{ - ActUserID: action.ActUserID, - UserID: 1, - RepoID: action.RepoID, - OpType: action.OpType, - }) - unittest.AssertExistsAndLoadBean(t, &activities_model.Action{ - ActUserID: action.ActUserID, - UserID: 4, - RepoID: action.RepoID, - OpType: action.OpType, - }) - unittest.AssertExistsAndLoadBean(t, &activities_model.Action{ - ActUserID: action.ActUserID, - UserID: 11, - RepoID: action.RepoID, - OpType: action.OpType, - }) -} - func TestConsistencyUpdateAction(t *testing.T) { if !setting.Database.Type.IsSQLite3() { t.Skip("Test is only for SQLite database.") @@ -128,37 +91,37 @@ func TestConsistencyUpdateAction(t *testing.T) { unittest.AssertExistsAndLoadBean(t, &activities_model.Action{ ID: int64(id), }) - _, err := db.GetEngine(db.DefaultContext).Exec(`UPDATE action SET created_unix = '' WHERE id = ?`, id) + _, err := db.GetEngine(t.Context()).Exec(`UPDATE action SET created_unix = '' WHERE id = ?`, id) assert.NoError(t, err) actions := make([]*activities_model.Action, 0, 1) // // XORM returns an error when created_unix is a string // - err = db.GetEngine(db.DefaultContext).Where("id = ?", id).Find(&actions) + err = db.GetEngine(t.Context()).Where("id = ?", id).Find(&actions) if assert.Error(t, err) { assert.Contains(t, err.Error(), "type string to a int64: invalid syntax") } // // Get rid of incorrectly set created_unix // - count, err := activities_model.CountActionCreatedUnixString(db.DefaultContext) + count, err := activities_model.CountActionCreatedUnixString(t.Context()) assert.NoError(t, err) assert.EqualValues(t, 1, count) - count, err = activities_model.FixActionCreatedUnixString(db.DefaultContext) + count, err = activities_model.FixActionCreatedUnixString(t.Context()) assert.NoError(t, err) assert.EqualValues(t, 1, count) - count, err = activities_model.CountActionCreatedUnixString(db.DefaultContext) + count, err = activities_model.CountActionCreatedUnixString(t.Context()) assert.NoError(t, err) assert.EqualValues(t, 0, count) - count, err = activities_model.FixActionCreatedUnixString(db.DefaultContext) + count, err = activities_model.FixActionCreatedUnixString(t.Context()) assert.NoError(t, err) assert.EqualValues(t, 0, count) // // XORM must be happy now // - assert.NoError(t, db.GetEngine(db.DefaultContext).Where("id = ?", id).Find(&actions)) + assert.NoError(t, db.GetEngine(t.Context()).Where("id = ?", id).Find(&actions)) unittest.CheckConsistencyFor(t, &activities_model.Action{}) } @@ -167,22 +130,22 @@ func TestDeleteIssueActions(t *testing.T) { // load an issue issue := unittest.AssertExistsAndLoadBean(t, &issue_model.Issue{ID: 4}) - assert.NotEqualValues(t, issue.ID, issue.Index) // it needs to use different ID/Index to test the DeleteIssueActions to delete some actions by IssueIndex + assert.NotEqual(t, issue.ID, issue.Index) // it needs to use different ID/Index to test the DeleteIssueActions to delete some actions by IssueIndex // insert a comment - err := db.Insert(db.DefaultContext, &issue_model.Comment{Type: issue_model.CommentTypeComment, IssueID: issue.ID}) + err := db.Insert(t.Context(), &issue_model.Comment{Type: issue_model.CommentTypeComment, IssueID: issue.ID}) assert.NoError(t, err) comment := unittest.AssertExistsAndLoadBean(t, &issue_model.Comment{Type: issue_model.CommentTypeComment, IssueID: issue.ID}) // truncate action table and insert some actions - err = db.TruncateBeans(db.DefaultContext, &activities_model.Action{}) + err = db.TruncateBeans(t.Context(), &activities_model.Action{}) assert.NoError(t, err) - err = db.Insert(db.DefaultContext, &activities_model.Action{ + err = db.Insert(t.Context(), &activities_model.Action{ OpType: activities_model.ActionCommentIssue, CommentID: comment.ID, }) assert.NoError(t, err) - err = db.Insert(db.DefaultContext, &activities_model.Action{ + err = db.Insert(t.Context(), &activities_model.Action{ OpType: activities_model.ActionCreateIssue, RepoID: issue.RepoID, Content: fmt.Sprintf("%d|content...", issue.Index), @@ -191,6 +154,6 @@ func TestDeleteIssueActions(t *testing.T) { // assert that the actions exist, then delete them unittest.AssertCount(t, &activities_model.Action{}, 2) - assert.NoError(t, activities_model.DeleteIssueActions(db.DefaultContext, issue.RepoID, issue.ID, issue.Index)) + assert.NoError(t, activities_model.DeleteIssueActions(t.Context(), issue.RepoID, issue.ID, issue.Index)) unittest.AssertCount(t, &activities_model.Action{}, 0) } diff --git a/models/activities/notification.go b/models/activities/notification.go index 6dde26fd53e..b482e6020af 100644 --- a/models/activities/notification.go +++ b/models/activities/notification.go @@ -280,11 +280,11 @@ func (n *Notification) HTMLURL(ctx context.Context) string { if n.Comment != nil { return n.Comment.HTMLURL(ctx) } - return n.Issue.HTMLURL() + return n.Issue.HTMLURL(ctx) case NotificationSourceCommit: - return n.Repository.HTMLURL() + "/commit/" + url.PathEscape(n.CommitID) + return n.Repository.HTMLURL(ctx) + "/commit/" + url.PathEscape(n.CommitID) case NotificationSourceRepository: - return n.Repository.HTMLURL() + return n.Repository.HTMLURL(ctx) } return "" } diff --git a/models/activities/notification_list.go b/models/activities/notification_list.go index 0cbb91df3cb..6539e14ea27 100644 --- a/models/activities/notification_list.go +++ b/models/activities/notification_list.go @@ -70,17 +70,9 @@ func (opts FindNotificationOptions) ToOrders() string { // for each watcher, or updates it if already exists // receiverID > 0 just send to receiver, else send to all watcher func CreateOrUpdateIssueNotifications(ctx context.Context, issueID, commentID, notificationAuthorID, receiverID int64) error { - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return err - } - defer committer.Close() - - if err := createOrUpdateIssueNotifications(ctx, issueID, commentID, notificationAuthorID, receiverID); err != nil { - return err - } - - return committer.Commit() + return db.WithTx(ctx, func(ctx context.Context) error { + return createOrUpdateIssueNotifications(ctx, issueID, commentID, notificationAuthorID, receiverID) + }) } func createOrUpdateIssueNotifications(ctx context.Context, issueID, commentID, notificationAuthorID, receiverID int64) error { @@ -208,10 +200,7 @@ func (nl NotificationList) LoadRepos(ctx context.Context) (repo_model.Repository repos := make(map[int64]*repo_model.Repository, len(repoIDs)) left := len(repoIDs) for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } + limit := min(left, db.DefaultMaxInSize) rows, err := db.GetEngine(ctx). In("id", repoIDs[:limit]). Rows(new(repo_model.Repository)) @@ -282,10 +271,7 @@ func (nl NotificationList) LoadIssues(ctx context.Context) ([]int, error) { issues := make(map[int64]*issues_model.Issue, len(issueIDs)) left := len(issueIDs) for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } + limit := min(left, db.DefaultMaxInSize) rows, err := db.GetEngine(ctx). In("id", issueIDs[:limit]). Rows(new(issues_model.Issue)) @@ -377,10 +363,7 @@ func (nl NotificationList) LoadUsers(ctx context.Context) ([]int, error) { users := make(map[int64]*user_model.User, len(userIDs)) left := len(userIDs) for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } + limit := min(left, db.DefaultMaxInSize) rows, err := db.GetEngine(ctx). In("id", userIDs[:limit]). Rows(new(user_model.User)) @@ -428,10 +411,7 @@ func (nl NotificationList) LoadComments(ctx context.Context) ([]int, error) { comments := make(map[int64]*issues_model.Comment, len(commentIDs)) left := len(commentIDs) for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } + limit := min(left, db.DefaultMaxInSize) rows, err := db.GetEngine(ctx). In("id", commentIDs[:limit]). Rows(new(issues_model.Comment)) diff --git a/models/activities/notification_test.go b/models/activities/notification_test.go index 52f0eacba12..6f2253c815d 100644 --- a/models/activities/notification_test.go +++ b/models/activities/notification_test.go @@ -20,7 +20,7 @@ func TestCreateOrUpdateIssueNotifications(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1}) - assert.NoError(t, activities_model.CreateOrUpdateIssueNotifications(db.DefaultContext, issue.ID, 0, 2, 0)) + assert.NoError(t, activities_model.CreateOrUpdateIssueNotifications(t.Context(), issue.ID, 0, 2, 0)) // User 9 is inactive, thus notifications for user 1 and 4 are created notf := unittest.AssertExistsAndLoadBean(t, &activities_model.Notification{UserID: 1, IssueID: issue.ID}) @@ -34,7 +34,7 @@ func TestCreateOrUpdateIssueNotifications(t *testing.T) { func TestNotificationsForUser(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) - notfs, err := db.Find[activities_model.Notification](db.DefaultContext, activities_model.FindNotificationOptions{ + notfs, err := db.Find[activities_model.Notification](t.Context(), activities_model.FindNotificationOptions{ UserID: user.ID, Status: []activities_model.NotificationStatus{ activities_model.NotificationStatusRead, @@ -44,36 +44,36 @@ func TestNotificationsForUser(t *testing.T) { assert.NoError(t, err) if assert.Len(t, notfs, 3) { assert.EqualValues(t, 5, notfs[0].ID) - assert.EqualValues(t, user.ID, notfs[0].UserID) + assert.Equal(t, user.ID, notfs[0].UserID) assert.EqualValues(t, 4, notfs[1].ID) - assert.EqualValues(t, user.ID, notfs[1].UserID) + assert.Equal(t, user.ID, notfs[1].UserID) assert.EqualValues(t, 2, notfs[2].ID) - assert.EqualValues(t, user.ID, notfs[2].UserID) + assert.Equal(t, user.ID, notfs[2].UserID) } } func TestNotification_GetRepo(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) notf := unittest.AssertExistsAndLoadBean(t, &activities_model.Notification{RepoID: 1}) - repo, err := notf.GetRepo(db.DefaultContext) + repo, err := notf.GetRepo(t.Context()) assert.NoError(t, err) assert.Equal(t, repo, notf.Repository) - assert.EqualValues(t, notf.RepoID, repo.ID) + assert.Equal(t, notf.RepoID, repo.ID) } func TestNotification_GetIssue(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) notf := unittest.AssertExistsAndLoadBean(t, &activities_model.Notification{RepoID: 1}) - issue, err := notf.GetIssue(db.DefaultContext) + issue, err := notf.GetIssue(t.Context()) assert.NoError(t, err) assert.Equal(t, issue, notf.Issue) - assert.EqualValues(t, notf.IssueID, issue.ID) + assert.Equal(t, notf.IssueID, issue.ID) } func TestGetNotificationCount(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) - cnt, err := db.Count[activities_model.Notification](db.DefaultContext, activities_model.FindNotificationOptions{ + cnt, err := db.Count[activities_model.Notification](t.Context(), activities_model.FindNotificationOptions{ UserID: user.ID, Status: []activities_model.NotificationStatus{ activities_model.NotificationStatusRead, @@ -82,7 +82,7 @@ func TestGetNotificationCount(t *testing.T) { assert.NoError(t, err) assert.EqualValues(t, 0, cnt) - cnt, err = db.Count[activities_model.Notification](db.DefaultContext, activities_model.FindNotificationOptions{ + cnt, err = db.Count[activities_model.Notification](t.Context(), activities_model.FindNotificationOptions{ UserID: user.ID, Status: []activities_model.NotificationStatus{ activities_model.NotificationStatusUnread, @@ -97,14 +97,14 @@ func TestSetNotificationStatus(t *testing.T) { user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) notf := unittest.AssertExistsAndLoadBean(t, &activities_model.Notification{UserID: user.ID, Status: activities_model.NotificationStatusRead}) - _, err := activities_model.SetNotificationStatus(db.DefaultContext, notf.ID, user, activities_model.NotificationStatusPinned) + _, err := activities_model.SetNotificationStatus(t.Context(), notf.ID, user, activities_model.NotificationStatusPinned) assert.NoError(t, err) unittest.AssertExistsAndLoadBean(t, &activities_model.Notification{ID: notf.ID, Status: activities_model.NotificationStatusPinned}) - _, err = activities_model.SetNotificationStatus(db.DefaultContext, 1, user, activities_model.NotificationStatusRead) + _, err = activities_model.SetNotificationStatus(t.Context(), 1, user, activities_model.NotificationStatusRead) assert.Error(t, err) - _, err = activities_model.SetNotificationStatus(db.DefaultContext, unittest.NonexistentID, user, activities_model.NotificationStatusRead) + _, err = activities_model.SetNotificationStatus(t.Context(), unittest.NonexistentID, user, activities_model.NotificationStatusRead) assert.Error(t, err) } @@ -117,7 +117,7 @@ func TestUpdateNotificationStatuses(t *testing.T) { &activities_model.Notification{UserID: user.ID, Status: activities_model.NotificationStatusRead}) notfPinned := unittest.AssertExistsAndLoadBean(t, &activities_model.Notification{UserID: user.ID, Status: activities_model.NotificationStatusPinned}) - assert.NoError(t, activities_model.UpdateNotificationStatuses(db.DefaultContext, user, activities_model.NotificationStatusUnread, activities_model.NotificationStatusRead)) + assert.NoError(t, activities_model.UpdateNotificationStatuses(t.Context(), user, activities_model.NotificationStatusUnread, activities_model.NotificationStatusRead)) unittest.AssertExistsAndLoadBean(t, &activities_model.Notification{ID: notfUnread.ID, Status: activities_model.NotificationStatusRead}) unittest.AssertExistsAndLoadBean(t, @@ -130,11 +130,11 @@ func TestSetIssueReadBy(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1}) - assert.NoError(t, db.WithTx(db.DefaultContext, func(ctx context.Context) error { + assert.NoError(t, db.WithTx(t.Context(), func(ctx context.Context) error { return activities_model.SetIssueReadBy(ctx, issue.ID, user.ID) })) - nt, err := activities_model.GetIssueNotification(db.DefaultContext, user.ID, issue.ID) + nt, err := activities_model.GetIssueNotification(t.Context(), user.ID, issue.ID) assert.NoError(t, err) - assert.EqualValues(t, activities_model.NotificationStatusRead, nt.Status) + assert.Equal(t, activities_model.NotificationStatusRead, nt.Status) } diff --git a/models/activities/repo_activity.go b/models/activities/repo_activity.go index 3ffad035b7a..aeaa452c9e9 100644 --- a/models/activities/repo_activity.go +++ b/models/activities/repo_activity.go @@ -16,6 +16,7 @@ import ( "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/gitrepo" + "xorm.io/builder" "xorm.io/xorm" ) @@ -138,10 +139,7 @@ func GetActivityStatsTopAuthors(ctx context.Context, repo *repo_model.Repository return v[i].Commits > v[j].Commits }) - cnt := count - if cnt > len(v) { - cnt = len(v) - } + cnt := min(count, len(v)) return v[:cnt], nil } @@ -337,8 +335,10 @@ func newlyCreatedIssues(ctx context.Context, repoID int64, fromTime time.Time) * func activeIssues(ctx context.Context, repoID int64, fromTime time.Time) *xorm.Session { sess := db.GetEngine(ctx).Where("issue.repo_id = ?", repoID). And("issue.is_pull = ?", false). - And("issue.created_unix >= ?", fromTime.Unix()). - Or("issue.closed_unix >= ?", fromTime.Unix()) + And(builder.Or( + builder.Gte{"issue.created_unix": fromTime.Unix()}, + builder.Gte{"issue.closed_unix": fromTime.Unix()}, + )) return sess } diff --git a/models/activities/statistic.go b/models/activities/statistic.go index ff81ad78a1d..940651d3592 100644 --- a/models/activities/statistic.go +++ b/models/activities/statistic.go @@ -17,13 +17,16 @@ import ( repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/models/webhook" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/structs" ) // Statistic contains the database statistics type Statistic struct { Counter struct { - User, Org, PublicKey, + UsersActive, UsersNotActive, + Org, PublicKey, Repo, Watch, Star, Access, Issue, IssueClosed, IssueOpen, Comment, Oauth, Follow, @@ -53,8 +56,20 @@ type IssueByRepositoryCount struct { // GetStatistic returns the database statistics func GetStatistic(ctx context.Context) (stats Statistic) { e := db.GetEngine(ctx) - stats.Counter.User = user_model.CountUsers(ctx, nil) - stats.Counter.Org, _ = db.Count[organization.Organization](ctx, organization.FindOrgOptions{IncludePrivate: true}) + + // Number of active users + usersActiveOpts := user_model.CountUserFilter{ + IsActive: optional.Some(true), + } + stats.Counter.UsersActive = user_model.CountUsers(ctx, &usersActiveOpts) + + // Number of inactive users + usersNotActiveOpts := user_model.CountUserFilter{ + IsActive: optional.Some(false), + } + stats.Counter.UsersNotActive = user_model.CountUsers(ctx, &usersNotActiveOpts) + + stats.Counter.Org, _ = db.Count[organization.Organization](ctx, organization.FindOrgOptions{IncludeVisibility: structs.VisibleTypePrivate}) stats.Counter.PublicKey, _ = e.Count(new(asymkey_model.PublicKey)) stats.Counter.Repo, _ = repo_model.CountRepositories(ctx, repo_model.CountRepositoryOptions{}) stats.Counter.Watch, _ = e.Count(new(repo_model.Watch)) diff --git a/models/activities/user_heatmap.go b/models/activities/user_heatmap.go index 1f8f0f590e1..ef67838be73 100644 --- a/models/activities/user_heatmap.go +++ b/models/activities/user_heatmap.go @@ -66,7 +66,7 @@ func getUserHeatmapData(ctx context.Context, user *user_model.User, team *organi Select(groupBy+" AS timestamp, count(user_id) as contributions"). Table("action"). Where(cond). - And("created_unix > ?", timeutil.TimeStampNow()-31536000). + And("created_unix > ?", timeutil.TimeStampNow()-(366+7)*86400). // (366+7) days to include the first week for the heatmap GroupBy(groupByName). OrderBy("timestamp"). Find(&hdata) diff --git a/models/activities/user_heatmap_test.go b/models/activities/user_heatmap_test.go index b7babcbde1d..66087325b1d 100644 --- a/models/activities/user_heatmap_test.go +++ b/models/activities/user_heatmap_test.go @@ -4,12 +4,10 @@ package activities_test import ( - "fmt" "testing" "time" activities_model "code.gitea.io/gitea/models/activities" - "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/json" @@ -65,15 +63,13 @@ func TestGetUserHeatmapDataByUser(t *testing.T) { for _, tc := range testCases { user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: tc.userID}) - doer := &user_model.User{ID: tc.doerID} - _, err := unittest.LoadBeanIfExists(doer) - assert.NoError(t, err) - if tc.doerID == 0 { - doer = nil + var doer *user_model.User + if tc.doerID != 0 { + doer = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: tc.doerID}) } // get the action for comparison - actions, count, err := activities_model.GetFeeds(db.DefaultContext, activities_model.GetFeedsOptions{ + actions, count, err := activities_model.GetFeeds(t.Context(), activities_model.GetFeedsOptions{ RequestedUser: user, Actor: doer, IncludePrivate: true, @@ -83,7 +79,7 @@ func TestGetUserHeatmapDataByUser(t *testing.T) { assert.NoError(t, err) // Get the heatmap and compare - heatmap, err := activities_model.GetUserHeatmapDataByUser(db.DefaultContext, user, doer) + heatmap, err := activities_model.GetUserHeatmapDataByUser(t.Context(), user, doer) var contributions int for _, hm := range heatmap { contributions += int(hm.Contributions) @@ -91,11 +87,11 @@ func TestGetUserHeatmapDataByUser(t *testing.T) { assert.NoError(t, err) assert.Len(t, actions, contributions, "invalid action count: did the test data became too old?") assert.Equal(t, count, int64(contributions)) - assert.Equal(t, tc.CountResult, contributions, fmt.Sprintf("testcase '%s'", tc.desc)) + assert.Equal(t, tc.CountResult, contributions, "testcase '%s'", tc.desc) // Test JSON rendering jsonData, err := json.Marshal(heatmap) assert.NoError(t, err) - assert.Equal(t, tc.JSONResult, string(jsonData)) + assert.JSONEq(t, tc.JSONResult, string(jsonData)) } } diff --git a/models/admin/task.go b/models/admin/task.go index 10f8e6d5704..0541a8ec78e 100644 --- a/models/admin/task.go +++ b/models/admin/task.go @@ -44,7 +44,7 @@ func init() { // TranslatableMessage represents JSON struct that can be translated with a Locale type TranslatableMessage struct { Format string - Args []any `json:"omitempty"` + Args []any `json:",omitempty"` } // LoadRepo loads repository of the task diff --git a/models/asymkey/error.go b/models/asymkey/error.go index 03bc82302f1..b7656245791 100644 --- a/models/asymkey/error.go +++ b/models/asymkey/error.go @@ -25,7 +25,7 @@ func (err ErrKeyUnableVerify) Error() string { } // ErrKeyIsPrivate is returned when the provided key is a private key not a public key -var ErrKeyIsPrivate = util.NewSilentWrapErrorf(util.ErrInvalidArgument, "the provided key is a private key") +var ErrKeyIsPrivate = util.ErrorWrap(util.ErrInvalidArgument, "the provided key is a private key") // ErrKeyNotExist represents a "KeyNotExist" kind of error. type ErrKeyNotExist struct { @@ -132,7 +132,7 @@ func IsErrGPGKeyParsing(err error) bool { } func (err ErrGPGKeyParsing) Error() string { - return fmt.Sprintf("failed to parse gpg key %s", err.ParseError.Error()) + return "failed to parse gpg key " + err.ParseError.Error() } // ErrGPGKeyNotExist represents a "GPGKeyNotExist" kind of error. @@ -217,6 +217,7 @@ func (err ErrGPGKeyAccessDenied) Unwrap() error { // ErrKeyAccessDenied represents a "KeyAccessDenied" kind of error. type ErrKeyAccessDenied struct { UserID int64 + RepoID int64 KeyID int64 Note string } @@ -228,8 +229,8 @@ func IsErrKeyAccessDenied(err error) bool { } func (err ErrKeyAccessDenied) Error() string { - return fmt.Sprintf("user does not have access to the key [user_id: %d, key_id: %d, note: %s]", - err.UserID, err.KeyID, err.Note) + 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.RepoID, err.KeyID, err.Note) } func (err ErrKeyAccessDenied) Unwrap() error { diff --git a/models/asymkey/gpg_key.go b/models/asymkey/gpg_key.go index 5236b2d4500..38de7cbda61 100644 --- a/models/asymkey/gpg_key.go +++ b/models/asymkey/gpg_key.go @@ -5,6 +5,7 @@ package asymkey import ( "context" + "errors" "fmt" "strings" "time" @@ -13,8 +14,8 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/timeutil" - "github.com/keybase/go-crypto/openpgp" - "github.com/keybase/go-crypto/openpgp/packet" + "github.com/ProtonMail/go-crypto/openpgp" + "github.com/ProtonMail/go-crypto/openpgp/packet" "xorm.io/builder" ) @@ -106,7 +107,7 @@ func GPGKeyToEntity(ctx context.Context, k *GPGKey) (*openpgp.Entity, error) { if err != nil { return nil, err } - keys, err := checkArmoredGPGKeyString(impKey.Content) + keys, err := CheckArmoredGPGKeyString(impKey.Content) if err != nil { return nil, err } @@ -115,7 +116,7 @@ func GPGKeyToEntity(ctx context.Context, k *GPGKey) (*openpgp.Entity, error) { // parseSubGPGKey parse a sub Key func parseSubGPGKey(ownerID int64, primaryID string, pubkey *packet.PublicKey, expiry time.Time) (*GPGKey, error) { - content, err := base64EncPubKey(pubkey) + content, err := Base64EncPubKey(pubkey) if err != nil { return nil, err } @@ -141,7 +142,11 @@ func parseGPGKey(ctx context.Context, ownerID int64, e *openpgp.Entity, verified // Parse Subkeys subkeys := make([]*GPGKey, len(e.Subkeys)) for i, k := range e.Subkeys { - subs, err := parseSubGPGKey(ownerID, pubkey.KeyIdString(), k.PublicKey, expiry) + subkeyExpiry := expiry + if k.Sig.KeyLifetimeSecs != nil { + subkeyExpiry = k.PublicKey.CreationTime.Add(time.Duration(*k.Sig.KeyLifetimeSecs) * time.Second) + } + subs, err := parseSubGPGKey(ownerID, pubkey.KeyIdString(), k.PublicKey, subkeyExpiry) if err != nil { return nil, ErrGPGKeyParsing{ParseError: err} } @@ -156,7 +161,7 @@ func parseGPGKey(ctx context.Context, ownerID int64, e *openpgp.Entity, verified emails := make([]*user_model.EmailAddress, 0, len(e.Identities)) for _, ident := range e.Identities { - if ident.Revocation != nil { + if ident.Revoked(time.Now()) { continue } email := strings.ToLower(strings.TrimSpace(ident.UserId.Email)) @@ -179,7 +184,7 @@ func parseGPGKey(ctx context.Context, ownerID int64, e *openpgp.Entity, verified } } - content, err := base64EncPubKey(pubkey) + content, err := Base64EncPubKey(pubkey) if err != nil { return nil, err } @@ -203,7 +208,7 @@ func parseGPGKey(ctx context.Context, ownerID int64, e *openpgp.Entity, verified // deleteGPGKey does the actual key deletion func deleteGPGKey(ctx context.Context, keyID string) (int64, error) { if keyID == "" { - return 0, fmt.Errorf("empty KeyId forbidden") // Should never happen but just to be sure + return 0, errors.New("empty KeyId forbidden") // Should never happen but just to be sure } // Delete imported key n, err := db.GetEngine(ctx).Where("key_id=?", keyID).Delete(new(GPGKeyImport)) @@ -223,45 +228,15 @@ func DeleteGPGKey(ctx context.Context, doer *user_model.User, id int64) (err err return fmt.Errorf("GetPublicKeyByID: %w", err) } - ctx, committer, err := db.TxContext(ctx) - if err != nil { + return db.WithTx(ctx, func(ctx context.Context) error { + _, err = deleteGPGKey(ctx, key.KeyID) return err - } - defer committer.Close() - - if _, err = deleteGPGKey(ctx, key.KeyID); err != nil { - return err - } - - return committer.Commit() + }) } -func checkKeyEmails(ctx context.Context, email string, keys ...*GPGKey) (bool, string) { - uid := int64(0) - var userEmails []*user_model.EmailAddress - var user *user_model.User - for _, key := range keys { - for _, e := range key.Emails { - if e.IsActivated && (email == "" || strings.EqualFold(e.Email, email)) { - return true, e.Email - } - } - if key.Verified && key.OwnerID != 0 { - if uid != key.OwnerID { - userEmails, _ = user_model.GetEmailAddresses(ctx, key.OwnerID) - uid = key.OwnerID - user = &user_model.User{ID: uid} - _, _ = user_model.GetUser(ctx, user) - } - for _, e := range userEmails { - if e.IsActivated && (email == "" || strings.EqualFold(e.Email, email)) { - return true, e.Email - } - } - if user.KeepEmailPrivate && strings.EqualFold(email, user.GetEmail()) { - return true, user.GetEmail() - } - } - } - return false, email +func FindGPGKeyWithSubKeys(ctx context.Context, keyID string) ([]*GPGKey, error) { + return db.Find[GPGKey](ctx, FindGPGKeyOptions{ + KeyID: keyID, + IncludeSubKeys: true, + }) } diff --git a/models/asymkey/gpg_key_add.go b/models/asymkey/gpg_key_add.go index 11124b13665..1c7d2c1da26 100644 --- a/models/asymkey/gpg_key_add.go +++ b/models/asymkey/gpg_key_add.go @@ -10,7 +10,7 @@ import ( "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/modules/log" - "github.com/keybase/go-crypto/openpgp" + "github.com/ProtonMail/go-crypto/openpgp" ) // __________________ ________ ____ __. @@ -67,7 +67,7 @@ func addGPGSubKey(ctx context.Context, key *GPGKey) (err error) { // AddGPGKey adds new public key to database. func AddGPGKey(ctx context.Context, ownerID int64, content, token, signature string) ([]*GPGKey, error) { - ekeys, err := checkArmoredGPGKeyString(content) + ekeys, err := CheckArmoredGPGKeyString(content) if err != nil { return nil, err } @@ -83,15 +83,15 @@ func AddGPGKey(ctx context.Context, ownerID int64, content, token, signature str verified := false // Handle provided signature if signature != "" { - signer, err := openpgp.CheckArmoredDetachedSignature(ekeys, strings.NewReader(token), strings.NewReader(signature)) + signer, err := openpgp.CheckArmoredDetachedSignature(ekeys, strings.NewReader(token), strings.NewReader(signature), nil) if err != nil { - signer, err = openpgp.CheckArmoredDetachedSignature(ekeys, strings.NewReader(token+"\n"), strings.NewReader(signature)) + signer, err = openpgp.CheckArmoredDetachedSignature(ekeys, strings.NewReader(token+"\n"), strings.NewReader(signature), nil) } if err != nil { - signer, err = openpgp.CheckArmoredDetachedSignature(ekeys, strings.NewReader(token+"\r\n"), strings.NewReader(signature)) + signer, err = openpgp.CheckArmoredDetachedSignature(ekeys, strings.NewReader(token+"\r\n"), strings.NewReader(signature), nil) } if err != nil { - log.Error("Unable to validate token signature. Error: %v", err) + log.Debug("AddGPGKey CheckArmoredDetachedSignature failed: %v", err) return nil, ErrGPGInvalidTokenSignature{ ID: ekeys[0].PrimaryKey.KeyIdString(), Wrapped: err, diff --git a/models/asymkey/gpg_key_commit_verification.go b/models/asymkey/gpg_key_commit_verification.go index 26fad3bb3f2..b85374e0735 100644 --- a/models/asymkey/gpg_key_commit_verification.go +++ b/models/asymkey/gpg_key_commit_verification.go @@ -4,40 +4,17 @@ package asymkey import ( - "context" + "errors" "fmt" "hash" - "strings" - "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/setting" - "github.com/keybase/go-crypto/openpgp/packet" + "github.com/ProtonMail/go-crypto/openpgp/packet" ) -// __________________ ________ ____ __. -// / _____/\______ \/ _____/ | |/ _|____ ___.__. -// / \ ___ | ___/ \ ___ | <_/ __ < | | -// \ \_\ \| | \ \_\ \ | | \ ___/\___ | -// \______ /|____| \______ / |____|__ \___ > ____| -// \/ \/ \/ \/\/ -// _________ .__ __ -// \_ ___ \ ____ _____ _____ |__|/ |_ -// / \ \/ / _ \ / \ / \| \ __\ -// \ \___( <_> ) Y Y \ Y Y \ || | -// \______ /\____/|__|_| /__|_| /__||__| -// \/ \/ \/ -// ____ ____ .__ _____.__ __ .__ -// \ \ / /___________|__|/ ____\__| ____ _____ _/ |_|__| ____ ____ -// \ Y // __ \_ __ \ \ __\| |/ ___\\__ \\ __\ |/ _ \ / \ -// \ /\ ___/| | \/ || | | \ \___ / __ \| | | ( <_> ) | \ -// \___/ \___ >__| |__||__| |__|\___ >____ /__| |__|\____/|___| / -// \/ \/ \/ \/ - // This file provides functions relating commit verification // CommitVerification represents a commit validation of signature @@ -45,8 +22,8 @@ type CommitVerification struct { Verified bool Warning bool Reason string - SigningUser *user_model.User - CommittingUser *user_model.User + SigningUser *user_model.User // if Verified, then SigningUser is non-nil + CommittingUser *user_model.User // if Verified, then CommittingUser is non-nil SigningEmail string SigningKey *GPGKey SigningSSHKey *PublicKey @@ -70,267 +47,10 @@ const ( NoKeyFound = "gpg.error.no_gpg_keys_found" ) -// ParseCommitsWithSignature checks if signaute of commits are corresponding to users gpg keys. -func ParseCommitsWithSignature(ctx context.Context, oldCommits []*user_model.UserCommit, repoTrustModel repo_model.TrustModelType, isOwnerMemberCollaborator func(*user_model.User) (bool, error)) []*SignCommit { - newCommits := make([]*SignCommit, 0, len(oldCommits)) - keyMap := map[string]bool{} - - for _, c := range oldCommits { - signCommit := &SignCommit{ - UserCommit: c, - Verification: ParseCommitWithSignature(ctx, c.Commit), - } - - _ = CalculateTrustStatus(signCommit.Verification, repoTrustModel, isOwnerMemberCollaborator, &keyMap) - - newCommits = append(newCommits, signCommit) - } - return newCommits -} - -// ParseCommitWithSignature check if signature is good against keystore. -func ParseCommitWithSignature(ctx context.Context, c *git.Commit) *CommitVerification { - var committer *user_model.User - if c.Committer != nil { - var err error - // Find Committer account - committer, err = user_model.GetUserByEmail(ctx, c.Committer.Email) // This finds the user by primary email or activated email so commit will not be valid if email is not - if err != nil { // Skipping not user for committer - committer = &user_model.User{ - Name: c.Committer.Name, - Email: c.Committer.Email, - } - // We can expect this to often be an ErrUserNotExist. in the case - // it is not, however, it is important to log it. - if !user_model.IsErrUserNotExist(err) { - log.Error("GetUserByEmail: %v", err) - return &CommitVerification{ - CommittingUser: committer, - Verified: false, - Reason: "gpg.error.no_committer_account", - } - } - } - } - - // If no signature just report the committer - if c.Signature == nil { - return &CommitVerification{ - CommittingUser: committer, - Verified: false, // Default value - Reason: "gpg.error.not_signed_commit", // Default value - } - } - - // If this a SSH signature handle it differently - if strings.HasPrefix(c.Signature.Signature, "-----BEGIN SSH SIGNATURE-----") { - return ParseCommitWithSSHSignature(ctx, c, committer) - } - - // Parsing signature - sig, err := extractSignature(c.Signature.Signature) - if err != nil { // Skipping failed to extract sign - log.Error("SignatureRead err: %v", err) - return &CommitVerification{ - CommittingUser: committer, - Verified: false, - Reason: "gpg.error.extract_sign", - } - } - - keyID := tryGetKeyIDFromSignature(sig) - defaultReason := NoKeyFound - - // First check if the sig has a keyID and if so just look at that - if commitVerification := hashAndVerifyForKeyID( - ctx, - sig, - c.Signature.Payload, - committer, - keyID, - setting.AppName, - ""); commitVerification != nil { - if commitVerification.Reason == BadSignature { - defaultReason = BadSignature - } else { - return commitVerification - } - } - - // Now try to associate the signature with the committer, if present - if committer.ID != 0 { - keys, err := db.Find[GPGKey](ctx, FindGPGKeyOptions{ - OwnerID: committer.ID, - }) - if err != nil { // Skipping failed to get gpg keys of user - log.Error("ListGPGKeys: %v", err) - return &CommitVerification{ - CommittingUser: committer, - Verified: false, - Reason: "gpg.error.failed_retrieval_gpg_keys", - } - } - - if err := GPGKeyList(keys).LoadSubKeys(ctx); err != nil { - log.Error("LoadSubKeys: %v", err) - return &CommitVerification{ - CommittingUser: committer, - Verified: false, - Reason: "gpg.error.failed_retrieval_gpg_keys", - } - } - - committerEmailAddresses, _ := user_model.GetEmailAddresses(ctx, committer.ID) - activated := false - for _, e := range committerEmailAddresses { - if e.IsActivated && strings.EqualFold(e.Email, c.Committer.Email) { - activated = true - break - } - } - - for _, k := range keys { - // Pre-check (& optimization) that emails attached to key can be attached to the committer email and can validate - canValidate := false - email := "" - if k.Verified && activated { - canValidate = true - email = c.Committer.Email - } - if !canValidate { - for _, e := range k.Emails { - if e.IsActivated && strings.EqualFold(e.Email, c.Committer.Email) { - canValidate = true - email = e.Email - break - } - } - } - if !canValidate { - continue // Skip this key - } - - commitVerification := hashAndVerifyWithSubKeysCommitVerification(sig, c.Signature.Payload, k, committer, committer, email) - if commitVerification != nil { - return commitVerification - } - } - } - - if setting.Repository.Signing.SigningKey != "" && setting.Repository.Signing.SigningKey != "default" && setting.Repository.Signing.SigningKey != "none" { - // OK we should try the default key - gpgSettings := git.GPGSettings{ - Sign: true, - KeyID: setting.Repository.Signing.SigningKey, - Name: setting.Repository.Signing.SigningName, - Email: setting.Repository.Signing.SigningEmail, - } - if err := gpgSettings.LoadPublicKeyContent(); err != nil { - log.Error("Error getting default signing key: %s %v", gpgSettings.KeyID, err) - } else if commitVerification := verifyWithGPGSettings(ctx, &gpgSettings, sig, c.Signature.Payload, committer, keyID); commitVerification != nil { - if commitVerification.Reason == BadSignature { - defaultReason = BadSignature - } else { - return commitVerification - } - } - } - - defaultGPGSettings, err := c.GetRepositoryDefaultPublicGPGKey(false) - if err != nil { - log.Error("Error getting default public gpg key: %v", err) - } else if defaultGPGSettings == nil { - log.Warn("Unable to get defaultGPGSettings for unattached commit: %s", c.ID.String()) - } else if defaultGPGSettings.Sign { - if commitVerification := verifyWithGPGSettings(ctx, defaultGPGSettings, sig, c.Signature.Payload, committer, keyID); commitVerification != nil { - if commitVerification.Reason == BadSignature { - defaultReason = BadSignature - } else { - return commitVerification - } - } - } - - return &CommitVerification{ // Default at this stage - CommittingUser: committer, - Verified: false, - Warning: defaultReason != NoKeyFound, - Reason: defaultReason, - SigningKey: &GPGKey{ - KeyID: keyID, - }, - } -} - -func verifyWithGPGSettings(ctx context.Context, gpgSettings *git.GPGSettings, sig *packet.Signature, payload string, committer *user_model.User, keyID string) *CommitVerification { - // First try to find the key in the db - if commitVerification := hashAndVerifyForKeyID(ctx, sig, payload, committer, gpgSettings.KeyID, gpgSettings.Name, gpgSettings.Email); commitVerification != nil { - return commitVerification - } - - // Otherwise we have to parse the key - ekeys, err := checkArmoredGPGKeyString(gpgSettings.PublicKeyContent) - if err != nil { - log.Error("Unable to get default signing key: %v", err) - return &CommitVerification{ - CommittingUser: committer, - Verified: false, - Reason: "gpg.error.generate_hash", - } - } - for _, ekey := range ekeys { - pubkey := ekey.PrimaryKey - content, err := base64EncPubKey(pubkey) - if err != nil { - return &CommitVerification{ - CommittingUser: committer, - Verified: false, - Reason: "gpg.error.generate_hash", - } - } - k := &GPGKey{ - Content: content, - CanSign: pubkey.CanSign(), - KeyID: pubkey.KeyIdString(), - } - for _, subKey := range ekey.Subkeys { - content, err := base64EncPubKey(subKey.PublicKey) - if err != nil { - return &CommitVerification{ - CommittingUser: committer, - Verified: false, - Reason: "gpg.error.generate_hash", - } - } - k.SubsKey = append(k.SubsKey, &GPGKey{ - Content: content, - CanSign: subKey.PublicKey.CanSign(), - KeyID: subKey.PublicKey.KeyIdString(), - }) - } - if commitVerification := hashAndVerifyWithSubKeysCommitVerification(sig, payload, k, committer, &user_model.User{ - Name: gpgSettings.Name, - Email: gpgSettings.Email, - }, gpgSettings.Email); commitVerification != nil { - return commitVerification - } - if keyID == k.KeyID { - // This is a bad situation ... We have a key id that matches our default key but the signature doesn't match. - return &CommitVerification{ - CommittingUser: committer, - Verified: false, - Warning: true, - Reason: BadSignature, - } - } - } - return nil -} - func verifySign(s *packet.Signature, h hash.Hash, k *GPGKey) error { // Check if key can sign if !k.CanSign { - return fmt.Errorf("key can not sign") + return errors.New("key can not sign") } // Decode key pkey, err := base64DecPubKey(k.Content) @@ -369,7 +89,7 @@ func hashAndVerifyWithSubKeys(sig *packet.Signature, payload string, k *GPGKey) return nil, nil } -func hashAndVerifyWithSubKeysCommitVerification(sig *packet.Signature, payload string, k *GPGKey, committer, signer *user_model.User, email string) *CommitVerification { +func HashAndVerifyWithSubKeysCommitVerification(sig *packet.Signature, payload string, k *GPGKey, committer, signer *user_model.User, email string) *CommitVerification { key, err := hashAndVerifyWithSubKeys(sig, payload, k) if err != nil { // Skipping failed to generate hash return &CommitVerification{ @@ -392,78 +112,6 @@ func hashAndVerifyWithSubKeysCommitVerification(sig *packet.Signature, payload s return nil } -func hashAndVerifyForKeyID(ctx context.Context, sig *packet.Signature, payload string, committer *user_model.User, keyID, name, email string) *CommitVerification { - if keyID == "" { - return nil - } - keys, err := db.Find[GPGKey](ctx, FindGPGKeyOptions{ - KeyID: keyID, - IncludeSubKeys: true, - }) - if err != nil { - log.Error("GetGPGKeysByKeyID: %v", err) - return &CommitVerification{ - CommittingUser: committer, - Verified: false, - Reason: "gpg.error.failed_retrieval_gpg_keys", - } - } - if len(keys) == 0 { - return nil - } - for _, key := range keys { - var primaryKeys []*GPGKey - if key.PrimaryKeyID != "" { - primaryKeys, err = db.Find[GPGKey](ctx, FindGPGKeyOptions{ - KeyID: key.PrimaryKeyID, - IncludeSubKeys: true, - }) - if err != nil { - log.Error("GetGPGKeysByKeyID: %v", err) - return &CommitVerification{ - CommittingUser: committer, - Verified: false, - Reason: "gpg.error.failed_retrieval_gpg_keys", - } - } - } - - activated, email := checkKeyEmails(ctx, email, append([]*GPGKey{key}, primaryKeys...)...) - if !activated { - continue - } - - signer := &user_model.User{ - Name: name, - Email: email, - } - if key.OwnerID != 0 { - owner, err := user_model.GetUserByID(ctx, key.OwnerID) - if err == nil { - signer = owner - } else if !user_model.IsErrUserNotExist(err) { - log.Error("Failed to user_model.GetUserByID: %d for key ID: %d (%s) %v", key.OwnerID, key.ID, key.KeyID, err) - return &CommitVerification{ - CommittingUser: committer, - Verified: false, - Reason: "gpg.error.no_committer_account", - } - } - } - commitVerification := hashAndVerifyWithSubKeysCommitVerification(sig, payload, key, committer, signer, email) - if commitVerification != nil { - return commitVerification - } - } - // This is a bad situation ... We have a key id that is in our database but the signature doesn't match. - return &CommitVerification{ - CommittingUser: committer, - Verified: false, - Warning: true, - Reason: BadSignature, - } -} - // CalculateTrustStatus will calculate the TrustStatus for a commit verification within a repository // There are several trust models in Gitea func CalculateTrustStatus(verification *CommitVerification, repoTrustModel repo_model.TrustModelType, isOwnerMemberCollaborator func(*user_model.User) (bool, error), keyMap *map[string]bool) error { diff --git a/models/asymkey/gpg_key_common.go b/models/asymkey/gpg_key_common.go index 28cb8f4e766..76f52a3ca47 100644 --- a/models/asymkey/gpg_key_common.go +++ b/models/asymkey/gpg_key_common.go @@ -7,15 +7,16 @@ import ( "bytes" "crypto" "encoding/base64" + "errors" "fmt" "hash" "io" "strings" "time" - "github.com/keybase/go-crypto/openpgp" - "github.com/keybase/go-crypto/openpgp/armor" - "github.com/keybase/go-crypto/openpgp/packet" + "github.com/ProtonMail/go-crypto/openpgp" + "github.com/ProtonMail/go-crypto/openpgp/armor" + "github.com/ProtonMail/go-crypto/openpgp/packet" ) // __________________ ________ ____ __. @@ -33,9 +34,9 @@ import ( // This file provides common functions relating to GPG Keys -// checkArmoredGPGKeyString checks if the given key string is a valid GPG armored key. +// CheckArmoredGPGKeyString checks if the given key string is a valid GPG armored key. // The function returns the actual public key on success -func checkArmoredGPGKeyString(content string) (openpgp.EntityList, error) { +func CheckArmoredGPGKeyString(content string) (openpgp.EntityList, error) { list, err := openpgp.ReadArmoredKeyRing(strings.NewReader(content)) if err != nil { return nil, ErrGPGKeyParsing{err} @@ -43,8 +44,8 @@ func checkArmoredGPGKeyString(content string) (openpgp.EntityList, error) { return list, nil } -// base64EncPubKey encode public key content to base 64 -func base64EncPubKey(pubkey *packet.PublicKey) (string, error) { +// Base64EncPubKey encode public key content to base 64 +func Base64EncPubKey(pubkey *packet.PublicKey) (string, error) { var w bytes.Buffer err := pubkey.Serialize(&w) if err != nil { @@ -75,12 +76,12 @@ func base64DecPubKey(content string) (*packet.PublicKey, error) { // Check type pkey, ok := p.(*packet.PublicKey) if !ok { - return nil, fmt.Errorf("key is not a public key") + return nil, errors.New("key is not a public key") } return pkey, nil } -// getExpiryTime extract the expire time of primary key based on sig +// getExpiryTime extract the expiry time of primary key based on sig func getExpiryTime(e *openpgp.Entity) time.Time { expiry := time.Time{} // Extract self-sign for expire date based on : https://github.com/golang/crypto/blob/master/openpgp/keys.go#L165 @@ -88,12 +89,12 @@ func getExpiryTime(e *openpgp.Entity) time.Time { for _, ident := range e.Identities { if selfSig == nil { selfSig = ident.SelfSignature - } else if ident.SelfSignature.IsPrimaryId != nil && *ident.SelfSignature.IsPrimaryId { + } else if ident.SelfSignature != nil && ident.SelfSignature.IsPrimaryId != nil && *ident.SelfSignature.IsPrimaryId { selfSig = ident.SelfSignature break } } - if selfSig.KeyLifetimeSecs != nil { + if selfSig != nil && selfSig.KeyLifetimeSecs != nil { expiry = e.PrimaryKey.CreationTime.Add(time.Duration(*selfSig.KeyLifetimeSecs) * time.Second) } return expiry @@ -119,23 +120,23 @@ func readArmoredSign(r io.Reader) (body io.Reader, err error) { return block.Body, nil } -func extractSignature(s string) (*packet.Signature, error) { +func ExtractSignature(s string) (*packet.Signature, error) { r, err := readArmoredSign(strings.NewReader(s)) if err != nil { - return nil, fmt.Errorf("Failed to read signature armor") + return nil, errors.New("Failed to read signature armor") } p, err := packet.Read(r) if err != nil { - return nil, fmt.Errorf("Failed to read signature packet") + return nil, errors.New("Failed to read signature packet") } sig, ok := p.(*packet.Signature) if !ok { - return nil, fmt.Errorf("Packet is not a signature") + return nil, errors.New("Packet is not a signature") } return sig, nil } -func tryGetKeyIDFromSignature(sig *packet.Signature) string { +func TryGetKeyIDFromSignature(sig *packet.Signature) string { if sig.IssuerKeyId != nil && (*sig.IssuerKeyId) != 0 { return fmt.Sprintf("%016X", *sig.IssuerKeyId) } diff --git a/models/asymkey/gpg_key_test.go b/models/asymkey/gpg_key_test.go index d3fbb01d82b..4621337f11b 100644 --- a/models/asymkey/gpg_key_test.go +++ b/models/asymkey/gpg_key_test.go @@ -13,8 +13,10 @@ import ( "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" - "github.com/keybase/go-crypto/openpgp/packet" + "github.com/ProtonMail/go-crypto/openpgp" + "github.com/ProtonMail/go-crypto/openpgp/packet" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestCheckArmoredGPGKeyString(t *testing.T) { @@ -49,7 +51,7 @@ MkM/fdpyc2hY7Dl/+qFmN5MG5yGmMpQcX+RNNR222ibNC1D3wg== =i9b7 -----END PGP PUBLIC KEY BLOCK-----` - key, err := checkArmoredGPGKeyString(testGPGArmor) + key, err := CheckArmoredGPGKeyString(testGPGArmor) assert.NoError(t, err, "Could not parse a valid GPG public armored rsa key", key) // TODO verify value of key } @@ -70,7 +72,7 @@ OyjLLnFQiVmq7kEA/0z0CQe3ZQiQIq5zrs7Nh1XRkFAo8GlU/SGC9XFFi722 =ZiSe -----END PGP PUBLIC KEY BLOCK-----` - key, err := checkArmoredGPGKeyString(testGPGArmor) + key, err := CheckArmoredGPGKeyString(testGPGArmor) assert.NoError(t, err, "Could not parse a valid GPG public armored brainpoolP256r1 key", key) // TODO verify value of key } @@ -106,15 +108,14 @@ Av844q/BfRuVsJsK1NDNG09LC30B0l3LKBqlrRmRTUMHtgchdX2dY+p7GPOoSzlR MkM/fdpyc2hY7Dl/+qFmN5MG5yGmMpQcX+RNNR222ibNC1D3wg== =i9b7 -----END PGP PUBLIC KEY BLOCK-----` - keys, err := checkArmoredGPGKeyString(testGPGArmor) - if !assert.NotEmpty(t, keys) { - return - } + keys, err := CheckArmoredGPGKeyString(testGPGArmor) + require.NotEmpty(t, keys) + ekey := keys[0] assert.NoError(t, err, "Could not parse a valid GPG armored key", ekey) pubkey := ekey.PrimaryKey - content, err := base64EncPubKey(pubkey) + content, err := Base64EncPubKey(pubkey) assert.NoError(t, err, "Could not base64 encode a valid PublicKey content", ekey) key := &GPGKey{ @@ -175,9 +176,9 @@ committer Antoine GIRARD 1489013107 +0100 Unknown GPG key with good email ` // Reading Sign - goodSig, err := extractSignature(testGoodSigArmor) + goodSig, err := ExtractSignature(testGoodSigArmor) assert.NoError(t, err, "Could not parse a valid GPG armored signature", testGoodSigArmor) - badSig, err := extractSignature(testBadSigArmor) + badSig, err := ExtractSignature(testBadSigArmor) assert.NoError(t, err, "Could not parse a valid GPG armored signature", testBadSigArmor) // Generating hash of commit @@ -231,7 +232,7 @@ Q0KHb+QcycSgbDx0ZAvdIacuKvBBcbxrsmFUI4LR+oIup0G9gUc0roPvr014jYQL =zHo9 -----END PGP PUBLIC KEY BLOCK-----` - keys, err := AddGPGKey(db.DefaultContext, 1, testEmailWithUpperCaseLetters, "", "") + keys, err := AddGPGKey(t.Context(), 1, testEmailWithUpperCaseLetters, "", "") assert.NoError(t, err) if assert.NotEmpty(t, keys) { key := keys[0] @@ -385,7 +386,7 @@ epiDVQ== =VSKJ -----END PGP PUBLIC KEY BLOCK----- ` - keys, err := checkArmoredGPGKeyString(testIssue6599) + keys, err := CheckArmoredGPGKeyString(testIssue6599) assert.NoError(t, err) if assert.NotEmpty(t, keys) { ekey := keys[0] @@ -395,11 +396,33 @@ epiDVQ== } func TestTryGetKeyIDFromSignature(t *testing.T) { - assert.Empty(t, tryGetKeyIDFromSignature(&packet.Signature{})) - assert.Equal(t, "038D1A3EADDBEA9C", tryGetKeyIDFromSignature(&packet.Signature{ + assert.Empty(t, TryGetKeyIDFromSignature(&packet.Signature{})) + assert.Equal(t, "038D1A3EADDBEA9C", TryGetKeyIDFromSignature(&packet.Signature{ IssuerKeyId: util.ToPointer(uint64(0x38D1A3EADDBEA9C)), })) - assert.Equal(t, "038D1A3EADDBEA9C", tryGetKeyIDFromSignature(&packet.Signature{ + assert.Equal(t, "038D1A3EADDBEA9C", TryGetKeyIDFromSignature(&packet.Signature{ IssuerFingerprint: []uint8{0xb, 0x23, 0x24, 0xc7, 0xe6, 0xfe, 0x4f, 0x3a, 0x6, 0x26, 0xc1, 0x21, 0x3, 0x8d, 0x1a, 0x3e, 0xad, 0xdb, 0xea, 0x9c}, })) } + +func TestParseGPGKey(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + assert.NoError(t, db.Insert(t.Context(), &user_model.EmailAddress{UID: 1, Email: "email1@example.com", IsActivated: true})) + + // create a key for test email + e, err := openpgp.NewEntity("name", "comment", "email1@example.com", nil) + require.NoError(t, err) + k, err := parseGPGKey(t.Context(), 1, e, true) + require.NoError(t, err) + assert.NotEmpty(t, k.KeyID) + assert.NotEmpty(t, k.Emails) // the key is valid, matches the email + + // then revoke the key + for _, id := range e.Identities { + id.Revocations = append(id.Revocations, &packet.Signature{RevocationReason: util.ToPointer(packet.KeyCompromised)}) + } + k, err = parseGPGKey(t.Context(), 1, e, true) + require.NoError(t, err) + assert.NotEmpty(t, k.KeyID) + assert.Empty(t, k.Emails) // the key is revoked, matches no email +} diff --git a/models/asymkey/gpg_key_verify.go b/models/asymkey/gpg_key_verify.go index 01812a2d54f..55c64973b41 100644 --- a/models/asymkey/gpg_key_verify.go +++ b/models/asymkey/gpg_key_verify.go @@ -14,97 +14,76 @@ import ( "code.gitea.io/gitea/modules/log" ) -// __________________ ________ ____ __. -// / _____/\______ \/ _____/ | |/ _|____ ___.__. -// / \ ___ | ___/ \ ___ | <_/ __ < | | -// \ \_\ \| | \ \_\ \ | | \ ___/\___ | -// \______ /|____| \______ / |____|__ \___ > ____| -// \/ \/ \/ \/\/ -// ____ ____ .__ _____ -// \ \ / /___________|__|/ ____\__.__. -// \ Y // __ \_ __ \ \ __< | | -// \ /\ ___/| | \/ || | \___ | -// \___/ \___ >__| |__||__| / ____| -// \/ \/ - // This file provides functions relating verifying gpg keys // VerifyGPGKey marks a GPG key as verified func VerifyGPGKey(ctx context.Context, ownerID int64, keyID, token, signature string) (string, error) { - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return "", err - } - defer committer.Close() + return db.WithTx2(ctx, func(ctx context.Context) (string, error) { + key := new(GPGKey) - key := new(GPGKey) - - has, err := db.GetEngine(ctx).Where("owner_id = ? AND key_id = ?", ownerID, keyID).Get(key) - if err != nil { - return "", err - } else if !has { - return "", ErrGPGKeyNotExist{} - } - - if err := key.LoadSubKeys(ctx); err != nil { - return "", err - } - - sig, err := extractSignature(signature) - if err != nil { - return "", ErrGPGInvalidTokenSignature{ - ID: key.KeyID, - Wrapped: err, + has, err := db.GetEngine(ctx).Where("owner_id = ? AND key_id = ?", ownerID, keyID).Get(key) + if err != nil { + return "", err + } else if !has { + return "", ErrGPGKeyNotExist{} } - } - signer, err := hashAndVerifyWithSubKeys(sig, token, key) - if err != nil { - return "", ErrGPGInvalidTokenSignature{ - ID: key.KeyID, - Wrapped: err, + if err := key.LoadSubKeys(ctx); err != nil { + return "", err } - } - if signer == nil { - signer, err = hashAndVerifyWithSubKeys(sig, token+"\n", key) + + sig, err := ExtractSignature(signature) if err != nil { return "", ErrGPGInvalidTokenSignature{ ID: key.KeyID, Wrapped: err, } } - } - if signer == nil { - signer, err = hashAndVerifyWithSubKeys(sig, token+"\n\n", key) + + signer, err := hashAndVerifyWithSubKeys(sig, token, key) if err != nil { return "", ErrGPGInvalidTokenSignature{ ID: key.KeyID, Wrapped: err, } } - } - - if signer == nil { - log.Error("Unable to validate token signature. Error: %v", err) - return "", ErrGPGInvalidTokenSignature{ - ID: key.KeyID, + if signer == nil { + signer, err = hashAndVerifyWithSubKeys(sig, token+"\n", key) + if err != nil { + return "", ErrGPGInvalidTokenSignature{ + ID: key.KeyID, + Wrapped: err, + } + } + } + if signer == nil { + signer, err = hashAndVerifyWithSubKeys(sig, token+"\n\n", key) + if err != nil { + return "", ErrGPGInvalidTokenSignature{ + ID: key.KeyID, + Wrapped: err, + } + } } - } - if signer.PrimaryKeyID != key.KeyID && signer.KeyID != key.KeyID { - return "", ErrGPGKeyNotExist{} - } + if signer == nil { + log.Debug("VerifyGPGKey failed: no signer") + return "", ErrGPGInvalidTokenSignature{ + ID: key.KeyID, + } + } - key.Verified = true - if _, err := db.GetEngine(ctx).ID(key.ID).SetExpr("verified", true).Update(new(GPGKey)); err != nil { - return "", err - } + if signer.PrimaryKeyID != key.KeyID && signer.KeyID != key.KeyID { + return "", ErrGPGKeyNotExist{} + } - if err := committer.Commit(); err != nil { - return "", err - } + key.Verified = true + if _, err := db.GetEngine(ctx).ID(key.ID).SetExpr("verified", true).Update(new(GPGKey)); err != nil { + return "", err + } - return key.KeyID, nil + return key.KeyID, nil + }) } // VerificationToken returns token for the user that will be valid in minutes (time) diff --git a/models/asymkey/ssh_key.go b/models/asymkey/ssh_key.go index 7a18732c327..87205f0651e 100644 --- a/models/asymkey/ssh_key.go +++ b/models/asymkey/ssh_key.go @@ -99,40 +99,36 @@ func AddPublicKey(ctx context.Context, ownerID int64, name, content string, auth return nil, err } - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return nil, err - } - defer committer.Close() + return db.WithTx2(ctx, func(ctx context.Context) (*PublicKey, error) { + if err := checkKeyFingerprint(ctx, fingerprint); err != nil { + return nil, err + } - if err := checkKeyFingerprint(ctx, fingerprint); err != nil { - return nil, err - } + // Key name of same user cannot be duplicated. + has, err := db.GetEngine(ctx). + Where("owner_id = ? AND name = ?", ownerID, name). + Get(new(PublicKey)) + if err != nil { + return nil, err + } else if has { + return nil, ErrKeyNameAlreadyUsed{ownerID, name} + } - // Key name of same user cannot be duplicated. - has, err := db.GetEngine(ctx). - Where("owner_id = ? AND name = ?", ownerID, name). - Get(new(PublicKey)) - if err != nil { - return nil, err - } else if has { - return nil, ErrKeyNameAlreadyUsed{ownerID, name} - } + key := &PublicKey{ + OwnerID: ownerID, + Name: name, + Fingerprint: fingerprint, + Content: content, + Mode: perm.AccessModeWrite, + Type: KeyTypeUser, + LoginSourceID: authSourceID, + } + if err = addKey(ctx, key); err != nil { + return nil, fmt.Errorf("addKey: %w", err) + } - key := &PublicKey{ - OwnerID: ownerID, - Name: name, - Fingerprint: fingerprint, - Content: content, - Mode: perm.AccessModeWrite, - Type: KeyTypeUser, - LoginSourceID: authSourceID, - } - if err = addKey(ctx, key); err != nil { - return nil, fmt.Errorf("addKey: %w", err) - } - - return key, committer.Commit() + return key, nil + }) } // GetPublicKeyByID returns public key by given ID. @@ -288,33 +284,24 @@ func PublicKeyIsExternallyManaged(ctx context.Context, id int64) (bool, error) { // deleteKeysMarkedForDeletion returns true if ssh keys needs update func deleteKeysMarkedForDeletion(ctx context.Context, keys []string) (bool, error) { - // Start session - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return false, err - } - defer committer.Close() - - // Delete keys marked for deletion - var sshKeysNeedUpdate bool - for _, KeyToDelete := range keys { - key, err := SearchPublicKeyByContent(ctx, KeyToDelete) - if err != nil { - log.Error("SearchPublicKeyByContent: %v", err) - continue + return db.WithTx2(ctx, func(ctx context.Context) (bool, error) { + // Delete keys marked for deletion + var sshKeysNeedUpdate bool + for _, KeyToDelete := range keys { + key, err := SearchPublicKeyByContent(ctx, KeyToDelete) + if err != nil { + log.Error("SearchPublicKeyByContent: %v", err) + continue + } + if _, err = db.DeleteByID[PublicKey](ctx, key.ID); err != nil { + log.Error("DeleteByID[PublicKey]: %v", err) + continue + } + sshKeysNeedUpdate = true } - if _, err = db.DeleteByID[PublicKey](ctx, key.ID); err != nil { - log.Error("DeleteByID[PublicKey]: %v", err) - continue - } - sshKeysNeedUpdate = true - } - if err := committer.Commit(); err != nil { - return false, err - } - - return sshKeysNeedUpdate, nil + return sshKeysNeedUpdate, nil + }) } // AddPublicKeysBySource add a users public keys. Returns true if there are changes. @@ -355,13 +342,13 @@ func AddPublicKeysBySource(ctx context.Context, usr *user_model.User, s *auth.So return sshKeysNeedUpdate } -// SynchronizePublicKeys updates a users public keys. Returns true if there are changes. +// SynchronizePublicKeys updates a user's public keys. Returns true if there are changes. func SynchronizePublicKeys(ctx context.Context, usr *user_model.User, s *auth.Source, sshPublicKeys []string) bool { var sshKeysNeedUpdate bool log.Trace("synchronizePublicKeys[%s]: Handling Public SSH Key synchronization for user %s", s.Name, usr.Name) - // Get Public Keys from DB with current LDAP source + // Get Public Keys from DB with the current auth source var giteaKeys []string keys, err := db.Find[PublicKey](ctx, FindPublicKeyOptions{ OwnerID: usr.ID, diff --git a/models/asymkey/ssh_key_commit_verification.go b/models/asymkey/ssh_key_commit_verification.go deleted file mode 100644 index 27c6df35786..00000000000 --- a/models/asymkey/ssh_key_commit_verification.go +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright 2021 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package asymkey - -import ( - "bytes" - "context" - "fmt" - "strings" - - "code.gitea.io/gitea/models/db" - user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/modules/git" - "code.gitea.io/gitea/modules/log" - - "github.com/42wim/sshsig" -) - -// ParseCommitWithSSHSignature check if signature is good against keystore. -func ParseCommitWithSSHSignature(ctx context.Context, c *git.Commit, committer *user_model.User) *CommitVerification { - // Now try to associate the signature with the committer, if present - if committer.ID != 0 { - keys, err := db.Find[PublicKey](ctx, FindPublicKeyOptions{ - OwnerID: committer.ID, - NotKeytype: KeyTypePrincipal, - }) - if err != nil { // Skipping failed to get ssh keys of user - log.Error("ListPublicKeys: %v", err) - return &CommitVerification{ - CommittingUser: committer, - Verified: false, - Reason: "gpg.error.failed_retrieval_gpg_keys", - } - } - - committerEmailAddresses, err := user_model.GetEmailAddresses(ctx, committer.ID) - if err != nil { - log.Error("GetEmailAddresses: %v", err) - } - - activated := false - for _, e := range committerEmailAddresses { - if e.IsActivated && strings.EqualFold(e.Email, c.Committer.Email) { - activated = true - break - } - } - - for _, k := range keys { - if k.Verified && activated { - commitVerification := verifySSHCommitVerification(c.Signature.Signature, c.Signature.Payload, k, committer, committer, c.Committer.Email) - if commitVerification != nil { - return commitVerification - } - } - } - } - - return &CommitVerification{ - CommittingUser: committer, - Verified: false, - Reason: NoKeyFound, - } -} - -func verifySSHCommitVerification(sig, payload string, k *PublicKey, committer, signer *user_model.User, email string) *CommitVerification { - if err := sshsig.Verify(bytes.NewBuffer([]byte(payload)), []byte(sig), []byte(k.Content), "git"); err != nil { - return nil - } - - return &CommitVerification{ // Everything is ok - CommittingUser: committer, - Verified: true, - Reason: fmt.Sprintf("%s / %s", signer.Name, k.Fingerprint), - SigningUser: signer, - SigningSSHKey: k, - SigningEmail: email, - } -} diff --git a/models/asymkey/ssh_key_deploy.go b/models/asymkey/ssh_key_deploy.go index 923c5020edc..4ab84eabcf6 100644 --- a/models/asymkey/ssh_key_deploy.go +++ b/models/asymkey/ssh_key_deploy.go @@ -125,39 +125,35 @@ func AddDeployKey(ctx context.Context, repoID int64, name, content string, readO accessMode = perm.AccessModeWrite } - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return nil, err - } - defer committer.Close() - - pkey, exist, err := db.Get[PublicKey](ctx, builder.Eq{"fingerprint": fingerprint}) - if err != nil { - return nil, err - } else if exist { - if pkey.Type != KeyTypeDeploy { - return nil, ErrKeyAlreadyExist{0, fingerprint, ""} + return db.WithTx2(ctx, func(ctx context.Context) (*DeployKey, error) { + pkey, exist, err := db.Get[PublicKey](ctx, builder.Eq{"fingerprint": fingerprint}) + if err != nil { + return nil, err + } else if exist { + if pkey.Type != KeyTypeDeploy { + return nil, ErrKeyAlreadyExist{0, fingerprint, ""} + } + } else { + // First time use this deploy key. + pkey = &PublicKey{ + Fingerprint: fingerprint, + Mode: accessMode, + Type: KeyTypeDeploy, + Content: content, + Name: name, + } + if err = addKey(ctx, pkey); err != nil { + return nil, fmt.Errorf("addKey: %w", err) + } } - } else { - // First time use this deploy key. - pkey = &PublicKey{ - Fingerprint: fingerprint, - Mode: accessMode, - Type: KeyTypeDeploy, - Content: content, - Name: name, - } - if err = addKey(ctx, pkey); err != nil { - return nil, fmt.Errorf("addKey: %w", err) - } - } - key, err := addDeployKey(ctx, pkey.ID, repoID, name, pkey.Fingerprint, accessMode) - if err != nil { - return nil, err - } + key, err := addDeployKey(ctx, pkey.ID, repoID, name, pkey.Fingerprint, accessMode) + if err != nil { + return nil, err + } - return key, committer.Commit() + return key, nil + }) } // GetDeployKeyByID returns deploy key by given ID. diff --git a/models/asymkey/ssh_key_fingerprint.go b/models/asymkey/ssh_key_fingerprint.go index 1ed3b5df2a1..b666469ae87 100644 --- a/models/asymkey/ssh_key_fingerprint.go +++ b/models/asymkey/ssh_key_fingerprint.go @@ -6,30 +6,16 @@ package asymkey import ( "context" "fmt" - "strings" "code.gitea.io/gitea/models/db" - "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/process" - "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/util" "golang.org/x/crypto/ssh" "xorm.io/builder" ) -// ___________.__ .__ __ -// \_ _____/|__| ____ ____ ________________________|__| _____/ |_ -// | __) | |/ \ / ___\_/ __ \_ __ \____ \_ __ \ |/ \ __\ -// | \ | | | \/ /_/ > ___/| | \/ |_> > | \/ | | \ | -// \___ / |__|___| /\___ / \___ >__| | __/|__| |__|___| /__| -// \/ \//_____/ \/ |__| \/ -// -// This file contains functions for fingerprinting SSH keys -// -// The database is used in checkKeyFingerprint however most of these functions probably belong in a module +// The database is used in checkKeyFingerprint. However, most of these functions probably belong in a module -// checkKeyFingerprint only checks if key fingerprint has been used as public key, +// checkKeyFingerprint only checks if key fingerprint has been used as a public key, // it is OK to use same key as deploy key for multiple repositories/users. func checkKeyFingerprint(ctx context.Context, fingerprint string) error { has, err := db.Exist[PublicKey](ctx, builder.Eq{"fingerprint": fingerprint}) @@ -41,29 +27,6 @@ func checkKeyFingerprint(ctx context.Context, fingerprint string) error { return nil } -func calcFingerprintSSHKeygen(publicKeyContent string) (string, error) { - // Calculate fingerprint. - tmpPath, err := writeTmpKeyFile(publicKeyContent) - if err != nil { - return "", err - } - defer func() { - if err := util.Remove(tmpPath); err != nil { - log.Warn("Unable to remove temporary key file: %s: Error: %v", tmpPath, err) - } - }() - stdout, stderr, err := process.GetManager().Exec("AddPublicKey", "ssh-keygen", "-lf", tmpPath) - if err != nil { - if strings.Contains(stderr, "is not a public key file") { - return "", ErrKeyUnableVerify{stderr} - } - return "", util.NewInvalidArgumentErrorf("'ssh-keygen -lf %s' failed with error '%s': %s", tmpPath, err, stderr) - } else if len(stdout) < 2 { - return "", util.NewInvalidArgumentErrorf("not enough output for calculating fingerprint: %s", stdout) - } - return strings.Split(stdout, " ")[1], nil -} - func calcFingerprintNative(publicKeyContent string) (string, error) { // Calculate fingerprint. pk, _, _, _, err := ssh.ParseAuthorizedKey([]byte(publicKeyContent)) @@ -75,15 +38,12 @@ func calcFingerprintNative(publicKeyContent string) (string, error) { // CalcFingerprint calculate public key's fingerprint func CalcFingerprint(publicKeyContent string) (string, error) { - // Call the method based on configuration - useNative := setting.SSH.KeygenPath == "" - calcFn := util.Iif(useNative, calcFingerprintNative, calcFingerprintSSHKeygen) - fp, err := calcFn(publicKeyContent) + fp, err := calcFingerprintNative(publicKeyContent) if err != nil { if IsErrKeyUnableVerify(err) { return "", err } - return "", fmt.Errorf("CalcFingerprint(%s): %w", util.Iif(useNative, "native", "ssh-keygen"), err) + return "", fmt.Errorf("CalcFingerprint: %w", err) } return fp, nil } diff --git a/models/asymkey/ssh_key_parse.go b/models/asymkey/ssh_key_parse.go index 94b1cf112b5..fc39f28624d 100644 --- a/models/asymkey/ssh_key_parse.go +++ b/models/asymkey/ssh_key_parse.go @@ -10,14 +10,12 @@ import ( "encoding/base64" "encoding/binary" "encoding/pem" + "errors" "fmt" "math/big" - "os" - "strconv" "strings" "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/process" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" @@ -93,7 +91,7 @@ func parseKeyString(content string) (string, error) { block, _ := pem.Decode([]byte(content)) if block == nil { - return "", fmt.Errorf("failed to parse PEM block containing the public key") + return "", errors.New("failed to parse PEM block containing the public key") } if strings.Contains(block.Type, "PRIVATE") { return "", ErrKeyIsPrivate @@ -174,20 +172,9 @@ func CheckPublicKeyString(content string) (_ string, err error) { return content, nil } - var ( - fnName string - keyType string - length int - ) - if len(setting.SSH.KeygenPath) == 0 { - fnName = "SSHNativeParsePublicKey" - keyType, length, err = SSHNativeParsePublicKey(content) - } else { - fnName = "SSHKeyGenParsePublicKey" - keyType, length, err = SSHKeyGenParsePublicKey(content) - } + keyType, length, err := SSHNativeParsePublicKey(content) if err != nil { - return "", fmt.Errorf("%s: %w", fnName, err) + return "", fmt.Errorf("SSHNativeParsePublicKey: %w", err) } log.Trace("Key info [native: %v]: %s-%d", setting.SSH.StartBuiltinServer, keyType, length) @@ -221,7 +208,7 @@ func SSHNativeParsePublicKey(keyLine string) (string, int, error) { // The ssh library can parse the key, so next we find out what key exactly we have. switch pkey.Type() { - case ssh.KeyAlgoDSA: + case ssh.KeyAlgoDSA: //nolint:staticcheck // it's deprecated rawPub := struct { Name string P, Q, G, Y *big.Int @@ -257,56 +244,3 @@ func SSHNativeParsePublicKey(keyLine string) (string, int, error) { } return "", 0, fmt.Errorf("unsupported key length detection for type: %s", pkey.Type()) } - -// writeTmpKeyFile writes key content to a temporary file -// and returns the name of that file, along with any possible errors. -func writeTmpKeyFile(content string) (string, error) { - tmpFile, err := os.CreateTemp(setting.SSH.KeyTestPath, "gitea_keytest") - if err != nil { - return "", fmt.Errorf("TempFile: %w", err) - } - defer tmpFile.Close() - - if _, err = tmpFile.WriteString(content); err != nil { - return "", fmt.Errorf("WriteString: %w", err) - } - return tmpFile.Name(), nil -} - -// SSHKeyGenParsePublicKey extracts key type and length using ssh-keygen. -func SSHKeyGenParsePublicKey(key string) (string, int, error) { - tmpName, err := writeTmpKeyFile(key) - if err != nil { - return "", 0, fmt.Errorf("writeTmpKeyFile: %w", err) - } - defer func() { - if err := util.Remove(tmpName); err != nil { - log.Warn("Unable to remove temporary key file: %s: Error: %v", tmpName, err) - } - }() - - keygenPath := setting.SSH.KeygenPath - if len(keygenPath) == 0 { - keygenPath = "ssh-keygen" - } - - stdout, stderr, err := process.GetManager().Exec("SSHKeyGenParsePublicKey", keygenPath, "-lf", tmpName) - if err != nil { - return "", 0, fmt.Errorf("fail to parse public key: %s - %s", err, stderr) - } - if strings.Contains(stdout, "is not a public key file") { - return "", 0, ErrKeyUnableVerify{stdout} - } - - fields := strings.Split(stdout, " ") - if len(fields) < 4 { - return "", 0, fmt.Errorf("invalid public key line: %s", stdout) - } - - keyType := strings.Trim(fields[len(fields)-1], "()\r\n") - length, err := strconv.ParseInt(fields[0], 10, 32) - if err != nil { - return "", 0, err - } - return strings.ToLower(keyType), int(length), nil -} diff --git a/models/asymkey/ssh_key_test.go b/models/asymkey/ssh_key_test.go index 3650f1892fc..d7f48ada033 100644 --- a/models/asymkey/ssh_key_test.go +++ b/models/asymkey/ssh_key_test.go @@ -12,7 +12,6 @@ import ( "strings" "testing" - "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/modules/setting" @@ -42,29 +41,7 @@ func Test_SSHParsePublicKey(t *testing.T) { keyTypeN, lengthN, err := SSHNativeParsePublicKey(tc.content) assert.NoError(t, err) assert.Equal(t, tc.keyType, keyTypeN) - assert.EqualValues(t, tc.length, lengthN) - }) - if tc.skipSSHKeygen { - return - } - t.Run("SSHKeygen", func(t *testing.T) { - keyTypeK, lengthK, err := SSHKeyGenParsePublicKey(tc.content) - if err != nil { - // Some servers do not support ecdsa format. - if !strings.Contains(err.Error(), "line 1 too long:") { - assert.FailNow(t, "%v", err) - } - } - assert.Equal(t, tc.keyType, keyTypeK) - assert.EqualValues(t, tc.length, lengthK) - }) - t.Run("SSHParseKeyNative", func(t *testing.T) { - keyTypeK, lengthK, err := SSHNativeParsePublicKey(tc.content) - if err != nil { - assert.FailNow(t, "%v", err) - } - assert.Equal(t, tc.keyType, keyTypeK) - assert.EqualValues(t, tc.length, lengthK) + assert.Equal(t, tc.length, lengthN) }) }) } @@ -186,14 +163,6 @@ func Test_calcFingerprint(t *testing.T) { assert.NoError(t, err) assert.Equal(t, tc.fp, fpN) }) - if tc.skipSSHKeygen { - return - } - t.Run("SSHKeygen", func(t *testing.T) { - fpK, err := calcFingerprintSSHKeygen(tc.content) - assert.NoError(t, err) - assert.Equal(t, tc.fp, fpK) - }) }) } } @@ -506,7 +475,7 @@ func runErr(t *testing.T, stdin []byte, args ...string) { func Test_PublicKeysAreExternallyManaged(t *testing.T) { key1 := unittest.AssertExistsAndLoadBean(t, &PublicKey{ID: 1}) - externals, err := PublicKeysAreExternallyManaged(db.DefaultContext, []*PublicKey{key1}) + externals, err := PublicKeysAreExternallyManaged(t.Context(), []*PublicKey{key1}) assert.NoError(t, err) assert.Len(t, externals, 1) assert.False(t, externals[0]) diff --git a/models/asymkey/ssh_key_verify.go b/models/asymkey/ssh_key_verify.go index 208288c77b6..04917239eed 100644 --- a/models/asymkey/ssh_key_verify.go +++ b/models/asymkey/ssh_key_verify.go @@ -4,8 +4,8 @@ package asymkey import ( - "bytes" "context" + "strings" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/modules/log" @@ -15,41 +15,33 @@ import ( // VerifySSHKey marks a SSH key as verified func VerifySSHKey(ctx context.Context, ownerID int64, fingerprint, token, signature string) (string, error) { - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return "", err - } - defer committer.Close() + return db.WithTx2(ctx, func(ctx context.Context) (string, error) { + key := new(PublicKey) - key := new(PublicKey) + has, err := db.GetEngine(ctx).Where("owner_id = ? AND fingerprint = ?", ownerID, fingerprint).Get(key) + if err != nil { + return "", err + } else if !has { + return "", ErrKeyNotExist{} + } - has, err := db.GetEngine(ctx).Where("owner_id = ? AND fingerprint = ?", ownerID, fingerprint).Get(key) - if err != nil { - return "", err - } else if !has { - return "", ErrKeyNotExist{} - } - - err = sshsig.Verify(bytes.NewBuffer([]byte(token)), []byte(signature), []byte(key.Content), "gitea") - if err != nil { - // edge case for Windows based shells that will add CR LF if piped to ssh-keygen command - // see https://github.com/PowerShell/PowerShell/issues/5974 - if sshsig.Verify(bytes.NewBuffer([]byte(token+"\r\n")), []byte(signature), []byte(key.Content), "gitea") != nil { - log.Error("Unable to validate token signature. Error: %v", err) - return "", ErrSSHInvalidTokenSignature{ - Fingerprint: key.Fingerprint, + err = sshsig.Verify(strings.NewReader(token), []byte(signature), []byte(key.Content), "gitea") + if err != nil { + // edge case for Windows based shells that will add CR LF if piped to ssh-keygen command + // see https://github.com/PowerShell/PowerShell/issues/5974 + if sshsig.Verify(strings.NewReader(token+"\r\n"), []byte(signature), []byte(key.Content), "gitea") != nil { + log.Debug("VerifySSHKey sshsig.Verify failed: %v", err) + return "", ErrSSHInvalidTokenSignature{ + Fingerprint: key.Fingerprint, + } } } - } - key.Verified = true - if _, err := db.GetEngine(ctx).ID(key.ID).Cols("verified").Update(key); err != nil { - return "", err - } + key.Verified = true + if _, err := db.GetEngine(ctx).ID(key.ID).Cols("verified").Update(key); err != nil { + return "", err + } - if err := committer.Commit(); err != nil { - return "", err - } - - return key.Fingerprint, nil + return key.Fingerprint, nil + }) } diff --git a/models/auth/access_token_scope.go b/models/auth/access_token_scope.go index c0d6ef140ff..70867794582 100644 --- a/models/auth/access_token_scope.go +++ b/models/auth/access_token_scope.go @@ -5,6 +5,7 @@ package auth import ( "fmt" + "slices" "strings" "code.gitea.io/gitea/models/perm" @@ -14,7 +15,7 @@ import ( type AccessTokenScopeCategory int const ( - AccessTokenScopeCategoryActivityPub = iota + AccessTokenScopeCategoryActivityPub AccessTokenScopeCategory = iota AccessTokenScopeCategoryAdmin AccessTokenScopeCategoryMisc // WARN: this is now just a placeholder, don't remove it which will change the following values AccessTokenScopeCategoryNotification @@ -206,6 +207,14 @@ var accessTokenScopes = map[AccessTokenScopeLevel]map[AccessTokenScopeCategory]A }, } +func GetAccessTokenCategories() (res []string) { + for _, cat := range accessTokenScopes[Read] { + res = append(res, strings.TrimPrefix(string(cat), "read:")) + } + slices.Sort(res) + return res +} + // GetRequiredScopes gets the specific scopes for a given level and categories func GetRequiredScopes(level AccessTokenScopeLevel, scopeCategories ...AccessTokenScopeCategory) []AccessTokenScope { scopes := make([]AccessTokenScope, 0, len(scopeCategories)) @@ -217,12 +226,7 @@ func GetRequiredScopes(level AccessTokenScopeLevel, scopeCategories ...AccessTok // ContainsCategory checks if a list of categories contains a specific category func ContainsCategory(categories []AccessTokenScopeCategory, category AccessTokenScopeCategory) bool { - for _, c := range categories { - if c == category { - return true - } - } - return false + return slices.Contains(categories, category) } // GetScopeLevelFromAccessMode converts permission access mode to scope level @@ -283,6 +287,9 @@ func (s AccessTokenScope) parse() (accessTokenScopeBitmap, error) { // StringSlice returns the AccessTokenScope as a []string func (s AccessTokenScope) StringSlice() []string { + if s == "" { + return nil + } return strings.Split(string(s), ",") } @@ -296,6 +303,10 @@ func (s AccessTokenScope) Normalize() (AccessTokenScope, error) { return bitmap.toScope(), nil } +func (s AccessTokenScope) HasPermissionScope() bool { + return s != "" && s != AccessTokenScopePublicOnly +} + // PublicOnly checks if this token scope is limited to public resources func (s AccessTokenScope) PublicOnly() (bool, error) { bitmap, err := s.parse() diff --git a/models/auth/access_token_scope_test.go b/models/auth/access_token_scope_test.go index 531e29b6f27..b0117a6e1ea 100644 --- a/models/auth/access_token_scope_test.go +++ b/models/auth/access_token_scope_test.go @@ -17,6 +17,7 @@ type scopeTestNormalize struct { } func TestAccessTokenScope_Normalize(t *testing.T) { + assert.Equal(t, []string{"activitypub", "admin", "issue", "misc", "notification", "organization", "package", "repository", "user", "commitstatus"}, GetAccessTokenCategories()) tests := []scopeTestNormalize{ {"", "", nil}, {"write:misc,write:notification,read:package,write:notification,public-only", "public-only,write:misc,write:notification,read:package", nil}, @@ -25,13 +26,13 @@ func TestAccessTokenScope_Normalize(t *testing.T) { {"write:activitypub,write:admin,write:misc,write:notification,write:organization,write:package,write:issue,write:repository,write:user,write:commitstatus,public-only", "public-only,all", nil}, } - for _, scope := range []string{"activitypub", "admin", "misc", "notification", "organization", "package", "issue", "repository", "user", "commitstatus"} { + for _, scope := range GetAccessTokenCategories() { tests = append(tests, - scopeTestNormalize{AccessTokenScope(fmt.Sprintf("read:%s", scope)), AccessTokenScope(fmt.Sprintf("read:%s", scope)), nil}, - scopeTestNormalize{AccessTokenScope(fmt.Sprintf("write:%s", scope)), AccessTokenScope(fmt.Sprintf("write:%s", scope)), nil}, - scopeTestNormalize{AccessTokenScope(fmt.Sprintf("write:%[1]s,read:%[1]s", scope)), AccessTokenScope(fmt.Sprintf("write:%s", scope)), nil}, - scopeTestNormalize{AccessTokenScope(fmt.Sprintf("read:%[1]s,write:%[1]s", scope)), AccessTokenScope(fmt.Sprintf("write:%s", scope)), nil}, - scopeTestNormalize{AccessTokenScope(fmt.Sprintf("read:%[1]s,write:%[1]s,write:%[1]s", scope)), AccessTokenScope(fmt.Sprintf("write:%s", scope)), nil}, + scopeTestNormalize{AccessTokenScope("read:" + scope), AccessTokenScope("read:" + scope), nil}, + scopeTestNormalize{AccessTokenScope("write:" + scope), AccessTokenScope("write:" + scope), nil}, + scopeTestNormalize{AccessTokenScope(fmt.Sprintf("write:%[1]s,read:%[1]s", scope)), AccessTokenScope("write:" + scope), nil}, + scopeTestNormalize{AccessTokenScope(fmt.Sprintf("read:%[1]s,write:%[1]s", scope)), AccessTokenScope("write:" + scope), nil}, + scopeTestNormalize{AccessTokenScope(fmt.Sprintf("read:%[1]s,write:%[1]s,write:%[1]s", scope)), AccessTokenScope("write:" + scope), nil}, ) } @@ -59,23 +60,23 @@ func TestAccessTokenScope_HasScope(t *testing.T) { {"public-only", "read:issue", false, nil}, } - for _, scope := range []string{"activitypub", "admin", "misc", "notification", "organization", "package", "issue", "repository", "user"} { + for _, scope := range GetAccessTokenCategories() { tests = append(tests, scopeTestHasScope{ - AccessTokenScope(fmt.Sprintf("read:%s", scope)), - AccessTokenScope(fmt.Sprintf("read:%s", scope)), true, nil, + AccessTokenScope("read:" + scope), + AccessTokenScope("read:" + scope), true, nil, }, scopeTestHasScope{ - AccessTokenScope(fmt.Sprintf("write:%s", scope)), - AccessTokenScope(fmt.Sprintf("write:%s", scope)), true, nil, + AccessTokenScope("write:" + scope), + AccessTokenScope("write:" + scope), true, nil, }, scopeTestHasScope{ - AccessTokenScope(fmt.Sprintf("write:%s", scope)), - AccessTokenScope(fmt.Sprintf("read:%s", scope)), true, nil, + AccessTokenScope("write:" + scope), + AccessTokenScope("read:" + scope), true, nil, }, scopeTestHasScope{ - AccessTokenScope(fmt.Sprintf("read:%s", scope)), - AccessTokenScope(fmt.Sprintf("write:%s", scope)), false, nil, + AccessTokenScope("read:" + scope), + AccessTokenScope("write:" + scope), false, nil, }, ) } diff --git a/models/auth/access_token_test.go b/models/auth/access_token_test.go index 4360f1a2143..9ae072cc5fa 100644 --- a/models/auth/access_token_test.go +++ b/models/auth/access_token_test.go @@ -19,7 +19,7 @@ func TestNewAccessToken(t *testing.T) { UID: 3, Name: "Token C", } - assert.NoError(t, auth_model.NewAccessToken(db.DefaultContext, token)) + assert.NoError(t, auth_model.NewAccessToken(t.Context(), token)) unittest.AssertExistsAndLoadBean(t, token) invalidToken := &auth_model.AccessToken{ @@ -27,7 +27,7 @@ func TestNewAccessToken(t *testing.T) { UID: 2, Name: "Token F", } - assert.Error(t, auth_model.NewAccessToken(db.DefaultContext, invalidToken)) + assert.Error(t, auth_model.NewAccessToken(t.Context(), invalidToken)) } func TestAccessTokenByNameExists(t *testing.T) { @@ -40,16 +40,16 @@ func TestAccessTokenByNameExists(t *testing.T) { } // Check to make sure it doesn't exists already - exist, err := auth_model.AccessTokenByNameExists(db.DefaultContext, token) + exist, err := auth_model.AccessTokenByNameExists(t.Context(), token) assert.NoError(t, err) assert.False(t, exist) // Save it to the database - assert.NoError(t, auth_model.NewAccessToken(db.DefaultContext, token)) + assert.NoError(t, auth_model.NewAccessToken(t.Context(), token)) unittest.AssertExistsAndLoadBean(t, token) // This token must be found by name in the DB now - exist, err = auth_model.AccessTokenByNameExists(db.DefaultContext, token) + exist, err = auth_model.AccessTokenByNameExists(t.Context(), token) assert.NoError(t, err) assert.True(t, exist) @@ -60,32 +60,32 @@ func TestAccessTokenByNameExists(t *testing.T) { // Name matches but different user ID, this shouldn't exists in the // database - exist, err = auth_model.AccessTokenByNameExists(db.DefaultContext, user4Token) + exist, err = auth_model.AccessTokenByNameExists(t.Context(), user4Token) assert.NoError(t, err) assert.False(t, exist) } func TestGetAccessTokenBySHA(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - token, err := auth_model.GetAccessTokenBySHA(db.DefaultContext, "d2c6c1ba3890b309189a8e618c72a162e4efbf36") + token, err := auth_model.GetAccessTokenBySHA(t.Context(), "d2c6c1ba3890b309189a8e618c72a162e4efbf36") assert.NoError(t, err) assert.Equal(t, int64(1), token.UID) assert.Equal(t, "Token A", token.Name) assert.Equal(t, "2b3668e11cb82d3af8c6e4524fc7841297668f5008d1626f0ad3417e9fa39af84c268248b78c481daa7e5dc437784003494f", token.TokenHash) assert.Equal(t, "e4efbf36", token.TokenLastEight) - _, err = auth_model.GetAccessTokenBySHA(db.DefaultContext, "notahash") + _, err = auth_model.GetAccessTokenBySHA(t.Context(), "notahash") assert.Error(t, err) assert.True(t, auth_model.IsErrAccessTokenNotExist(err)) - _, err = auth_model.GetAccessTokenBySHA(db.DefaultContext, "") + _, err = auth_model.GetAccessTokenBySHA(t.Context(), "") assert.Error(t, err) assert.True(t, auth_model.IsErrAccessTokenEmpty(err)) } func TestListAccessTokens(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - tokens, err := db.Find[auth_model.AccessToken](db.DefaultContext, auth_model.ListAccessTokensOptions{UserID: 1}) + tokens, err := db.Find[auth_model.AccessToken](t.Context(), auth_model.ListAccessTokensOptions{UserID: 1}) assert.NoError(t, err) if assert.Len(t, tokens, 2) { assert.Equal(t, int64(1), tokens[0].UID) @@ -94,39 +94,39 @@ func TestListAccessTokens(t *testing.T) { assert.Contains(t, []string{tokens[0].Name, tokens[1].Name}, "Token B") } - tokens, err = db.Find[auth_model.AccessToken](db.DefaultContext, auth_model.ListAccessTokensOptions{UserID: 2}) + tokens, err = db.Find[auth_model.AccessToken](t.Context(), auth_model.ListAccessTokensOptions{UserID: 2}) assert.NoError(t, err) if assert.Len(t, tokens, 1) { assert.Equal(t, int64(2), tokens[0].UID) assert.Equal(t, "Token A", tokens[0].Name) } - tokens, err = db.Find[auth_model.AccessToken](db.DefaultContext, auth_model.ListAccessTokensOptions{UserID: 100}) + tokens, err = db.Find[auth_model.AccessToken](t.Context(), auth_model.ListAccessTokensOptions{UserID: 100}) assert.NoError(t, err) assert.Empty(t, tokens) } func TestUpdateAccessToken(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - token, err := auth_model.GetAccessTokenBySHA(db.DefaultContext, "4c6f36e6cf498e2a448662f915d932c09c5a146c") + token, err := auth_model.GetAccessTokenBySHA(t.Context(), "4c6f36e6cf498e2a448662f915d932c09c5a146c") assert.NoError(t, err) token.Name = "Token Z" - assert.NoError(t, auth_model.UpdateAccessToken(db.DefaultContext, token)) + assert.NoError(t, auth_model.UpdateAccessToken(t.Context(), token)) unittest.AssertExistsAndLoadBean(t, token) } func TestDeleteAccessTokenByID(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - token, err := auth_model.GetAccessTokenBySHA(db.DefaultContext, "4c6f36e6cf498e2a448662f915d932c09c5a146c") + token, err := auth_model.GetAccessTokenBySHA(t.Context(), "4c6f36e6cf498e2a448662f915d932c09c5a146c") assert.NoError(t, err) assert.Equal(t, int64(1), token.UID) - assert.NoError(t, auth_model.DeleteAccessTokenByID(db.DefaultContext, token.ID, 1)) + assert.NoError(t, auth_model.DeleteAccessTokenByID(t.Context(), token.ID, 1)) unittest.AssertNotExistsBean(t, token) - err = auth_model.DeleteAccessTokenByID(db.DefaultContext, 100, 100) + err = auth_model.DeleteAccessTokenByID(t.Context(), 100, 100) assert.Error(t, err) assert.True(t, auth_model.IsErrAccessTokenNotExist(err)) } diff --git a/models/auth/auth_token.go b/models/auth/auth_token.go index 81f07d1a838..54ff5a0d754 100644 --- a/models/auth/auth_token.go +++ b/models/auth/auth_token.go @@ -15,7 +15,7 @@ import ( var ErrAuthTokenNotExist = util.NewNotExistErrorf("auth token does not exist") -type AuthToken struct { //nolint:revive +type AuthToken struct { //nolint:revive // export stutter ID string `xorm:"pk"` TokenHash string UserID int64 `xorm:"INDEX"` diff --git a/models/auth/oauth2.go b/models/auth/oauth2.go index c270e4856e7..d6648413069 100644 --- a/models/auth/oauth2.go +++ b/models/auth/oauth2.go @@ -12,6 +12,7 @@ import ( "fmt" "net" "net/url" + "slices" "strings" "code.gitea.io/gitea/models/db" @@ -288,35 +289,31 @@ type UpdateOAuth2ApplicationOptions struct { // UpdateOAuth2Application updates an oauth2 application func UpdateOAuth2Application(ctx context.Context, opts UpdateOAuth2ApplicationOptions) (*OAuth2Application, error) { - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return nil, err - } - defer committer.Close() + return db.WithTx2(ctx, func(ctx context.Context) (*OAuth2Application, error) { + app, err := GetOAuth2ApplicationByID(ctx, opts.ID) + if err != nil { + return nil, err + } + if app.UID != opts.UserID { + return nil, errors.New("UID mismatch") + } + builtinApps := BuiltinApplications() + if _, builtin := builtinApps[app.ClientID]; builtin { + return nil, fmt.Errorf("failed to edit OAuth2 application: application is locked: %s", app.ClientID) + } - app, err := GetOAuth2ApplicationByID(ctx, opts.ID) - if err != nil { - return nil, err - } - if app.UID != opts.UserID { - return nil, errors.New("UID mismatch") - } - builtinApps := BuiltinApplications() - if _, builtin := builtinApps[app.ClientID]; builtin { - return nil, fmt.Errorf("failed to edit OAuth2 application: application is locked: %s", app.ClientID) - } + app.Name = opts.Name + app.RedirectURIs = opts.RedirectURIs + app.ConfidentialClient = opts.ConfidentialClient + app.SkipSecondaryAuthorization = opts.SkipSecondaryAuthorization - app.Name = opts.Name - app.RedirectURIs = opts.RedirectURIs - app.ConfidentialClient = opts.ConfidentialClient - app.SkipSecondaryAuthorization = opts.SkipSecondaryAuthorization + if err = updateOAuth2Application(ctx, app); err != nil { + return nil, err + } + app.ClientSecret = "" - if err = updateOAuth2Application(ctx, app); err != nil { - return nil, err - } - app.ClientSecret = "" - - return app, committer.Commit() + return app, nil + }) } func updateOAuth2Application(ctx context.Context, app *OAuth2Application) error { @@ -357,23 +354,17 @@ func deleteOAuth2Application(ctx context.Context, id, userid int64) error { // DeleteOAuth2Application deletes the application with the given id and the grants and auth codes related to it. It checks if the userid was the creator of the app. func DeleteOAuth2Application(ctx context.Context, id, userid int64) error { - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return err - } - defer committer.Close() - app, err := GetOAuth2ApplicationByID(ctx, id) - if err != nil { - return err - } - builtinApps := BuiltinApplications() - if _, builtin := builtinApps[app.ClientID]; builtin { - return fmt.Errorf("failed to delete OAuth2 application: application is locked: %s", app.ClientID) - } - if err := deleteOAuth2Application(ctx, id, userid); err != nil { - return err - } - return committer.Commit() + return db.WithTx(ctx, func(ctx context.Context) error { + app, err := GetOAuth2ApplicationByID(ctx, id) + if err != nil { + return err + } + builtinApps := BuiltinApplications() + if _, builtin := builtinApps[app.ClientID]; builtin { + return fmt.Errorf("failed to delete OAuth2 application: application is locked: %s", app.ClientID) + } + return deleteOAuth2Application(ctx, id, userid) + }) } ////////////////////////////////////////////////////// @@ -511,12 +502,7 @@ func (grant *OAuth2Grant) IncreaseCounter(ctx context.Context) error { // ScopeContains returns true if the grant scope contains the specified scope func (grant *OAuth2Grant) ScopeContains(scope string) bool { - for _, currentScope := range strings.Split(grant.Scope, " ") { - if scope == currentScope { - return true - } - } - return false + return slices.Contains(strings.Split(grant.Scope, " "), scope) } // SetNonce updates the current nonce value of a grant @@ -616,8 +602,8 @@ func (err ErrOAuthApplicationNotFound) Unwrap() error { return util.ErrNotExist } -// GetActiveOAuth2SourceByName returns a OAuth2 AuthSource based on the given name -func GetActiveOAuth2SourceByName(ctx context.Context, name string) (*Source, error) { +// GetActiveOAuth2SourceByAuthName returns a OAuth2 AuthSource based on the given name +func GetActiveOAuth2SourceByAuthName(ctx context.Context, name string) (*Source, error) { authSource := new(Source) has, err := db.GetEngine(ctx).Where("name = ? and type = ? and is_active = ?", name, OAuth2, true).Get(authSource) if err != nil { diff --git a/models/auth/oauth2_test.go b/models/auth/oauth2_test.go index 0829d31d51b..97f750755a2 100644 --- a/models/auth/oauth2_test.go +++ b/models/auth/oauth2_test.go @@ -7,7 +7,6 @@ import ( "testing" auth_model "code.gitea.io/gitea/models/auth" - "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/unittest" "github.com/stretchr/testify/assert" @@ -16,17 +15,17 @@ import ( func TestOAuth2Application_GenerateClientSecret(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) app := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Application{ID: 1}) - secret, err := app.GenerateClientSecret(db.DefaultContext) + secret, err := app.GenerateClientSecret(t.Context()) assert.NoError(t, err) - assert.True(t, len(secret) > 0) + assert.NotEmpty(t, secret) unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Application{ID: 1, ClientSecret: app.ClientSecret}) } func BenchmarkOAuth2Application_GenerateClientSecret(b *testing.B) { assert.NoError(b, unittest.PrepareTestDatabase()) app := unittest.AssertExistsAndLoadBean(b, &auth_model.OAuth2Application{ID: 1}) - for i := 0; i < b.N; i++ { - _, _ = app.GenerateClientSecret(db.DefaultContext) + for b.Loop() { + _, _ = app.GenerateClientSecret(b.Context()) } } @@ -76,7 +75,7 @@ func TestOAuth2Application_ContainsRedirect_Slash(t *testing.T) { func TestOAuth2Application_ValidateClientSecret(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) app := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Application{ID: 1}) - secret, err := app.GenerateClientSecret(db.DefaultContext) + secret, err := app.GenerateClientSecret(t.Context()) assert.NoError(t, err) assert.True(t, app.ValidateClientSecret([]byte(secret))) assert.False(t, app.ValidateClientSecret([]byte("fewijfowejgfiowjeoifew"))) @@ -84,18 +83,18 @@ func TestOAuth2Application_ValidateClientSecret(t *testing.T) { func TestGetOAuth2ApplicationByClientID(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - app, err := auth_model.GetOAuth2ApplicationByClientID(db.DefaultContext, "da7da3ba-9a13-4167-856f-3899de0b0138") + app, err := auth_model.GetOAuth2ApplicationByClientID(t.Context(), "da7da3ba-9a13-4167-856f-3899de0b0138") assert.NoError(t, err) assert.Equal(t, "da7da3ba-9a13-4167-856f-3899de0b0138", app.ClientID) - app, err = auth_model.GetOAuth2ApplicationByClientID(db.DefaultContext, "invalid client id") + app, err = auth_model.GetOAuth2ApplicationByClientID(t.Context(), "invalid client id") assert.Error(t, err) assert.Nil(t, app) } func TestCreateOAuth2Application(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - app, err := auth_model.CreateOAuth2Application(db.DefaultContext, auth_model.CreateOAuth2ApplicationOptions{Name: "newapp", UserID: 1}) + app, err := auth_model.CreateOAuth2Application(t.Context(), auth_model.CreateOAuth2ApplicationOptions{Name: "newapp", UserID: 1}) assert.NoError(t, err) assert.Equal(t, "newapp", app.Name) assert.Len(t, app.ClientID, 36) @@ -109,11 +108,11 @@ func TestOAuth2Application_TableName(t *testing.T) { func TestOAuth2Application_GetGrantByUserID(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) app := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Application{ID: 1}) - grant, err := app.GetGrantByUserID(db.DefaultContext, 1) + grant, err := app.GetGrantByUserID(t.Context(), 1) assert.NoError(t, err) assert.Equal(t, int64(1), grant.UserID) - grant, err = app.GetGrantByUserID(db.DefaultContext, 34923458) + grant, err = app.GetGrantByUserID(t.Context(), 34923458) assert.NoError(t, err) assert.Nil(t, grant) } @@ -121,23 +120,23 @@ func TestOAuth2Application_GetGrantByUserID(t *testing.T) { func TestOAuth2Application_CreateGrant(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) app := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Application{ID: 1}) - grant, err := app.CreateGrant(db.DefaultContext, 2, "") + grant, err := app.CreateGrant(t.Context(), 2, "") assert.NoError(t, err) assert.NotNil(t, grant) assert.Equal(t, int64(2), grant.UserID) assert.Equal(t, int64(1), grant.ApplicationID) - assert.Equal(t, "", grant.Scope) + assert.Empty(t, grant.Scope) } //////////////////// Grant func TestGetOAuth2GrantByID(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - grant, err := auth_model.GetOAuth2GrantByID(db.DefaultContext, 1) + grant, err := auth_model.GetOAuth2GrantByID(t.Context(), 1) assert.NoError(t, err) assert.Equal(t, int64(1), grant.ID) - grant, err = auth_model.GetOAuth2GrantByID(db.DefaultContext, 34923458) + grant, err = auth_model.GetOAuth2GrantByID(t.Context(), 34923458) assert.NoError(t, err) assert.Nil(t, grant) } @@ -145,7 +144,7 @@ func TestGetOAuth2GrantByID(t *testing.T) { func TestOAuth2Grant_IncreaseCounter(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) grant := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Grant{ID: 1, Counter: 1}) - assert.NoError(t, grant.IncreaseCounter(db.DefaultContext)) + assert.NoError(t, grant.IncreaseCounter(t.Context())) assert.Equal(t, int64(2), grant.Counter) unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Grant{ID: 1, Counter: 2}) } @@ -162,10 +161,10 @@ func TestOAuth2Grant_ScopeContains(t *testing.T) { func TestOAuth2Grant_GenerateNewAuthorizationCode(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) grant := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Grant{ID: 1}) - code, err := grant.GenerateNewAuthorizationCode(db.DefaultContext, "https://example2.com/callback", "CjvyTLSdR47G5zYenDA-eDWW4lRrO8yvjcWwbD_deOg", "S256") + code, err := grant.GenerateNewAuthorizationCode(t.Context(), "https://example2.com/callback", "CjvyTLSdR47G5zYenDA-eDWW4lRrO8yvjcWwbD_deOg", "S256") assert.NoError(t, err) assert.NotNil(t, code) - assert.True(t, len(code.Code) > 32) // secret length > 32 + assert.Greater(t, len(code.Code), 32) // secret length > 32 } func TestOAuth2Grant_TableName(t *testing.T) { @@ -174,20 +173,20 @@ func TestOAuth2Grant_TableName(t *testing.T) { func TestGetOAuth2GrantsByUserID(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - result, err := auth_model.GetOAuth2GrantsByUserID(db.DefaultContext, 1) + result, err := auth_model.GetOAuth2GrantsByUserID(t.Context(), 1) assert.NoError(t, err) assert.Len(t, result, 1) assert.Equal(t, int64(1), result[0].ID) assert.Equal(t, result[0].ApplicationID, result[0].Application.ID) - result, err = auth_model.GetOAuth2GrantsByUserID(db.DefaultContext, 34134) + result, err = auth_model.GetOAuth2GrantsByUserID(t.Context(), 34134) assert.NoError(t, err) assert.Empty(t, result) } func TestRevokeOAuth2Grant(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - assert.NoError(t, auth_model.RevokeOAuth2Grant(db.DefaultContext, 1, 1)) + assert.NoError(t, auth_model.RevokeOAuth2Grant(t.Context(), 1, 1)) unittest.AssertNotExistsBean(t, &auth_model.OAuth2Grant{ID: 1, UserID: 1}) } @@ -195,13 +194,13 @@ func TestRevokeOAuth2Grant(t *testing.T) { func TestGetOAuth2AuthorizationByCode(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - code, err := auth_model.GetOAuth2AuthorizationByCode(db.DefaultContext, "authcode") + code, err := auth_model.GetOAuth2AuthorizationByCode(t.Context(), "authcode") assert.NoError(t, err) assert.NotNil(t, code) assert.Equal(t, "authcode", code.Code) assert.Equal(t, int64(1), code.ID) - code, err = auth_model.GetOAuth2AuthorizationByCode(db.DefaultContext, "does not exist") + code, err = auth_model.GetOAuth2AuthorizationByCode(t.Context(), "does not exist") assert.NoError(t, err) assert.Nil(t, code) } @@ -256,7 +255,7 @@ func TestOAuth2AuthorizationCode_GenerateRedirectURI(t *testing.T) { func TestOAuth2AuthorizationCode_Invalidate(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) code := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2AuthorizationCode{Code: "authcode"}) - assert.NoError(t, code.Invalidate(db.DefaultContext)) + assert.NoError(t, code.Invalidate(t.Context())) unittest.AssertNotExistsBean(t, &auth_model.OAuth2AuthorizationCode{Code: "authcode"}) } diff --git a/models/auth/session.go b/models/auth/session.go index 75a205f702b..dbdcde03a0b 100644 --- a/models/auth/session.go +++ b/models/auth/session.go @@ -35,26 +35,22 @@ func UpdateSession(ctx context.Context, key string, data []byte) error { // ReadSession reads the data for the provided session func ReadSession(ctx context.Context, key string) (*Session, error) { - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return nil, err - } - defer committer.Close() - - session, exist, err := db.Get[Session](ctx, builder.Eq{"`key`": key}) - if err != nil { - return nil, err - } else if !exist { - session = &Session{ - Key: key, - Expiry: timeutil.TimeStampNow(), - } - if err := db.Insert(ctx, session); err != nil { + return db.WithTx2(ctx, func(ctx context.Context) (*Session, error) { + session, exist, err := db.Get[Session](ctx, builder.Eq{"`key`": key}) + if err != nil { return nil, err + } else if !exist { + session = &Session{ + Key: key, + Expiry: timeutil.TimeStampNow(), + } + if err := db.Insert(ctx, session); err != nil { + return nil, err + } } - } - return session, committer.Commit() + return session, nil + }) } // ExistSession checks if a session exists @@ -72,40 +68,36 @@ func DestroySession(ctx context.Context, key string) error { // RegenerateSession regenerates a session from the old id func RegenerateSession(ctx context.Context, oldKey, newKey string) (*Session, error) { - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return nil, err - } - defer committer.Close() + return db.WithTx2(ctx, func(ctx context.Context) (*Session, error) { + if has, err := db.Exist[Session](ctx, builder.Eq{"`key`": newKey}); err != nil { + return nil, err + } else if has { + return nil, fmt.Errorf("session Key: %s already exists", newKey) + } - if has, err := db.Exist[Session](ctx, builder.Eq{"`key`": newKey}); err != nil { - return nil, err - } else if has { - return nil, fmt.Errorf("session Key: %s already exists", newKey) - } + if has, err := db.Exist[Session](ctx, builder.Eq{"`key`": oldKey}); err != nil { + return nil, err + } else if !has { + if err := db.Insert(ctx, &Session{ + Key: oldKey, + Expiry: timeutil.TimeStampNow(), + }); err != nil { + return nil, err + } + } - if has, err := db.Exist[Session](ctx, builder.Eq{"`key`": oldKey}); err != nil { - return nil, err - } else if !has { - if err := db.Insert(ctx, &Session{ - Key: oldKey, - Expiry: timeutil.TimeStampNow(), - }); err != nil { + if _, err := db.Exec(ctx, "UPDATE `session` SET `key` = ? WHERE `key`=?", newKey, oldKey); err != nil { return nil, err } - } - if _, err := db.Exec(ctx, "UPDATE "+db.TableName(&Session{})+" SET `key` = ? WHERE `key`=?", newKey, oldKey); err != nil { - return nil, err - } + s, _, err := db.Get[Session](ctx, builder.Eq{"`key`": newKey}) + if err != nil { + // is not exist, it should be impossible + return nil, err + } - s, _, err := db.Get[Session](ctx, builder.Eq{"`key`": newKey}) - if err != nil { - // is not exist, it should be impossible - return nil, err - } - - return s, committer.Commit() + return s, nil + }) } // CountSessions returns the number of sessions diff --git a/models/auth/source.go b/models/auth/source.go index a3a250cd91d..08cfc9615b0 100644 --- a/models/auth/source.go +++ b/models/auth/source.go @@ -58,6 +58,15 @@ var Names = map[Type]string{ // Config represents login config as far as the db is concerned type Config interface { convert.Conversion + SetAuthSource(*Source) +} + +type ConfigBase struct { + AuthSource *Source +} + +func (p *ConfigBase) SetAuthSource(s *Source) { + p.AuthSource = s } // SkipVerifiable configurations provide a IsSkipVerify to check if SkipVerify is set @@ -104,19 +113,15 @@ func RegisterTypeConfig(typ Type, exemplar Config) { } } -// SourceSettable configurations can have their authSource set on them -type SourceSettable interface { - SetAuthSource(*Source) -} - // Source represents an external way for authorizing users. type Source struct { - ID int64 `xorm:"pk autoincr"` - Type Type - Name string `xorm:"UNIQUE"` - IsActive bool `xorm:"INDEX NOT NULL DEFAULT false"` - IsSyncEnabled bool `xorm:"INDEX NOT NULL DEFAULT false"` - Cfg convert.Conversion `xorm:"TEXT"` + ID int64 `xorm:"pk autoincr"` + Type Type + Name string `xorm:"UNIQUE"` + IsActive bool `xorm:"INDEX NOT NULL DEFAULT false"` + IsSyncEnabled bool `xorm:"INDEX NOT NULL DEFAULT false"` + TwoFactorPolicy string `xorm:"two_factor_policy NOT NULL DEFAULT ''"` + Cfg Config `xorm:"TEXT"` CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` @@ -140,9 +145,7 @@ func (source *Source) BeforeSet(colName string, val xorm.Cell) { return } source.Cfg = constructor() - if settable, ok := source.Cfg.(SourceSettable); ok { - settable.SetAuthSource(source) - } + source.Cfg.SetAuthSource(source) } } @@ -200,6 +203,10 @@ func (source *Source) SkipVerify() bool { return ok && skipVerifiable.IsSkipVerify() } +func (source *Source) TwoFactorShouldSkip() bool { + return source.TwoFactorPolicy == "skip" +} + // CreateSource inserts a AuthSource in the DB if not already // existing with the given name. func CreateSource(ctx context.Context, source *Source) error { @@ -223,9 +230,7 @@ func CreateSource(ctx context.Context, source *Source) error { return nil } - if settable, ok := source.Cfg.(SourceSettable); ok { - settable.SetAuthSource(source) - } + source.Cfg.SetAuthSource(source) registerableSource, ok := source.Cfg.(RegisterableSource) if !ok { @@ -320,9 +325,7 @@ func UpdateSource(ctx context.Context, source *Source) error { return nil } - if settable, ok := source.Cfg.(SourceSettable); ok { - settable.SetAuthSource(source) - } + source.Cfg.SetAuthSource(source) registerableSource, ok := source.Cfg.(RegisterableSource) if !ok { @@ -331,7 +334,7 @@ func UpdateSource(ctx context.Context, source *Source) error { err = registerableSource.RegisterSource() if err != nil { - // restore original values since we cannot update the provider it self + // restore original values since we cannot update the provider itself if _, err := db.GetEngine(ctx).ID(source.ID).AllCols().Update(originalSource); err != nil { log.Error("UpdateSource: Error while wrapOpenIDConnectInitializeError: %v", err) } diff --git a/models/auth/source_test.go b/models/auth/source_test.go index 36e76d5e281..ebc462c5811 100644 --- a/models/auth/source_test.go +++ b/models/auth/source_test.go @@ -8,15 +8,17 @@ import ( "testing" auth_model "code.gitea.io/gitea/models/auth" - "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/modules/json" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "xorm.io/xorm/schemas" ) type TestSource struct { + auth_model.ConfigBase + Provider string ClientID string ClientSecret string @@ -37,12 +39,12 @@ func (source *TestSource) ToDB() ([]byte, error) { func TestDumpAuthSource(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - authSourceSchema, err := db.TableInfo(new(auth_model.Source)) + authSourceSchema, err := unittest.GetXORMEngine().TableInfo(new(auth_model.Source)) assert.NoError(t, err) auth_model.RegisterTypeConfig(auth_model.OAuth2, new(TestSource)) - auth_model.CreateSource(db.DefaultContext, &auth_model.Source{ + auth_model.CreateSource(t.Context(), &auth_model.Source{ Type: auth_model.OAuth2, Name: "TestSource", IsActive: false, @@ -54,7 +56,8 @@ func TestDumpAuthSource(t *testing.T) { 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 := unittest.GetXORMEngine() + require.NoError(t, engine.DumpTables([]*schemas.Table{authSourceSchema}, sb)) assert.Contains(t, sb.String(), `"Provider":"ConvertibleSourceName"`) } diff --git a/models/auth/twofactor.go b/models/auth/twofactor.go index d0c341a192f..200ce7c7c0e 100644 --- a/models/auth/twofactor.go +++ b/models/auth/twofactor.go @@ -164,3 +164,13 @@ func DeleteTwoFactorByID(ctx context.Context, id, userID int64) error { } return nil } + +func HasTwoFactorOrWebAuthn(ctx context.Context, id int64) (bool, error) { + has, err := HasTwoFactorByUID(ctx, id) + if err != nil { + return false, err + } else if has { + return true, nil + } + return HasWebAuthnRegistrationsByUID(ctx, id) +} diff --git a/models/auth/webauthn_test.go b/models/auth/webauthn_test.go index f1cf398adf5..41c5f6de94a 100644 --- a/models/auth/webauthn_test.go +++ b/models/auth/webauthn_test.go @@ -7,7 +7,6 @@ import ( "testing" auth_model "code.gitea.io/gitea/models/auth" - "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/unittest" "github.com/go-webauthn/webauthn/webauthn" @@ -17,11 +16,11 @@ import ( func TestGetWebAuthnCredentialByID(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - res, err := auth_model.GetWebAuthnCredentialByID(db.DefaultContext, 1) + res, err := auth_model.GetWebAuthnCredentialByID(t.Context(), 1) assert.NoError(t, err) assert.Equal(t, "WebAuthn credential", res.Name) - _, err = auth_model.GetWebAuthnCredentialByID(db.DefaultContext, 342432) + _, err = auth_model.GetWebAuthnCredentialByID(t.Context(), 342432) assert.Error(t, err) assert.True(t, auth_model.IsErrWebAuthnCredentialNotExist(err)) } @@ -29,7 +28,7 @@ func TestGetWebAuthnCredentialByID(t *testing.T) { func TestGetWebAuthnCredentialsByUID(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - res, err := auth_model.GetWebAuthnCredentialsByUID(db.DefaultContext, 32) + res, err := auth_model.GetWebAuthnCredentialsByUID(t.Context(), 32) assert.NoError(t, err) assert.Len(t, res, 1) assert.Equal(t, "WebAuthn credential", res[0].Name) @@ -43,25 +42,25 @@ func TestWebAuthnCredential_UpdateSignCount(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) cred := unittest.AssertExistsAndLoadBean(t, &auth_model.WebAuthnCredential{ID: 1}) cred.SignCount = 1 - assert.NoError(t, cred.UpdateSignCount(db.DefaultContext)) - unittest.AssertExistsIf(t, true, &auth_model.WebAuthnCredential{ID: 1, SignCount: 1}) + assert.NoError(t, cred.UpdateSignCount(t.Context())) + unittest.AssertExistsAndLoadBean(t, &auth_model.WebAuthnCredential{ID: 1, SignCount: 1}) } func TestWebAuthnCredential_UpdateLargeCounter(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) cred := unittest.AssertExistsAndLoadBean(t, &auth_model.WebAuthnCredential{ID: 1}) cred.SignCount = 0xffffffff - assert.NoError(t, cred.UpdateSignCount(db.DefaultContext)) - unittest.AssertExistsIf(t, true, &auth_model.WebAuthnCredential{ID: 1, SignCount: 0xffffffff}) + assert.NoError(t, cred.UpdateSignCount(t.Context())) + unittest.AssertExistsAndLoadBean(t, &auth_model.WebAuthnCredential{ID: 1, SignCount: 0xffffffff}) } func TestCreateCredential(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - res, err := auth_model.CreateCredential(db.DefaultContext, 1, "WebAuthn Created Credential", &webauthn.Credential{ID: []byte("Test")}) + res, err := auth_model.CreateCredential(t.Context(), 1, "WebAuthn Created Credential", &webauthn.Credential{ID: []byte("Test")}) assert.NoError(t, err) assert.Equal(t, "WebAuthn Created Credential", res.Name) assert.Equal(t, []byte("Test"), res.CredentialID) - unittest.AssertExistsIf(t, true, &auth_model.WebAuthnCredential{Name: "WebAuthn Created Credential", UserID: 1}) + unittest.AssertExistsAndLoadBean(t, &auth_model.WebAuthnCredential{Name: "WebAuthn Created Credential", UserID: 1}) } diff --git a/models/avatars/avatar_test.go b/models/avatars/avatar_test.go index c8f7a6574bf..43a062cc2a1 100644 --- a/models/avatars/avatar_test.go +++ b/models/avatars/avatar_test.go @@ -7,7 +7,6 @@ import ( "testing" avatars_model "code.gitea.io/gitea/models/avatars" - "code.gitea.io/gitea/models/db" system_model "code.gitea.io/gitea/models/system" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting/config" @@ -18,14 +17,14 @@ import ( const gravatarSource = "https://secure.gravatar.com/avatar/" func disableGravatar(t *testing.T) { - err := system_model.SetSettings(db.DefaultContext, map[string]string{setting.Config().Picture.EnableFederatedAvatar.DynKey(): "false"}) + err := system_model.SetSettings(t.Context(), map[string]string{setting.Config().Picture.EnableFederatedAvatar.DynKey(): "false"}) assert.NoError(t, err) - err = system_model.SetSettings(db.DefaultContext, map[string]string{setting.Config().Picture.DisableGravatar.DynKey(): "true"}) + err = system_model.SetSettings(t.Context(), map[string]string{setting.Config().Picture.DisableGravatar.DynKey(): "true"}) assert.NoError(t, err) } func enableGravatar(t *testing.T) { - err := system_model.SetSettings(db.DefaultContext, map[string]string{setting.Config().Picture.DisableGravatar.DynKey(): "false"}) + err := system_model.SetSettings(t.Context(), map[string]string{setting.Config().Picture.DisableGravatar.DynKey(): "false"}) assert.NoError(t, err) setting.GravatarSource = gravatarSource } @@ -47,12 +46,12 @@ func TestSizedAvatarLink(t *testing.T) { disableGravatar(t) config.GetDynGetter().InvalidateCache() assert.Equal(t, "/testsuburl/assets/img/avatar_default.png", - avatars_model.GenerateEmailAvatarFastLink(db.DefaultContext, "gitea@example.com", 100)) + avatars_model.GenerateEmailAvatarFastLink(t.Context(), "gitea@example.com", 100)) enableGravatar(t) config.GetDynGetter().InvalidateCache() assert.Equal(t, "https://secure.gravatar.com/avatar/353cbad9b58e69c96154ad99f92bedc7?d=identicon&s=100", - avatars_model.GenerateEmailAvatarFastLink(db.DefaultContext, "gitea@example.com", 100), + avatars_model.GenerateEmailAvatarFastLink(t.Context(), "gitea@example.com", 100), ) } diff --git a/models/db/collation.go b/models/db/collation.go index a7db9f54423..79ade873803 100644 --- a/models/db/collation.go +++ b/models/db/collation.go @@ -140,7 +140,7 @@ func CheckCollations(x *xorm.Engine) (*CheckCollationsResult, error) { } func CheckCollationsDefaultEngine() (*CheckCollationsResult, error) { - return CheckCollations(x) + return CheckCollations(xormEngine) } func alterDatabaseCollation(x *xorm.Engine, collation string) error { diff --git a/models/db/context.go b/models/db/context.go index 171e26b933f..8bb14f1389b 100644 --- a/models/db/context.go +++ b/models/db/context.go @@ -17,35 +17,12 @@ import ( "xorm.io/xorm" ) -// DefaultContext is the default context to run xorm queries in -// will be overwritten by Init with HammerContext -var DefaultContext context.Context - type engineContextKeyType struct{} var engineContextKey = engineContextKeyType{} -// Context represents a db context -type Context struct { - context.Context - engine Engine -} - -func newContext(ctx context.Context, e Engine) *Context { - return &Context{Context: ctx, engine: e} -} - -// Value shadows Value for context.Context but allows us to get ourselves and an Engined object -func (ctx *Context) Value(key any) any { - if key == engineContextKey { - return ctx - } - return ctx.Context.Value(key) -} - -// WithContext returns this engine tied to this context -func (ctx *Context) WithContext(other context.Context) *Context { - return newContext(ctx, ctx.engine.Context(other)) +func withContextEngine(ctx context.Context, e Engine) context.Context { + return context.WithValue(ctx, engineContextKey, e) } var ( @@ -67,7 +44,7 @@ func contextSafetyCheck(e Engine) { _ = e.SQL("SELECT 1").Iterate(&m{}, func(int, any) error { callers := make([]uintptr, 32) callerNum := runtime.Callers(1, callers) - for i := 0; i < callerNum; i++ { + for i := range callerNum { if funcName := runtime.FuncForPC(callers[i]).Name(); funcName == "xorm.io/xorm.(*Session).Iterate" { contextSafetyDeniedFuncPCs = append(contextSafetyDeniedFuncPCs, callers[i]) } @@ -82,31 +59,26 @@ func contextSafetyCheck(e Engine) { // it should be very fast: xxxx ns/op callers := make([]uintptr, 32) callerNum := runtime.Callers(3, callers) // skip 3: runtime.Callers, contextSafetyCheck, GetEngine - for i := 0; i < callerNum; i++ { + for i := range callerNum { if slices.Contains(contextSafetyDeniedFuncPCs, callers[i]) { - panic(errors.New("using database context in an iterator would cause corrupted results")) + panic(errors.New("using session context in an iterator would cause corrupted results")) } } } // GetEngine gets an existing db Engine/Statement or creates a new Session func GetEngine(ctx context.Context) Engine { - if e := getExistingEngine(ctx); e != nil { - return e + if engine, ok := ctx.Value(engineContextKey).(Engine); ok { + // if reusing the existing session, need to do "contextSafetyCheck" because the Iterate creates a "autoResetStatement=false" session + contextSafetyCheck(engine) + return engine } - return x.Context(ctx) + // no need to do "contextSafetyCheck" because it's a new Session + return xormEngine.Context(ctx) } -// getExistingEngine gets an existing db Engine/Statement from this context or returns nil -func getExistingEngine(ctx context.Context) (e Engine) { - defer func() { contextSafetyCheck(e) }() - if engined, ok := ctx.(*Context); ok { - return engined.engine - } - if engined, ok := ctx.Value(engineContextKey).(*Context); ok { - return engined.engine - } - return nil +func GetXORMEngineForTesting() *xorm.Engine { + return xormEngine } // Committer represents an interface to Commit or Close the Context @@ -150,25 +122,24 @@ func (c *halfCommitter) Close() error { // So calling `Commit()` will do nothing, but calling `Close()` without calling `Commit()` will rollback the transaction. // And all operations submitted by the caller stack will be rollbacked as well, not only the operations in the current function. // d. It doesn't mean rollback is forbidden, but always do it only when there is an error, and you do want to rollback. -func TxContext(parentCtx context.Context) (*Context, Committer, error) { - if sess, ok := inTransaction(parentCtx); ok { - return newContext(parentCtx, sess), &halfCommitter{committer: sess}, nil +func TxContext(parentCtx context.Context) (context.Context, Committer, error) { + if sess := getTransactionSession(parentCtx); sess != nil { + return withContextEngine(parentCtx, sess), &halfCommitter{committer: sess}, nil } - sess := x.NewSession() + sess := xormEngine.NewSession() if err := sess.Begin(); err != nil { _ = sess.Close() return nil, nil, err } - - return newContext(DefaultContext, sess), sess, nil + return withContextEngine(parentCtx, sess), sess, nil } // WithTx represents executing database operations on a transaction, if the transaction exist, // this function will reuse it otherwise will create a new one and close it when finished. func WithTx(parentCtx context.Context, f func(ctx context.Context) error) error { - if sess, ok := inTransaction(parentCtx); ok { - err := f(newContext(parentCtx, sess)) + if sess := getTransactionSession(parentCtx); sess != nil { + err := f(withContextEngine(parentCtx, sess)) if err != nil { // rollback immediately, in case the caller ignores returned error and tries to commit the transaction. _ = sess.Close() @@ -178,14 +149,23 @@ func WithTx(parentCtx context.Context, f func(ctx context.Context) error) error return txWithNoCheck(parentCtx, f) } +// WithTx2 is similar to WithTx, but it has two return values: result and error. +func WithTx2[T any](parentCtx context.Context, f func(ctx context.Context) (T, error)) (ret T, errRet error) { + errRet = WithTx(parentCtx, func(ctx context.Context) (errInner error) { + ret, errInner = f(ctx) + return errInner + }) + return ret, errRet +} + func txWithNoCheck(parentCtx context.Context, f func(ctx context.Context) error) error { - sess := x.NewSession() + sess := xormEngine.NewSession() defer sess.Close() if err := sess.Begin(); err != nil { return err } - if err := f(newContext(parentCtx, sess)); err != nil { + if err := f(withContextEngine(parentCtx, sess)); err != nil { return err } @@ -289,6 +269,9 @@ func FindIDs(ctx context.Context, tableName, idCol string, cond builder.Cond) ([ // DecrByIDs decreases the given column for entities of the "bean" type with one of the given ids by one // Timestamps of the entities won't be updated func DecrByIDs(ctx context.Context, ids []int64, decrCol string, bean any) error { + if len(ids) == 0 { + return nil + } _, err := GetEngine(ctx).Decr(decrCol).In("id", ids).NoAutoCondition().NoAutoTime().Update(bean) return err } @@ -320,32 +303,15 @@ func CountByBean(ctx context.Context, bean any) (int64, error) { return GetEngine(ctx).Count(bean) } -// TableName returns the table name according a bean object -func TableName(bean any) string { - return x.TableName(bean) -} - // InTransaction returns true if the engine is in a transaction otherwise return false func InTransaction(ctx context.Context) bool { - _, ok := inTransaction(ctx) - return ok + return getTransactionSession(ctx) != nil } -func inTransaction(ctx context.Context) (*xorm.Session, bool) { - e := getExistingEngine(ctx) - if e == nil { - return nil, false - } - - switch t := e.(type) { - case *xorm.Engine: - return nil, false - case *xorm.Session: - if t.IsInTx() { - return t, true - } - return nil, false - default: - return nil, false +func getTransactionSession(ctx context.Context) *xorm.Session { + e, _ := ctx.Value(engineContextKey).(Engine) + if sess, ok := e.(*xorm.Session); ok && sess.IsInTx() { + return sess } + return nil } diff --git a/models/db/context_committer_test.go b/models/db/context_committer_test.go index 38e91f22edb..90f6aaefa1e 100644 --- a/models/db/context_committer_test.go +++ b/models/db/context_committer_test.go @@ -4,7 +4,7 @@ package db // it's not db_test, because this file is for testing the private type halfCommitter import ( - "fmt" + "errors" "testing" "github.com/stretchr/testify/assert" @@ -39,7 +39,7 @@ func Test_halfCommitter(t *testing.T) { /* Do something like: - ctx, committer, err := db.TxContext(db.DefaultContext) + ctx, committer, err := db.TxContext(t.Context()) if err != nil { return nil } @@ -80,7 +80,7 @@ func Test_halfCommitter(t *testing.T) { testWithCommitter(mockCommitter, func(committer Committer) error { defer committer.Close() if true { - return fmt.Errorf("error") + return errors.New("error") } return committer.Commit() }) @@ -94,7 +94,7 @@ func Test_halfCommitter(t *testing.T) { testWithCommitter(mockCommitter, func(committer Committer) error { committer.Close() committer.Commit() - return fmt.Errorf("error") + return errors.New("error") }) mockCommitter.Assert(t) diff --git a/models/db/context_test.go b/models/db/context_test.go index e8c6b74d93f..1719a7bfe85 100644 --- a/models/db/context_test.go +++ b/models/db/context_test.go @@ -15,13 +15,13 @@ import ( func TestInTransaction(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - assert.False(t, db.InTransaction(db.DefaultContext)) - assert.NoError(t, db.WithTx(db.DefaultContext, func(ctx context.Context) error { + assert.False(t, db.InTransaction(t.Context())) + assert.NoError(t, db.WithTx(t.Context(), func(ctx context.Context) error { assert.True(t, db.InTransaction(ctx)) return nil })) - ctx, committer, err := db.TxContext(db.DefaultContext) + ctx, committer, err := db.TxContext(t.Context()) assert.NoError(t, err) defer committer.Close() assert.True(t, db.InTransaction(ctx)) @@ -35,14 +35,14 @@ func TestTxContext(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) { // create new transaction - ctx, committer, err := db.TxContext(db.DefaultContext) + ctx, committer, err := db.TxContext(t.Context()) assert.NoError(t, err) assert.True(t, db.InTransaction(ctx)) assert.NoError(t, committer.Commit()) } { // reuse the transaction created by TxContext and commit it - ctx, committer, err := db.TxContext(db.DefaultContext) + ctx, committer, err := db.TxContext(t.Context()) engine := db.GetEngine(ctx) assert.NoError(t, err) assert.True(t, db.InTransaction(ctx)) @@ -57,7 +57,7 @@ func TestTxContext(t *testing.T) { } { // reuse the transaction created by TxContext and close it - ctx, committer, err := db.TxContext(db.DefaultContext) + ctx, committer, err := db.TxContext(t.Context()) engine := db.GetEngine(ctx) assert.NoError(t, err) assert.True(t, db.InTransaction(ctx)) @@ -72,7 +72,7 @@ func TestTxContext(t *testing.T) { } { // reuse the transaction created by WithTx - assert.NoError(t, db.WithTx(db.DefaultContext, func(ctx context.Context) error { + assert.NoError(t, db.WithTx(t.Context(), func(ctx context.Context) error { assert.True(t, db.InTransaction(ctx)) { ctx, committer, err := db.TxContext(ctx) @@ -93,38 +93,43 @@ func TestContextSafety(t *testing.T) { ID int64 } assert.NoError(t, unittest.GetXORMEngine().Sync(&TestModel1{}, &TestModel2{})) - assert.NoError(t, db.TruncateBeans(db.DefaultContext, &TestModel1{}, &TestModel2{})) + assert.NoError(t, db.TruncateBeans(t.Context(), &TestModel1{}, &TestModel2{})) testCount := 10 for i := 1; i <= testCount; i++ { - assert.NoError(t, db.Insert(db.DefaultContext, &TestModel1{ID: int64(i)})) - assert.NoError(t, db.Insert(db.DefaultContext, &TestModel2{ID: int64(-i)})) + assert.NoError(t, db.Insert(t.Context(), &TestModel1{ID: int64(i)})) + assert.NoError(t, db.Insert(t.Context(), &TestModel2{ID: int64(-i)})) } - actualCount := 0 - // here: db.GetEngine(db.DefaultContext) is a new *Session created from *Engine - _ = db.WithTx(db.DefaultContext, func(ctx context.Context) error { - _ = db.GetEngine(ctx).Iterate(&TestModel1{}, func(i int, bean any) error { - // here: db.GetEngine(ctx) is always the unclosed "Iterate" *Session with autoResetStatement=false, - // and the internal states (including "cond" and others) are always there and not be reset in this callback. - m1 := bean.(*TestModel1) - assert.EqualValues(t, i+1, m1.ID) + t.Run("Show-XORM-Bug", func(t *testing.T) { + actualCount := 0 + // here: db.GetEngine(t.Context()) is a new *Session created from *Engine + _ = db.WithTx(t.Context(), func(ctx context.Context) error { + _ = db.GetEngine(ctx).Iterate(&TestModel1{}, func(i int, bean any) error { + // here: db.GetEngine(ctx) is always the unclosed "Iterate" *Session with autoResetStatement=false, + // and the internal states (including "cond" and others) are always there and not be reset in this callback. + m1 := bean.(*TestModel1) + assert.EqualValues(t, i+1, m1.ID) - // here: XORM bug, it fails because the SQL becomes "WHERE id=-1", "WHERE id=-1 AND id=-2", "WHERE id=-1 AND id=-2 AND id=-3" ... - // and it conflicts with the "Iterate"'s internal states. - // has, err := db.GetEngine(ctx).Get(&TestModel2{ID: -m1.ID}) + // here: XORM bug, it fails because the SQL becomes "WHERE id=-1", "WHERE id=-1 AND id=-2", "WHERE id=-1 AND id=-2 AND id=-3" ... + // and it conflicts with the "Iterate"'s internal states. + // has, err := db.GetEngine(ctx).Get(&TestModel2{ID: -m1.ID}) - actualCount++ + actualCount++ + return nil + }) return nil }) - return nil + assert.Equal(t, testCount, actualCount) }) - assert.EqualValues(t, testCount, actualCount) - // deny the bad usages - assert.PanicsWithError(t, "using database context in an iterator would cause corrupted results", func() { - _ = unittest.GetXORMEngine().Iterate(&TestModel1{}, func(i int, bean any) error { - _ = db.GetEngine(db.DefaultContext) - return nil + t.Run("DenyBadUsage", func(t *testing.T) { + assert.PanicsWithError(t, "using session context in an iterator would cause corrupted results", func() { + _ = db.WithTx(t.Context(), func(ctx context.Context) error { + return db.GetEngine(ctx).Iterate(&TestModel1{}, func(i int, bean any) error { + _ = db.GetEngine(ctx) + return nil + }) + }) }) }) } diff --git a/models/db/convert.go b/models/db/convert.go index 8c124471aba..80b0f7b04b4 100644 --- a/models/db/convert.go +++ b/models/db/convert.go @@ -16,30 +16,30 @@ import ( // ConvertDatabaseTable converts database and tables from utf8 to utf8mb4 if it's mysql and set ROW_FORMAT=dynamic func ConvertDatabaseTable() error { - if x.Dialect().URI().DBType != schemas.MYSQL { + if xormEngine.Dialect().URI().DBType != schemas.MYSQL { return nil } - r, err := CheckCollations(x) + r, err := CheckCollations(xormEngine) if err != nil { 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 { return err } - tables, err := x.DBMetas() + tables, err := xormEngine.DBMetas() if err != nil { return err } 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 } - 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 } } @@ -49,11 +49,11 @@ func ConvertDatabaseTable() error { // ConvertVarcharToNVarchar converts database and tables from varchar to nvarchar if it's mssql func ConvertVarcharToNVarchar() error { - if x.Dialect().URI().DBType != schemas.MSSQL { + if xormEngine.Dialect().URI().DBType != schemas.MSSQL { return nil } - sess := x.NewSession() + sess := xormEngine.NewSession() 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) + ')' FROM SYS.columns SC diff --git a/models/db/engine.go b/models/db/engine.go index b17188945a4..b08799210e8 100755 --- a/models/db/engine.go +++ b/models/db/engine.go @@ -8,18 +8,10 @@ import ( "context" "database/sql" "fmt" - "io" "reflect" "strings" - "time" - - "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/setting" "xorm.io/xorm" - "xorm.io/xorm/contexts" - "xorm.io/xorm/names" - "xorm.io/xorm/schemas" _ "github.com/go-sql-driver/mysql" // Needed for the MySQL driver _ "github.com/lib/pq" // Needed for the Postgresql driver @@ -27,9 +19,9 @@ import ( ) var ( - x *xorm.Engine - tables []any - initFuncs []func() error + xormEngine *xorm.Engine + registeredModels []any + registeredInitFuncs []func() error ) // Engine represents a xorm engine or session. @@ -66,171 +58,43 @@ type Engine interface { Cols(...string) *xorm.Session Context(ctx context.Context) *xorm.Session Ping() error + IsTableExist(tableNameOrBean any) (bool, error) } -// TableInfo returns table's information via an object -func TableInfo(v any) (*schemas.Table, error) { - return x.TableInfo(v) -} +var ( + _ Engine = (*xorm.Engine)(nil) + _ Engine = (*xorm.Session)(nil) +) -// DumpTables dump tables information -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 +// RegisterModel registers model, if initFuncs provided, it will be invoked after data model sync func RegisterModel(bean any, initFunc ...func() error) { - tables = append(tables, bean) - if len(initFuncs) > 0 && initFunc[0] != nil { - initFuncs = append(initFuncs, initFunc[0]) + registeredModels = append(registeredModels, bean) + if len(registeredInitFuncs) > 0 && initFunc[0] != nil { + 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 func SyncAllTables() error { - _, err := x.StoreEngine("InnoDB").SyncWithOptions(xorm.SyncOptions{ + _, err := xormEngine.StoreEngine("InnoDB").SyncWithOptions(xorm.SyncOptions{ WarnIfDatabaseColumnMissed: true, - }, tables...) + }, registeredModels...) 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 func NamesToBean(names ...string) ([]any, error) { beans := []any{} if len(names) == 0 { - beans = append(beans, tables...) + beans = append(beans, registeredModels...) return beans, nil } // Need to map provided names to beans... 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(x.TableName(bean))] = bean - beanMap[strings.ToLower(x.TableName(bean, true))] = bean + beanMap[strings.ToLower(xormEngine.TableName(bean))] = bean + beanMap[strings.ToLower(xormEngine.TableName(bean, true))] = bean } gotBean := make(map[any]bool) @@ -247,36 +111,9 @@ func NamesToBean(names ...string) ([]any, error) { 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 func MaxBatchInsertSize(bean any) int { - t, err := x.TableInfo(bean) + t, err := xormEngine.TableInfo(bean) if err != nil { return 50 } @@ -285,18 +122,18 @@ func MaxBatchInsertSize(bean any) int { // IsTableNotEmpty returns true if table has at least one record 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 func DeleteAllRecords(tableName string) error { - _, err := x.Exec(fmt.Sprintf("DELETE FROM %s", tableName)) + _, err := xormEngine.Exec("DELETE FROM " + tableName) return err } // GetMaxID will return max id of the table 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 } @@ -308,24 +145,3 @@ func SetLogSQL(ctx context.Context, on bool) { sess.Engine().ShowSQL(on) } } - -type SlowQueryHook struct { - Threshold time.Duration - Logger log.Logger -} - -var _ contexts.Hook = &SlowQueryHook{} - -func (SlowQueryHook) BeforeProcess(c *contexts.ContextHook) (context.Context, error) { - return c.Ctx, nil -} - -func (h *SlowQueryHook) AfterProcess(c *contexts.ContextHook) error { - if c.ExecuteTime >= h.Threshold { - // 8 is the amount of skips passed to runtime.Caller, so that in the log the correct function - // is being displayed (the function that ultimately wants to execute the query in the code) - // instead of the function of the slow query hook being called. - h.Logger.Log(8, log.WARN, "[Slow SQL Query] %s %v - %v", c.SQL, c.Args, c.ExecuteTime) - } - return nil -} diff --git a/models/db/engine_dump.go b/models/db/engine_dump.go new file mode 100644 index 00000000000..63f2d4e093c --- /dev/null +++ b/models/db/engine_dump.go @@ -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) +} diff --git a/models/db/engine_hook.go b/models/db/engine_hook.go new file mode 100644 index 00000000000..8709a2c2a13 --- /dev/null +++ b/models/db/engine_hook.go @@ -0,0 +1,47 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package db + +import ( + "context" + "time" + + "code.gitea.io/gitea/modules/gtprof" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" + + "xorm.io/xorm/contexts" +) + +type EngineHook struct { + Threshold time.Duration + Logger log.Logger +} + +var _ contexts.Hook = (*EngineHook)(nil) + +func (*EngineHook) BeforeProcess(c *contexts.ContextHook) (context.Context, error) { + ctx, _ := gtprof.GetTracer().Start(c.Ctx, gtprof.TraceSpanDatabase) + return ctx, nil +} + +func (h *EngineHook) AfterProcess(c *contexts.ContextHook) error { + span := gtprof.GetContextSpan(c.Ctx) + if span != nil { + // Do not record SQL parameters here: + // * It shouldn't expose the parameters because they contain sensitive information, end users need to report the trace details safely. + // * Some parameters contain quite long texts, waste memory and are difficult to display. + span.SetAttributeString(gtprof.TraceAttrDbSQL, c.SQL) + span.End() + } else { + setting.PanicInDevOrTesting("span in database engine hook is nil") + } + 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.Event{Level: log.WARN}, "[Slow SQL Query] %s %v - %v", c.SQL, c.Args, c.ExecuteTime) + } + return nil +} diff --git a/models/db/engine_init.go b/models/db/engine_init.go new file mode 100644 index 00000000000..f26189b8054 --- /dev/null +++ b/models/db/engine_init.go @@ -0,0 +1,139 @@ +// 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 + } + switch setting.Database.Type { + case "mysql": + engine.Dialect().SetParams(map[string]string{"rowFormat": "DYNAMIC"}) + case "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 XORM's default context +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) + + if setting.Database.SlowQueryThreshold > 0 { + xe.AddHook(&EngineHook{ + 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 + xormEngine.SetDefaultContext(ctx) +} + +// 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 `xormContext` 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 + } +} + +// InitEngineWithMigration initializes a new xorm.Engine and sets it as the XORM's default context +// 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(context.Context, *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(ctx, 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 +} diff --git a/models/db/engine_test.go b/models/db/engine_test.go index e3dbfbdc242..1c218df77f3 100644 --- a/models/db/engine_test.go +++ b/models/db/engine_test.go @@ -15,6 +15,7 @@ import ( _ "code.gitea.io/gitea/cmd" // for TestPrimaryKeys "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestDumpDatabase(t *testing.T) { @@ -26,7 +27,7 @@ func TestDumpDatabase(t *testing.T) { ID int64 `xorm:"pk autoincr"` Version int64 } - assert.NoError(t, db.GetEngine(db.DefaultContext).Sync(new(Version))) + assert.NoError(t, db.GetEngine(t.Context()).Sync(new(Version))) for _, dbType := range setting.SupportedDatabaseTypes { assert.NoError(t, db.DumpDatabase(filepath.Join(dir, dbType+".sql"), dbType)) @@ -36,22 +37,22 @@ func TestDumpDatabase(t *testing.T) { func TestDeleteOrphanedObjects(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - countBefore, err := db.GetEngine(db.DefaultContext).Count(&issues_model.PullRequest{}) + countBefore, err := db.GetEngine(t.Context()).Count(&issues_model.PullRequest{}) assert.NoError(t, err) - _, err = db.GetEngine(db.DefaultContext).Insert(&issues_model.PullRequest{IssueID: 1000}, &issues_model.PullRequest{IssueID: 1001}, &issues_model.PullRequest{IssueID: 1003}) + _, err = db.GetEngine(t.Context()).Insert(&issues_model.PullRequest{IssueID: 1000}, &issues_model.PullRequest{IssueID: 1001}, &issues_model.PullRequest{IssueID: 1003}) assert.NoError(t, err) - orphaned, err := db.CountOrphanedObjects(db.DefaultContext, "pull_request", "issue", "pull_request.issue_id=issue.id") + orphaned, err := db.CountOrphanedObjects(t.Context(), "pull_request", "issue", "pull_request.issue_id=issue.id") assert.NoError(t, err) assert.EqualValues(t, 3, orphaned) - err = db.DeleteOrphanedObjects(db.DefaultContext, "pull_request", "issue", "pull_request.issue_id=issue.id") + err = db.DeleteOrphanedObjects(t.Context(), "pull_request", "issue", "pull_request.issue_id=issue.id") assert.NoError(t, err) - countAfter, err := db.GetEngine(db.DefaultContext).Count(&issues_model.PullRequest{}) + countAfter, err := db.GetEngine(t.Context()).Count(&issues_model.PullRequest{}) assert.NoError(t, err) - assert.EqualValues(t, countBefore, countAfter) + assert.Equal(t, countBefore, countAfter) } func TestPrimaryKeys(t *testing.T) { @@ -62,16 +63,14 @@ func TestPrimaryKeys(t *testing.T) { // Import "code.gitea.io/gitea/cmd" to make sure each db.RegisterModel in init functions has been called. beans, err := db.NamesToBean() - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) whitelist := map[string]string{ "the_table_name_to_skip_checking": "Write a note here to explain why", } for _, bean := range beans { - table, err := db.TableInfo(bean) + table, err := db.GetXORMEngineForTesting().TableInfo(bean) if err != nil { t.Fatal(err) } @@ -79,8 +78,6 @@ func TestPrimaryKeys(t *testing.T) { t.Logf("ignore %q because %q", table.Name, why) continue } - if len(table.PrimaryKeys) == 0 { - t.Errorf("table %q has no primary key", table.Name) - } + assert.NotEmpty(t, table.PrimaryKeys, "table %q has no primary key", table.Name) } } diff --git a/models/db/error.go b/models/db/error.go index 665e970e17c..d47c7adac49 100644 --- a/models/db/error.go +++ b/models/db/error.go @@ -65,7 +65,7 @@ func (err ErrNotExist) Error() string { if err.ID != 0 { return fmt.Sprintf("%s does not exist [id: %d]", name, err.ID) } - return fmt.Sprintf("%s does not exist", name) + return name + " does not exist" } // Unwrap unwraps this as a ErrNotExist err diff --git a/models/db/index.go b/models/db/index.go index 29254b1f07a..7a11645bd45 100644 --- a/models/db/index.go +++ b/models/db/index.go @@ -19,12 +19,7 @@ type ResourceIndex struct { MaxIndex int64 `xorm:"index"` } -var ( - // ErrResouceOutdated represents an error when request resource outdated - ErrResouceOutdated = errors.New("resource outdated") - // ErrGetResourceIndexFailed represents an error when resource index retries 3 times - ErrGetResourceIndexFailed = errors.New("get resource index failed") -) +var ErrGetResourceIndexFailed = errors.New("get resource index failed") // SyncMaxResourceIndex sync the max index with the resource func SyncMaxResourceIndex(ctx context.Context, tableName string, groupID, maxIndex int64) (err error) { diff --git a/models/db/index_test.go b/models/db/index_test.go index 5fce0a60129..e2af7f35bb1 100644 --- a/models/db/index_test.go +++ b/models/db/index_test.go @@ -35,30 +35,30 @@ func TestSyncMaxResourceIndex(t *testing.T) { xe := unittest.GetXORMEngine() assert.NoError(t, xe.Sync(&TestIndex{})) - err := db.SyncMaxResourceIndex(db.DefaultContext, "test_index", 10, 51) + err := db.SyncMaxResourceIndex(t.Context(), "test_index", 10, 51) assert.NoError(t, err) // sync new max index - maxIndex, err := getCurrentResourceIndex(db.DefaultContext, "test_index", 10) + maxIndex, err := getCurrentResourceIndex(t.Context(), "test_index", 10) assert.NoError(t, err) assert.EqualValues(t, 51, maxIndex) // smaller index doesn't change - err = db.SyncMaxResourceIndex(db.DefaultContext, "test_index", 10, 30) + err = db.SyncMaxResourceIndex(t.Context(), "test_index", 10, 30) assert.NoError(t, err) - maxIndex, err = getCurrentResourceIndex(db.DefaultContext, "test_index", 10) + maxIndex, err = getCurrentResourceIndex(t.Context(), "test_index", 10) assert.NoError(t, err) assert.EqualValues(t, 51, maxIndex) // larger index changes - err = db.SyncMaxResourceIndex(db.DefaultContext, "test_index", 10, 62) + err = db.SyncMaxResourceIndex(t.Context(), "test_index", 10, 62) assert.NoError(t, err) - maxIndex, err = getCurrentResourceIndex(db.DefaultContext, "test_index", 10) + maxIndex, err = getCurrentResourceIndex(t.Context(), "test_index", 10) assert.NoError(t, err) assert.EqualValues(t, 62, maxIndex) // commit transaction - err = db.WithTx(db.DefaultContext, func(ctx context.Context) error { + err = db.WithTx(t.Context(), func(ctx context.Context) error { err = db.SyncMaxResourceIndex(ctx, "test_index", 10, 73) assert.NoError(t, err) maxIndex, err = getCurrentResourceIndex(ctx, "test_index", 10) @@ -67,12 +67,12 @@ func TestSyncMaxResourceIndex(t *testing.T) { return nil }) assert.NoError(t, err) - maxIndex, err = getCurrentResourceIndex(db.DefaultContext, "test_index", 10) + maxIndex, err = getCurrentResourceIndex(t.Context(), "test_index", 10) assert.NoError(t, err) assert.EqualValues(t, 73, maxIndex) // rollback transaction - err = db.WithTx(db.DefaultContext, func(ctx context.Context) error { + err = db.WithTx(t.Context(), func(ctx context.Context) error { err = db.SyncMaxResourceIndex(ctx, "test_index", 10, 84) maxIndex, err = getCurrentResourceIndex(ctx, "test_index", 10) assert.NoError(t, err) @@ -80,7 +80,7 @@ func TestSyncMaxResourceIndex(t *testing.T) { return errors.New("test rollback") }) assert.Error(t, err) - maxIndex, err = getCurrentResourceIndex(db.DefaultContext, "test_index", 10) + maxIndex, err = getCurrentResourceIndex(t.Context(), "test_index", 10) assert.NoError(t, err) assert.EqualValues(t, 73, maxIndex) // the max index doesn't change because the transaction was rolled back } @@ -91,36 +91,36 @@ func TestGetNextResourceIndex(t *testing.T) { assert.NoError(t, xe.Sync(&TestIndex{})) // create a new record - maxIndex, err := db.GetNextResourceIndex(db.DefaultContext, "test_index", 20) + maxIndex, err := db.GetNextResourceIndex(t.Context(), "test_index", 20) assert.NoError(t, err) assert.EqualValues(t, 1, maxIndex) // increase the existing record - maxIndex, err = db.GetNextResourceIndex(db.DefaultContext, "test_index", 20) + maxIndex, err = db.GetNextResourceIndex(t.Context(), "test_index", 20) assert.NoError(t, err) assert.EqualValues(t, 2, maxIndex) // commit transaction - err = db.WithTx(db.DefaultContext, func(ctx context.Context) error { + err = db.WithTx(t.Context(), func(ctx context.Context) error { maxIndex, err = db.GetNextResourceIndex(ctx, "test_index", 20) assert.NoError(t, err) assert.EqualValues(t, 3, maxIndex) return nil }) assert.NoError(t, err) - maxIndex, err = getCurrentResourceIndex(db.DefaultContext, "test_index", 20) + maxIndex, err = getCurrentResourceIndex(t.Context(), "test_index", 20) assert.NoError(t, err) assert.EqualValues(t, 3, maxIndex) // rollback transaction - err = db.WithTx(db.DefaultContext, func(ctx context.Context) error { + err = db.WithTx(t.Context(), func(ctx context.Context) error { maxIndex, err = db.GetNextResourceIndex(ctx, "test_index", 20) assert.NoError(t, err) assert.EqualValues(t, 4, maxIndex) return errors.New("test rollback") }) assert.Error(t, err) - maxIndex, err = getCurrentResourceIndex(db.DefaultContext, "test_index", 20) + maxIndex, err = getCurrentResourceIndex(t.Context(), "test_index", 20) assert.NoError(t, err) assert.EqualValues(t, 3, maxIndex) // the max index doesn't change because the transaction was rolled back } diff --git a/models/db/install/db.go b/models/db/install/db.go index 1b3b2ec3e99..a84e5e54fe6 100644 --- a/models/db/install/db.go +++ b/models/db/install/db.go @@ -4,27 +4,22 @@ package install import ( + "context" + "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/modules/setting" - - "xorm.io/xorm" ) -func getXORMEngine() *xorm.Engine { - return db.GetEngine(db.DefaultContext).(*xorm.Engine) -} - // CheckDatabaseConnection checks the database connection -func CheckDatabaseConnection() error { - e := db.GetEngine(db.DefaultContext) - _, err := e.Exec("SELECT 1") +func CheckDatabaseConnection(ctx context.Context) error { + _, err := db.GetEngine(ctx).Exec("SELECT 1") return err } // GetMigrationVersion gets the database migration version -func GetMigrationVersion() (int64, error) { +func GetMigrationVersion(ctx context.Context) (int64, error) { var installedDbVersion int64 - x := getXORMEngine() + x := db.GetEngine(ctx) exist, err := x.IsTableExist("version") if err != nil { return 0, err @@ -40,8 +35,8 @@ func GetMigrationVersion() (int64, error) { } // HasPostInstallationUsers checks whether there are users after installation -func HasPostInstallationUsers() (bool, error) { - x := getXORMEngine() +func HasPostInstallationUsers(ctx context.Context) (bool, error) { + x := db.GetEngine(ctx) exist, err := x.IsTableExist("user") if err != nil { return false, err diff --git a/models/db/iterate_test.go b/models/db/iterate_test.go index 0f6ba2cc94a..21bb0c0b4bb 100644 --- a/models/db/iterate_test.go +++ b/models/db/iterate_test.go @@ -19,18 +19,18 @@ func TestIterate(t *testing.T) { xe := unittest.GetXORMEngine() assert.NoError(t, xe.Sync(&repo_model.RepoUnit{})) - cnt, err := db.GetEngine(db.DefaultContext).Count(&repo_model.RepoUnit{}) + cnt, err := db.GetEngine(t.Context()).Count(&repo_model.RepoUnit{}) assert.NoError(t, err) var repoUnitCnt int - err = db.Iterate(db.DefaultContext, nil, func(ctx context.Context, repo *repo_model.RepoUnit) error { + err = db.Iterate(t.Context(), nil, func(ctx context.Context, repo *repo_model.RepoUnit) error { repoUnitCnt++ return nil }) assert.NoError(t, err) assert.EqualValues(t, cnt, repoUnitCnt) - err = db.Iterate(db.DefaultContext, nil, func(ctx context.Context, repoUnit *repo_model.RepoUnit) error { + err = db.Iterate(t.Context(), nil, func(ctx context.Context, repoUnit *repo_model.RepoUnit) error { has, err := db.ExistByID[repo_model.RepoUnit](ctx, repoUnit.ID) if err != nil { return err @@ -38,8 +38,6 @@ func TestIterate(t *testing.T) { if !has { return db.ErrNotExist{Resource: "repo_unit", ID: repoUnit.ID} } - assert.EqualValues(t, repoUnit.RepoID, repoUnit.RepoID) - assert.EqualValues(t, repoUnit.CreatedUnix, repoUnit.CreatedUnix) return nil }) assert.NoError(t, err) diff --git a/models/db/list_test.go b/models/db/list_test.go index 45194611f8a..ddcfff1f88d 100644 --- a/models/db/list_test.go +++ b/models/db/list_test.go @@ -32,21 +32,21 @@ func TestFind(t *testing.T) { assert.NoError(t, xe.Sync(&repo_model.RepoUnit{})) var repoUnitCount int - _, err := db.GetEngine(db.DefaultContext).SQL("SELECT COUNT(*) FROM repo_unit").Get(&repoUnitCount) + _, err := db.GetEngine(t.Context()).SQL("SELECT COUNT(*) FROM repo_unit").Get(&repoUnitCount) assert.NoError(t, err) assert.NotEmpty(t, repoUnitCount) opts := mockListOptions{} - repoUnits, err := db.Find[repo_model.RepoUnit](db.DefaultContext, opts) + repoUnits, err := db.Find[repo_model.RepoUnit](t.Context(), opts) assert.NoError(t, err) assert.Len(t, repoUnits, repoUnitCount) - cnt, err := db.Count[repo_model.RepoUnit](db.DefaultContext, opts) + cnt, err := db.Count[repo_model.RepoUnit](t.Context(), opts) assert.NoError(t, err) assert.EqualValues(t, repoUnitCount, cnt) - repoUnits, newCnt, err := db.FindAndCount[repo_model.RepoUnit](db.DefaultContext, opts) + repoUnits, newCnt, err := db.FindAndCount[repo_model.RepoUnit](t.Context(), opts) assert.NoError(t, err) - assert.EqualValues(t, cnt, newCnt) + assert.Equal(t, cnt, newCnt) assert.Len(t, repoUnits, repoUnitCount) } diff --git a/models/db/log.go b/models/db/log.go index 307788ea2e4..a9df6f541db 100644 --- a/models/db/log.go +++ b/models/db/log.go @@ -29,7 +29,7 @@ const stackLevel = 8 // Log a message with defined skip and at logging level func (l *XORMLogBridge) Log(skip int, level log.Level, format string, v ...any) { - l.logger.Log(skip+1, level, format, v...) + l.logger.Log(skip+1, &log.Event{Level: level}, format, v...) } // Debug show debug log diff --git a/models/db/name.go b/models/db/name.go index 51be33a8bcb..48c7fdbce5f 100644 --- a/models/db/name.go +++ b/models/db/name.go @@ -5,21 +5,13 @@ package db import ( "fmt" - "regexp" + "slices" "strings" "unicode/utf8" "code.gitea.io/gitea/modules/util" ) -var ( - // ErrNameEmpty name is empty error - ErrNameEmpty = util.SilentWrap{Message: "name is empty", Err: util.ErrInvalidArgument} - - // AlphaDashDotPattern characters prohibited in a user name (anything except A-Za-z0-9_.-) - AlphaDashDotPattern = regexp.MustCompile(`[^\w-\.]`) -) - // ErrNameReserved represents a "reserved name" error. type ErrNameReserved struct { Name string @@ -82,20 +74,18 @@ func (err ErrNameCharsNotAllowed) Unwrap() error { // IsUsableName checks if name is reserved or pattern of name is not allowed // based on given reserved names and patterns. -// Names are exact match, patterns can be prefix or suffix match with placeholder '*'. -func IsUsableName(names, patterns []string, name string) error { +// Names are exact match, patterns can be a prefix or suffix match with placeholder '*'. +func IsUsableName(reservedNames, reservedPatterns []string, name string) error { name = strings.TrimSpace(strings.ToLower(name)) if utf8.RuneCountInString(name) == 0 { - return ErrNameEmpty + return util.NewInvalidArgumentErrorf("name is empty") } - for i := range names { - if name == names[i] { - return ErrNameReserved{name} - } + if slices.Contains(reservedNames, name) { + return ErrNameReserved{name} } - for _, pat := range patterns { + for _, pat := range reservedPatterns { if pat[0] == '*' && strings.HasSuffix(name, pat[1:]) || (pat[len(pat)-1] == '*' && strings.HasPrefix(name, pat[:len(pat)-1])) { return ErrNamePatternNotAllowed{pat} diff --git a/models/db/search.go b/models/db/search.go index 37565f45e1f..44d54f21fcb 100644 --- a/models/db/search.go +++ b/models/db/search.go @@ -26,8 +26,6 @@ const ( SearchOrderByForksReverse SearchOrderBy = "num_forks DESC" ) -const ( - // Which means a condition to filter the records which don't match any id. - // It's different from zero which means the condition could be ignored. - NoConditionID = -1 -) +// NoConditionID means a condition to filter the records which don't match any id. +// eg: "milestone_id=-1" means "find the items without any milestone. +const NoConditionID int64 = -1 diff --git a/models/db/sequence.go b/models/db/sequence.go index f49ad935de0..9adc5113acc 100644 --- a/models/db/sequence.go +++ b/models/db/sequence.go @@ -17,11 +17,11 @@ func CountBadSequences(_ context.Context) (int64, error) { return 0, nil } - sess := x.NewSession() + sess := xormEngine.NewSession() defer sess.Close() var sequences []string - schema := x.Dialect().URI().Schema + schema := xormEngine.Dialect().URI().Schema 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 { @@ -38,7 +38,7 @@ func FixBadSequences(_ context.Context) error { return nil } - sess := x.NewSession() + sess := xormEngine.NewSession() defer sess.Close() if err := sess.Begin(); err != nil { return err diff --git a/models/db/sql_postgres_with_schema.go b/models/db/sql_postgres_with_schema.go index 64b61b2ef34..812fe4a6a61 100644 --- a/models/db/sql_postgres_with_schema.go +++ b/models/db/sql_postgres_with_schema.go @@ -39,7 +39,7 @@ func (d *postgresSchemaDriver) Open(name string) (driver.Conn, error) { // golangci lint is incorrect here - there is no benefit to using driver.ExecerContext here // and in any case pq does not implement it - if execer, ok := conn.(driver.Execer); ok { //nolint:staticcheck + if execer, ok := conn.(driver.Execer); ok { //nolint:staticcheck // see above _, err := execer.Exec(`SELECT set_config( 'search_path', $1 || ',' || current_setting('search_path'), @@ -64,7 +64,7 @@ func (d *postgresSchemaDriver) Open(name string) (driver.Conn, error) { // driver.String.ConvertValue will never return err for string // golangci lint is incorrect here - there is no benefit to using stmt.ExecWithContext here - _, err = stmt.Exec([]driver.Value{schemaValue}) //nolint:staticcheck + _, err = stmt.Exec([]driver.Value{schemaValue}) //nolint:staticcheck // see above if err != nil { _ = conn.Close() return nil, err diff --git a/models/dbfs/dbfile.go b/models/dbfs/dbfile.go index dd27b5c36b7..eaf506fbe6b 100644 --- a/models/dbfs/dbfile.go +++ b/models/dbfs/dbfile.go @@ -46,10 +46,7 @@ func (f *file) readAt(fileMeta *dbfsMeta, offset int64, p []byte) (n int, err er blobPos := int(offset % f.blockSize) blobOffset := offset - int64(blobPos) blobRemaining := int(f.blockSize) - blobPos - needRead := len(p) - if needRead > blobRemaining { - needRead = blobRemaining - } + needRead := min(len(p), blobRemaining) if blobOffset+int64(blobPos)+int64(needRead) > fileMeta.FileSize { needRead = int(fileMeta.FileSize - blobOffset - int64(blobPos)) } @@ -66,14 +63,8 @@ func (f *file) readAt(fileMeta *dbfsMeta, offset int64, p []byte) (n int, err er blobData = nil } - canCopy := len(blobData) - blobPos - if canCopy <= 0 { - canCopy = 0 - } - realRead := needRead - if realRead > canCopy { - realRead = canCopy - } + canCopy := max(len(blobData)-blobPos, 0) + realRead := min(needRead, canCopy) if realRead > 0 { copy(p[:realRead], fileData.BlobData[blobPos:blobPos+realRead]) } @@ -113,10 +104,7 @@ func (f *file) Write(p []byte) (n int, err error) { blobPos := int(f.offset % f.blockSize) blobOffset := f.offset - int64(blobPos) blobRemaining := int(f.blockSize) - blobPos - needWrite := len(p) - if needWrite > blobRemaining { - needWrite = blobRemaining - } + needWrite := min(len(p), blobRemaining) buf := make([]byte, f.blockSize) readBytes, err := f.readAt(fileMeta, blobOffset, buf) if err != nil && !errors.Is(err, io.EOF) { diff --git a/models/dbfs/dbfs_test.go b/models/dbfs/dbfs_test.go index 96cb1014c71..e1ecd871e4d 100644 --- a/models/dbfs/dbfs_test.go +++ b/models/dbfs/dbfs_test.go @@ -9,8 +9,6 @@ import ( "os" "testing" - "code.gitea.io/gitea/models/db" - "github.com/stretchr/testify/assert" ) @@ -26,20 +24,20 @@ func TestDbfsBasic(t *testing.T) { defer changeDefaultFileBlockSize(4)() // test basic write/read - f, err := OpenFile(db.DefaultContext, "test.txt", os.O_RDWR|os.O_CREATE) + f, err := OpenFile(t.Context(), "test.txt", os.O_RDWR|os.O_CREATE) assert.NoError(t, err) n, err := f.Write([]byte("0123456789")) // blocks: 0123 4567 89 assert.NoError(t, err) - assert.EqualValues(t, 10, n) + assert.Equal(t, 10, n) _, err = f.Seek(0, io.SeekStart) assert.NoError(t, err) buf, err := io.ReadAll(f) assert.NoError(t, err) - assert.EqualValues(t, 10, n) - assert.EqualValues(t, "0123456789", string(buf)) + assert.Equal(t, 10, n) + assert.Equal(t, "0123456789", string(buf)) // write some new data _, err = f.Seek(1, io.SeekStart) @@ -50,14 +48,14 @@ func TestDbfsBasic(t *testing.T) { // read from offset buf, err = io.ReadAll(f) assert.NoError(t, err) - assert.EqualValues(t, "9", string(buf)) + assert.Equal(t, "9", string(buf)) // read all _, err = f.Seek(0, io.SeekStart) assert.NoError(t, err) buf, err = io.ReadAll(f) assert.NoError(t, err) - assert.EqualValues(t, "0bcdefghi9", string(buf)) + assert.Equal(t, "0bcdefghi9", string(buf)) // write to new size _, err = f.Seek(-1, io.SeekEnd) @@ -68,7 +66,7 @@ func TestDbfsBasic(t *testing.T) { assert.NoError(t, err) buf, err = io.ReadAll(f) assert.NoError(t, err) - assert.EqualValues(t, "0bcdefghiJKLMNOP", string(buf)) + assert.Equal(t, "0bcdefghiJKLMNOP", string(buf)) // write beyond EOF and fill with zero _, err = f.Seek(5, io.SeekCurrent) @@ -79,7 +77,7 @@ func TestDbfsBasic(t *testing.T) { assert.NoError(t, err) buf, err = io.ReadAll(f) assert.NoError(t, err) - assert.EqualValues(t, "0bcdefghiJKLMNOP\x00\x00\x00\x00\x00xyzu", string(buf)) + assert.Equal(t, "0bcdefghiJKLMNOP\x00\x00\x00\x00\x00xyzu", string(buf)) // write to the block with zeros _, err = f.Seek(-6, io.SeekCurrent) @@ -90,34 +88,34 @@ func TestDbfsBasic(t *testing.T) { assert.NoError(t, err) buf, err = io.ReadAll(f) assert.NoError(t, err) - assert.EqualValues(t, "0bcdefghiJKLMNOP\x00\x00\x00ABCDzu", string(buf)) + assert.Equal(t, "0bcdefghiJKLMNOP\x00\x00\x00ABCDzu", string(buf)) assert.NoError(t, f.Close()) // test rename - err = Rename(db.DefaultContext, "test.txt", "test2.txt") + err = Rename(t.Context(), "test.txt", "test2.txt") assert.NoError(t, err) - _, err = OpenFile(db.DefaultContext, "test.txt", os.O_RDONLY) + _, err = OpenFile(t.Context(), "test.txt", os.O_RDONLY) assert.Error(t, err) - f, err = OpenFile(db.DefaultContext, "test2.txt", os.O_RDONLY) + f, err = OpenFile(t.Context(), "test2.txt", os.O_RDONLY) assert.NoError(t, err) assert.NoError(t, f.Close()) // test remove - err = Remove(db.DefaultContext, "test2.txt") + err = Remove(t.Context(), "test2.txt") assert.NoError(t, err) - _, err = OpenFile(db.DefaultContext, "test2.txt", os.O_RDONLY) + _, err = OpenFile(t.Context(), "test2.txt", os.O_RDONLY) assert.Error(t, err) // test stat - f, err = OpenFile(db.DefaultContext, "test/test.txt", os.O_RDWR|os.O_CREATE) + f, err = OpenFile(t.Context(), "test/test.txt", os.O_RDWR|os.O_CREATE) assert.NoError(t, err) stat, err := f.Stat() assert.NoError(t, err) - assert.EqualValues(t, "test.txt", stat.Name()) + assert.Equal(t, "test.txt", stat.Name()) assert.EqualValues(t, 0, stat.Size()) _, err = f.Write([]byte("0123456789")) assert.NoError(t, err) @@ -129,11 +127,11 @@ func TestDbfsBasic(t *testing.T) { func TestDbfsReadWrite(t *testing.T) { defer changeDefaultFileBlockSize(4)() - f1, err := OpenFile(db.DefaultContext, "test.log", os.O_RDWR|os.O_CREATE) + f1, err := OpenFile(t.Context(), "test.log", os.O_RDWR|os.O_CREATE) assert.NoError(t, err) defer f1.Close() - f2, err := OpenFile(db.DefaultContext, "test.log", os.O_RDONLY) + f2, err := OpenFile(t.Context(), "test.log", os.O_RDONLY) assert.NoError(t, err) defer f2.Close() @@ -144,7 +142,7 @@ func TestDbfsReadWrite(t *testing.T) { line, err := f2r.ReadString('\n') assert.NoError(t, err) - assert.EqualValues(t, "line 1\n", line) + assert.Equal(t, "line 1\n", line) _, err = f2r.ReadString('\n') assert.ErrorIs(t, err, io.EOF) @@ -153,7 +151,7 @@ func TestDbfsReadWrite(t *testing.T) { line, err = f2r.ReadString('\n') assert.NoError(t, err) - assert.EqualValues(t, "line 2\n", line) + assert.Equal(t, "line 2\n", line) _, err = f2r.ReadString('\n') assert.ErrorIs(t, err, io.EOF) } @@ -161,7 +159,7 @@ func TestDbfsReadWrite(t *testing.T) { func TestDbfsSeekWrite(t *testing.T) { defer changeDefaultFileBlockSize(4)() - f, err := OpenFile(db.DefaultContext, "test2.log", os.O_RDWR|os.O_CREATE) + f, err := OpenFile(t.Context(), "test2.log", os.O_RDWR|os.O_CREATE) assert.NoError(t, err) defer f.Close() @@ -180,11 +178,11 @@ func TestDbfsSeekWrite(t *testing.T) { _, err = f.Write([]byte("333")) assert.NoError(t, err) - fr, err := OpenFile(db.DefaultContext, "test2.log", os.O_RDONLY) + fr, err := OpenFile(t.Context(), "test2.log", os.O_RDONLY) assert.NoError(t, err) defer f.Close() buf, err := io.ReadAll(fr) assert.NoError(t, err) - assert.EqualValues(t, "111333", string(buf)) + assert.Equal(t, "111333", string(buf)) } diff --git a/models/error.go b/models/error.go deleted file mode 100644 index 75c53245de5..00000000000 --- a/models/error.go +++ /dev/null @@ -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) -} diff --git a/models/fixture_generation.go b/models/fixture_generation.go deleted file mode 100644 index 6234caefada..00000000000 --- a/models/fixture_generation.go +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright 2020 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package models - -import ( - "context" - "fmt" - "strings" - - "code.gitea.io/gitea/models/db" - access_model "code.gitea.io/gitea/models/perm/access" - repo_model "code.gitea.io/gitea/models/repo" -) - -// GetYamlFixturesAccess returns a string containing the contents -// for the access table, as recalculated using repo.RecalculateAccesses() -func GetYamlFixturesAccess(ctx context.Context) (string, error) { - repos := make([]*repo_model.Repository, 0, 50) - if err := db.GetEngine(ctx).Find(&repos); err != nil { - return "", err - } - - for _, repo := range repos { - repo.MustOwner(ctx) - if err := access_model.RecalculateAccesses(ctx, repo); err != nil { - return "", err - } - } - - var b strings.Builder - - accesses := make([]*access_model.Access, 0, 200) - if err := db.GetEngine(ctx).OrderBy("user_id, repo_id").Find(&accesses); err != nil { - return "", err - } - - for i, a := range accesses { - fmt.Fprintf(&b, "-\n") - fmt.Fprintf(&b, " id: %d\n", i+1) - fmt.Fprintf(&b, " user_id: %d\n", a.UserID) - fmt.Fprintf(&b, " repo_id: %d\n", a.RepoID) - fmt.Fprintf(&b, " mode: %d\n", a.Mode) - if i < len(accesses)-1 { - fmt.Fprintf(&b, "\n") - } - } - - return b.String(), nil -} diff --git a/models/fixture_test.go b/models/fixture_test.go deleted file mode 100644 index de5f412388d..00000000000 --- a/models/fixture_test.go +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright 2020 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package models - -import ( - "context" - "os" - "path/filepath" - "testing" - - "code.gitea.io/gitea/models/db" - "code.gitea.io/gitea/models/unittest" - "code.gitea.io/gitea/modules/util" - - "github.com/stretchr/testify/assert" -) - -func TestFixtureGeneration(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - test := func(ctx context.Context, gen func(ctx context.Context) (string, error), name string) { - expected, err := gen(ctx) - if !assert.NoError(t, err) { - return - } - p := filepath.Join(unittest.FixturesDir(), name+".yml") - bytes, err := os.ReadFile(p) - if !assert.NoError(t, err) { - return - } - data := string(util.NormalizeEOL(bytes)) - assert.EqualValues(t, expected, data, "Differences detected for %s", p) - } - - test(db.DefaultContext, GetYamlFixturesAccess, "access") -} diff --git a/models/fixtures/access.yml b/models/fixtures/access.yml index 4171e31fef7..596046e9502 100644 --- a/models/fixtures/access.yml +++ b/models/fixtures/access.yml @@ -171,3 +171,9 @@ user_id: 40 repo_id: 61 mode: 4 + +- + id: 30 + user_id: 40 + repo_id: 1 + mode: 2 diff --git a/models/fixtures/action_artifact.yml b/models/fixtures/action_artifact.yml index 2c51c11ebd0..ee8ef0d5cec 100644 --- a/models/fixtures/action_artifact.yml +++ b/models/fixtures/action_artifact.yml @@ -11,6 +11,24 @@ content_encoding: "" artifact_path: "abc.txt" artifact_name: "artifact-download" + status: 2 + created_unix: 1712338649 + updated_unix: 1712338649 + expired_unix: 1720114649 + +- + id: 2 + run_id: 791 + runner_id: 1 + repo_id: 4 + owner_id: 1 + commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0 + storage_path: "" + file_size: 1024 + file_compressed_size: 1024 + content_encoding: "30/20/1712348022422036662.chunk" + artifact_path: "abc.txt" + artifact_name: "artifact-download-incomplete" status: 1 created_unix: 1712338649 updated_unix: 1712338649 @@ -69,3 +87,57 @@ created_unix: 1730330775 updated_unix: 1730330775 expired_unix: 1738106775 + +- + id: 23 + run_id: 793 + runner_id: 1 + repo_id: 2 + owner_id: 2 + commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0 + storage_path: "27/5/1730330775594233150.chunk" + file_size: 1024 + file_compressed_size: 1024 + content_encoding: "application/zip" + artifact_path: "artifact-v4-download.zip" + artifact_name: "artifact-v4-download" + status: 2 + created_unix: 1730330775 + updated_unix: 1730330775 + expired_unix: 1738106775 + +- + id: 24 + run_id: 795 + runner_id: 1 + repo_id: 2 + owner_id: 2 + commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0 + storage_path: "27/5/1730330775594233150.chunk" + file_size: 1024 + file_compressed_size: 1024 + content_encoding: "application/zip" + artifact_path: "artifact-795-1.zip" + artifact_name: "artifact-795-1" + status: 2 + created_unix: 1730330775 + updated_unix: 1730330775 + expired_unix: 1738106775 + +- + id: 25 + run_id: 795 + runner_id: 1 + repo_id: 2 + owner_id: 2 + commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0 + storage_path: "27/5/1730330775594233150.chunk" + file_size: 1024 + file_compressed_size: 1024 + content_encoding: "application/zip" + artifact_path: "artifact-795-2.zip" + artifact_name: "artifact-795-2" + status: 2 + created_unix: 1730330775 + updated_unix: 1730330775 + expired_unix: 1738106775 diff --git a/models/fixtures/action_run.yml b/models/fixtures/action_run.yml index a42ab77ca5b..09dfa6cccbb 100644 --- a/models/fixtures/action_run.yml +++ b/models/fixtures/action_run.yml @@ -9,6 +9,7 @@ ref: "refs/heads/master" commit_sha: "c2d72f548424103f01ee1dc02889c1e2bff816b0" event: "push" + trigger_event: "push" is_fork_pull_request: 0 status: 1 started: 1683636528 @@ -28,6 +29,7 @@ ref: "refs/heads/master" commit_sha: "c2d72f548424103f01ee1dc02889c1e2bff816b0" event: "push" + trigger_event: "push" is_fork_pull_request: 0 status: 1 started: 1683636528 @@ -36,3 +38,104 @@ updated: 1683636626 need_approval: 0 approved_by: 0 +- + id: 793 + title: "job output" + repo_id: 4 + owner_id: 1 + workflow_id: "test.yaml" + index: 189 + trigger_user_id: 1 + ref: "refs/heads/master" + commit_sha: "c2d72f548424103f01ee1dc02889c1e2bff816b0" + event: "push" + trigger_event: "push" + is_fork_pull_request: 0 + status: 6 # running + started: 1683636528 + stopped: 1683636626 + created: 1683636108 + updated: 1683636626 + need_approval: 0 + approved_by: 0 +- + id: 794 + title: "job output" + repo_id: 4 + owner_id: 1 + workflow_id: "test.yaml" + index: 190 + trigger_user_id: 1 + ref: "refs/heads/test" + commit_sha: "c2d72f548424103f01ee1dc02889c1e2bff816b0" + event: "push" + trigger_event: "push" + is_fork_pull_request: 0 + status: 1 + started: 1683636528 + stopped: 1683636626 + created: 1683636108 + updated: 1683636626 + need_approval: 0 + approved_by: 0 +- + id: 802 + title: "workflow run list" + repo_id: 5 + owner_id: 3 + workflow_id: "test.yaml" + index: 191 + trigger_user_id: 1 + ref: "refs/heads/test" + commit_sha: "c2d72f548424103f01ee1dc02889c1e2bff816b0" + event: "push" + trigger_event: "push" + is_fork_pull_request: 0 + status: 1 + started: 1683636528 + stopped: 1683636626 + created: 1683636108 + updated: 1683636626 + need_approval: 0 + approved_by: 0 +- + id: 803 + title: "workflow run list for user" + repo_id: 2 + owner_id: 0 + workflow_id: "test.yaml" + index: 192 + trigger_user_id: 1 + ref: "refs/heads/test" + commit_sha: "c2d72f548424103f01ee1dc02889c1e2bff816b0" + event: "push" + trigger_event: "push" + is_fork_pull_request: 0 + status: 1 + started: 1683636528 + stopped: 1683636626 + created: 1683636108 + updated: 1683636626 + need_approval: 0 + approved_by: 0 + +- + id: 795 + title: "to be deleted (test)" + repo_id: 2 + owner_id: 2 + workflow_id: "test.yaml" + index: 191 + trigger_user_id: 1 + ref: "refs/heads/test" + commit_sha: "c2d72f548424103f01ee1dc02889c1e2bff816b0" + event: "push" + trigger_event: "push" + is_fork_pull_request: 0 + status: 2 + started: 1683636528 + stopped: 1683636626 + created: 1683636108 + updated: 1683636626 + need_approval: 0 + approved_by: 0 diff --git a/models/fixtures/action_run_job.yml b/models/fixtures/action_run_job.yml index fd90f4fd5d2..6c06d94aa44 100644 --- a/models/fixtures/action_run_job.yml +++ b/models/fixtures/action_run_job.yml @@ -26,3 +26,106 @@ status: 1 started: 1683636528 stopped: 1683636626 +- + id: 194 + run_id: 793 + repo_id: 4 + owner_id: 1 + commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0 + is_fork_pull_request: 0 + name: job1 (1) + attempt: 1 + job_id: job1 + task_id: 49 + status: 1 + started: 1683636528 + stopped: 1683636626 +- + id: 195 + run_id: 793 + repo_id: 4 + owner_id: 1 + commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0 + is_fork_pull_request: 0 + name: job1 (2) + attempt: 1 + job_id: job1 + task_id: 50 + status: 1 + started: 1683636528 + stopped: 1683636626 +- + id: 196 + run_id: 793 + repo_id: 4 + owner_id: 1 + commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0 + is_fork_pull_request: 0 + name: job2 + attempt: 1 + job_id: job2 + needs: '["job1"]' + task_id: 51 + status: 5 + started: 1683636528 + stopped: 1683636626 + +- + id: 198 + run_id: 795 + repo_id: 2 + owner_id: 2 + commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0 + is_fork_pull_request: 0 + name: job_1 + attempt: 1 + job_id: job_1 + task_id: 53 + status: 1 + started: 1683636528 + stopped: 1683636626 + +- + id: 199 + run_id: 795 + repo_id: 2 + owner_id: 2 + commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0 + is_fork_pull_request: 0 + name: job_2 + attempt: 1 + job_id: job_2 + task_id: 54 + status: 2 + started: 1683636528 + stopped: 1683636626 +- + id: 203 + run_id: 802 + repo_id: 5 + owner_id: 0 + commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0 + is_fork_pull_request: 0 + name: job2 + attempt: 1 + job_id: job2 + needs: '["job1"]' + task_id: 51 + status: 5 + started: 1683636528 + stopped: 1683636626 +- + id: 204 + run_id: 803 + repo_id: 2 + owner_id: 0 + commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0 + is_fork_pull_request: 0 + name: job2 + attempt: 1 + job_id: job2 + needs: '["job1"]' + task_id: 51 + status: 5 + started: 1683636528 + stopped: 1683636626 diff --git a/models/fixtures/action_runner.yml b/models/fixtures/action_runner.yml new file mode 100644 index 00000000000..ecb72140065 --- /dev/null +++ b/models/fixtures/action_runner.yml @@ -0,0 +1,51 @@ +- + id: 34346 + name: runner_to_be_deleted-user + uuid: 3EF231BD-FBB7-4E4B-9602-E6F28363EF18 + token_hash: 3EF231BD-FBB7-4E4B-9602-E6F28363EF18 + version: "1.0.0" + owner_id: 1 + repo_id: 0 + description: "This runner is going to be deleted" + agent_labels: '["runner_to_be_deleted","linux"]' +- + id: 34347 + name: runner_to_be_deleted-org + uuid: 3EF231BD-FBB7-4E4B-9602-E6F28363EF19 + token_hash: 3EF231BD-FBB7-4E4B-9602-E6F28363EF19 + version: "1.0.0" + owner_id: 3 + repo_id: 0 + description: "This runner is going to be deleted" + agent_labels: '["runner_to_be_deleted","linux"]' +- + id: 34348 + name: runner_to_be_deleted-repo1 + uuid: 3EF231BD-FBB7-4E4B-9602-E6F28363EF20 + token_hash: 3EF231BD-FBB7-4E4B-9602-E6F28363EF20 + version: "1.0.0" + owner_id: 0 + repo_id: 1 + description: "This runner is going to be deleted" + agent_labels: '["runner_to_be_deleted","linux"]' +- + id: 34349 + name: runner_to_be_deleted + uuid: 3EF231BD-FBB7-4E4B-9602-E6F28363EF17 + token_hash: 3EF231BD-FBB7-4E4B-9602-E6F28363EF17 + version: "1.0.0" + owner_id: 0 + repo_id: 0 + description: "This runner is going to be deleted" + agent_labels: '["runner_to_be_deleted","linux"]' +- + id: 34350 + name: runner_to_be_deleted-org-ephemeral + uuid: 3FF231BD-FBB7-4E4B-9602-E6F28363EF20 + token_hash: 3FF231BD-FBB7-4E4B-9602-E6F28363EF20 + ephemeral: true + version: "1.0.0" + owner_id: 3 + repo_id: 0 + description: "This runner is going to be deleted" + agent_labels: '["runner_to_be_deleted","linux"]' diff --git a/models/fixtures/action_task.yml b/models/fixtures/action_task.yml index d88a8ed8a91..c79fb070506 100644 --- a/models/fixtures/action_task.yml +++ b/models/fixtures/action_task.yml @@ -57,3 +57,123 @@ log_length: 707 log_size: 90179 log_expired: 0 +- + id: 49 + job_id: 194 + attempt: 1 + runner_id: 1 + status: 1 # success + started: 1683636528 + stopped: 1683636626 + repo_id: 4 + owner_id: 1 + commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0 + is_fork_pull_request: 0 + token_hash: b8d3962425466b6709b9ac51446f93260c54afe8e7b6d3686e34f991fb8a8953822b0deed86fe41a103f34bc48dbc4784220 + token_salt: ffffffffff + token_last_eight: ffffffff + log_filename: artifact-test2/2f/47.log + log_in_storage: 1 + log_length: 707 + log_size: 90179 + log_expired: 0 +- + id: 50 + job_id: 195 + attempt: 1 + runner_id: 1 + status: 1 # success + started: 1683636528 + stopped: 1683636626 + repo_id: 4 + owner_id: 1 + commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0 + is_fork_pull_request: 0 + token_hash: b8d3962425466b6709b9ac51446f93260c54afe8e7b6d3686e34f991fb8a8953822b0deed86fe41a103f34bc48dbc4784221 + token_salt: ffffffffff + token_last_eight: ffffffff + log_filename: artifact-test2/2f/47.log + log_in_storage: 1 + log_length: 707 + log_size: 90179 + log_expired: 0 +- + id: 51 + job_id: 196 + attempt: 1 + runner_id: 1 + status: 6 # running + started: 1683636528 + stopped: 1683636626 + repo_id: 4 + owner_id: 1 + commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0 + is_fork_pull_request: 0 + token_hash: b8d3962425466b6709b9ac51446f93260c54afe8e7b6d3686e34f991fb8a8953822b0deed86fe41a103f34bc48dbc4784222 + token_salt: ffffffffff + token_last_eight: ffffffff + log_filename: artifact-test2/2f/47.log + log_in_storage: 1 + log_length: 707 + log_size: 90179 + log_expired: 0 +- + id: 52 + job_id: 196 + attempt: 1 + runner_id: 34350 + status: 6 # running + started: 1683636528 + stopped: 1683636626 + repo_id: 4 + owner_id: 1 + commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0 + is_fork_pull_request: 0 + token_hash: f8d3962425466b6709b9ac51446f93260c54afe8e7b6d3686e34f991fb8a8953822b0deed86fe41a103f34bc48dbc4784222 + token_salt: ffffffffff + token_last_eight: ffffffff + log_filename: artifact-test2/2f/47.log + log_in_storage: 1 + log_length: 707 + log_size: 90179 + log_expired: 0 +- + id: 53 + job_id: 198 + attempt: 1 + runner_id: 1 + status: 1 + started: 1683636528 + stopped: 1683636626 + repo_id: 2 + owner_id: 2 + commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0 + is_fork_pull_request: 0 + token_hash: b8d3962425466b6709b9ac51446f93260c54afe8e7b6d3686e34f991fb8a8953822b0deed86fe41a103f34bc48dbc4784223 + token_salt: ffffffffff + token_last_eight: ffffffff + log_filename: artifact-test2/2f/47.log + log_in_storage: 1 + log_length: 0 + log_size: 0 + log_expired: 0 +- + id: 54 + job_id: 199 + attempt: 1 + runner_id: 1 + status: 2 + started: 1683636528 + stopped: 1683636626 + repo_id: 2 + owner_id: 2 + commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0 + is_fork_pull_request: 0 + token_hash: b8d3962425466b6709b9ac51446f93260c54afe8e7b6d3686e34f991fb8a8953822b0deed86fe41a103f34bc48dbc4784224 + token_salt: ffffffffff + token_last_eight: ffffffff + log_filename: artifact-test2/2f/47.log + log_in_storage: 1 + log_length: 0 + log_size: 0 + log_expired: 0 diff --git a/models/fixtures/action_task_output.yml b/models/fixtures/action_task_output.yml new file mode 100644 index 00000000000..314e9f7115b --- /dev/null +++ b/models/fixtures/action_task_output.yml @@ -0,0 +1,20 @@ +- + id: 1 + task_id: 49 + output_key: output_a + output_value: abc +- + id: 2 + task_id: 49 + output_key: output_b + output_value: '' +- + id: 3 + task_id: 50 + output_key: output_a + output_value: '' +- + id: 4 + task_id: 50 + output_key: output_b + output_value: bbb diff --git a/models/fixtures/branch.yml b/models/fixtures/branch.yml index c7bdff77339..03e21d04b45 100644 --- a/models/fixtures/branch.yml +++ b/models/fixtures/branch.yml @@ -81,3 +81,135 @@ is_deleted: false deleted_by_id: 0 deleted_unix: 0 + +- + id: 15 + repo_id: 4 + name: 'master' + commit_id: 'c7cd3cd144e6d23c9d6f3d07e52b2c1a956e0338' + commit_message: 'add Readme' + commit_time: 1588147171 + pusher_id: 13 + is_deleted: false + deleted_by_id: 0 + deleted_unix: 0 + +- + id: 16 + repo_id: 16 + name: 'master' + commit_id: '69554a64c1e6030f051e5c3f94bfbd773cd6a324' + commit_message: 'not signed commit' + commit_time: 1502042309 + pusher_id: 2 + is_deleted: false + deleted_by_id: 0 + deleted_unix: 0 + +- + id: 17 + repo_id: 16 + name: 'not-signed' + commit_id: '69554a64c1e6030f051e5c3f94bfbd773cd6a324' + commit_message: 'not signed commit' + commit_time: 1502042309 + pusher_id: 2 + is_deleted: false + deleted_by_id: 0 + deleted_unix: 0 + +- + id: 18 + repo_id: 16 + name: 'good-sign-not-yet-validated' + commit_id: '27566bd5738fc8b4e3fef3c5e72cce608537bd95' + commit_message: 'good signed commit (with not yet validated email)' + commit_time: 1502042234 + pusher_id: 2 + is_deleted: false + deleted_by_id: 0 + deleted_unix: 0 + +- + id: 19 + repo_id: 16 + name: 'good-sign' + commit_id: 'f27c2b2b03dcab38beaf89b0ab4ff61f6de63441' + commit_message: 'good signed commit' + commit_time: 1502042101 + pusher_id: 2 + is_deleted: false + deleted_by_id: 0 + deleted_unix: 0 + +- + id: 20 + repo_id: 1 + name: 'feature/1' + commit_id: '65f1bf27bc3bf70f64657658635e66094edbcb4d' + commit_message: 'Initial commit' + commit_time: 1489950479 + pusher_id: 2 + is_deleted: false + deleted_by_id: 0 + deleted_unix: 0 + +- + id: 21 + repo_id: 49 + name: 'master' + commit_id: 'aacbdfe9e1c4b47f60abe81849045fa4e96f1d75' + commit_message: "Add 'test/test.txt'" + commit_time: 1572535577 + pusher_id: 2 + is_deleted: false + deleted_by_id: 0 + deleted_unix: 0 + +- + id: 22 + repo_id: 1 + name: 'develop' + commit_id: '65f1bf27bc3bf70f64657658635e66094edbcb4d' + commit_message: "Initial commit" + commit_time: 1489927679 + pusher_id: 1 + is_deleted: false + deleted_by_id: 0 + deleted_unix: 0 + +- + id: 23 + repo_id: 3 + name: 'master' + commit_id: '2a47ca4b614a9f5a43abbd5ad851a54a616ffee6' + commit_message: "init project" + commit_time: 1497448461 + pusher_id: 1 + is_deleted: false + deleted_by_id: 0 + deleted_unix: 0 + +- + id: 24 + repo_id: 3 + name: 'test_branch' + commit_id: 'd22b4d4daa5be07329fcef6ed458f00cf3392da0' + commit_message: "test commit" + commit_time: 1602935385 + pusher_id: 1 + is_deleted: false + deleted_by_id: 0 + deleted_unix: 0 + +- + id: 25 + repo_id: 54 + name: 'master' + commit_id: '73cf03db6ece34e12bf91e8853dc58f678f2f82d' + commit_message: 'Initial commit' + commit_time: 1671663402 + pusher_id: 2 + is_deleted: false + deleted_by_id: 0 + deleted_unix: 0 diff --git a/models/fixtures/commit_status.yml b/models/fixtures/commit_status.yml index 20d57975ef4..87c652e53ab 100644 --- a/models/fixtures/commit_status.yml +++ b/models/fixtures/commit_status.yml @@ -7,6 +7,7 @@ target_url: https://example.com/builds/ description: My awesome CI-service context: ci/awesomeness + context_hash: c65f4d64a3b14a3eced0c9b36799e66e1bd5ced7 creator_id: 2 - @@ -18,6 +19,7 @@ target_url: https://example.com/converage/ description: My awesome Coverage service context: cov/awesomeness + context_hash: 3929ac7bccd3fa1bf9b38ddedb77973b1b9a8cfe creator_id: 2 - @@ -29,6 +31,7 @@ target_url: https://example.com/converage/ description: My awesome Coverage service context: cov/awesomeness + context_hash: 3929ac7bccd3fa1bf9b38ddedb77973b1b9a8cfe creator_id: 2 - @@ -40,6 +43,7 @@ target_url: https://example.com/builds/ description: My awesome CI-service context: ci/awesomeness + context_hash: c65f4d64a3b14a3eced0c9b36799e66e1bd5ced7 creator_id: 2 - @@ -51,4 +55,5 @@ target_url: https://example.com/builds/ description: My awesome deploy service context: deploy/awesomeness + context_hash: ae9547713a6665fc4261d0756904932085a41cf2 creator_id: 2 diff --git a/models/fixtures/email_address.yml b/models/fixtures/email_address.yml index b2a04326358..0f6bd9ee6df 100644 --- a/models/fixtures/email_address.yml +++ b/models/fixtures/email_address.yml @@ -81,7 +81,7 @@ - id: 11 uid: 4 - email: user4@example.com + email: User4@Example.Com lower_email: user4@example.com is_activated: true is_primary: true diff --git a/models/fixtures/hook_task.yml b/models/fixtures/hook_task.yml index d573406b362..6023719b1ee 100644 --- a/models/fixtures/hook_task.yml +++ b/models/fixtures/hook_task.yml @@ -18,7 +18,7 @@ id: 2 hook_id: 1 uuid: uuid2 - is_delivered: false + is_delivered: true - id: 3 diff --git a/models/fixtures/issue_pin.yml b/models/fixtures/issue_pin.yml new file mode 100644 index 00000000000..14b7a72d847 --- /dev/null +++ b/models/fixtures/issue_pin.yml @@ -0,0 +1,6 @@ +- + id: 1 + repo_id: 2 + issue_id: 4 + is_pull: false + pin_order: 1 diff --git a/models/fixtures/label.yml b/models/fixtures/label.yml index 2242b90dcdc..acfac749686 100644 --- a/models/fixtures/label.yml +++ b/models/fixtures/label.yml @@ -96,3 +96,14 @@ num_issues: 0 num_closed_issues: 0 archived_unix: 0 + +- + id: 10 + repo_id: 3 + org_id: 0 + name: repo3label1 + color: '#112233' + exclusive: false + num_issues: 0 + num_closed_issues: 0 + archived_unix: 0 diff --git a/models/fixtures/protected_tag.yml b/models/fixtures/protected_tag.yml index dbec52c0c23..1944e7bd84c 100644 --- a/models/fixtures/protected_tag.yml +++ b/models/fixtures/protected_tag.yml @@ -2,23 +2,23 @@ id: 1 repo_id: 4 name_pattern: /v.+/ - allowlist_user_i_ds: [] - allowlist_team_i_ds: [] + allowlist_user_i_ds: "[]" + allowlist_team_i_ds: "[]" created_unix: 1715596037 updated_unix: 1715596037 - id: 2 repo_id: 1 name_pattern: v-* - allowlist_user_i_ds: [] - allowlist_team_i_ds: [] + allowlist_user_i_ds: "[]" + allowlist_team_i_ds: "[]" created_unix: 1715596037 updated_unix: 1715596037 - id: 3 repo_id: 1 name_pattern: v-1.1 - allowlist_user_i_ds: [2] - allowlist_team_i_ds: [] + allowlist_user_i_ds: "[2]" + allowlist_team_i_ds: "[]" created_unix: 1715596037 updated_unix: 1715596037 diff --git a/models/fixtures/public_key.yml b/models/fixtures/public_key.yml index ae620ee2d19..856b0e3fb29 100644 --- a/models/fixtures/public_key.yml +++ b/models/fixtures/public_key.yml @@ -9,3 +9,4 @@ created_unix: 1559593109 updated_unix: 1565224552 login_source_id: 0 + verified: false diff --git a/models/fixtures/repo_transfer.yml b/models/fixtures/repo_transfer.yml index db92c952482..b12e6b207f4 100644 --- a/models/fixtures/repo_transfer.yml +++ b/models/fixtures/repo_transfer.yml @@ -21,3 +21,11 @@ repo_id: 32 created_unix: 1553610671 updated_unix: 1553610671 + +- + id: 4 + doer_id: 3 + recipient_id: 1 + repo_id: 5 + created_unix: 1553610671 + updated_unix: 1553610671 diff --git a/models/fixtures/repository.yml b/models/fixtures/repository.yml index bbb028eb7bb..552a78cbd27 100644 --- a/models/fixtures/repository.yml +++ b/models/fixtures/repository.yml @@ -1694,19 +1694,6 @@ is_fsck_enabled: true close_issues_via_commit_in_any_branch: false -- - id: 59 - owner_id: 2 - owner_name: user2 - lower_name: test_commit_revert - name: test_commit_revert - default_branch: main - is_empty: false - is_archived: false - is_private: true - status: 0 - num_issues: 0 - - id: 60 owner_id: 40 diff --git a/models/fixtures/review.yml b/models/fixtures/review.yml index 0438ceadae2..5b8bbceca9e 100644 --- a/models/fixtures/review.yml +++ b/models/fixtures/review.yml @@ -4,6 +4,7 @@ reviewer_id: 1 issue_id: 2 content: "Demo Review" + original_author_id: 0 updated_unix: 946684810 created_unix: 946684810 - @@ -12,6 +13,7 @@ reviewer_id: 534543 issue_id: 534543 content: "Invalid Review #1" + original_author_id: 0 updated_unix: 946684810 created_unix: 946684810 - @@ -20,6 +22,7 @@ reviewer_id: 1 issue_id: 343545 content: "Invalid Review #2" + original_author_id: 0 updated_unix: 946684810 created_unix: 946684810 - @@ -28,6 +31,7 @@ reviewer_id: 1 issue_id: 2 content: "Pending Review" + original_author_id: 0 updated_unix: 946684810 created_unix: 946684810 - @@ -36,6 +40,7 @@ reviewer_id: 1 issue_id: 3 content: "New review 1" + original_author_id: 0 updated_unix: 946684810 created_unix: 946684810 - @@ -61,8 +66,8 @@ type: 1 reviewer_id: 4 issue_id: 3 - original_author_id: 0 content: "New review 5" + original_author_id: 0 commit_id: 8091a55037cd59e47293aca02981b5a67076b364 stale: true updated_unix: 946684813 @@ -73,9 +78,9 @@ reviewer_id: 2 issue_id: 3 content: "New review 3 rejected" + original_author_id: 0 updated_unix: 946684814 created_unix: 946684814 - original_author_id: 0 - id: 10 @@ -83,6 +88,7 @@ reviewer_id: 100 issue_id: 3 content: "a deleted user's review" + original_author_id: 0 official: true updated_unix: 946684815 created_unix: 946684815 @@ -112,6 +118,7 @@ reviewer_id: 5 issue_id: 11 content: "old review from user5" + original_author_id: 0 updated_unix: 946684820 created_unix: 946684820 @@ -121,6 +128,7 @@ reviewer_id: 5 issue_id: 11 content: "duplicate review from user5 (latest)" + original_author_id: 0 updated_unix: 946684830 created_unix: 946684830 @@ -130,6 +138,7 @@ reviewer_id: 6 issue_id: 11 content: "singular review from org6 and final review for this pr" + original_author_id: 0 updated_unix: 946684831 created_unix: 946684831 @@ -139,6 +148,7 @@ reviewer_id: 20 issue_id: 20 content: "review request for user20" + original_author_id: 0 updated_unix: 946684832 created_unix: 946684832 @@ -148,6 +158,7 @@ reviewer_id: 20 issue_id: 20 content: "review approved by user20" + original_author_id: 0 updated_unix: 946684833 created_unix: 946684833 @@ -158,6 +169,7 @@ reviewer_team_id: 5 issue_id: 20 content: "review request for team5" + original_author_id: 0 updated_unix: 946684834 created_unix: 946684834 @@ -168,6 +180,7 @@ reviewer_team_id: 0 issue_id: 20 content: "review request for user15" + original_author_id: 0 updated_unix: 946684835 created_unix: 946684835 @@ -177,6 +190,7 @@ reviewer_id: 1 issue_id: 2 content: "Review Comment" + original_author_id: 0 updated_unix: 946684810 created_unix: 946684810 @@ -186,6 +200,7 @@ reviewer_id: 5 issue_id: 3 content: "reviewed by user5" + original_author_id: 0 commit_id: 4a357436d925b5c974181ff12a994538ddc5a269 updated_unix: 946684816 created_unix: 946684816 @@ -196,5 +211,6 @@ reviewer_id: 5 issue_id: 3 content: "review request for user5" + original_author_id: 0 updated_unix: 946684817 created_unix: 946684817 diff --git a/models/fixtures/user.yml b/models/fixtures/user.yml index b3bece5589c..976a236011c 100644 --- a/models/fixtures/user.yml +++ b/models/fixtures/user.yml @@ -67,7 +67,7 @@ num_followers: 2 num_following: 1 num_stars: 2 - num_repos: 15 + num_repos: 14 num_teams: 0 num_members: 0 visibility: 0 diff --git a/models/fixtures/webhook.yml b/models/fixtures/webhook.yml index f62bae1f311..ec282914b81 100644 --- a/models/fixtures/webhook.yml +++ b/models/fixtures/webhook.yml @@ -1,7 +1,7 @@ - id: 1 repo_id: 1 - url: www.example.com/url1 + url: https://www.example.com/url1 content_type: 1 # json events: '{"push_only":true,"send_everything":false,"choose_events":false,"events":{"create":false,"push":true,"pull_request":false}}' is_active: true @@ -9,7 +9,7 @@ - id: 2 repo_id: 1 - url: www.example.com/url2 + url: https://www.example.com/url2 content_type: 1 # json events: '{"push_only":false,"send_everything":false,"choose_events":false,"events":{"create":false,"push":true,"pull_request":true}}' is_active: false @@ -18,14 +18,35 @@ id: 3 owner_id: 3 repo_id: 3 - url: www.example.com/url3 + url: https://www.example.com/url3 content_type: 1 # json events: '{"push_only":false,"send_everything":false,"choose_events":false,"events":{"create":false,"push":true,"pull_request":true}}' is_active: true + - id: 4 repo_id: 2 - url: www.example.com/url4 + url: https://www.example.com/url4 content_type: 1 # json events: '{"push_only":true,"branch_filter":"{master,feature*}"}' is_active: true + +- + id: 5 + repo_id: 0 + owner_id: 0 + url: https://www.example.com/url5 + content_type: 1 # json + events: '{"push_only":true,"branch_filter":"{master,feature*}"}' + is_active: true + is_system_webhook: true + +- + id: 6 + repo_id: 0 + owner_id: 0 + url: https://www.example.com/url6 + content_type: 1 # json + events: '{"push_only":true,"branch_filter":"{master,feature*}"}' + is_active: true + is_system_webhook: false diff --git a/models/git/branch.go b/models/git/branch.go index ba1ada5517d..6021e1101f6 100644 --- a/models/git/branch.go +++ b/models/git/branch.go @@ -12,6 +12,7 @@ import ( repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/optional" @@ -166,12 +167,40 @@ func GetBranch(ctx context.Context, repoID int64, branchName string) (*Branch, e BranchName: branchName, } } + // FIXME: this design is not right: it doesn't check `branch.IsDeleted`, it doesn't make sense to make callers to check IsDeleted again and again. + // It causes inconsistency with `GetBranches` and `git.GetBranch`, and will lead to strange bugs + // In the future, there should be 2 functions: `GetBranchExisting` and `GetBranchWithDeleted` return &branch, nil } -func GetBranches(ctx context.Context, repoID int64, branchNames []string) ([]*Branch, error) { +// IsBranchExist returns true if the branch exists in the repository. +func IsBranchExist(ctx context.Context, repoID int64, branchName string) (bool, error) { + var branch Branch + has, err := db.GetEngine(ctx).Where("repo_id=?", repoID).And("name=?", branchName).Get(&branch) + if err != nil { + return false, err + } else if !has { + return false, nil + } + return !branch.IsDeleted, nil +} + +func GetBranches(ctx context.Context, repoID int64, branchNames []string, includeDeleted bool) ([]*Branch, error) { branches := make([]*Branch, 0, len(branchNames)) - return branches, db.GetEngine(ctx).Where("repo_id=?", repoID).In("name", branchNames).Find(&branches) + + sess := db.GetEngine(ctx).Where("repo_id=?", repoID).In("name", branchNames) + if !includeDeleted { + sess.And("is_deleted=?", false) + } + return branches, sess.Find(&branches) +} + +func BranchesToNamesSet(branches []*Branch) container.Set[string] { + names := make(container.Set[string], len(branches)) + for _, branch := range branches { + names.Add(branch.Name) + } + return names } func AddBranches(ctx context.Context, branches []*Branch) error { @@ -206,6 +235,11 @@ func GetDeletedBranchByID(ctx context.Context, repoID, branchID int64) (*Branch, return &branch, nil } +func DeleteRepoBranches(ctx context.Context, repoID int64) error { + _, err := db.GetEngine(ctx).Where("repo_id=?", repoID).Delete(new(Branch)) + return err +} + func DeleteBranches(ctx context.Context, repoID, doerID int64, branchIDs []int64) error { return db.WithTx(ctx, func(ctx context.Context) error { branches := make([]*Branch, 0, len(branchIDs)) @@ -426,6 +460,8 @@ type FindRecentlyPushedNewBranchesOptions struct { } type RecentlyPushedNewBranch struct { + BranchRepo *repo_model.Repository + BranchName string BranchDisplayName string BranchLink string BranchCompareURL string @@ -436,7 +472,7 @@ type RecentlyPushedNewBranch struct { // if opts.CommitAfterUnix is 0, we will find the branches that were committed to in the last 2 hours // if opts.ListOptions is not set, we will only display top 2 latest branches. // Protected branches will be skipped since they are unlikely to be used to create new PRs. -func FindRecentlyPushedNewBranches(ctx context.Context, doer *user_model.User, opts *FindRecentlyPushedNewBranchesOptions) ([]*RecentlyPushedNewBranch, error) { +func FindRecentlyPushedNewBranches(ctx context.Context, doer *user_model.User, opts FindRecentlyPushedNewBranchesOptions) ([]*RecentlyPushedNewBranch, error) { if doer == nil { return []*RecentlyPushedNewBranch{}, nil } @@ -451,7 +487,7 @@ func FindRecentlyPushedNewBranches(ctx context.Context, doer *user_model.User, o ForkFrom: opts.BaseRepo.ID, Archived: optional.Some(false), } - repoCond := repo_model.SearchRepositoryCondition(&repoOpts).And(repo_model.AccessibleRepositoryCondition(doer, unit.TypeCode)) + repoCond := repo_model.SearchRepositoryCondition(repoOpts).And(repo_model.AccessibleRepositoryCondition(doer, unit.TypeCode)) if opts.Repo.ID == opts.BaseRepo.ID { // should also include the base repo's branches repoCond = repoCond.Or(builder.Eq{"id": opts.BaseRepo.ID}) @@ -526,7 +562,9 @@ func FindRecentlyPushedNewBranches(ctx context.Context, doer *user_model.User, o branchDisplayName = fmt.Sprintf("%s:%s", branch.Repo.FullName(), branchDisplayName) } newBranches = append(newBranches, &RecentlyPushedNewBranch{ + BranchRepo: branch.Repo, BranchDisplayName: branchDisplayName, + BranchName: branch.Name, BranchLink: fmt.Sprintf("%s/src/branch/%s", branch.Repo.Link(), util.PathEscapeSegments(branch.Name)), BranchCompareURL: branch.Repo.ComposeBranchCompareURL(opts.BaseRepo, branch.Name), CommitTime: branch.CommitTime, diff --git a/models/git/branch_test.go b/models/git/branch_test.go index b8ea663e81c..5be435172b8 100644 --- a/models/git/branch_test.go +++ b/models/git/branch_test.go @@ -21,12 +21,12 @@ import ( func TestAddDeletedBranch(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) - assert.EqualValues(t, git.Sha1ObjectFormat.Name(), repo.ObjectFormatName) + assert.Equal(t, git.Sha1ObjectFormat.Name(), repo.ObjectFormatName) firstBranch := unittest.AssertExistsAndLoadBean(t, &git_model.Branch{ID: 1}) assert.True(t, firstBranch.IsDeleted) - assert.NoError(t, git_model.AddDeletedBranch(db.DefaultContext, repo.ID, firstBranch.Name, firstBranch.DeletedByID)) - assert.NoError(t, git_model.AddDeletedBranch(db.DefaultContext, repo.ID, "branch2", int64(1))) + assert.NoError(t, git_model.AddDeletedBranch(t.Context(), repo.ID, firstBranch.Name, firstBranch.DeletedByID)) + assert.NoError(t, git_model.AddDeletedBranch(t.Context(), repo.ID, "branch2", int64(1))) secondBranch := unittest.AssertExistsAndLoadBean(t, &git_model.Branch{RepoID: repo.ID, Name: "branch2"}) assert.True(t, secondBranch.IsDeleted) @@ -39,7 +39,7 @@ func TestAddDeletedBranch(t *testing.T) { }, } - _, err := git_model.UpdateBranch(db.DefaultContext, repo.ID, secondBranch.PusherID, secondBranch.Name, commit) + _, err := git_model.UpdateBranch(t.Context(), repo.ID, secondBranch.PusherID, secondBranch.Name, commit) assert.NoError(t, err) } @@ -47,7 +47,7 @@ func TestGetDeletedBranches(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) - branches, err := db.Find[git_model.Branch](db.DefaultContext, git_model.FindBranchOptions{ + branches, err := db.Find[git_model.Branch](t.Context(), git_model.FindBranchOptions{ ListOptions: db.ListOptionsAll, RepoID: repo.ID, IsDeletedBranch: optional.Some(true), @@ -71,13 +71,13 @@ func TestDeletedBranchLoadUser(t *testing.T) { branch := getDeletedBranch(t, firstBranch) assert.Nil(t, branch.DeletedBy) - branch.LoadDeletedBy(db.DefaultContext) + branch.LoadDeletedBy(t.Context()) assert.NotNil(t, branch.DeletedBy) assert.Equal(t, "user1", branch.DeletedBy.Name) branch = getDeletedBranch(t, secondBranch) assert.Nil(t, branch.DeletedBy) - branch.LoadDeletedBy(db.DefaultContext) + branch.LoadDeletedBy(t.Context()) assert.NotNil(t, branch.DeletedBy) assert.Equal(t, "Ghost", branch.DeletedBy.Name) } @@ -88,7 +88,7 @@ func TestRemoveDeletedBranch(t *testing.T) { firstBranch := unittest.AssertExistsAndLoadBean(t, &git_model.Branch{ID: 1}) - err := git_model.RemoveDeletedBranchByID(db.DefaultContext, repo.ID, 1) + err := git_model.RemoveDeletedBranchByID(t.Context(), repo.ID, 1) assert.NoError(t, err) unittest.AssertNotExistsBean(t, firstBranch) unittest.AssertExistsAndLoadBean(t, &git_model.Branch{ID: 2}) @@ -97,7 +97,7 @@ func TestRemoveDeletedBranch(t *testing.T) { func getDeletedBranch(t *testing.T, branch *git_model.Branch) *git_model.Branch { repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) - deletedBranch, err := git_model.GetDeletedBranchByID(db.DefaultContext, repo.ID, branch.ID) + deletedBranch, err := git_model.GetDeletedBranchByID(t.Context(), repo.ID, branch.ID) assert.NoError(t, err) assert.Equal(t, branch.ID, deletedBranch.ID) assert.Equal(t, branch.Name, deletedBranch.Name) @@ -109,12 +109,12 @@ func getDeletedBranch(t *testing.T, branch *git_model.Branch) *git_model.Branch func TestFindRenamedBranch(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - branch, exist, err := git_model.FindRenamedBranch(db.DefaultContext, 1, "dev") + branch, exist, err := git_model.FindRenamedBranch(t.Context(), 1, "dev") assert.NoError(t, err) assert.True(t, exist) assert.Equal(t, "master", branch.To) - _, exist, err = git_model.FindRenamedBranch(db.DefaultContext, 1, "unknow") + _, exist, err = git_model.FindRenamedBranch(t.Context(), 1, "unknow") assert.NoError(t, err) assert.False(t, exist) } @@ -124,7 +124,7 @@ func TestRenameBranch(t *testing.T) { repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) _isDefault := false - ctx, committer, err := db.TxContext(db.DefaultContext) + ctx, committer, err := db.TxContext(t.Context()) defer committer.Close() assert.NoError(t, err) assert.NoError(t, git_model.UpdateProtectBranch(ctx, repo1, &git_model.ProtectedBranch{ @@ -133,7 +133,7 @@ func TestRenameBranch(t *testing.T) { }, git_model.WhitelistOptions{})) assert.NoError(t, committer.Commit()) - assert.NoError(t, git_model.RenameBranch(db.DefaultContext, repo1, "master", "main", func(ctx context.Context, isDefault bool) error { + assert.NoError(t, git_model.RenameBranch(t.Context(), repo1, "master", "main", func(ctx context.Context, isDefault bool) error { _isDefault = isDefault return nil })) @@ -167,7 +167,7 @@ func TestOnlyGetDeletedBranchOnCorrectRepo(t *testing.T) { // is actually on repo with ID 1. repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}) - deletedBranch, err := git_model.GetDeletedBranchByID(db.DefaultContext, repo2.ID, 1) + deletedBranch, err := git_model.GetDeletedBranchByID(t.Context(), repo2.ID, 1) // Expect error, and the returned branch is nil. assert.Error(t, err) @@ -177,7 +177,7 @@ func TestOnlyGetDeletedBranchOnCorrectRepo(t *testing.T) { // This should return the deletedBranch. repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) - deletedBranch, err = git_model.GetDeletedBranchByID(db.DefaultContext, repo1.ID, 1) + deletedBranch, err = git_model.GetDeletedBranchByID(t.Context(), repo1.ID, 1) // Expect no error, and the returned branch to be not nil. assert.NoError(t, err) diff --git a/models/git/commit_status.go b/models/git/commit_status.go index 0579a41209e..e255bca5d02 100644 --- a/models/git/commit_status.go +++ b/models/git/commit_status.go @@ -17,10 +17,10 @@ import ( "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/commitstatus" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" - api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/translation" @@ -30,17 +30,17 @@ import ( // CommitStatus holds a single Status of a single Commit type CommitStatus struct { - ID int64 `xorm:"pk autoincr"` - Index int64 `xorm:"INDEX UNIQUE(repo_sha_index)"` - RepoID int64 `xorm:"INDEX UNIQUE(repo_sha_index)"` - Repo *repo_model.Repository `xorm:"-"` - State api.CommitStatusState `xorm:"VARCHAR(7) NOT NULL"` - SHA string `xorm:"VARCHAR(64) NOT NULL INDEX UNIQUE(repo_sha_index)"` - TargetURL string `xorm:"TEXT"` - Description string `xorm:"TEXT"` - ContextHash string `xorm:"VARCHAR(64) index"` - Context string `xorm:"TEXT"` - Creator *user_model.User `xorm:"-"` + ID int64 `xorm:"pk autoincr"` + Index int64 `xorm:"INDEX UNIQUE(repo_sha_index)"` + RepoID int64 `xorm:"INDEX UNIQUE(repo_sha_index)"` + Repo *repo_model.Repository `xorm:"-"` + State commitstatus.CommitStatusState `xorm:"VARCHAR(7) NOT NULL"` + SHA string `xorm:"VARCHAR(64) NOT NULL INDEX UNIQUE(repo_sha_index)"` + TargetURL string `xorm:"TEXT"` + Description string `xorm:"TEXT"` + ContextHash string `xorm:"VARCHAR(64) index"` + Context string `xorm:"TEXT"` + Creator *user_model.User `xorm:"-"` CreatorID int64 CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` @@ -222,7 +222,7 @@ func (status *CommitStatus) HideActionsURL(ctx context.Context) { } } - prefix := fmt.Sprintf("%s/actions", status.Repo.Link()) + prefix := status.Repo.Link() + "/actions" if strings.HasPrefix(status.TargetURL, prefix) { status.TargetURL = "" } @@ -230,22 +230,25 @@ func (status *CommitStatus) HideActionsURL(ctx context.Context) { // CalcCommitStatus returns commit status state via some status, the commit statues should order by id desc func CalcCommitStatus(statuses []*CommitStatus) *CommitStatus { - var lastStatus *CommitStatus - state := api.CommitStatusSuccess + if len(statuses) == 0 { + return nil + } + + states := make(commitstatus.CommitStatusStates, 0, len(statuses)) + targetURL := "" for _, status := range statuses { - if status.State.NoBetterThan(state) { - state = status.State - lastStatus = status + states = append(states, status.State) + if status.TargetURL != "" { + targetURL = status.TargetURL } } - if lastStatus == nil { - if len(statuses) > 0 { - lastStatus = statuses[0] - } else { - lastStatus = &CommitStatus{} - } + + return &CommitStatus{ + RepoID: statuses[0].RepoID, + SHA: statuses[0].SHA, + State: states.Combine(), + TargetURL: targetURL, } - return lastStatus } // CommitStatusOptions holds the options for query commit statuses @@ -298,27 +301,37 @@ type CommitStatusIndex struct { MaxIndex int64 `xorm:"index"` } +func makeRepoCommitQuery(ctx context.Context, repoID int64, sha string) *xorm.Session { + return db.GetEngine(ctx).Table(&CommitStatus{}). + Where("repo_id = ?", repoID).And("sha = ?", sha) +} + // GetLatestCommitStatus returns all statuses with a unique context for a given commit. -func GetLatestCommitStatus(ctx context.Context, repoID int64, sha string, listOptions db.ListOptions) ([]*CommitStatus, int64, error) { - getBase := func() *xorm.Session { - return db.GetEngine(ctx).Table(&CommitStatus{}). - Where("repo_id = ?", repoID).And("sha = ?", sha) - } +func GetLatestCommitStatus(ctx context.Context, repoID int64, sha string, listOptions db.ListOptions) ([]*CommitStatus, error) { indices := make([]int64, 0, 10) - sess := getBase().Select("max( `index` ) as `index`"). - GroupBy("context_hash").OrderBy("max( `index` ) desc") + sess := makeRepoCommitQuery(ctx, repoID, sha). + Select("max( `index` ) as `index`"). + GroupBy("context_hash"). + OrderBy("max( `index` ) desc") if !listOptions.IsListAll() { sess = db.SetSessionPagination(sess, &listOptions) } - count, err := sess.FindAndCount(&indices) - if err != nil { - return nil, count, err + if err := sess.Find(&indices); err != nil { + return nil, err } statuses := make([]*CommitStatus, 0, len(indices)) if len(indices) == 0 { - return statuses, count, nil + return statuses, nil } - return statuses, count, getBase().And(builder.In("`index`", indices)).Find(&statuses) + err := makeRepoCommitQuery(ctx, repoID, sha).And(builder.In("`index`", indices)).Find(&statuses) + return statuses, err +} + +func CountLatestCommitStatus(ctx context.Context, repoID int64, sha string) (int64, error) { + return makeRepoCommitQuery(ctx, repoID, sha). + Select("count(context_hash)"). + GroupBy("context_hash"). + Count() } // GetLatestCommitStatusForPairs returns all statuses with a unique context for a given list of repo-sha pairs @@ -453,40 +466,35 @@ func NewCommitStatus(ctx context.Context, opts NewCommitStatusOptions) error { return fmt.Errorf("NewCommitStatus[nil, %s]: no repository specified", opts.SHA) } - repoPath := opts.Repo.RepoPath() if opts.Creator == nil { - return fmt.Errorf("NewCommitStatus[%s, %s]: no user specified", repoPath, opts.SHA) + return fmt.Errorf("NewCommitStatus[%s, %s]: no user specified", opts.Repo.FullName(), opts.SHA) } - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return fmt.Errorf("NewCommitStatus[repo_id: %d, user_id: %d, sha: %s]: %w", opts.Repo.ID, opts.Creator.ID, opts.SHA, err) - } - defer committer.Close() + return db.WithTx(ctx, func(ctx context.Context) error { + // Get the next Status Index + idx, err := GetNextCommitStatusIndex(ctx, opts.Repo.ID, opts.SHA.String()) + if err != nil { + return fmt.Errorf("generate commit status index failed: %w", err) + } - // Get the next Status Index - idx, err := GetNextCommitStatusIndex(ctx, opts.Repo.ID, opts.SHA.String()) - if err != nil { - return fmt.Errorf("generate commit status index failed: %w", err) - } + opts.CommitStatus.Description = strings.TrimSpace(opts.CommitStatus.Description) + opts.CommitStatus.Context = strings.TrimSpace(opts.CommitStatus.Context) + opts.CommitStatus.TargetURL = strings.TrimSpace(opts.CommitStatus.TargetURL) + opts.CommitStatus.SHA = opts.SHA.String() + opts.CommitStatus.CreatorID = opts.Creator.ID + opts.CommitStatus.RepoID = opts.Repo.ID + opts.CommitStatus.Index = idx + log.Debug("NewCommitStatus[%s, %s]: %d", opts.Repo.FullName(), opts.SHA, opts.CommitStatus.Index) - opts.CommitStatus.Description = strings.TrimSpace(opts.CommitStatus.Description) - opts.CommitStatus.Context = strings.TrimSpace(opts.CommitStatus.Context) - opts.CommitStatus.TargetURL = strings.TrimSpace(opts.CommitStatus.TargetURL) - opts.CommitStatus.SHA = opts.SHA.String() - opts.CommitStatus.CreatorID = opts.Creator.ID - opts.CommitStatus.RepoID = opts.Repo.ID - opts.CommitStatus.Index = idx - log.Debug("NewCommitStatus[%s, %s]: %d", repoPath, opts.SHA, opts.CommitStatus.Index) + opts.CommitStatus.ContextHash = hashCommitStatusContext(opts.CommitStatus.Context) - opts.CommitStatus.ContextHash = hashCommitStatusContext(opts.CommitStatus.Context) + // Insert new CommitStatus + if err = db.Insert(ctx, opts.CommitStatus); err != nil { + return fmt.Errorf("insert CommitStatus[%s, %s]: %w", opts.Repo.FullName(), opts.SHA, err) + } - // Insert new CommitStatus - if _, err = db.GetEngine(ctx).Insert(opts.CommitStatus); err != nil { - return fmt.Errorf("insert CommitStatus[%s, %s]: %w", repoPath, opts.SHA, err) - } - - return committer.Commit() + return nil + }) } // SignCommitWithStatuses represents a commit with validation of signature and status state. @@ -496,47 +504,11 @@ type SignCommitWithStatuses struct { *asymkey_model.SignCommit } -// ParseCommitsWithStatus checks commits latest statuses and calculates its worst status state -func ParseCommitsWithStatus(ctx context.Context, oldCommits []*asymkey_model.SignCommit, repo *repo_model.Repository) []*SignCommitWithStatuses { - newCommits := make([]*SignCommitWithStatuses, 0, len(oldCommits)) - - for _, c := range oldCommits { - commit := &SignCommitWithStatuses{ - SignCommit: c, - } - statuses, _, err := GetLatestCommitStatus(ctx, repo.ID, commit.ID.String(), db.ListOptions{}) - if err != nil { - log.Error("GetLatestCommitStatus: %v", err) - } else { - commit.Statuses = statuses - commit.Status = CalcCommitStatus(statuses) - } - - newCommits = append(newCommits, commit) - } - return newCommits -} - // hashCommitStatusContext hash context func hashCommitStatusContext(context string) string { return fmt.Sprintf("%x", sha1.Sum([]byte(context))) } -// ConvertFromGitCommit converts git commits into SignCommitWithStatuses -func ConvertFromGitCommit(ctx context.Context, commits []*git.Commit, repo *repo_model.Repository) []*SignCommitWithStatuses { - return ParseCommitsWithStatus(ctx, - asymkey_model.ParseCommitsWithSignature( - ctx, - user_model.ValidateCommitsWithEmails(ctx, commits), - repo.GetTrustModel(), - func(user *user_model.User) (bool, error) { - return repo_model.IsOwnerMemberCollaborator(ctx, repo, user.ID) - }, - ), - repo, - ) -} - // CommitStatusesHideActionsURL hide Gitea Actions urls func CommitStatusesHideActionsURL(ctx context.Context, statuses []*CommitStatus) { idToRepos := make(map[int64]*repo_model.Repository) diff --git a/models/git/commit_status_summary.go b/models/git/commit_status_summary.go index 7603e7aa654..dd416fa0159 100644 --- a/models/git/commit_status_summary.go +++ b/models/git/commit_status_summary.go @@ -7,19 +7,19 @@ import ( "context" "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/commitstatus" "code.gitea.io/gitea/modules/setting" - api "code.gitea.io/gitea/modules/structs" "xorm.io/builder" ) // CommitStatusSummary holds the latest commit Status of a single Commit type CommitStatusSummary struct { - ID int64 `xorm:"pk autoincr"` - RepoID int64 `xorm:"INDEX UNIQUE(repo_id_sha)"` - SHA string `xorm:"VARCHAR(64) NOT NULL INDEX UNIQUE(repo_id_sha)"` - State api.CommitStatusState `xorm:"VARCHAR(7) NOT NULL"` - TargetURL string `xorm:"TEXT"` + ID int64 `xorm:"pk autoincr"` + RepoID int64 `xorm:"INDEX UNIQUE(repo_id_sha)"` + SHA string `xorm:"VARCHAR(64) NOT NULL INDEX UNIQUE(repo_id_sha)"` + State commitstatus.CommitStatusState `xorm:"VARCHAR(7) NOT NULL"` + TargetURL string `xorm:"TEXT"` } func init() { @@ -55,11 +55,15 @@ func GetLatestCommitStatusForRepoAndSHAs(ctx context.Context, repoSHAs []RepoSHA } func UpdateCommitStatusSummary(ctx context.Context, repoID int64, sha string) error { - commitStatuses, _, err := GetLatestCommitStatus(ctx, repoID, sha, db.ListOptionsAll) + commitStatuses, err := GetLatestCommitStatus(ctx, repoID, sha, db.ListOptionsAll) if err != nil { return err } - state := CalcCommitStatus(commitStatuses) + // it guarantees that commitStatuses is not empty because this function is always called after a commit status is created + if len(commitStatuses) == 0 { + setting.PanicInDevOrTesting("no commit statuses found for repo %d and sha %s", repoID, sha) + } + state := CalcCommitStatus(commitStatuses) // non-empty commitStatuses is guaranteed // mysql will return 0 when update a record which state hasn't been changed which behaviour is different from other database, // so we need to use insert in on duplicate if setting.Database.Type.IsMySQL() { diff --git a/models/git/commit_status_test.go b/models/git/commit_status_test.go index 7ac4da68103..d1b9dfc3bf9 100644 --- a/models/git/commit_status_test.go +++ b/models/git/commit_status_test.go @@ -14,9 +14,8 @@ import ( repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/commitstatus" "code.gitea.io/gitea/modules/gitrepo" - "code.gitea.io/gitea/modules/structs" "github.com/stretchr/testify/assert" ) @@ -26,44 +25,44 @@ func TestGetCommitStatuses(t *testing.T) { repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) - sha1 := "1234123412341234123412341234123412341234" + sha1 := "1234123412341234123412341234123412341234" // the mocked commit ID in test fixtures - statuses, maxResults, err := db.FindAndCount[git_model.CommitStatus](db.DefaultContext, &git_model.CommitStatusOptions{ + statuses, maxResults, err := db.FindAndCount[git_model.CommitStatus](t.Context(), &git_model.CommitStatusOptions{ ListOptions: db.ListOptions{Page: 1, PageSize: 50}, RepoID: repo1.ID, SHA: sha1, }) assert.NoError(t, err) - assert.Equal(t, int(maxResults), 5) + assert.Equal(t, 5, int(maxResults)) assert.Len(t, statuses, 5) assert.Equal(t, "ci/awesomeness", statuses[0].Context) - assert.Equal(t, structs.CommitStatusPending, statuses[0].State) - assert.Equal(t, "https://try.gitea.io/api/v1/repos/user2/repo1/statuses/1234123412341234123412341234123412341234", statuses[0].APIURL(db.DefaultContext)) + assert.Equal(t, commitstatus.CommitStatusPending, statuses[0].State) + assert.Equal(t, "https://try.gitea.io/api/v1/repos/user2/repo1/statuses/1234123412341234123412341234123412341234", statuses[0].APIURL(t.Context())) assert.Equal(t, "cov/awesomeness", statuses[1].Context) - assert.Equal(t, structs.CommitStatusWarning, statuses[1].State) - assert.Equal(t, "https://try.gitea.io/api/v1/repos/user2/repo1/statuses/1234123412341234123412341234123412341234", statuses[1].APIURL(db.DefaultContext)) + assert.Equal(t, commitstatus.CommitStatusWarning, statuses[1].State) + assert.Equal(t, "https://try.gitea.io/api/v1/repos/user2/repo1/statuses/1234123412341234123412341234123412341234", statuses[1].APIURL(t.Context())) assert.Equal(t, "cov/awesomeness", statuses[2].Context) - assert.Equal(t, structs.CommitStatusSuccess, statuses[2].State) - assert.Equal(t, "https://try.gitea.io/api/v1/repos/user2/repo1/statuses/1234123412341234123412341234123412341234", statuses[2].APIURL(db.DefaultContext)) + assert.Equal(t, commitstatus.CommitStatusSuccess, statuses[2].State) + assert.Equal(t, "https://try.gitea.io/api/v1/repos/user2/repo1/statuses/1234123412341234123412341234123412341234", statuses[2].APIURL(t.Context())) assert.Equal(t, "ci/awesomeness", statuses[3].Context) - assert.Equal(t, structs.CommitStatusFailure, statuses[3].State) - assert.Equal(t, "https://try.gitea.io/api/v1/repos/user2/repo1/statuses/1234123412341234123412341234123412341234", statuses[3].APIURL(db.DefaultContext)) + assert.Equal(t, commitstatus.CommitStatusFailure, statuses[3].State) + assert.Equal(t, "https://try.gitea.io/api/v1/repos/user2/repo1/statuses/1234123412341234123412341234123412341234", statuses[3].APIURL(t.Context())) assert.Equal(t, "deploy/awesomeness", statuses[4].Context) - assert.Equal(t, structs.CommitStatusError, statuses[4].State) - assert.Equal(t, "https://try.gitea.io/api/v1/repos/user2/repo1/statuses/1234123412341234123412341234123412341234", statuses[4].APIURL(db.DefaultContext)) + assert.Equal(t, commitstatus.CommitStatusError, statuses[4].State) + assert.Equal(t, "https://try.gitea.io/api/v1/repos/user2/repo1/statuses/1234123412341234123412341234123412341234", statuses[4].APIURL(t.Context())) - statuses, maxResults, err = db.FindAndCount[git_model.CommitStatus](db.DefaultContext, &git_model.CommitStatusOptions{ + statuses, maxResults, err = db.FindAndCount[git_model.CommitStatus](t.Context(), &git_model.CommitStatusOptions{ ListOptions: db.ListOptions{Page: 2, PageSize: 50}, RepoID: repo1.ID, SHA: sha1, }) assert.NoError(t, err) - assert.Equal(t, int(maxResults), 5) + assert.Equal(t, 5, int(maxResults)) assert.Empty(t, statuses) } @@ -75,110 +74,110 @@ func Test_CalcCommitStatus(t *testing.T) { { statuses: []*git_model.CommitStatus{ { - State: structs.CommitStatusPending, + State: commitstatus.CommitStatusPending, }, }, expected: &git_model.CommitStatus{ - State: structs.CommitStatusPending, + State: commitstatus.CommitStatusPending, }, }, { statuses: []*git_model.CommitStatus{ { - State: structs.CommitStatusSuccess, + State: commitstatus.CommitStatusSuccess, }, { - State: structs.CommitStatusPending, + State: commitstatus.CommitStatusPending, }, }, expected: &git_model.CommitStatus{ - State: structs.CommitStatusPending, + State: commitstatus.CommitStatusPending, }, }, { statuses: []*git_model.CommitStatus{ { - State: structs.CommitStatusSuccess, + State: commitstatus.CommitStatusSuccess, }, { - State: structs.CommitStatusPending, + State: commitstatus.CommitStatusPending, }, { - State: structs.CommitStatusSuccess, + State: commitstatus.CommitStatusSuccess, }, }, expected: &git_model.CommitStatus{ - State: structs.CommitStatusPending, + State: commitstatus.CommitStatusPending, }, }, { statuses: []*git_model.CommitStatus{ { - State: structs.CommitStatusError, + State: commitstatus.CommitStatusError, }, { - State: structs.CommitStatusPending, + State: commitstatus.CommitStatusPending, }, { - State: structs.CommitStatusSuccess, + State: commitstatus.CommitStatusSuccess, }, }, expected: &git_model.CommitStatus{ - State: structs.CommitStatusError, + State: commitstatus.CommitStatusFailure, }, }, { statuses: []*git_model.CommitStatus{ { - State: structs.CommitStatusWarning, + State: commitstatus.CommitStatusWarning, }, { - State: structs.CommitStatusPending, + State: commitstatus.CommitStatusPending, }, { - State: structs.CommitStatusSuccess, + State: commitstatus.CommitStatusSuccess, }, }, expected: &git_model.CommitStatus{ - State: structs.CommitStatusWarning, + State: commitstatus.CommitStatusPending, }, }, { statuses: []*git_model.CommitStatus{ { - State: structs.CommitStatusSuccess, + State: commitstatus.CommitStatusSuccess, }, { - State: structs.CommitStatusSuccess, + State: commitstatus.CommitStatusSuccess, }, { - State: structs.CommitStatusSuccess, + State: commitstatus.CommitStatusSuccess, }, }, expected: &git_model.CommitStatus{ - State: structs.CommitStatusSuccess, + State: commitstatus.CommitStatusSuccess, }, }, { statuses: []*git_model.CommitStatus{ { - State: structs.CommitStatusFailure, + State: commitstatus.CommitStatusFailure, }, { - State: structs.CommitStatusError, + State: commitstatus.CommitStatusError, }, { - State: structs.CommitStatusWarning, + State: commitstatus.CommitStatusWarning, }, }, expected: &git_model.CommitStatus{ - State: structs.CommitStatusError, + State: commitstatus.CommitStatusFailure, }, }, } for _, kase := range kases { - assert.Equal(t, kase.expected, git_model.CalcCommitStatus(kase.statuses)) + assert.Equal(t, kase.expected, git_model.CalcCommitStatus(kase.statuses), "statuses: %v", kase.statuses) } } @@ -187,7 +186,7 @@ func TestFindRepoRecentCommitStatusContexts(t *testing.T) { repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}) user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) - gitRepo, err := gitrepo.OpenRepository(git.DefaultContext, repo2) + gitRepo, err := gitrepo.OpenRepository(t.Context(), repo2) assert.NoError(t, err) defer gitRepo.Close() @@ -195,7 +194,7 @@ func TestFindRepoRecentCommitStatusContexts(t *testing.T) { assert.NoError(t, err) defer func() { - _, err := db.DeleteByBean(db.DefaultContext, &git_model.CommitStatus{ + _, err := db.DeleteByBean(t.Context(), &git_model.CommitStatus{ RepoID: repo2.ID, CreatorID: user2.ID, SHA: commit.ID.String(), @@ -203,31 +202,31 @@ func TestFindRepoRecentCommitStatusContexts(t *testing.T) { assert.NoError(t, err) }() - err = git_model.NewCommitStatus(db.DefaultContext, git_model.NewCommitStatusOptions{ + err = git_model.NewCommitStatus(t.Context(), git_model.NewCommitStatusOptions{ Repo: repo2, Creator: user2, SHA: commit.ID, CommitStatus: &git_model.CommitStatus{ - State: structs.CommitStatusFailure, + State: commitstatus.CommitStatusFailure, TargetURL: "https://example.com/tests/", Context: "compliance/lint-backend", }, }) assert.NoError(t, err) - err = git_model.NewCommitStatus(db.DefaultContext, git_model.NewCommitStatusOptions{ + err = git_model.NewCommitStatus(t.Context(), git_model.NewCommitStatusOptions{ Repo: repo2, Creator: user2, SHA: commit.ID, CommitStatus: &git_model.CommitStatus{ - State: structs.CommitStatusSuccess, + State: commitstatus.CommitStatusSuccess, TargetURL: "https://example.com/tests/", Context: "compliance/lint-backend", }, }) assert.NoError(t, err) - contexts, err := git_model.FindRepoRecentCommitStatusContexts(db.DefaultContext, repo2.ID, time.Hour) + contexts, err := git_model.FindRepoRecentCommitStatusContexts(t.Context(), repo2.ID, time.Hour) assert.NoError(t, err) if assert.Len(t, contexts, 1) { assert.Equal(t, "compliance/lint-backend", contexts[0]) @@ -239,7 +238,7 @@ func TestCommitStatusesHideActionsURL(t *testing.T) { repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4}) run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: 791, RepoID: repo.ID}) - assert.NoError(t, run.LoadAttributes(db.DefaultContext)) + assert.NoError(t, run.LoadAttributes(t.Context())) statuses := []*git_model.CommitStatus{ { @@ -252,7 +251,30 @@ func TestCommitStatusesHideActionsURL(t *testing.T) { }, } - git_model.CommitStatusesHideActionsURL(db.DefaultContext, statuses) + git_model.CommitStatusesHideActionsURL(t.Context(), statuses) assert.Empty(t, statuses[0].TargetURL) assert.Equal(t, "https://mycicd.org/1", statuses[1].TargetURL) } + +func TestGetCountLatestCommitStatus(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + + sha1 := "1234123412341234123412341234123412341234" // the mocked commit ID in test fixtures + + commitStatuses, err := git_model.GetLatestCommitStatus(t.Context(), repo1.ID, sha1, db.ListOptions{ + Page: 1, + PageSize: 2, + }) + assert.NoError(t, err) + assert.Len(t, commitStatuses, 2) + assert.Equal(t, commitstatus.CommitStatusFailure, commitStatuses[0].State) + assert.Equal(t, "ci/awesomeness", commitStatuses[0].Context) + assert.Equal(t, commitstatus.CommitStatusError, commitStatuses[1].State) + assert.Equal(t, "deploy/awesomeness", commitStatuses[1].Context) + + count, err := git_model.CountLatestCommitStatus(t.Context(), repo1.ID, sha1) + assert.NoError(t, err) + assert.EqualValues(t, 3, count) +} diff --git a/models/git/lfs.go b/models/git/lfs.go index bb6361050aa..c471baf5880 100644 --- a/models/git/lfs.go +++ b/models/git/lfs.go @@ -112,7 +112,6 @@ type LFSMetaObject struct { ID int64 `xorm:"pk autoincr"` lfs.Pointer `xorm:"extends"` RepositoryID int64 `xorm:"UNIQUE(s) INDEX NOT NULL"` - Existing bool `xorm:"-"` CreatedUnix timeutil.TimeStamp `xorm:"created"` UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` } @@ -136,26 +135,18 @@ var ErrLFSObjectNotExist = db.ErrNotExist{Resource: "LFS Meta object"} // NewLFSMetaObject stores a given populated LFSMetaObject structure in the database // if it is not already present. func NewLFSMetaObject(ctx context.Context, repoID int64, p lfs.Pointer) (*LFSMetaObject, error) { - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return nil, err - } - defer committer.Close() - m, exist, err := db.Get[LFSMetaObject](ctx, builder.Eq{"repository_id": repoID, "oid": p.Oid}) if err != nil { return nil, err } else if exist { - m.Existing = true - return m, committer.Commit() + return m, nil } m = &LFSMetaObject{Pointer: p, RepositoryID: repoID} if err = db.Insert(ctx, m); err != nil { return nil, err } - - return m, committer.Commit() + return m, nil } // GetLFSMetaObjectByOid selects a LFSMetaObject entry from database by its OID. diff --git a/models/git/lfs_lock.go b/models/git/lfs_lock.go index 07ce7d4abf3..c5f9a4e6dec 100644 --- a/models/git/lfs_lock.go +++ b/models/git/lfs_lock.go @@ -70,32 +70,28 @@ func (l *LFSLock) LoadOwner(ctx context.Context) error { // CreateLFSLock creates a new lock. func CreateLFSLock(ctx context.Context, repo *repo_model.Repository, lock *LFSLock) (*LFSLock, error) { - dbCtx, committer, err := db.TxContext(ctx) - if err != nil { - return nil, err - } - defer committer.Close() + return db.WithTx2(ctx, func(ctx context.Context) (*LFSLock, error) { + if err := CheckLFSAccessForRepo(ctx, lock.OwnerID, repo, perm.AccessModeWrite); err != nil { + return nil, err + } - if err := CheckLFSAccessForRepo(dbCtx, lock.OwnerID, repo, perm.AccessModeWrite); err != nil { - return nil, err - } + lock.Path = util.PathJoinRel(lock.Path) + lock.RepoID = repo.ID - lock.Path = util.PathJoinRel(lock.Path) - lock.RepoID = repo.ID + l, err := GetLFSLock(ctx, repo, lock.Path) + if err == nil { + return l, ErrLFSLockAlreadyExist{lock.RepoID, lock.Path} + } + if !IsErrLFSLockNotExist(err) { + return nil, err + } - l, err := GetLFSLock(dbCtx, repo, lock.Path) - if err == nil { - return l, ErrLFSLockAlreadyExist{lock.RepoID, lock.Path} - } - if !IsErrLFSLockNotExist(err) { - return nil, err - } + if err := db.Insert(ctx, lock); err != nil { + return nil, err + } - if err := db.Insert(dbCtx, lock); err != nil { - return nil, err - } - - return lock, committer.Commit() + return lock, nil + }) } // GetLFSLock returns release by given path. @@ -163,30 +159,26 @@ func CountLFSLockByRepoID(ctx context.Context, repoID int64) (int64, error) { // DeleteLFSLockByID deletes a lock by given ID. func DeleteLFSLockByID(ctx context.Context, id int64, repo *repo_model.Repository, u *user_model.User, force bool) (*LFSLock, error) { - dbCtx, committer, err := db.TxContext(ctx) - if err != nil { - return nil, err - } - defer committer.Close() + return db.WithTx2(ctx, func(ctx context.Context) (*LFSLock, error) { + lock, err := GetLFSLockByID(ctx, id) + if err != nil { + return nil, err + } - lock, err := GetLFSLockByID(dbCtx, id) - if err != nil { - return nil, err - } + if err := CheckLFSAccessForRepo(ctx, u.ID, repo, perm.AccessModeWrite); err != nil { + return nil, err + } - if err := CheckLFSAccessForRepo(dbCtx, u.ID, repo, perm.AccessModeWrite); err != nil { - return nil, err - } + if !force && u.ID != lock.OwnerID { + return nil, errors.New("user doesn't own lock and force flag is not set") + } - if !force && u.ID != lock.OwnerID { - return nil, errors.New("user doesn't own lock and force flag is not set") - } + if _, err := db.GetEngine(ctx).ID(id).Delete(new(LFSLock)); err != nil { + return nil, err + } - if _, err := db.GetEngine(dbCtx).ID(id).Delete(new(LFSLock)); err != nil { - return nil, err - } - - return lock, committer.Commit() + return lock, nil + }) } // CheckLFSAccessForRepo check needed access mode base on action diff --git a/models/git/protected_branch.go b/models/git/protected_branch.go index a3caed73c40..55bbe6938cd 100644 --- a/models/git/protected_branch.go +++ b/models/git/protected_branch.go @@ -246,7 +246,7 @@ func (protectBranch *ProtectedBranch) GetUnprotectedFilePatterns() []glob.Glob { func getFilePatterns(filePatterns string) []glob.Glob { extarr := make([]glob.Glob, 0, 10) - for _, expr := range strings.Split(strings.ToLower(filePatterns), ";") { + for expr := range strings.SplitSeq(strings.ToLower(filePatterns), ";") { expr = strings.TrimSpace(expr) if expr != "" { if g, err := glob.Compile(expr, '.', '/'); err != nil { @@ -518,7 +518,7 @@ func updateTeamWhitelist(ctx context.Context, repo *repo_model.Repository, curre return currentWhitelist, nil } - teams, err := organization.GetTeamsWithAccessToRepo(ctx, repo.OwnerID, repo.ID, perm.AccessModeRead) + teams, err := organization.GetTeamsWithAccessToAnyRepoUnit(ctx, repo.OwnerID, repo.ID, perm.AccessModeRead, unit.TypeCode, unit.TypePullRequests) if err != nil { return nil, fmt.Errorf("GetTeamsWithAccessToRepo [org_id: %d, repo_id: %d]: %v", repo.OwnerID, repo.ID, err) } diff --git a/models/git/protected_branch_list_test.go b/models/git/protected_branch_list_test.go index a46402c5432..298c2fa0741 100644 --- a/models/git/protected_branch_list_test.go +++ b/models/git/protected_branch_list_test.go @@ -70,7 +70,7 @@ func TestBranchRuleMatchPriority(t *testing.T) { assert.Error(t, fmt.Errorf("no matched rules but expected %s[%d]", kase.Rules[kase.ExpectedMatchIdx], kase.ExpectedMatchIdx)) } } else { - assert.EqualValues(t, kase.Rules[kase.ExpectedMatchIdx], matchedPB.RuleName) + assert.Equal(t, kase.Rules[kase.ExpectedMatchIdx], matchedPB.RuleName) } } } diff --git a/models/git/protected_branch_test.go b/models/git/protected_branch_test.go index 49d433f845d..3aa1d7daa8a 100644 --- a/models/git/protected_branch_test.go +++ b/models/git/protected_branch_test.go @@ -4,7 +4,6 @@ package git import ( - "fmt" "testing" "code.gitea.io/gitea/models/db" @@ -75,8 +74,8 @@ func TestBranchRuleMatch(t *testing.T) { } else { infact = " not" } - assert.EqualValues(t, kase.ExpectedMatch, pb.Match(kase.BranchName), - fmt.Sprintf("%s should%s match %s but it is%s", kase.BranchName, should, kase.Rule, infact), + assert.Equal(t, kase.ExpectedMatch, pb.Match(kase.BranchName), + "%s should%s match %s but it is%s", kase.BranchName, should, kase.Rule, infact, ) } } @@ -106,17 +105,17 @@ func TestUpdateProtectBranchPriorities(t *testing.T) { } for _, pb := range protectedBranches { - _, err := db.GetEngine(db.DefaultContext).Insert(pb) + _, err := db.GetEngine(t.Context()).Insert(pb) assert.NoError(t, err) } // Test updating priorities newPriorities := []int64{protectedBranches[2].ID, protectedBranches[0].ID, protectedBranches[1].ID} - err := UpdateProtectBranchPriorities(db.DefaultContext, repo, newPriorities) + err := UpdateProtectBranchPriorities(t.Context(), repo, newPriorities) assert.NoError(t, err) // Verify new priorities - pbs, err := FindRepoProtectedBranchRules(db.DefaultContext, repo.ID) + pbs, err := FindRepoProtectedBranchRules(t.Context(), repo.ID) assert.NoError(t, err) expectedPriorities := map[string]int64{ @@ -134,7 +133,7 @@ func TestNewProtectBranchPriority(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) - err := UpdateProtectBranch(db.DefaultContext, repo, &ProtectedBranch{ + err := UpdateProtectBranch(t.Context(), repo, &ProtectedBranch{ RepoID: repo.ID, RuleName: "branch-1", Priority: 1, @@ -147,10 +146,10 @@ func TestNewProtectBranchPriority(t *testing.T) { // Priority intentionally omitted } - err = UpdateProtectBranch(db.DefaultContext, repo, newPB, WhitelistOptions{}) + err = UpdateProtectBranch(t.Context(), repo, newPB, WhitelistOptions{}) assert.NoError(t, err) - savedPB2, err := GetFirstMatchProtectedBranchRule(db.DefaultContext, repo.ID, "branch-2") + savedPB2, err := GetFirstMatchProtectedBranchRule(t.Context(), repo.ID, "branch-2") assert.NoError(t, err) assert.Equal(t, int64(2), savedPB2.Priority) } diff --git a/models/git/protected_tag_test.go b/models/git/protected_tag_test.go index 164c33e28fd..252eaf1bbae 100644 --- a/models/git/protected_tag_test.go +++ b/models/git/protected_tag_test.go @@ -6,7 +6,6 @@ package git_test import ( "testing" - "code.gitea.io/gitea/models/db" git_model "code.gitea.io/gitea/models/git" "code.gitea.io/gitea/models/unittest" @@ -17,29 +16,29 @@ func TestIsUserAllowed(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) pt := &git_model.ProtectedTag{} - allowed, err := git_model.IsUserAllowedModifyTag(db.DefaultContext, pt, 1) + allowed, err := git_model.IsUserAllowedModifyTag(t.Context(), pt, 1) assert.NoError(t, err) assert.False(t, allowed) pt = &git_model.ProtectedTag{ AllowlistUserIDs: []int64{1}, } - allowed, err = git_model.IsUserAllowedModifyTag(db.DefaultContext, pt, 1) + allowed, err = git_model.IsUserAllowedModifyTag(t.Context(), pt, 1) assert.NoError(t, err) assert.True(t, allowed) - allowed, err = git_model.IsUserAllowedModifyTag(db.DefaultContext, pt, 2) + allowed, err = git_model.IsUserAllowedModifyTag(t.Context(), pt, 2) assert.NoError(t, err) assert.False(t, allowed) pt = &git_model.ProtectedTag{ AllowlistTeamIDs: []int64{1}, } - allowed, err = git_model.IsUserAllowedModifyTag(db.DefaultContext, pt, 1) + allowed, err = git_model.IsUserAllowedModifyTag(t.Context(), pt, 1) assert.NoError(t, err) assert.False(t, allowed) - allowed, err = git_model.IsUserAllowedModifyTag(db.DefaultContext, pt, 2) + allowed, err = git_model.IsUserAllowedModifyTag(t.Context(), pt, 2) assert.NoError(t, err) assert.True(t, allowed) @@ -47,11 +46,11 @@ func TestIsUserAllowed(t *testing.T) { AllowlistUserIDs: []int64{1}, AllowlistTeamIDs: []int64{1}, } - allowed, err = git_model.IsUserAllowedModifyTag(db.DefaultContext, pt, 1) + allowed, err = git_model.IsUserAllowedModifyTag(t.Context(), pt, 1) assert.NoError(t, err) assert.True(t, allowed) - allowed, err = git_model.IsUserAllowedModifyTag(db.DefaultContext, pt, 2) + allowed, err = git_model.IsUserAllowedModifyTag(t.Context(), pt, 2) assert.NoError(t, err) assert.True(t, allowed) } @@ -135,7 +134,7 @@ func TestIsUserAllowedToControlTag(t *testing.T) { } for n, c := range cases { - isAllowed, err := git_model.IsUserAllowedToControlTag(db.DefaultContext, protectedTags, c.name, c.userid) + isAllowed, err := git_model.IsUserAllowedToControlTag(t.Context(), protectedTags, c.name, c.userid) assert.NoError(t, err) assert.Equal(t, c.allowed, isAllowed, "case %d: error should match", n) } @@ -157,7 +156,7 @@ func TestIsUserAllowedToControlTag(t *testing.T) { } for n, c := range cases { - isAllowed, err := git_model.IsUserAllowedToControlTag(db.DefaultContext, protectedTags, c.name, c.userid) + isAllowed, err := git_model.IsUserAllowedToControlTag(t.Context(), protectedTags, c.name, c.userid) assert.NoError(t, err) assert.Equal(t, c.allowed, isAllowed, "case %d: error should match", n) } diff --git a/models/issues/assignees_test.go b/models/issues/assignees_test.go index 2c33efd99e6..13922899dc0 100644 --- a/models/issues/assignees_test.go +++ b/models/issues/assignees_test.go @@ -6,7 +6,6 @@ package issues_test import ( "testing" - "code.gitea.io/gitea/models/db" issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" @@ -18,35 +17,35 @@ func TestUpdateAssignee(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) // Fake issue with assignees - issue, err := issues_model.GetIssueByID(db.DefaultContext, 1) + issue, err := issues_model.GetIssueByID(t.Context(), 1) assert.NoError(t, err) - err = issue.LoadAttributes(db.DefaultContext) + err = issue.LoadAttributes(t.Context()) assert.NoError(t, err) // Assign multiple users - user2, err := user_model.GetUserByID(db.DefaultContext, 2) + user2, err := user_model.GetUserByID(t.Context(), 2) assert.NoError(t, err) - _, _, err = issues_model.ToggleIssueAssignee(db.DefaultContext, issue, &user_model.User{ID: 1}, user2.ID) + _, _, err = issues_model.ToggleIssueAssignee(t.Context(), issue, &user_model.User{ID: 1}, user2.ID) assert.NoError(t, err) - org3, err := user_model.GetUserByID(db.DefaultContext, 3) + org3, err := user_model.GetUserByID(t.Context(), 3) assert.NoError(t, err) - _, _, err = issues_model.ToggleIssueAssignee(db.DefaultContext, issue, &user_model.User{ID: 1}, org3.ID) + _, _, err = issues_model.ToggleIssueAssignee(t.Context(), issue, &user_model.User{ID: 1}, org3.ID) assert.NoError(t, err) - user1, err := user_model.GetUserByID(db.DefaultContext, 1) // This user is already assigned (see the definition in fixtures), so running UpdateAssignee should unassign him + user1, err := user_model.GetUserByID(t.Context(), 1) // This user is already assigned (see the definition in fixtures), so running UpdateAssignee should unassign him assert.NoError(t, err) - _, _, err = issues_model.ToggleIssueAssignee(db.DefaultContext, issue, &user_model.User{ID: 1}, user1.ID) + _, _, err = issues_model.ToggleIssueAssignee(t.Context(), issue, &user_model.User{ID: 1}, user1.ID) assert.NoError(t, err) // Check if he got removed - isAssigned, err := issues_model.IsUserAssignedToIssue(db.DefaultContext, issue, user1) + isAssigned, err := issues_model.IsUserAssignedToIssue(t.Context(), issue, user1) assert.NoError(t, err) assert.False(t, isAssigned) // Check if they're all there - err = issue.LoadAssignees(db.DefaultContext) + err = issue.LoadAssignees(t.Context()) assert.NoError(t, err) var expectedAssignees []*user_model.User @@ -57,12 +56,12 @@ func TestUpdateAssignee(t *testing.T) { } // Check if the user is assigned - isAssigned, err = issues_model.IsUserAssignedToIssue(db.DefaultContext, issue, user2) + isAssigned, err = issues_model.IsUserAssignedToIssue(t.Context(), issue, user2) assert.NoError(t, err) assert.True(t, isAssigned) // This user should not be assigned - isAssigned, err = issues_model.IsUserAssignedToIssue(db.DefaultContext, issue, &user_model.User{ID: 4}) + isAssigned, err = issues_model.IsUserAssignedToIssue(t.Context(), issue, &user_model.User{ID: 4}) assert.NoError(t, err) assert.False(t, isAssigned) } @@ -73,22 +72,22 @@ func TestMakeIDsFromAPIAssigneesToAdd(t *testing.T) { _ = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) _ = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) - IDs, err := issues_model.MakeIDsFromAPIAssigneesToAdd(db.DefaultContext, "", []string{""}) + IDs, err := issues_model.MakeIDsFromAPIAssigneesToAdd(t.Context(), "", []string{""}) assert.NoError(t, err) assert.Equal(t, []int64{}, IDs) - _, err = issues_model.MakeIDsFromAPIAssigneesToAdd(db.DefaultContext, "", []string{"none_existing_user"}) + _, err = issues_model.MakeIDsFromAPIAssigneesToAdd(t.Context(), "", []string{"none_existing_user"}) assert.Error(t, err) - IDs, err = issues_model.MakeIDsFromAPIAssigneesToAdd(db.DefaultContext, "user1", []string{"user1"}) + IDs, err = issues_model.MakeIDsFromAPIAssigneesToAdd(t.Context(), "user1", []string{"user1"}) assert.NoError(t, err) assert.Equal(t, []int64{1}, IDs) - IDs, err = issues_model.MakeIDsFromAPIAssigneesToAdd(db.DefaultContext, "user2", []string{""}) + IDs, err = issues_model.MakeIDsFromAPIAssigneesToAdd(t.Context(), "user2", []string{""}) assert.NoError(t, err) assert.Equal(t, []int64{2}, IDs) - IDs, err = issues_model.MakeIDsFromAPIAssigneesToAdd(db.DefaultContext, "", []string{"user1", "user2"}) + IDs, err = issues_model.MakeIDsFromAPIAssigneesToAdd(t.Context(), "", []string{"user1", "user2"}) assert.NoError(t, err) assert.Equal(t, []int64{1, 2}, IDs) } diff --git a/models/issues/comment.go b/models/issues/comment.go index 6650817e506..a366b7c6a9c 100644 --- a/models/issues/comment.go +++ b/models/issues/comment.go @@ -9,6 +9,7 @@ import ( "context" "fmt" "html/template" + "slices" "strconv" "unicode/utf8" @@ -19,8 +20,6 @@ import ( repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/container" - "code.gitea.io/gitea/modules/gitrepo" - "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/references" @@ -112,8 +111,10 @@ const ( CommentTypePRScheduledToAutoMerge // 34 pr was scheduled to auto merge when checks succeed CommentTypePRUnScheduledToAutoMerge // 35 pr was un scheduled to auto merge when checks succeed - CommentTypePin // 36 pin Issue - CommentTypeUnpin // 37 unpin Issue + CommentTypePin // 36 pin Issue/PullRequest + CommentTypeUnpin // 37 unpin Issue/PullRequest + + CommentTypeChangeTimeEstimate // 38 Change time estimate ) var commentStrings = []string{ @@ -155,6 +156,7 @@ var commentStrings = []string{ "pull_cancel_scheduled_merge", "pin", "unpin", + "change_time_estimate", } func (t CommentType) String() string { @@ -194,6 +196,15 @@ func (t CommentType) HasMailReplySupport() bool { return false } +func (t CommentType) CountedAsConversation() bool { + return slices.Contains(ConversationCountedCommentType(), t) +} + +// ConversationCountedCommentType returns the comment types that are counted as a conversation +func ConversationCountedCommentType() []CommentType { + return []CommentType{CommentTypeComment, CommentTypeReview} +} + // RoleInRepo presents the user's participation in the repo type RoleInRepo string @@ -403,7 +414,7 @@ func (c *Comment) HTMLURL(ctx context.Context) string { log.Error("loadRepo(%d): %v", c.Issue.RepoID, err) return "" } - return c.Issue.HTMLURL() + c.hashLink(ctx) + return c.Issue.HTMLURL(ctx) + c.hashLink(ctx) } // Link formats a relative URL-string to the issue-comment @@ -472,7 +483,7 @@ func (c *Comment) IssueURL(ctx context.Context) string { log.Error("loadRepo(%d): %v", c.Issue.RepoID, err) return "" } - return c.Issue.HTMLURL() + return c.Issue.HTMLURL(ctx) } // PRURL formats a URL-string to the pull-request @@ -492,7 +503,7 @@ func (c *Comment) PRURL(ctx context.Context) string { if !c.Issue.IsPull { return "" } - return c.Issue.HTMLURL() + return c.Issue.HTMLURL(ctx) } // CommentHashTag returns unique hash tag for comment id. @@ -589,26 +600,26 @@ func (c *Comment) LoadAttachments(ctx context.Context) error { return nil } -// UpdateAttachments update attachments by UUIDs for the comment -func (c *Comment) UpdateAttachments(ctx context.Context, uuids []string) error { - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return err +// UpdateCommentAttachments update attachments by UUIDs for the comment +func UpdateCommentAttachments(ctx context.Context, c *Comment, uuids []string) error { + if len(uuids) == 0 { + return nil } - defer committer.Close() - - attachments, err := repo_model.GetAttachmentsByUUIDs(ctx, uuids) - if err != nil { - return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %w", uuids, err) - } - for i := 0; i < len(attachments); i++ { - attachments[i].IssueID = c.IssueID - attachments[i].CommentID = c.ID - if err := repo_model.UpdateAttachment(ctx, attachments[i]); err != nil { - return fmt.Errorf("update attachment [id: %d]: %w", attachments[i].ID, err) + return db.WithTx(ctx, func(ctx context.Context) error { + attachments, err := repo_model.GetAttachmentsByUUIDs(ctx, uuids) + if err != nil { + return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %w", uuids, err) } - } - return committer.Commit() + for i := range attachments { + attachments[i].IssueID = c.IssueID + attachments[i].CommentID = c.ID + if err := repo_model.UpdateAttachment(ctx, attachments[i]); err != nil { + return fmt.Errorf("update attachment [id: %d]: %w", attachments[i].ID, err) + } + } + c.Attachments = attachments + return nil + }) } // LoadAssigneeUserAndTeam if comment.Type is CommentTypeAssignees, then load assignees @@ -704,7 +715,8 @@ func (c *Comment) LoadReactions(ctx context.Context, repo *repo_model.Repository return nil } -func (c *Comment) loadReview(ctx context.Context) (err error) { +// LoadReview loads the associated review +func (c *Comment) LoadReview(ctx context.Context) (err error) { if c.ReviewID == 0 { return nil } @@ -721,11 +733,6 @@ func (c *Comment) loadReview(ctx context.Context) (err error) { return nil } -// LoadReview loads the associated review -func (c *Comment) LoadReview(ctx context.Context) error { - return c.loadReview(ctx) -} - // DiffSide returns "previous" if Comment.Line is a LOC of the previous changes and "proposed" if it is a LOC of the proposed changes. func (c *Comment) DiffSide() string { if c.Line < 0 { @@ -757,130 +764,87 @@ func (c *Comment) CodeCommentLink(ctx context.Context) string { return fmt.Sprintf("%s/files#%s", c.Issue.Link(), c.HashTag()) } -// LoadPushCommits Load push commits -func (c *Comment) LoadPushCommits(ctx context.Context) (err error) { - if c.Content == "" || c.Commits != nil || c.Type != CommentTypePullRequestPush { - return nil - } - - var data PushActionContent - - err = json.Unmarshal([]byte(c.Content), &data) - if err != nil { - return err - } - - c.IsForcePush = data.IsForcePush - - if c.IsForcePush { - if len(data.CommitIDs) != 2 { - return nil - } - c.OldCommit = data.CommitIDs[0] - c.NewCommit = data.CommitIDs[1] - } else { - gitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, c.Issue.Repo) - if err != nil { - return err - } - defer closer.Close() - - c.Commits = git_model.ConvertFromGitCommit(ctx, gitRepo.GetCommitsFromIDs(data.CommitIDs), c.Issue.Repo) - c.CommitsNum = int64(len(c.Commits)) - } - - return err -} - // CreateComment creates comment with context func CreateComment(ctx context.Context, opts *CreateCommentOptions) (_ *Comment, err error) { - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return nil, err - } - defer committer.Close() - - e := db.GetEngine(ctx) - var LabelID int64 - if opts.Label != nil { - LabelID = opts.Label.ID - } - - var commentMetaData *CommentMetaData - if opts.ProjectColumnTitle != "" { - commentMetaData = &CommentMetaData{ - ProjectColumnID: opts.ProjectColumnID, - ProjectColumnTitle: opts.ProjectColumnTitle, - ProjectTitle: opts.ProjectTitle, + return db.WithTx2(ctx, func(ctx context.Context) (*Comment, error) { + var LabelID int64 + if opts.Label != nil { + LabelID = opts.Label.ID } - } - comment := &Comment{ - Type: opts.Type, - PosterID: opts.Doer.ID, - Poster: opts.Doer, - IssueID: opts.Issue.ID, - LabelID: LabelID, - OldMilestoneID: opts.OldMilestoneID, - MilestoneID: opts.MilestoneID, - OldProjectID: opts.OldProjectID, - ProjectID: opts.ProjectID, - TimeID: opts.TimeID, - RemovedAssignee: opts.RemovedAssignee, - AssigneeID: opts.AssigneeID, - AssigneeTeamID: opts.AssigneeTeamID, - CommitID: opts.CommitID, - CommitSHA: opts.CommitSHA, - Line: opts.LineNum, - Content: opts.Content, - OldTitle: opts.OldTitle, - NewTitle: opts.NewTitle, - OldRef: opts.OldRef, - NewRef: opts.NewRef, - DependentIssueID: opts.DependentIssueID, - TreePath: opts.TreePath, - ReviewID: opts.ReviewID, - Patch: opts.Patch, - RefRepoID: opts.RefRepoID, - RefIssueID: opts.RefIssueID, - RefCommentID: opts.RefCommentID, - RefAction: opts.RefAction, - RefIsPull: opts.RefIsPull, - IsForcePush: opts.IsForcePush, - Invalidated: opts.Invalidated, - CommentMetaData: commentMetaData, - } - if _, err = e.Insert(comment); err != nil { - return nil, err - } + var commentMetaData *CommentMetaData + if opts.ProjectColumnTitle != "" { + commentMetaData = &CommentMetaData{ + ProjectColumnID: opts.ProjectColumnID, + ProjectColumnTitle: opts.ProjectColumnTitle, + ProjectTitle: opts.ProjectTitle, + } + } - if err = opts.Repo.LoadOwner(ctx); err != nil { - return nil, err - } + comment := &Comment{ + Type: opts.Type, + PosterID: opts.Doer.ID, + Poster: opts.Doer, + IssueID: opts.Issue.ID, + LabelID: LabelID, + OldMilestoneID: opts.OldMilestoneID, + MilestoneID: opts.MilestoneID, + OldProjectID: opts.OldProjectID, + ProjectID: opts.ProjectID, + TimeID: opts.TimeID, + RemovedAssignee: opts.RemovedAssignee, + AssigneeID: opts.AssigneeID, + AssigneeTeamID: opts.AssigneeTeamID, + CommitID: opts.CommitID, + CommitSHA: opts.CommitSHA, + Line: opts.LineNum, + Content: opts.Content, + OldTitle: opts.OldTitle, + NewTitle: opts.NewTitle, + OldRef: opts.OldRef, + NewRef: opts.NewRef, + DependentIssueID: opts.DependentIssueID, + TreePath: opts.TreePath, + ReviewID: opts.ReviewID, + Patch: opts.Patch, + RefRepoID: opts.RefRepoID, + RefIssueID: opts.RefIssueID, + RefCommentID: opts.RefCommentID, + RefAction: opts.RefAction, + RefIsPull: opts.RefIsPull, + IsForcePush: opts.IsForcePush, + Invalidated: opts.Invalidated, + CommentMetaData: commentMetaData, + } + if err = db.Insert(ctx, comment); err != nil { + return nil, err + } - if err = updateCommentInfos(ctx, opts, comment); err != nil { - return nil, err - } + if err = opts.Repo.LoadOwner(ctx); err != nil { + return nil, err + } - if err = comment.AddCrossReferences(ctx, opts.Doer, false); err != nil { - return nil, err - } - if err = committer.Commit(); err != nil { - return nil, err - } - return comment, nil + if err = updateCommentInfos(ctx, opts, comment); err != nil { + return nil, err + } + + if err = comment.AddCrossReferences(ctx, opts.Doer, false); err != nil { + return nil, err + } + return comment, nil + }) } func updateCommentInfos(ctx context.Context, opts *CreateCommentOptions, comment *Comment) (err error) { // Check comment type. switch opts.Type { case CommentTypeCode: - if err = updateAttachments(ctx, opts, comment); err != nil { + if err = UpdateCommentAttachments(ctx, comment, opts.Attachments); err != nil { return err } if comment.ReviewID != 0 { if comment.Review == nil { - if err := comment.loadReview(ctx); err != nil { + if err := comment.LoadReview(ctx); err != nil { return err } } @@ -890,12 +854,12 @@ func updateCommentInfos(ctx context.Context, opts *CreateCommentOptions, comment } fallthrough case CommentTypeComment: - if _, err = db.Exec(ctx, "UPDATE `issue` SET num_comments=num_comments+1 WHERE id=?", opts.Issue.ID); err != nil { + if err := UpdateIssueNumComments(ctx, opts.Issue.ID); err != nil { return err } fallthrough case CommentTypeReview: - if err = updateAttachments(ctx, opts, comment); err != nil { + if err = UpdateCommentAttachments(ctx, comment, opts.Attachments); err != nil { return err } case CommentTypeReopen, CommentTypeClose: @@ -907,23 +871,6 @@ func updateCommentInfos(ctx context.Context, opts *CreateCommentOptions, comment return UpdateIssueCols(ctx, opts.Issue, "updated_unix") } -func updateAttachments(ctx context.Context, opts *CreateCommentOptions, comment *Comment) error { - attachments, err := repo_model.GetAttachmentsByUUIDs(ctx, opts.Attachments) - if err != nil { - return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %w", opts.Attachments, err) - } - for i := range attachments { - attachments[i].IssueID = opts.Issue.ID - attachments[i].CommentID = comment.ID - // No assign value could be 0, so ignore AllCols(). - if _, err = db.GetEngine(ctx).ID(attachments[i].ID).Update(attachments[i]); err != nil { - return fmt.Errorf("update attachment [%d]: %w", attachments[i].ID, err) - } - } - comment.Attachments = attachments - return nil -} - func createDeadlineComment(ctx context.Context, doer *user_model.User, issue *Issue, newDeadlineUnix timeutil.TimeStamp) (*Comment, error) { var content string var commentType CommentType @@ -1137,33 +1084,21 @@ func UpdateCommentInvalidate(ctx context.Context, c *Comment) error { // UpdateComment updates information of comment. func UpdateComment(ctx context.Context, c *Comment, contentVersion int, doer *user_model.User) error { - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return err - } - defer committer.Close() - sess := db.GetEngine(ctx) + return db.WithTx(ctx, func(ctx context.Context) error { + c.ContentVersion = contentVersion + 1 - c.ContentVersion = contentVersion + 1 - - affected, err := sess.ID(c.ID).AllCols().Where("content_version = ?", contentVersion).Update(c) - if err != nil { - return err - } - if affected == 0 { - return ErrCommentAlreadyChanged - } - if err := c.LoadIssue(ctx); err != nil { - return err - } - if err := c.AddCrossReferences(ctx, doer, true); err != nil { - return err - } - if err := committer.Commit(); err != nil { - return fmt.Errorf("Commit: %w", err) - } - - return nil + affected, err := db.GetEngine(ctx).ID(c.ID).AllCols().Where("content_version = ?", contentVersion).Update(c) + if err != nil { + return err + } + if affected == 0 { + return ErrCommentAlreadyChanged + } + if err := c.LoadIssue(ctx); err != nil { + return err + } + return c.AddCrossReferences(ctx, doer, true) + }) } // DeleteComment deletes the comment @@ -1179,8 +1114,8 @@ func DeleteComment(ctx context.Context, comment *Comment) error { return err } - if comment.Type == CommentTypeComment { - if _, err := e.ID(comment.IssueID).Decr("num_comments").Update(new(Issue)); err != nil { + if comment.Type.CountedAsConversation() { + if err := UpdateIssueNumComments(ctx, comment.IssueID); err != nil { return err } } @@ -1297,6 +1232,21 @@ func (c *Comment) HasOriginalAuthor() bool { return c.OriginalAuthor != "" && c.OriginalAuthorID != 0 } +func UpdateIssueNumCommentsBuilder(issueID int64) *builder.Builder { + subQuery := builder.Select("COUNT(*)").From("`comment`").Where( + builder.Eq{"issue_id": issueID}.And( + builder.In("`type`", ConversationCountedCommentType()), + )) + + return builder.Update(builder.Eq{"num_comments": subQuery}). + From("`issue`").Where(builder.Eq{"id": issueID}) +} + +func UpdateIssueNumComments(ctx context.Context, issueID int64) error { + _, err := db.GetEngine(ctx).Exec(UpdateIssueNumCommentsBuilder(issueID)) + return err +} + // InsertIssueComments inserts many comments of issues. func InsertIssueComments(ctx context.Context, comments []*Comment) error { if len(comments) == 0 { @@ -1307,32 +1257,28 @@ func InsertIssueComments(ctx context.Context, comments []*Comment) error { return comment.IssueID, true }) - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return err - } - defer committer.Close() - for _, comment := range comments { - if _, err := db.GetEngine(ctx).NoAutoTime().Insert(comment); err != nil { - return err + return db.WithTx(ctx, func(ctx context.Context) error { + for _, comment := range comments { + if _, err := db.GetEngine(ctx).NoAutoTime().Insert(comment); err != nil { + return err + } + + for _, reaction := range comment.Reactions { + reaction.IssueID = comment.IssueID + reaction.CommentID = comment.ID + } + if len(comment.Reactions) > 0 { + if err := db.Insert(ctx, comment.Reactions); err != nil { + return err + } + } } - for _, reaction := range comment.Reactions { - reaction.IssueID = comment.IssueID - reaction.CommentID = comment.ID - } - if len(comment.Reactions) > 0 { - if err := db.Insert(ctx, comment.Reactions); err != nil { + for _, issueID := range issueIDs { + if err := UpdateIssueNumComments(ctx, issueID); err != nil { return err } } - } - - for _, issueID := range issueIDs { - if _, err := db.Exec(ctx, "UPDATE issue set num_comments = (SELECT count(*) FROM comment WHERE issue_id = ? AND `type`=?) WHERE id = ?", - issueID, CommentTypeComment, issueID); err != nil { - return err - } - } - return committer.Commit() + return nil + }) } diff --git a/models/issues/comment_code.go b/models/issues/comment_code.go index 67a77ceb13f..55e67a1243b 100644 --- a/models/issues/comment_code.go +++ b/models/issues/comment_code.go @@ -5,6 +5,7 @@ package issues import ( "context" + "strconv" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/renderhelper" @@ -86,8 +87,10 @@ func findCodeComments(ctx context.Context, opts FindCommentsOptions, issue *Issu ids = append(ids, comment.ReviewID) } } - if err := e.In("id", ids).Find(&reviews); err != nil { - return nil, err + if len(ids) > 0 { + if err := e.In("id", ids).Find(&reviews); err != nil { + return nil, err + } } n := 0 @@ -112,7 +115,9 @@ func findCodeComments(ctx context.Context, opts FindCommentsOptions, issue *Issu } var err error - rctx := renderhelper.NewRenderContextRepoComment(ctx, issue.Repo) + rctx := renderhelper.NewRenderContextRepoComment(ctx, issue.Repo, renderhelper.RepoCommentOptions{ + FootnoteContextID: strconv.FormatInt(comment.ID, 10), + }) if comment.RenderedContent, err = markdown.RenderString(rctx, comment.Content); err != nil { return nil, err } diff --git a/models/issues/comment_list.go b/models/issues/comment_list.go index 61ac1c8f562..f6c485449f6 100644 --- a/models/issues/comment_list.go +++ b/models/issues/comment_list.go @@ -26,14 +26,14 @@ func (comments CommentList) LoadPosters(ctx context.Context) error { return c.PosterID, c.Poster == nil && c.PosterID > 0 }) - posterMaps, err := getPostersByIDs(ctx, posterIDs) + posterMaps, err := user_model.GetUsersMapByIDs(ctx, posterIDs) if err != nil { return err } for _, comment := range comments { if comment.Poster == nil { - comment.Poster = getPoster(comment.PosterID, posterMaps) + comment.Poster = user_model.GetPossibleUserFromMap(comment.PosterID, posterMaps) } } return nil @@ -41,7 +41,7 @@ func (comments CommentList) LoadPosters(ctx context.Context) error { func (comments CommentList) getLabelIDs() []int64 { return container.FilterSlice(comments, func(comment *Comment) (int64, bool) { - return comment.LabelID, comment.LabelID > 0 + return comment.LabelID, comment.LabelID > 0 && comment.Label == nil }) } @@ -51,13 +51,13 @@ func (comments CommentList) loadLabels(ctx context.Context) error { } labelIDs := comments.getLabelIDs() + if len(labelIDs) == 0 { + return nil + } commentLabels := make(map[int64]*Label, len(labelIDs)) left := len(labelIDs) for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } + limit := min(left, db.DefaultMaxInSize) rows, err := db.GetEngine(ctx). In("id", labelIDs[:limit]). Rows(new(Label)) @@ -104,10 +104,7 @@ func (comments CommentList) loadMilestones(ctx context.Context) error { milestoneMaps := make(map[int64]*Milestone, len(milestoneIDs)) left := len(milestoneIDs) for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } + limit := min(left, db.DefaultMaxInSize) err := db.GetEngine(ctx). In("id", milestoneIDs[:limit]). Find(&milestoneMaps) @@ -118,8 +115,8 @@ func (comments CommentList) loadMilestones(ctx context.Context) error { milestoneIDs = milestoneIDs[limit:] } - for _, issue := range comments { - issue.Milestone = milestoneMaps[issue.MilestoneID] + for _, comment := range comments { + comment.Milestone = milestoneMaps[comment.MilestoneID] } return nil } @@ -143,10 +140,7 @@ func (comments CommentList) loadOldMilestones(ctx context.Context) error { milestoneMaps := make(map[int64]*Milestone, len(milestoneIDs)) left := len(milestoneIDs) for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } + limit := min(left, db.DefaultMaxInSize) err := db.GetEngine(ctx). In("id", milestoneIDs[:limit]). Find(&milestoneMaps) @@ -175,13 +169,13 @@ func (comments CommentList) loadAssignees(ctx context.Context) error { } assigneeIDs := comments.getAssigneeIDs() + if len(assigneeIDs) == 0 { + return nil + } assignees := make(map[int64]*user_model.User, len(assigneeIDs)) left := len(assigneeIDs) for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } + limit := min(left, db.DefaultMaxInSize) rows, err := db.GetEngine(ctx). In("id", assigneeIDs[:limit]). Rows(new(user_model.User)) @@ -250,10 +244,7 @@ func (comments CommentList) LoadIssues(ctx context.Context) error { issues := make(map[int64]*Issue, len(issueIDs)) left := len(issueIDs) for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } + limit := min(left, db.DefaultMaxInSize) rows, err := db.GetEngine(ctx). In("id", issueIDs[:limit]). Rows(new(Issue)) @@ -301,13 +292,13 @@ func (comments CommentList) loadDependentIssues(ctx context.Context) error { e := db.GetEngine(ctx) issueIDs := comments.getDependentIssueIDs() + if len(issueIDs) == 0 { + return nil + } issues := make(map[int64]*Issue, len(issueIDs)) left := len(issueIDs) for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } + limit := min(left, db.DefaultMaxInSize) rows, err := e. In("id", issueIDs[:limit]). Rows(new(Issue)) @@ -383,10 +374,7 @@ func (comments CommentList) LoadAttachments(ctx context.Context) (err error) { commentsIDs := comments.getAttachmentCommentIDs() left := len(commentsIDs) for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } + limit := min(left, db.DefaultMaxInSize) rows, err := db.GetEngine(ctx). In("comment_id", commentsIDs[:limit]). Rows(new(repo_model.Attachment)) @@ -427,6 +415,9 @@ func (comments CommentList) loadReviews(ctx context.Context) error { } reviewIDs := comments.getReviewIDs() + if len(reviewIDs) == 0 { + return nil + } reviews := make(map[int64]*Review, len(reviewIDs)) if err := db.GetEngine(ctx).In("id", reviewIDs).Find(&reviews); err != nil { return err diff --git a/models/issues/comment_test.go b/models/issues/comment_test.go index c5bbfdedc28..3660f9c9398 100644 --- a/models/issues/comment_test.go +++ b/models/issues/comment_test.go @@ -24,7 +24,7 @@ func TestCreateComment(t *testing.T) { doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) now := time.Now().Unix() - comment, err := issues_model.CreateComment(db.DefaultContext, &issues_model.CreateCommentOptions{ + comment, err := issues_model.CreateComment(t.Context(), &issues_model.CreateCommentOptions{ Type: issues_model.CommentTypeComment, Doer: doer, Repo: repo, @@ -34,10 +34,10 @@ func TestCreateComment(t *testing.T) { assert.NoError(t, err) then := time.Now().Unix() - assert.EqualValues(t, issues_model.CommentTypeComment, comment.Type) - assert.EqualValues(t, "Hello", comment.Content) - assert.EqualValues(t, issue.ID, comment.IssueID) - assert.EqualValues(t, doer.ID, comment.PosterID) + assert.Equal(t, issues_model.CommentTypeComment, comment.Type) + assert.Equal(t, "Hello", comment.Content) + assert.Equal(t, issue.ID, comment.IssueID) + assert.Equal(t, doer.ID, comment.PosterID) unittest.AssertInt64InRange(t, now, then, int64(comment.CreatedUnix)) unittest.AssertExistsAndLoadBean(t, comment) // assert actually added to DB @@ -45,12 +45,30 @@ func TestCreateComment(t *testing.T) { unittest.AssertInt64InRange(t, now, then, int64(updatedIssue.UpdatedUnix)) } +func Test_UpdateCommentAttachment(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 1}) + attachment := repo_model.Attachment{ + Name: "test.txt", + } + assert.NoError(t, db.Insert(t.Context(), &attachment)) + + err := issues_model.UpdateCommentAttachments(t.Context(), comment, []string{attachment.UUID}) + assert.NoError(t, err) + + attachment2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: attachment.ID}) + assert.Equal(t, attachment.Name, attachment2.Name) + assert.Equal(t, comment.ID, attachment2.CommentID) + assert.Equal(t, comment.IssueID, attachment2.IssueID) +} + func TestFetchCodeComments(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2}) user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) - res, err := issues_model.FetchCodeComments(db.DefaultContext, issue, user, false) + res, err := issues_model.FetchCodeComments(t.Context(), issue, user, false) assert.NoError(t, err) assert.Contains(t, res, "README.md") assert.Contains(t, res["README.md"], int64(4)) @@ -58,13 +76,13 @@ func TestFetchCodeComments(t *testing.T) { assert.Equal(t, int64(4), res["README.md"][4][0].ID) user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) - res, err = issues_model.FetchCodeComments(db.DefaultContext, issue, user2, false) + res, err = issues_model.FetchCodeComments(t.Context(), issue, user2, false) assert.NoError(t, err) assert.Len(t, res, 1) } func TestAsCommentType(t *testing.T) { - assert.Equal(t, issues_model.CommentType(0), issues_model.CommentTypeComment) + assert.Equal(t, issues_model.CommentTypeComment, issues_model.CommentType(0)) assert.Equal(t, issues_model.CommentTypeUndefined, issues_model.AsCommentType("")) assert.Equal(t, issues_model.CommentTypeUndefined, issues_model.AsCommentType("nonsense")) assert.Equal(t, issues_model.CommentTypeComment, issues_model.AsCommentType("comment")) @@ -74,7 +92,7 @@ func TestAsCommentType(t *testing.T) { func TestMigrate_InsertIssueComments(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1}) - _ = issue.LoadRepo(db.DefaultContext) + _ = issue.LoadRepo(t.Context()) owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: issue.Repo.OwnerID}) reaction := &issues_model.Reaction{ Type: "heart", @@ -89,11 +107,20 @@ func TestMigrate_InsertIssueComments(t *testing.T) { Reactions: []*issues_model.Reaction{reaction}, } - err := issues_model.InsertIssueComments(db.DefaultContext, []*issues_model.Comment{comment}) + err := issues_model.InsertIssueComments(t.Context(), []*issues_model.Comment{comment}) assert.NoError(t, err) issueModified := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1}) - assert.EqualValues(t, issue.NumComments+1, issueModified.NumComments) + assert.Equal(t, issue.NumComments+1, issueModified.NumComments) unittest.CheckConsistencyFor(t, &issues_model.Issue{}) } + +func Test_UpdateIssueNumComments(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + issue2 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2}) + + assert.NoError(t, issues_model.UpdateIssueNumComments(t.Context(), issue2.ID)) + issue2 = unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2}) + assert.Equal(t, 1, issue2.NumComments) +} diff --git a/models/issues/content_history_test.go b/models/issues/content_history_test.go index 1caa73a9487..d2fcc82b1d5 100644 --- a/models/issues/content_history_test.go +++ b/models/issues/content_history_test.go @@ -17,7 +17,7 @@ import ( func TestContentHistory(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - dbCtx := db.DefaultContext + dbCtx := t.Context() timeStampNow := timeutil.TimeStampNow() _ = issues_model.SaveIssueContentHistory(dbCtx, 1, 10, 0, timeStampNow, "i-a", true) @@ -82,18 +82,18 @@ func TestContentHistory(t *testing.T) { func TestHasIssueContentHistoryForCommentOnly(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - _ = db.TruncateBeans(db.DefaultContext, &issues_model.ContentHistory{}) + _ = db.TruncateBeans(t.Context(), &issues_model.ContentHistory{}) - hasHistory1, _ := issues_model.HasIssueContentHistory(db.DefaultContext, 10, 0) + hasHistory1, _ := issues_model.HasIssueContentHistory(t.Context(), 10, 0) assert.False(t, hasHistory1) - hasHistory2, _ := issues_model.HasIssueContentHistory(db.DefaultContext, 10, 100) + hasHistory2, _ := issues_model.HasIssueContentHistory(t.Context(), 10, 100) assert.False(t, hasHistory2) - _ = issues_model.SaveIssueContentHistory(db.DefaultContext, 1, 10, 100, timeutil.TimeStampNow(), "c-a", true) - _ = issues_model.SaveIssueContentHistory(db.DefaultContext, 1, 10, 100, timeutil.TimeStampNow().Add(5), "c-b", false) + _ = issues_model.SaveIssueContentHistory(t.Context(), 1, 10, 100, timeutil.TimeStampNow(), "c-a", true) + _ = issues_model.SaveIssueContentHistory(t.Context(), 1, 10, 100, timeutil.TimeStampNow().Add(5), "c-b", false) - hasHistory1, _ = issues_model.HasIssueContentHistory(db.DefaultContext, 10, 0) + hasHistory1, _ = issues_model.HasIssueContentHistory(t.Context(), 10, 0) assert.False(t, hasHistory1) - hasHistory2, _ = issues_model.HasIssueContentHistory(db.DefaultContext, 10, 100) + hasHistory2, _ = issues_model.HasIssueContentHistory(t.Context(), 10, 100) assert.True(t, hasHistory2) } diff --git a/models/issues/dependency.go b/models/issues/dependency.go index 146dd1887da..0eaa47e3593 100644 --- a/models/issues/dependency.go +++ b/models/issues/dependency.go @@ -128,79 +128,64 @@ const ( // CreateIssueDependency creates a new dependency for an issue func CreateIssueDependency(ctx context.Context, user *user_model.User, issue, dep *Issue) error { - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return err - } - defer committer.Close() + return db.WithTx(ctx, func(ctx context.Context) error { + // Check if it already exists + exists, err := issueDepExists(ctx, issue.ID, dep.ID) + if err != nil { + return err + } + if exists { + return ErrDependencyExists{issue.ID, dep.ID} + } + // And if it would be circular + circular, err := issueDepExists(ctx, dep.ID, issue.ID) + if err != nil { + return err + } + if circular { + return ErrCircularDependency{issue.ID, dep.ID} + } - // Check if it already exists - exists, err := issueDepExists(ctx, issue.ID, dep.ID) - if err != nil { - return err - } - if exists { - return ErrDependencyExists{issue.ID, dep.ID} - } - // And if it would be circular - circular, err := issueDepExists(ctx, dep.ID, issue.ID) - if err != nil { - return err - } - if circular { - return ErrCircularDependency{issue.ID, dep.ID} - } + if err := db.Insert(ctx, &IssueDependency{ + UserID: user.ID, + IssueID: issue.ID, + DependencyID: dep.ID, + }); err != nil { + return err + } - if err := db.Insert(ctx, &IssueDependency{ - UserID: user.ID, - IssueID: issue.ID, - DependencyID: dep.ID, - }); err != nil { - return err - } - - // Add comment referencing the new dependency - if err = createIssueDependencyComment(ctx, user, issue, dep, true); err != nil { - return err - } - - return committer.Commit() + // Add comment referencing the new dependency + return createIssueDependencyComment(ctx, user, issue, dep, true) + }) } // RemoveIssueDependency removes a dependency from an issue func RemoveIssueDependency(ctx context.Context, user *user_model.User, issue, dep *Issue, depType DependencyType) (err error) { - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return err - } - defer committer.Close() + return db.WithTx(ctx, func(ctx context.Context) error { + var issueDepToDelete IssueDependency - var issueDepToDelete IssueDependency + switch depType { + case DependencyTypeBlockedBy: + issueDepToDelete = IssueDependency{IssueID: issue.ID, DependencyID: dep.ID} + case DependencyTypeBlocking: + issueDepToDelete = IssueDependency{IssueID: dep.ID, DependencyID: issue.ID} + default: + return ErrUnknownDependencyType{depType} + } - switch depType { - case DependencyTypeBlockedBy: - issueDepToDelete = IssueDependency{IssueID: issue.ID, DependencyID: dep.ID} - case DependencyTypeBlocking: - issueDepToDelete = IssueDependency{IssueID: dep.ID, DependencyID: issue.ID} - default: - return ErrUnknownDependencyType{depType} - } + affected, err := db.GetEngine(ctx).Delete(&issueDepToDelete) + if err != nil { + return err + } - affected, err := db.GetEngine(ctx).Delete(&issueDepToDelete) - if err != nil { - return err - } + // If we deleted nothing, the dependency did not exist + if affected <= 0 { + return ErrDependencyNotExists{issue.ID, dep.ID} + } - // If we deleted nothing, the dependency did not exist - if affected <= 0 { - return ErrDependencyNotExists{issue.ID, dep.ID} - } - - // Add comment referencing the removed dependency - if err = createIssueDependencyComment(ctx, user, issue, dep, false); err != nil { - return err - } - return committer.Commit() + // Add comment referencing the removed dependency + return createIssueDependencyComment(ctx, user, issue, dep, false) + }) } // Check if the dependency already exists diff --git a/models/issues/dependency_test.go b/models/issues/dependency_test.go index 6eed483cc9b..0e6a870ff9e 100644 --- a/models/issues/dependency_test.go +++ b/models/issues/dependency_test.go @@ -6,7 +6,6 @@ package issues_test import ( "testing" - "code.gitea.io/gitea/models/db" issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" @@ -18,45 +17,56 @@ func TestCreateIssueDependency(t *testing.T) { // Prepare assert.NoError(t, unittest.PrepareTestDatabase()) - user1, err := user_model.GetUserByID(db.DefaultContext, 1) + user1, err := user_model.GetUserByID(t.Context(), 1) assert.NoError(t, err) - issue1, err := issues_model.GetIssueByID(db.DefaultContext, 1) + issue1, err := issues_model.GetIssueByID(t.Context(), 1) assert.NoError(t, err) - issue2, err := issues_model.GetIssueByID(db.DefaultContext, 2) + issue2, err := issues_model.GetIssueByID(t.Context(), 2) assert.NoError(t, err) // Create a dependency and check if it was successful - err = issues_model.CreateIssueDependency(db.DefaultContext, user1, issue1, issue2) + err = issues_model.CreateIssueDependency(t.Context(), user1, issue1, issue2) assert.NoError(t, err) // Do it again to see if it will check if the dependency already exists - err = issues_model.CreateIssueDependency(db.DefaultContext, user1, issue1, issue2) + err = issues_model.CreateIssueDependency(t.Context(), user1, issue1, issue2) assert.Error(t, err) assert.True(t, issues_model.IsErrDependencyExists(err)) // Check for circular dependencies - err = issues_model.CreateIssueDependency(db.DefaultContext, user1, issue2, issue1) + err = issues_model.CreateIssueDependency(t.Context(), user1, issue2, issue1) assert.Error(t, err) assert.True(t, issues_model.IsErrCircularDependency(err)) _ = unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{Type: issues_model.CommentTypeAddDependency, PosterID: user1.ID, IssueID: issue1.ID}) // Check if dependencies left is correct - left, err := issues_model.IssueNoDependenciesLeft(db.DefaultContext, issue1) + left, err := issues_model.IssueNoDependenciesLeft(t.Context(), issue1) assert.NoError(t, err) assert.False(t, left) // Close #2 and check again - _, err = issues_model.ChangeIssueStatus(db.DefaultContext, issue2, user1, true) + _, err = issues_model.CloseIssue(t.Context(), issue2, user1) assert.NoError(t, err) - left, err = issues_model.IssueNoDependenciesLeft(db.DefaultContext, issue1) + issue2Closed, err := issues_model.GetIssueByID(t.Context(), 2) + assert.NoError(t, err) + assert.True(t, issue2Closed.IsClosed) + + left, err = issues_model.IssueNoDependenciesLeft(t.Context(), issue1) assert.NoError(t, err) assert.True(t, left) // Test removing the dependency - err = issues_model.RemoveIssueDependency(db.DefaultContext, user1, issue1, issue2, issues_model.DependencyTypeBlockedBy) + err = issues_model.RemoveIssueDependency(t.Context(), user1, issue1, issue2, issues_model.DependencyTypeBlockedBy) assert.NoError(t, err) + + _, err = issues_model.ReopenIssue(t.Context(), issue2, user1) + assert.NoError(t, err) + + issue2Reopened, err := issues_model.GetIssueByID(t.Context(), 2) + assert.NoError(t, err) + assert.False(t, issue2Reopened.IsClosed) } diff --git a/models/issues/issue.go b/models/issues/issue.go index fd89267b84d..053b96dceb5 100644 --- a/models/issues/issue.go +++ b/models/issues/issue.go @@ -10,6 +10,7 @@ import ( "html/template" "regexp" "slices" + "strconv" "code.gitea.io/gitea/models/db" project_model "code.gitea.io/gitea/models/project" @@ -17,6 +18,7 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/timeutil" @@ -46,23 +48,6 @@ func (err ErrIssueNotExist) Unwrap() error { return util.ErrNotExist } -// ErrIssueIsClosed represents a "IssueIsClosed" kind of error. -type ErrIssueIsClosed struct { - ID int64 - RepoID int64 - Index int64 -} - -// IsErrIssueIsClosed checks if an error is a ErrIssueNotExist. -func IsErrIssueIsClosed(err error) bool { - _, ok := err.(ErrIssueIsClosed) - return ok -} - -func (err ErrIssueIsClosed) Error() string { - return fmt.Sprintf("issue is closed [id: %d, repo_id: %d, index: %d]", err.ID, err.RepoID, err.Index) -} - // ErrNewIssueInsert is used when the INSERT statement in newIssue fails type ErrNewIssueInsert struct { OriginalError error @@ -78,22 +63,6 @@ func (err ErrNewIssueInsert) Error() string { return err.OriginalError.Error() } -// ErrIssueWasClosed is used when close a closed issue -type ErrIssueWasClosed struct { - ID int64 - Index int64 -} - -// IsErrIssueWasClosed checks if an error is a ErrIssueWasClosed. -func IsErrIssueWasClosed(err error) bool { - _, ok := err.(ErrIssueWasClosed) - return ok -} - -func (err ErrIssueWasClosed) Error() string { - return fmt.Sprintf("Issue [%d] %d was already closed", err.ID, err.Index) -} - var ErrIssueAlreadyChanged = util.NewInvalidArgumentErrorf("the issue is already changed") // Issue represents an issue or pull request of repository. @@ -125,8 +94,11 @@ type Issue struct { IsPull bool `xorm:"INDEX"` // Indicates whether is a pull request or not. PullRequest *PullRequest `xorm:"-"` NumComments int - Ref string - PinOrder int `xorm:"DEFAULT 0"` + + // TODO: RemoveIssueRef: see "repo/issue/branch_selector_field.tmpl" + Ref string + + PinOrder int `xorm:"-"` // 0 means not loaded, -1 means loaded but not pinned DeadlineUnix timeutil.TimeStamp `xorm:"INDEX"` @@ -147,6 +119,9 @@ type Issue struct { // For view issue page. ShowRole RoleDescriptor `xorm:"-"` + + // Time estimate + TimeEstimate int64 `xorm:"NOT NULL DEFAULT 0"` } var ( @@ -265,6 +240,9 @@ func (issue *Issue) loadCommentsByType(ctx context.Context, tp CommentType) (err IssueID: issue.ID, Type: tp, }) + for _, comment := range issue.Comments { + comment.Issue = issue + } return err } @@ -314,6 +292,23 @@ func (issue *Issue) LoadMilestone(ctx context.Context) (err error) { return nil } +func (issue *Issue) LoadPinOrder(ctx context.Context) error { + if issue.PinOrder != 0 { + return nil + } + issuePin, err := GetIssuePin(ctx, issue) + if err != nil && !db.IsErrNotExist(err) { + return err + } + + if issuePin != nil { + issue.PinOrder = issuePin.PinOrder + } else { + issue.PinOrder = -1 + } + return nil +} + // LoadAttributes loads the attribute of this issue. func (issue *Issue) LoadAttributes(ctx context.Context) (err error) { if err = issue.LoadRepo(ctx); err != nil { @@ -353,6 +348,10 @@ func (issue *Issue) LoadAttributes(ctx context.Context) (err error) { return err } + if err = issue.LoadPinOrder(ctx); err != nil { + return err + } + if err = issue.Comments.LoadAttributes(ctx); err != nil { return err } @@ -365,6 +364,14 @@ func (issue *Issue) LoadAttributes(ctx context.Context) (err error) { return issue.loadReactions(ctx) } +// IsPinned returns if a Issue is pinned +func (issue *Issue) IsPinned() bool { + if issue.PinOrder == 0 { + setting.PanicInDevOrTesting("issue's pinorder has not been loaded") + } + return issue.PinOrder > 0 +} + func (issue *Issue) ResetAttributesLoaded() { issue.isLabelsLoaded = false issue.isMilestoneLoaded = false @@ -398,14 +405,14 @@ func (issue *Issue) APIURL(ctx context.Context) string { } // HTMLURL returns the absolute URL to this issue. -func (issue *Issue) HTMLURL() string { +func (issue *Issue) HTMLURL(ctx context.Context) string { var path string if issue.IsPull { path = "pulls" } else { path = "issues" } - return fmt.Sprintf("%s/%s/%d", issue.Repo.HTMLURL(), path, issue.Index) + return fmt.Sprintf("%s/%s/%d", issue.Repo.HTMLURL(ctx), path, issue.Index) } // Link returns the issue's relative URL. @@ -525,6 +532,45 @@ func GetIssueByIndex(ctx context.Context, repoID, index int64) (*Issue, error) { return issue, nil } +func isPullToCond(isPull optional.Option[bool]) builder.Cond { + if isPull.Has() { + return builder.Eq{"is_pull": isPull.Value()} + } + return builder.NewCond() +} + +func FindLatestUpdatedIssues(ctx context.Context, repoID int64, isPull optional.Option[bool], pageSize int) (IssueList, error) { + issues := make([]*Issue, 0, pageSize) + err := db.GetEngine(ctx).Where("repo_id = ?", repoID). + And(isPullToCond(isPull)). + OrderBy("updated_unix DESC"). + Limit(pageSize). + Find(&issues) + return issues, err +} + +func FindIssuesSuggestionByKeyword(ctx context.Context, repoID int64, keyword string, isPull optional.Option[bool], excludedID int64, pageSize int) (IssueList, error) { + cond := builder.NewCond() + if excludedID > 0 { + cond = cond.And(builder.Neq{"`id`": excludedID}) + } + + // It seems that GitHub searches both title and content (maybe sorting by the search engine's ranking system?) + // The first PR (https://github.com/go-gitea/gitea/pull/32327) uses "search indexer" to search "name(title) + content" + // But it seems that searching "content" (especially LIKE by DB engine) generates worse (unusable) results. + // So now (https://github.com/go-gitea/gitea/pull/33538) it only searches "name(title)", leave the improvements to the future. + cond = cond.And(db.BuildCaseInsensitiveLike("`name`", keyword)) + + issues := make([]*Issue, 0, pageSize) + err := db.GetEngine(ctx).Where("repo_id = ?", repoID). + And(isPullToCond(isPull)). + And(cond). + OrderBy("updated_unix DESC, `index` DESC"). + Limit(pageSize). + Find(&issues) + return issues, err +} + // GetIssueWithAttrsByIndex returns issue by index in a repository. func GetIssueWithAttrsByIndex(ctx context.Context, repoID, index int64) (*Issue, error) { issue, err := GetIssueByIndex(ctx, repoID, index) @@ -550,6 +596,9 @@ func GetIssueByID(ctx context.Context, id int64) (*Issue, error) { // If keepOrder is true, the order of the returned issues will be the same as the given IDs. func GetIssuesByIDs(ctx context.Context, issueIDs []int64, keepOrder ...bool) (IssueList, error) { issues := make([]*Issue, 0, len(issueIDs)) + if len(issueIDs) == 0 { + return issues, nil + } if err := db.GetEngine(ctx).In("id", issueIDs).Find(&issues); err != nil { return nil, err @@ -704,204 +753,16 @@ func (issue *Issue) HasOriginalAuthor() bool { return issue.OriginalAuthor != "" && issue.OriginalAuthorID != 0 } -var ErrIssueMaxPinReached = util.NewInvalidArgumentErrorf("the max number of pinned issues has been readched") - -// IsPinned returns if a Issue is pinned -func (issue *Issue) IsPinned() bool { - return issue.PinOrder != 0 -} - -// Pin pins a Issue -func (issue *Issue) Pin(ctx context.Context, user *user_model.User) error { - // If the Issue is already pinned, we don't need to pin it twice - if issue.IsPinned() { - return nil - } - - var maxPin int - _, err := db.GetEngine(ctx).SQL("SELECT MAX(pin_order) FROM issue WHERE repo_id = ? AND is_pull = ?", issue.RepoID, issue.IsPull).Get(&maxPin) - if err != nil { - return err - } - - // Check if the maximum allowed Pins reached - if maxPin >= setting.Repository.Issue.MaxPinned { - return ErrIssueMaxPinReached - } - - _, err = db.GetEngine(ctx).Table("issue"). - Where("id = ?", issue.ID). - Update(map[string]any{ - "pin_order": maxPin + 1, - }) - if err != nil { - return err - } - - // Add the pin event to the history - opts := &CreateCommentOptions{ - Type: CommentTypePin, - Doer: user, - Repo: issue.Repo, - Issue: issue, - } - if _, err = CreateComment(ctx, opts); err != nil { - return err - } - - return nil -} - -// UnpinIssue unpins a Issue -func (issue *Issue) Unpin(ctx context.Context, user *user_model.User) error { - // If the Issue is not pinned, we don't need to unpin it - if !issue.IsPinned() { - return nil - } - - // This sets the Pin for all Issues that come after the unpined Issue to the correct value - _, err := db.GetEngine(ctx).Exec("UPDATE issue SET pin_order = pin_order - 1 WHERE repo_id = ? AND is_pull = ? AND pin_order > ?", issue.RepoID, issue.IsPull, issue.PinOrder) - if err != nil { - return err - } - - _, err = db.GetEngine(ctx).Table("issue"). - Where("id = ?", issue.ID). - Update(map[string]any{ - "pin_order": 0, - }) - if err != nil { - return err - } - - // Add the unpin event to the history - opts := &CreateCommentOptions{ - Type: CommentTypeUnpin, - Doer: user, - Repo: issue.Repo, - Issue: issue, - } - if _, err = CreateComment(ctx, opts); err != nil { - return err - } - - return nil -} - -// PinOrUnpin pins or unpins a Issue -func (issue *Issue) PinOrUnpin(ctx context.Context, user *user_model.User) error { - if !issue.IsPinned() { - return issue.Pin(ctx, user) - } - - return issue.Unpin(ctx, user) -} - -// MovePin moves a Pinned Issue to a new Position -func (issue *Issue) MovePin(ctx context.Context, newPosition int) error { - // If the Issue is not pinned, we can't move them - if !issue.IsPinned() { - return nil - } - - if newPosition < 1 { - return fmt.Errorf("The Position can't be lower than 1") - } - - dbctx, committer, err := db.TxContext(ctx) - if err != nil { - return err - } - defer committer.Close() - - var maxPin int - _, err = db.GetEngine(dbctx).SQL("SELECT MAX(pin_order) FROM issue WHERE repo_id = ? AND is_pull = ?", issue.RepoID, issue.IsPull).Get(&maxPin) - if err != nil { - return err - } - - // If the new Position bigger than the current Maximum, set it to the Maximum - if newPosition > maxPin+1 { - newPosition = maxPin + 1 - } - - // Lower the Position of all Pinned Issue that came after the current Position - _, err = db.GetEngine(dbctx).Exec("UPDATE issue SET pin_order = pin_order - 1 WHERE repo_id = ? AND is_pull = ? AND pin_order > ?", issue.RepoID, issue.IsPull, issue.PinOrder) - if err != nil { - return err - } - - // Higher the Position of all Pinned Issues that comes after the new Position - _, err = db.GetEngine(dbctx).Exec("UPDATE issue SET pin_order = pin_order + 1 WHERE repo_id = ? AND is_pull = ? AND pin_order >= ?", issue.RepoID, issue.IsPull, newPosition) - if err != nil { - return err - } - - _, err = db.GetEngine(dbctx).Table("issue"). - Where("id = ?", issue.ID). - Update(map[string]any{ - "pin_order": newPosition, - }) - if err != nil { - return err - } - - return committer.Commit() -} - -// GetPinnedIssues returns the pinned Issues for the given Repo and type -func GetPinnedIssues(ctx context.Context, repoID int64, isPull bool) (IssueList, error) { - issues := make(IssueList, 0) - - err := db.GetEngine(ctx). - Table("issue"). - Where("repo_id = ?", repoID). - And("is_pull = ?", isPull). - And("pin_order > 0"). - OrderBy("pin_order"). - Find(&issues) - if err != nil { - return nil, err - } - - err = issues.LoadAttributes(ctx) - if err != nil { - return nil, err - } - - return issues, nil -} - -// IsNewPinAllowed returns if a new Issue or Pull request can be pinned -func IsNewPinAllowed(ctx context.Context, repoID int64, isPull bool) (bool, error) { - var maxPin int - _, err := db.GetEngine(ctx).SQL("SELECT COUNT(pin_order) FROM issue WHERE repo_id = ? AND is_pull = ? AND pin_order > 0", repoID, isPull).Get(&maxPin) - if err != nil { - return false, err - } - - return maxPin < setting.Repository.Issue.MaxPinned, nil -} - -// IsErrIssueMaxPinReached returns if the error is, that the User can't pin more Issues -func IsErrIssueMaxPinReached(err error) bool { - return err == ErrIssueMaxPinReached -} - // InsertIssues insert issues to database func InsertIssues(ctx context.Context, issues ...*Issue) error { - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return err - } - defer committer.Close() - - for _, issue := range issues { - if err := insertIssue(ctx, issue); err != nil { - return err + return db.WithTx(ctx, func(ctx context.Context) error { + for _, issue := range issues { + if err := insertIssue(ctx, issue); err != nil { + return err + } } - } - return committer.Commit() + return nil + }) } func insertIssue(ctx context.Context, issue *Issue) error { @@ -934,3 +795,28 @@ func insertIssue(ctx context.Context, issue *Issue) error { return nil } + +// ChangeIssueTimeEstimate changes the plan time of this issue, as the given user. +func ChangeIssueTimeEstimate(ctx context.Context, issue *Issue, doer *user_model.User, timeEstimate int64) error { + return db.WithTx(ctx, func(ctx context.Context) error { + if err := UpdateIssueCols(ctx, &Issue{ID: issue.ID, TimeEstimate: timeEstimate}, "time_estimate"); err != nil { + return fmt.Errorf("updateIssueCols: %w", err) + } + + if err := issue.LoadRepo(ctx); err != nil { + return fmt.Errorf("loadRepo: %w", err) + } + + opts := &CreateCommentOptions{ + Type: CommentTypeChangeTimeEstimate, + Doer: doer, + Repo: issue.Repo, + Issue: issue, + Content: strconv.FormatInt(timeEstimate, 10), + } + if _, err := CreateComment(ctx, opts); err != nil { + return fmt.Errorf("createComment: %w", err) + } + return nil + }) +} diff --git a/models/issues/issue_index.go b/models/issues/issue_index.go index 16274d0ef09..1fe4a08a09b 100644 --- a/models/issues/issue_index.go +++ b/models/issues/issue_index.go @@ -12,20 +12,12 @@ import ( // RecalculateIssueIndexForRepo create issue_index for repo if not exist and // update it based on highest index of existing issues assigned to a repo func RecalculateIssueIndexForRepo(ctx context.Context, repoID int64) error { - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return err - } - defer committer.Close() + return db.WithTx(ctx, func(ctx context.Context) error { + var maxIndex int64 + if _, err := db.GetEngine(ctx).Select(" MAX(`index`)").Table("issue").Where("repo_id=?", repoID).Get(&maxIndex); err != nil { + return err + } - var max int64 - if _, err = db.GetEngine(ctx).Select(" MAX(`index`)").Table("issue").Where("repo_id=?", repoID).Get(&max); err != nil { - return err - } - - if err = db.SyncMaxResourceIndex(ctx, "issue_index", repoID, max); err != nil { - return err - } - - return committer.Commit() + return db.SyncMaxResourceIndex(ctx, "issue_index", repoID, maxIndex) + }) } diff --git a/models/issues/issue_label.go b/models/issues/issue_label.go index 10fc8214542..151469a9b8b 100644 --- a/models/issues/issue_label.go +++ b/models/issues/issue_label.go @@ -88,36 +88,28 @@ func NewIssueLabel(ctx context.Context, issue *Issue, label *Label, doer *user_m return nil } - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return err - } - defer committer.Close() + return db.WithTx(ctx, func(ctx context.Context) error { + if err = issue.LoadRepo(ctx); err != nil { + return err + } - if err = issue.LoadRepo(ctx); err != nil { - return err - } + // Do NOT add invalid labels + if issue.RepoID != label.RepoID && issue.Repo.OwnerID != label.OrgID { + return nil + } - // Do NOT add invalid labels - if issue.RepoID != label.RepoID && issue.Repo.OwnerID != label.OrgID { - return nil - } + if err = RemoveDuplicateExclusiveIssueLabels(ctx, issue, label, doer); err != nil { + return nil + } - if err = RemoveDuplicateExclusiveIssueLabels(ctx, issue, label, doer); err != nil { - return nil - } + if err = newIssueLabel(ctx, issue, label, doer); err != nil { + return err + } - if err = newIssueLabel(ctx, issue, label, doer); err != nil { - return err - } - - issue.isLabelsLoaded = false - issue.Labels = nil - if err = issue.LoadLabels(ctx); err != nil { - return err - } - - return committer.Commit() + issue.isLabelsLoaded = false + issue.Labels = nil + return issue.LoadLabels(ctx) + }) } // newIssueLabels add labels to an issue. It will check if the labels are valid for the issue @@ -151,24 +143,16 @@ func newIssueLabels(ctx context.Context, issue *Issue, labels []*Label, doer *us // NewIssueLabels creates a list of issue-label relations. func NewIssueLabels(ctx context.Context, issue *Issue, labels []*Label, doer *user_model.User) (err error) { - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return err - } - defer committer.Close() + return db.WithTx(ctx, func(ctx context.Context) error { + if err = newIssueLabels(ctx, issue, labels, doer); err != nil { + return err + } - if err = newIssueLabels(ctx, issue, labels, doer); err != nil { - return err - } - - // reload all labels - issue.isLabelsLoaded = false - issue.Labels = nil - if err = issue.LoadLabels(ctx); err != nil { - return err - } - - return committer.Commit() + // reload all labels + issue.isLabelsLoaded = false + issue.Labels = nil + return issue.LoadLabels(ctx) + }) } func deleteIssueLabel(ctx context.Context, issue *Issue, label *Label, doer *user_model.User) (err error) { @@ -206,6 +190,7 @@ func DeleteIssueLabel(ctx context.Context, issue *Issue, label *Label, doer *use } issue.Labels = nil + issue.isLabelsLoaded = false return issue.LoadLabels(ctx) } @@ -364,35 +349,23 @@ func clearIssueLabels(ctx context.Context, issue *Issue, doer *user_model.User) // ClearIssueLabels removes all issue labels as the given user. // Triggers appropriate WebHooks, if any. func ClearIssueLabels(ctx context.Context, issue *Issue, doer *user_model.User) (err error) { - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return err - } - defer committer.Close() + return db.WithTx(ctx, func(ctx context.Context) error { + if err := issue.LoadRepo(ctx); err != nil { + return err + } else if err = issue.LoadPullRequest(ctx); err != nil { + return err + } - if err := issue.LoadRepo(ctx); err != nil { - return err - } else if err = issue.LoadPullRequest(ctx); err != nil { - return err - } + perm, err := access_model.GetUserRepoPermission(ctx, issue.Repo, doer) + if err != nil { + return err + } + if !perm.CanWriteIssuesOrPulls(issue.IsPull) { + return ErrRepoLabelNotExist{} + } - perm, err := access_model.GetUserRepoPermission(ctx, issue.Repo, doer) - if err != nil { - return err - } - if !perm.CanWriteIssuesOrPulls(issue.IsPull) { - return ErrRepoLabelNotExist{} - } - - if err = clearIssueLabels(ctx, issue, doer); err != nil { - return err - } - - if err = committer.Commit(); err != nil { - return fmt.Errorf("Commit: %w", err) - } - - return nil + return clearIssueLabels(ctx, issue, doer) + }) } type labelSorter []*Label @@ -437,69 +410,61 @@ func RemoveDuplicateExclusiveLabels(labels []*Label) []*Label { // ReplaceIssueLabels removes all current labels and add new labels to the issue. // Triggers appropriate WebHooks, if any. func ReplaceIssueLabels(ctx context.Context, issue *Issue, labels []*Label, doer *user_model.User) (err error) { - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return err - } - defer committer.Close() + return db.WithTx(ctx, func(ctx context.Context) error { + if err = issue.LoadRepo(ctx); err != nil { + return err + } - if err = issue.LoadRepo(ctx); err != nil { - return err - } + if err = issue.LoadLabels(ctx); err != nil { + return err + } - if err = issue.LoadLabels(ctx); err != nil { - return err - } + labels = RemoveDuplicateExclusiveLabels(labels) - labels = RemoveDuplicateExclusiveLabels(labels) + sort.Sort(labelSorter(labels)) + sort.Sort(labelSorter(issue.Labels)) - sort.Sort(labelSorter(labels)) - sort.Sort(labelSorter(issue.Labels)) + var toAdd, toRemove []*Label - var toAdd, toRemove []*Label + addIndex, removeIndex := 0, 0 + for addIndex < len(labels) && removeIndex < len(issue.Labels) { + addLabel := labels[addIndex] + removeLabel := issue.Labels[removeIndex] + if addLabel.ID == removeLabel.ID { + // Silently drop invalid labels + if removeLabel.RepoID != issue.RepoID && removeLabel.OrgID != issue.Repo.OwnerID { + toRemove = append(toRemove, removeLabel) + } - addIndex, removeIndex := 0, 0 - for addIndex < len(labels) && removeIndex < len(issue.Labels) { - addLabel := labels[addIndex] - removeLabel := issue.Labels[removeIndex] - if addLabel.ID == removeLabel.ID { - // Silently drop invalid labels - if removeLabel.RepoID != issue.RepoID && removeLabel.OrgID != issue.Repo.OwnerID { + addIndex++ + removeIndex++ + } else if addLabel.ID < removeLabel.ID { + // Only add if the label is valid + if addLabel.RepoID == issue.RepoID || addLabel.OrgID == issue.Repo.OwnerID { + toAdd = append(toAdd, addLabel) + } + addIndex++ + } else { toRemove = append(toRemove, removeLabel) + removeIndex++ } + } + toAdd = append(toAdd, labels[addIndex:]...) + toRemove = append(toRemove, issue.Labels[removeIndex:]...) - addIndex++ - removeIndex++ - } else if addLabel.ID < removeLabel.ID { - // Only add if the label is valid - if addLabel.RepoID == issue.RepoID || addLabel.OrgID == issue.Repo.OwnerID { - toAdd = append(toAdd, addLabel) + if len(toAdd) > 0 { + if err = newIssueLabels(ctx, issue, toAdd, doer); err != nil { + return fmt.Errorf("addLabels: %w", err) } - addIndex++ - } else { - toRemove = append(toRemove, removeLabel) - removeIndex++ } - } - toAdd = append(toAdd, labels[addIndex:]...) - toRemove = append(toRemove, issue.Labels[removeIndex:]...) - if len(toAdd) > 0 { - if err = newIssueLabels(ctx, issue, toAdd, doer); err != nil { - return fmt.Errorf("addLabels: %w", err) + for _, l := range toRemove { + if err = deleteIssueLabel(ctx, issue, l, doer); err != nil { + return fmt.Errorf("removeLabel: %w", err) + } } - } - for _, l := range toRemove { - if err = deleteIssueLabel(ctx, issue, l, doer); err != nil { - return fmt.Errorf("removeLabel: %w", err) - } - } - - issue.Labels = nil - if err = issue.LoadLabels(ctx); err != nil { - return err - } - - return committer.Commit() + issue.Labels = nil + return issue.LoadLabels(ctx) + }) } diff --git a/models/issues/issue_label_test.go b/models/issues/issue_label_test.go index 0470b99e248..6ccf6debaea 100644 --- a/models/issues/issue_label_test.go +++ b/models/issues/issue_label_test.go @@ -6,7 +6,6 @@ package issues_test import ( "testing" - "code.gitea.io/gitea/models/db" issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" @@ -22,7 +21,7 @@ func TestNewIssueLabelsScope(t *testing.T) { label2 := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 8}) doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) - assert.NoError(t, issues_model.NewIssueLabels(db.DefaultContext, issue, []*issues_model.Label{label1, label2}, doer)) + assert.NoError(t, issues_model.NewIssueLabels(t.Context(), issue, []*issues_model.Label{label1, label2}, doer)) assert.Len(t, issue.Labels, 1) assert.Equal(t, label2.ID, issue.Labels[0].ID) diff --git a/models/issues/issue_list.go b/models/issues/issue_list.go index 22a4548adc2..26b93189b8b 100644 --- a/models/issues/issue_list.go +++ b/models/issues/issue_list.go @@ -42,10 +42,7 @@ func (issues IssueList) LoadRepositories(ctx context.Context) (repo_model.Reposi repoMaps := make(map[int64]*repo_model.Repository, len(repoIDs)) left := len(repoIDs) for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } + limit := min(left, db.DefaultMaxInSize) err := db.GetEngine(ctx). In("id", repoIDs[:limit]). Find(&repoMaps) @@ -81,53 +78,19 @@ func (issues IssueList) LoadPosters(ctx context.Context) error { return issue.PosterID, issue.Poster == nil && issue.PosterID > 0 }) - posterMaps, err := getPostersByIDs(ctx, posterIDs) + posterMaps, err := user_model.GetUsersMapByIDs(ctx, posterIDs) if err != nil { return err } for _, issue := range issues { if issue.Poster == nil { - issue.Poster = getPoster(issue.PosterID, posterMaps) + issue.Poster = user_model.GetPossibleUserFromMap(issue.PosterID, posterMaps) } } return nil } -func getPostersByIDs(ctx context.Context, posterIDs []int64) (map[int64]*user_model.User, error) { - posterMaps := make(map[int64]*user_model.User, len(posterIDs)) - left := len(posterIDs) - for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } - err := db.GetEngine(ctx). - In("id", posterIDs[:limit]). - Find(&posterMaps) - if err != nil { - return nil, err - } - left -= limit - posterIDs = posterIDs[limit:] - } - return posterMaps, nil -} - -func getPoster(posterID int64, posterMaps map[int64]*user_model.User) *user_model.User { - if posterID == user_model.ActionsUserID { - return user_model.NewActionsUser() - } - if posterID <= 0 { - return nil - } - poster, ok := posterMaps[posterID] - if !ok { - return user_model.NewGhostUser() - } - return poster -} - func (issues IssueList) getIssueIDs() []int64 { ids := make([]int64, 0, len(issues)) for _, issue := range issues { @@ -150,10 +113,7 @@ func (issues IssueList) LoadLabels(ctx context.Context) error { issueIDs := issues.getIssueIDs() left := len(issueIDs) for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } + limit := min(left, db.DefaultMaxInSize) rows, err := db.GetEngine(ctx).Table("label"). Join("LEFT", "issue_label", "issue_label.label_id = label.id"). In("issue_label.issue_id", issueIDs[:limit]). @@ -205,10 +165,7 @@ func (issues IssueList) LoadMilestones(ctx context.Context) error { milestoneMaps := make(map[int64]*Milestone, len(milestoneIDs)) left := len(milestoneIDs) for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } + limit := min(left, db.DefaultMaxInSize) err := db.GetEngine(ctx). In("id", milestoneIDs[:limit]). Find(&milestoneMaps) @@ -237,10 +194,7 @@ func (issues IssueList) LoadProjects(ctx context.Context) error { } for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } + limit := min(left, db.DefaultMaxInSize) projects := make([]*projectWithIssueID, 0, limit) err := db.GetEngine(ctx). @@ -279,10 +233,7 @@ func (issues IssueList) LoadAssignees(ctx context.Context) error { issueIDs := issues.getIssueIDs() left := len(issueIDs) for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } + limit := min(left, db.DefaultMaxInSize) rows, err := db.GetEngine(ctx).Table("issue_assignees"). Join("INNER", "`user`", "`user`.id = `issue_assignees`.assignee_id"). In("`issue_assignees`.issue_id", issueIDs[:limit]).OrderBy(user_model.GetOrderByName()). @@ -340,10 +291,7 @@ func (issues IssueList) LoadPullRequests(ctx context.Context) error { pullRequestMaps := make(map[int64]*PullRequest, len(issuesIDs)) left := len(issuesIDs) for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } + limit := min(left, db.DefaultMaxInSize) rows, err := db.GetEngine(ctx). In("issue_id", issuesIDs[:limit]). Rows(new(PullRequest)) @@ -388,10 +336,7 @@ func (issues IssueList) LoadAttachments(ctx context.Context) (err error) { issuesIDs := issues.getIssueIDs() left := len(issuesIDs) for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } + limit := min(left, db.DefaultMaxInSize) rows, err := db.GetEngine(ctx). In("issue_id", issuesIDs[:limit]). Rows(new(repo_model.Attachment)) @@ -433,10 +378,7 @@ func (issues IssueList) loadComments(ctx context.Context, cond builder.Cond) (er issuesIDs := issues.getIssueIDs() left := len(issuesIDs) for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } + limit := min(left, db.DefaultMaxInSize) rows, err := db.GetEngine(ctx).Table("comment"). Join("INNER", "issue", "issue.id = comment.issue_id"). In("issue.id", issuesIDs[:limit]). @@ -500,10 +442,7 @@ func (issues IssueList) loadTotalTrackedTimes(ctx context.Context) (err error) { left := len(ids) for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } + limit := min(left, db.DefaultMaxInSize) // select issue_id, sum(time) from tracked_time where issue_id in () group by issue_id rows, err := db.GetEngine(ctx).Table("tracked_time"). @@ -540,6 +479,39 @@ func (issues IssueList) loadTotalTrackedTimes(ctx context.Context) (err error) { return nil } +func (issues IssueList) LoadPinOrder(ctx context.Context) error { + if len(issues) == 0 { + return nil + } + + issueIDs := container.FilterSlice(issues, func(issue *Issue) (int64, bool) { + return issue.ID, issue.PinOrder == 0 + }) + if len(issueIDs) == 0 { + return nil + } + issuePins, err := GetIssuePinsByIssueIDs(ctx, issueIDs) + if err != nil { + return err + } + + for _, issue := range issues { + if issue.PinOrder != 0 { + continue + } + for _, pin := range issuePins { + if pin.IssueID == issue.ID { + issue.PinOrder = pin.PinOrder + break + } + } + if issue.PinOrder == 0 { + issue.PinOrder = -1 + } + } + return nil +} + // loadAttributes loads all attributes, expect for attachments and comments func (issues IssueList) LoadAttributes(ctx context.Context) error { if _, err := issues.LoadRepositories(ctx); err != nil { diff --git a/models/issues/issue_list_test.go b/models/issues/issue_list_test.go index 9069e1012da..e9dc412331d 100644 --- a/models/issues/issue_list_test.go +++ b/models/issues/issue_list_test.go @@ -6,7 +6,6 @@ package issues_test import ( "testing" - "code.gitea.io/gitea/models/db" issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/modules/setting" @@ -23,11 +22,11 @@ func TestIssueList_LoadRepositories(t *testing.T) { unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 4}), } - repos, err := issueList.LoadRepositories(db.DefaultContext) + repos, err := issueList.LoadRepositories(t.Context()) assert.NoError(t, err) assert.Len(t, repos, 2) for _, issue := range issueList { - assert.EqualValues(t, issue.RepoID, issue.Repo.ID) + assert.Equal(t, issue.RepoID, issue.Repo.ID) } } @@ -39,30 +38,30 @@ func TestIssueList_LoadAttributes(t *testing.T) { unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 4}), } - assert.NoError(t, issueList.LoadAttributes(db.DefaultContext)) + assert.NoError(t, issueList.LoadAttributes(t.Context())) for _, issue := range issueList { - assert.EqualValues(t, issue.RepoID, issue.Repo.ID) + assert.Equal(t, issue.RepoID, issue.Repo.ID) for _, label := range issue.Labels { - assert.EqualValues(t, issue.RepoID, label.RepoID) + assert.Equal(t, issue.RepoID, label.RepoID) unittest.AssertExistsAndLoadBean(t, &issues_model.IssueLabel{IssueID: issue.ID, LabelID: label.ID}) } if issue.PosterID > 0 { - assert.EqualValues(t, issue.PosterID, issue.Poster.ID) + assert.Equal(t, issue.PosterID, issue.Poster.ID) } if issue.AssigneeID > 0 { - assert.EqualValues(t, issue.AssigneeID, issue.Assignee.ID) + assert.Equal(t, issue.AssigneeID, issue.Assignee.ID) } if issue.MilestoneID > 0 { - assert.EqualValues(t, issue.MilestoneID, issue.Milestone.ID) + assert.Equal(t, issue.MilestoneID, issue.Milestone.ID) } if issue.IsPull { - assert.EqualValues(t, issue.ID, issue.PullRequest.IssueID) + assert.Equal(t, issue.ID, issue.PullRequest.IssueID) } for _, attachment := range issue.Attachments { - assert.EqualValues(t, issue.ID, attachment.IssueID) + assert.Equal(t, issue.ID, attachment.IssueID) } for _, comment := range issue.Comments { - assert.EqualValues(t, issue.ID, comment.IssueID) + assert.Equal(t, issue.ID, comment.IssueID) } if issue.ID == int64(1) { assert.Equal(t, int64(400), issue.TotalTrackedTime) diff --git a/models/issues/issue_lock.go b/models/issues/issue_lock.go index b21629b529a..2e5bf64cc65 100644 --- a/models/issues/issue_lock.go +++ b/models/issues/issue_lock.go @@ -12,8 +12,14 @@ import ( // IssueLockOptions defines options for locking and/or unlocking an issue/PR type IssueLockOptions struct { - Doer *user_model.User - Issue *Issue + Doer *user_model.User + Issue *Issue + + // Reason is the doer-provided comment message for the locked issue + // GitHub doesn't support changing the "reasons" by config file, so GitHub has pre-defined "reason" enum values. + // Gitea is not like GitHub, it allows site admin to define customized "reasons" in the config file. + // So the API caller might not know what kind of "reasons" are valid, and the customized reasons are not translatable. + // To make things clear and simple: doer have the chance to use any reason they like, we do not do validation. Reason string } @@ -41,26 +47,19 @@ func updateIssueLock(ctx context.Context, opts *IssueLockOptions, lock bool) err commentType = CommentTypeUnlock } - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return err - } - defer committer.Close() + return db.WithTx(ctx, func(ctx context.Context) error { + if err := UpdateIssueCols(ctx, opts.Issue, "is_locked"); err != nil { + return err + } - if err := UpdateIssueCols(ctx, opts.Issue, "is_locked"); err != nil { + opt := &CreateCommentOptions{ + Doer: opts.Doer, + Issue: opts.Issue, + Repo: opts.Issue.Repo, + Type: commentType, + Content: opts.Reason, + } + _, err := CreateComment(ctx, opt) return err - } - - opt := &CreateCommentOptions{ - Doer: opts.Doer, - Issue: opts.Issue, - Repo: opts.Issue.Repo, - Type: commentType, - Content: opts.Reason, - } - if _, err := CreateComment(ctx, opt); err != nil { - return err - } - - return committer.Commit() + }) } diff --git a/models/issues/issue_pin.go b/models/issues/issue_pin.go new file mode 100644 index 00000000000..ae6195b05dd --- /dev/null +++ b/models/issues/issue_pin.go @@ -0,0 +1,246 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package issues + +import ( + "context" + "errors" + "sort" + + "code.gitea.io/gitea/models/db" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" +) + +type IssuePin struct { + ID int64 `xorm:"pk autoincr"` + RepoID int64 `xorm:"UNIQUE(s) NOT NULL"` + IssueID int64 `xorm:"UNIQUE(s) NOT NULL"` + IsPull bool `xorm:"NOT NULL"` + PinOrder int `xorm:"DEFAULT 0"` +} + +var ErrIssueMaxPinReached = util.NewInvalidArgumentErrorf("the max number of pinned issues has been readched") + +// IsErrIssueMaxPinReached returns if the error is, that the User can't pin more Issues +func IsErrIssueMaxPinReached(err error) bool { + return err == ErrIssueMaxPinReached +} + +func init() { + db.RegisterModel(new(IssuePin)) +} + +func GetIssuePin(ctx context.Context, issue *Issue) (*IssuePin, error) { + pin := new(IssuePin) + has, err := db.GetEngine(ctx). + Where("repo_id = ?", issue.RepoID). + And("issue_id = ?", issue.ID).Get(pin) + if err != nil { + return nil, err + } else if !has { + return nil, db.ErrNotExist{ + Resource: "IssuePin", + ID: issue.ID, + } + } + return pin, nil +} + +func GetIssuePinsByIssueIDs(ctx context.Context, issueIDs []int64) ([]IssuePin, error) { + var pins []IssuePin + if err := db.GetEngine(ctx).In("issue_id", issueIDs).Find(&pins); err != nil { + return nil, err + } + return pins, nil +} + +// Pin pins a Issue +func PinIssue(ctx context.Context, issue *Issue, user *user_model.User) error { + return db.WithTx(ctx, func(ctx context.Context) error { + pinnedIssuesNum, err := getPinnedIssuesNum(ctx, issue.RepoID, issue.IsPull) + if err != nil { + return err + } + + // Check if the maximum allowed Pins reached + if pinnedIssuesNum >= setting.Repository.Issue.MaxPinned { + return ErrIssueMaxPinReached + } + + pinnedIssuesMaxPinOrder, err := getPinnedIssuesMaxPinOrder(ctx, issue.RepoID, issue.IsPull) + if err != nil { + return err + } + + if _, err = db.GetEngine(ctx).Insert(&IssuePin{ + RepoID: issue.RepoID, + IssueID: issue.ID, + IsPull: issue.IsPull, + PinOrder: pinnedIssuesMaxPinOrder + 1, + }); err != nil { + return err + } + + // Add the pin event to the history + _, err = CreateComment(ctx, &CreateCommentOptions{ + Type: CommentTypePin, + Doer: user, + Repo: issue.Repo, + Issue: issue, + }) + return err + }) +} + +// UnpinIssue unpins a Issue +func UnpinIssue(ctx context.Context, issue *Issue, user *user_model.User) error { + return db.WithTx(ctx, func(ctx context.Context) error { + // This sets the Pin for all Issues that come after the unpined Issue to the correct value + cnt, err := db.GetEngine(ctx).Where("issue_id=?", issue.ID).Delete(new(IssuePin)) + if err != nil { + return err + } + if cnt == 0 { + return nil + } + + // Add the unpin event to the history + _, err = CreateComment(ctx, &CreateCommentOptions{ + Type: CommentTypeUnpin, + Doer: user, + Repo: issue.Repo, + Issue: issue, + }) + return err + }) +} + +func getPinnedIssuesNum(ctx context.Context, repoID int64, isPull bool) (int, error) { + var pinnedIssuesNum int + _, err := db.GetEngine(ctx).SQL("SELECT count(pin_order) FROM issue_pin WHERE repo_id = ? AND is_pull = ?", repoID, isPull).Get(&pinnedIssuesNum) + return pinnedIssuesNum, err +} + +func getPinnedIssuesMaxPinOrder(ctx context.Context, repoID int64, isPull bool) (int, error) { + var maxPinnedIssuesMaxPinOrder int + _, err := db.GetEngine(ctx).SQL("SELECT max(pin_order) FROM issue_pin WHERE repo_id = ? AND is_pull = ?", repoID, isPull).Get(&maxPinnedIssuesMaxPinOrder) + return maxPinnedIssuesMaxPinOrder, err +} + +// MovePin moves a Pinned Issue to a new Position +func MovePin(ctx context.Context, issue *Issue, newPosition int) error { + if newPosition < 1 { + return errors.New("The Position can't be lower than 1") + } + + issuePin, err := GetIssuePin(ctx, issue) + if err != nil { + return err + } + if issuePin.PinOrder == newPosition { + return nil + } + + return db.WithTx(ctx, func(ctx context.Context) error { + if issuePin.PinOrder > newPosition { // move the issue to a lower position + _, err = db.GetEngine(ctx).Exec("UPDATE issue_pin SET pin_order = pin_order + 1 WHERE repo_id = ? AND is_pull = ? AND pin_order >= ? AND pin_order < ?", issue.RepoID, issue.IsPull, newPosition, issuePin.PinOrder) + } else { // move the issue to a higher position + // Lower the Position of all Pinned Issue that came after the current Position + _, err = db.GetEngine(ctx).Exec("UPDATE issue_pin SET pin_order = pin_order - 1 WHERE repo_id = ? AND is_pull = ? AND pin_order > ? AND pin_order <= ?", issue.RepoID, issue.IsPull, issuePin.PinOrder, newPosition) + } + if err != nil { + return err + } + + _, err = db.GetEngine(ctx). + Table("issue_pin"). + Where("id = ?", issuePin.ID). + Update(map[string]any{ + "pin_order": newPosition, + }) + return err + }) +} + +func GetPinnedIssueIDs(ctx context.Context, repoID int64, isPull bool) ([]int64, error) { + var issuePins []IssuePin + if err := db.GetEngine(ctx). + Table("issue_pin"). + Where("repo_id = ?", repoID). + And("is_pull = ?", isPull). + Find(&issuePins); err != nil { + return nil, err + } + + sort.Slice(issuePins, func(i, j int) bool { + return issuePins[i].PinOrder < issuePins[j].PinOrder + }) + + var ids []int64 + for _, pin := range issuePins { + ids = append(ids, pin.IssueID) + } + return ids, nil +} + +func GetIssuePinsByRepoID(ctx context.Context, repoID int64, isPull bool) ([]*IssuePin, error) { + var pins []*IssuePin + if err := db.GetEngine(ctx).Where("repo_id = ? AND is_pull = ?", repoID, isPull).Find(&pins); err != nil { + return nil, err + } + return pins, nil +} + +// GetPinnedIssues returns the pinned Issues for the given Repo and type +func GetPinnedIssues(ctx context.Context, repoID int64, isPull bool) (IssueList, error) { + issuePins, err := GetIssuePinsByRepoID(ctx, repoID, isPull) + if err != nil { + return nil, err + } + if len(issuePins) == 0 { + return IssueList{}, nil + } + ids := make([]int64, 0, len(issuePins)) + for _, pin := range issuePins { + ids = append(ids, pin.IssueID) + } + + issues := make(IssueList, 0, len(ids)) + if err := db.GetEngine(ctx).In("id", ids).Find(&issues); err != nil { + return nil, err + } + for _, issue := range issues { + for _, pin := range issuePins { + if pin.IssueID == issue.ID { + issue.PinOrder = pin.PinOrder + break + } + } + if (!setting.IsProd || setting.IsInTesting) && issue.PinOrder == 0 { + panic("It should not happen that a pinned Issue has no PinOrder") + } + } + sort.Slice(issues, func(i, j int) bool { + return issues[i].PinOrder < issues[j].PinOrder + }) + + if err = issues.LoadAttributes(ctx); err != nil { + return nil, err + } + + return issues, nil +} + +// IsNewPinAllowed returns if a new Issue or Pull request can be pinned +func IsNewPinAllowed(ctx context.Context, repoID int64, isPull bool) (bool, error) { + var maxPin int + _, err := db.GetEngine(ctx).SQL("SELECT COUNT(pin_order) FROM issue_pin WHERE repo_id = ? AND is_pull = ?", repoID, isPull).Get(&maxPin) + if err != nil { + return false, err + } + + return maxPin < setting.Repository.Issue.MaxPinned, nil +} diff --git a/models/issues/issue_project.go b/models/issues/issue_project.go index c4515fd8985..01852447834 100644 --- a/models/issues/issue_project.go +++ b/models/issues/issue_project.go @@ -38,13 +38,30 @@ func (issue *Issue) projectID(ctx context.Context) int64 { } // ProjectColumnID return project column id if issue was assigned to one -func (issue *Issue) ProjectColumnID(ctx context.Context) int64 { +func (issue *Issue) ProjectColumnID(ctx context.Context) (int64, error) { var ip project_model.ProjectIssue has, err := db.GetEngine(ctx).Where("issue_id=?", issue.ID).Get(&ip) - if err != nil || !has { - return 0 + if err != nil { + return 0, err + } else if !has { + return 0, nil } - return ip.ProjectColumnID + return ip.ProjectColumnID, nil +} + +func LoadProjectIssueColumnMap(ctx context.Context, projectID, defaultColumnID int64) (map[int64]int64, error) { + issues := make([]project_model.ProjectIssue, 0) + if err := db.GetEngine(ctx).Where("project_id=?", projectID).Find(&issues); err != nil { + return nil, err + } + result := make(map[int64]int64, len(issues)) + for _, issue := range issues { + if issue.ProjectColumnID == 0 { + issue.ProjectColumnID = defaultColumnID + } + result[issue.IssueID] = issue.ProjectColumnID + } + return result, nil } // LoadIssuesFromColumn load issues assigned to this column @@ -59,11 +76,11 @@ func LoadIssuesFromColumn(ctx context.Context, b *project_model.Column, opts *Is } if b.Default { - issues, err := Issues(ctx, &IssuesOptions{ - ProjectColumnID: db.NoConditionID, - ProjectID: b.ProjectID, - SortType: "project-column-sorting", - }) + issues, err := Issues(ctx, opts.Copy(func(o *IssuesOptions) { + o.ProjectColumnID = db.NoConditionID + o.ProjectID = b.ProjectID + o.SortType = "project-column-sorting" + })) if err != nil { return nil, err } @@ -77,19 +94,6 @@ func LoadIssuesFromColumn(ctx context.Context, b *project_model.Column, opts *Is return issueList, nil } -// LoadIssuesFromColumnList load issues assigned to the columns -func LoadIssuesFromColumnList(ctx context.Context, bs project_model.ColumnList, opts *IssuesOptions) (map[int64]IssueList, error) { - issuesMap := make(map[int64]IssueList, len(bs)) - for i := range bs { - il, err := LoadIssuesFromColumn(ctx, bs[i], opts) - if err != nil { - return nil, err - } - issuesMap[bs[i].ID] = il - } - return issuesMap, nil -} - // IssueAssignOrRemoveProject changes the project associated with an issue // If newProjectID is 0, the issue is removed from the project func IssueAssignOrRemoveProject(ctx context.Context, issue *Issue, doer *user_model.User, newProjectID, newColumnID int64) error { @@ -110,7 +114,7 @@ func IssueAssignOrRemoveProject(ctx context.Context, issue *Issue, doer *user_mo return util.NewPermissionDeniedErrorf("issue %d can't be accessed by project %d", issue.ID, newProject.ID) } if newColumnID == 0 { - newDefaultColumn, err := newProject.GetDefaultColumn(ctx) + newDefaultColumn, err := newProject.MustDefaultColumn(ctx) if err != nil { return err } diff --git a/models/issues/issue_search.go b/models/issues/issue_search.go index 5948a67d4eb..79bd6a19b0d 100644 --- a/models/issues/issue_search.go +++ b/models/issues/issue_search.go @@ -21,14 +21,16 @@ import ( "xorm.io/xorm" ) +const ScopeSortPrefix = "scope-" + // IssuesOptions represents options of an issue. -type IssuesOptions struct { //nolint +type IssuesOptions struct { //nolint:revive // export stutter Paginator *db.ListOptions RepoIDs []int64 // overwrites RepoCond if the length is not 0 AllPublic bool // include also all public repositories RepoCond builder.Cond - AssigneeID int64 - PosterID int64 + AssigneeID string // "(none)" or "(any)" or a user ID + PosterID string // "(none)" or "(any)" or a user ID MentionedID int64 ReviewRequestedID int64 ReviewedID int64 @@ -49,9 +51,9 @@ type IssuesOptions struct { //nolint // prioritize issues from this repo PriorityRepoID int64 IsArchived optional.Option[bool] - Org *organization.Organization // issues permission scope - Team *organization.Team // issues permission scope - User *user_model.User // issues permission scope + Owner *user_model.User // issues permission scope, it could be an organization or a user + Team *organization.Team // issues permission scope + Doer *user_model.User // issues permission scope } // Copy returns a copy of the options. @@ -70,11 +72,24 @@ func (o *IssuesOptions) Copy(edit ...func(options *IssuesOptions)) *IssuesOption // applySorts sort an issues-related session based on the provided // sortType string func applySorts(sess *xorm.Session, sortType string, priorityRepoID int64) { + // Since this sortType is dynamically created, it has to be treated specially. + if after, ok := strings.CutPrefix(sortType, ScopeSortPrefix); ok { + scope := after + sess.Join("LEFT", "issue_label", "issue.id = issue_label.issue_id") + // "exclusive_order=0" means "no order is set", so exclude it from the JOIN criteria and then "LEFT JOIN" result is also null + sess.Join("LEFT", "label", "label.id = issue_label.label_id AND label.exclusive_order <> 0 AND label.name LIKE ?", scope+"/%") + // Use COALESCE to make sure we sort NULL last regardless of backend DB (2147483647 == max int) + sess.OrderBy("COALESCE(label.exclusive_order, 2147483647) ASC").Desc("issue.id") + return + } + switch sortType { case "oldest": sess.Asc("issue.created_unix").Asc("issue.id") case "recentupdate": sess.Desc("issue.updated_unix").Desc("issue.created_unix").Desc("issue.id") + case "recentclose": + sess.Desc("issue.closed_unix").Desc("issue.created_unix").Desc("issue.id") case "leastupdate": sess.Asc("issue.updated_unix").Asc("issue.created_unix").Asc("issue.id") case "mostcomment": @@ -231,15 +246,8 @@ func applyConditions(sess *xorm.Session, opts *IssuesOptions) { sess.And("issue.is_closed=?", opts.IsClosed.Value()) } - if opts.AssigneeID > 0 { - applyAssigneeCondition(sess, opts.AssigneeID) - } else if opts.AssigneeID == db.NoConditionID { - sess.Where("issue.id NOT IN (SELECT issue_id FROM issue_assignees)") - } - - if opts.PosterID > 0 { - applyPosterCondition(sess, opts.PosterID) - } + applyAssigneeCondition(sess, opts.AssigneeID) + applyPosterCondition(sess, opts.PosterID) if opts.MentionedID > 0 { applyMentionedCondition(sess, opts.MentionedID) @@ -280,8 +288,12 @@ func applyConditions(sess *xorm.Session, opts *IssuesOptions) { applyLabelsCondition(sess, opts) - if opts.User != nil { - sess.And(issuePullAccessibleRepoCond("issue.repo_id", opts.User.ID, opts.Org, opts.Team, opts.IsPull.Value())) + if opts.Owner != nil { + sess.And(repo_model.UserOwnedRepoCond(opts.Owner.ID)) + } + + if opts.Doer != nil && !opts.Doer.IsAdmin { + sess.And(issuePullAccessibleRepoCond("issue.repo_id", opts.Doer.ID, opts.Owner, opts.Team, opts.IsPull.Value())) } } @@ -328,20 +340,20 @@ func teamUnitsRepoCond(id string, userID, orgID, teamID int64, units ...unit.Typ } // issuePullAccessibleRepoCond userID must not be zero, this condition require join repository table -func issuePullAccessibleRepoCond(repoIDstr string, userID int64, org *organization.Organization, team *organization.Team, isPull bool) builder.Cond { +func issuePullAccessibleRepoCond(repoIDstr string, userID int64, owner *user_model.User, team *organization.Team, isPull bool) builder.Cond { cond := builder.NewCond() unitType := unit.TypeIssues if isPull { unitType = unit.TypePullRequests } - if org != nil { + if owner != nil && owner.IsOrganization() { if team != nil { - cond = cond.And(teamUnitsRepoCond(repoIDstr, userID, org.ID, team.ID, unitType)) // special team member repos + cond = cond.And(teamUnitsRepoCond(repoIDstr, userID, owner.ID, team.ID, unitType)) // special team member repos } else { cond = cond.And( builder.Or( - repo_model.UserOrgUnitRepoCond(repoIDstr, userID, org.ID, unitType), // team member repos - repo_model.UserOrgPublicUnitRepoCond(userID, org.ID), // user org public non-member repos, TODO: check repo has issues + repo_model.UserOrgUnitRepoCond(repoIDstr, userID, owner.ID, unitType), // team member repos + repo_model.UserOrgPublicUnitRepoCond(userID, owner.ID), // user org public non-member repos, TODO: check repo has issues ), ) } @@ -359,13 +371,26 @@ func issuePullAccessibleRepoCond(repoIDstr string, userID int64, org *organizati return cond } -func applyAssigneeCondition(sess *xorm.Session, assigneeID int64) { - sess.Join("INNER", "issue_assignees", "issue.id = issue_assignees.issue_id"). - And("issue_assignees.assignee_id = ?", assigneeID) +func applyAssigneeCondition(sess *xorm.Session, assigneeID string) { + // old logic: 0 is also treated as "not filtering assignee", because the "assignee" was read as FormInt64 + if assigneeID == "(none)" { + sess.Where("issue.id NOT IN (SELECT issue_id FROM issue_assignees)") + } else if assigneeID == "(any)" { + sess.Where("issue.id IN (SELECT issue_id FROM issue_assignees)") + } else if assigneeIDInt64, _ := strconv.ParseInt(assigneeID, 10, 64); assigneeIDInt64 > 0 { + sess.Join("INNER", "issue_assignees", "issue.id = issue_assignees.issue_id"). + And("issue_assignees.assignee_id = ?", assigneeIDInt64) + } } -func applyPosterCondition(sess *xorm.Session, posterID int64) { - sess.And("issue.poster_id=?", posterID) +func applyPosterCondition(sess *xorm.Session, posterID string) { + // Actually every issue has a poster. + // The "(none)" is for internal usage only: when doer tries to search non-existing user as poster, use "(none)" to return empty result. + if posterID == "(none)" { + sess.And("issue.poster_id=0") + } else if posterIDInt64, _ := strconv.ParseInt(posterID, 10, 64); posterIDInt64 > 0 { + sess.And("issue.poster_id=?", posterIDInt64) + } } func applyMentionedCondition(sess *xorm.Session, mentionedID int64) { diff --git a/models/issues/issue_stats.go b/models/issues/issue_stats.go index 39326616f88..adedaa3d3a0 100644 --- a/models/issues/issue_stats.go +++ b/models/issues/issue_stats.go @@ -94,10 +94,7 @@ func GetIssueStats(ctx context.Context, opts *IssuesOptions) (*IssueStats, error // ids in a temporary table and join from them. accum := &IssueStats{} for i := 0; i < len(opts.IssueIDs); { - chunk := i + MaxQueryParameters - if chunk > len(opts.IssueIDs) { - chunk = len(opts.IssueIDs) - } + chunk := min(i+MaxQueryParameters, len(opts.IssueIDs)) stats, err := getIssueStatsChunk(ctx, opts, opts.IssueIDs[i:chunk]) if err != nil { return nil, err @@ -107,7 +104,7 @@ func GetIssueStats(ctx context.Context, opts *IssuesOptions) (*IssueStats, error accum.YourRepositoriesCount += stats.YourRepositoriesCount accum.AssignCount += stats.AssignCount accum.CreateCount += stats.CreateCount - accum.OpenCount += stats.MentionCount + accum.MentionCount += stats.MentionCount accum.ReviewRequestedCount += stats.ReviewRequestedCount accum.ReviewedCount += stats.ReviewedCount i = chunk @@ -151,15 +148,9 @@ func applyIssuesOptions(sess *xorm.Session, opts *IssuesOptions, issueIDs []int6 applyProjectCondition(sess, opts) - if opts.AssigneeID > 0 { - applyAssigneeCondition(sess, opts.AssigneeID) - } else if opts.AssigneeID == db.NoConditionID { - sess.Where("issue.id NOT IN (SELECT issue_id FROM issue_assignees)") - } + applyAssigneeCondition(sess, opts.AssigneeID) - if opts.PosterID > 0 { - applyPosterCondition(sess, opts.PosterID) - } + applyPosterCondition(sess, opts.PosterID) if opts.MentionedID > 0 { applyMentionedCondition(sess, opts.MentionedID) diff --git a/models/issues/issue_test.go b/models/issues/issue_test.go index 1bbc0eee564..09fd4926673 100644 --- a/models/issues/issue_test.go +++ b/models/issues/issue_test.go @@ -4,8 +4,8 @@ package issues_test import ( - "context" "fmt" + "slices" "sort" "sync" "testing" @@ -34,7 +34,7 @@ func TestIssue_ReplaceLabels(t *testing.T) { for i, labelID := range labelIDs { labels[i] = unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: labelID, RepoID: repo.ID}) } - assert.NoError(t, issues_model.ReplaceIssueLabels(db.DefaultContext, issue, labels, doer)) + assert.NoError(t, issues_model.ReplaceIssueLabels(t.Context(), issue, labels, doer)) unittest.AssertCount(t, &issues_model.IssueLabel{IssueID: issueID}, len(expectedLabelIDs)) for _, labelID := range expectedLabelIDs { unittest.AssertExistsAndLoadBean(t, &issues_model.IssueLabel{IssueID: issueID, LabelID: labelID}) @@ -54,7 +54,7 @@ func TestIssue_ReplaceLabels(t *testing.T) { func Test_GetIssueIDsByRepoID(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - ids, err := issues_model.GetIssueIDsByRepoID(db.DefaultContext, 1) + ids, err := issues_model.GetIssueIDsByRepoID(t.Context(), 1) assert.NoError(t, err) assert.Len(t, ids, 5) } @@ -62,16 +62,16 @@ func Test_GetIssueIDsByRepoID(t *testing.T) { func TestIssueAPIURL(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1}) - err := issue.LoadAttributes(db.DefaultContext) + err := issue.LoadAttributes(t.Context()) assert.NoError(t, err) - assert.Equal(t, "https://try.gitea.io/api/v1/repos/user2/repo1/issues/1", issue.APIURL(db.DefaultContext)) + assert.Equal(t, "https://try.gitea.io/api/v1/repos/user2/repo1/issues/1", issue.APIURL(t.Context())) } func TestGetIssuesByIDs(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) testSuccess := func(expectedIssueIDs, nonExistentIssueIDs []int64) { - issues, err := issues_model.GetIssuesByIDs(db.DefaultContext, append(expectedIssueIDs, nonExistentIssueIDs...), true) + issues, err := issues_model.GetIssuesByIDs(t.Context(), append(expectedIssueIDs, nonExistentIssueIDs...), true) assert.NoError(t, err) actualIssueIDs := make([]int64, len(issues)) for i, issue := range issues { @@ -88,9 +88,9 @@ func TestGetParticipantIDsByIssue(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) checkParticipants := func(issueID int64, userIDs []int) { - issue, err := issues_model.GetIssueByID(db.DefaultContext, issueID) + issue, err := issues_model.GetIssueByID(t.Context(), issueID) assert.NoError(t, err) - participants, err := issue.GetParticipantIDsByIssue(db.DefaultContext) + participants, err := issue.GetParticipantIDsByIssue(t.Context()) if assert.NoError(t, err) { participantsIDs := make([]int, len(participants)) for i, uid := range participants { @@ -122,7 +122,7 @@ func TestIssue_ClearLabels(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: test.issueID}) doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: test.doerID}) - assert.NoError(t, issues_model.ClearIssueLabels(db.DefaultContext, issue, doer)) + assert.NoError(t, issues_model.ClearIssueLabels(t.Context(), issue, doer)) unittest.AssertNotExistsBean(t, &issues_model.IssueLabel{IssueID: test.issueID}) } } @@ -138,12 +138,12 @@ func TestUpdateIssueCols(t *testing.T) { issue.Content = "This should have no effect" now := time.Now().Unix() - assert.NoError(t, issues_model.UpdateIssueCols(db.DefaultContext, issue, "name")) + assert.NoError(t, issues_model.UpdateIssueCols(t.Context(), issue, "name")) then := time.Now().Unix() updatedIssue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: issue.ID}) - assert.EqualValues(t, newTitle, updatedIssue.Title) - assert.EqualValues(t, prevContent, updatedIssue.Content) + assert.Equal(t, newTitle, updatedIssue.Title) + assert.Equal(t, prevContent, updatedIssue.Content) unittest.AssertInt64InRange(t, now, then, int64(updatedIssue.UpdatedUnix)) } @@ -155,7 +155,7 @@ func TestIssues(t *testing.T) { }{ { issues_model.IssuesOptions{ - AssigneeID: 1, + AssigneeID: "1", SortType: "oldest", }, []int64{1, 6}, @@ -198,11 +198,11 @@ func TestIssues(t *testing.T) { []int64{2}, }, } { - issues, err := issues_model.Issues(db.DefaultContext, &test.Opts) + issues, err := issues_model.Issues(t.Context(), &test.Opts) assert.NoError(t, err) if assert.Len(t, issues, len(test.ExpectedIssueIDs)) { for i, issue := range issues { - assert.EqualValues(t, test.ExpectedIssueIDs[i], issue.ID) + assert.Equal(t, test.ExpectedIssueIDs[i], issue.ID) } } } @@ -210,9 +210,9 @@ func TestIssues(t *testing.T) { func TestIssue_loadTotalTimes(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - ms, err := issues_model.GetIssueByID(db.DefaultContext, 2) + ms, err := issues_model.GetIssueByID(t.Context(), 2) assert.NoError(t, err) - assert.NoError(t, ms.LoadTotalTimes(db.DefaultContext)) + assert.NoError(t, ms.LoadTotalTimes(t.Context())) assert.Equal(t, int64(3682), ms.TotalTrackedTime) } @@ -229,16 +229,16 @@ func testInsertIssue(t *testing.T, title, content string, expectIndex int64) *is Title: title, Content: content, } - err := issues_model.NewIssue(db.DefaultContext, repo, &issue, nil, nil) + err := issues_model.NewIssue(t.Context(), repo, &issue, nil, nil) assert.NoError(t, err) - has, err := db.GetEngine(db.DefaultContext).ID(issue.ID).Get(&newIssue) + has, err := db.GetEngine(t.Context()).ID(issue.ID).Get(&newIssue) assert.NoError(t, err) assert.True(t, has) - assert.EqualValues(t, issue.Title, newIssue.Title) - assert.EqualValues(t, issue.Content, newIssue.Content) + assert.Equal(t, issue.Title, newIssue.Title) + assert.Equal(t, issue.Content, newIssue.Content) if expectIndex > 0 { - assert.EqualValues(t, expectIndex, newIssue.Index) + assert.Equal(t, expectIndex, newIssue.Index) } }) return &newIssue @@ -249,11 +249,11 @@ func TestIssue_InsertIssue(t *testing.T) { // there are 5 issues and max index is 5 on repository 1, so this one should 6 issue := testInsertIssue(t, "my issue1", "special issue's comments?", 6) - _, err := db.DeleteByID[issues_model.Issue](db.DefaultContext, issue.ID) + _, err := db.DeleteByID[issues_model.Issue](t.Context(), issue.ID) assert.NoError(t, err) issue = testInsertIssue(t, `my issue2, this is my son's love \n \r \ `, "special issue's '' comments?", 7) - _, err = db.DeleteByID[issues_model.Issue](db.DefaultContext, issue.ID) + _, err = db.DeleteByID[issues_model.Issue](t.Context(), issue.ID) assert.NoError(t, err) } @@ -265,14 +265,14 @@ func TestIssue_ResolveMentions(t *testing.T) { r := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerID: o.ID, LowerName: repo}) issue := &issues_model.Issue{RepoID: r.ID} d := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: doer}) - resolved, err := issues_model.ResolveIssueMentionsByVisibility(db.DefaultContext, issue, d, mentions) + resolved, err := issues_model.ResolveIssueMentionsByVisibility(t.Context(), issue, d, mentions) assert.NoError(t, err) ids := make([]int64, len(resolved)) for i, user := range resolved { ids[i] = user.ID } - sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] }) - assert.EqualValues(t, expected, ids) + slices.Sort(ids) + assert.Equal(t, expected, ids) } // Public repo, existing user @@ -293,7 +293,7 @@ func TestResourceIndex(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) var wg sync.WaitGroup - for i := 0; i < 100; i++ { + for i := range 100 { wg.Add(1) go func(i int) { testInsertIssue(t, fmt.Sprintf("issue %d", i+1), "my issue", 0) @@ -315,7 +315,7 @@ func TestCorrectIssueStats(t *testing.T) { issueAmount := issues_model.MaxQueryParameters + 10 var wg sync.WaitGroup - for i := 0; i < issueAmount; i++ { + for i := range issueAmount { wg.Add(1) go func(i int) { testInsertIssue(t, fmt.Sprintf("Issue %d", i+1), "Bugs are nasty", 0) @@ -325,7 +325,7 @@ func TestCorrectIssueStats(t *testing.T) { wg.Wait() // Now we will get all issueID's that match the "Bugs are nasty" query. - issues, err := issues_model.Issues(context.TODO(), &issues_model.IssuesOptions{ + issues, err := issues_model.Issues(t.Context(), &issues_model.IssuesOptions{ Paginator: &db.ListOptions{ PageSize: issueAmount, }, @@ -345,7 +345,7 @@ func TestCorrectIssueStats(t *testing.T) { // Now we will call the GetIssueStats with these IDs and if working, // get the correct stats back. - issueStats, err := issues_model.GetIssueStats(db.DefaultContext, &issues_model.IssuesOptions{ + issueStats, err := issues_model.GetIssueStats(t.Context(), &issues_model.IssuesOptions{ RepoIDs: []int64{1}, IssueIDs: ids, }) @@ -361,7 +361,7 @@ func TestMilestoneList_LoadTotalTrackedTimes(t *testing.T) { unittest.AssertExistsAndLoadBean(t, &issues_model.Milestone{ID: 1}), } - assert.NoError(t, miles.LoadTotalTrackedTimes(db.DefaultContext)) + assert.NoError(t, miles.LoadTotalTrackedTimes(t.Context())) assert.Equal(t, int64(3682), miles[0].TotalTrackedTime) } @@ -370,14 +370,14 @@ func TestLoadTotalTrackedTime(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) milestone := unittest.AssertExistsAndLoadBean(t, &issues_model.Milestone{ID: 1}) - assert.NoError(t, milestone.LoadTotalTrackedTime(db.DefaultContext)) + assert.NoError(t, milestone.LoadTotalTrackedTime(t.Context())) assert.Equal(t, int64(3682), milestone.TotalTrackedTime) } func TestCountIssues(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - count, err := issues_model.CountIssues(db.DefaultContext, &issues_model.IssuesOptions{}) + count, err := issues_model.CountIssues(t.Context(), &issues_model.IssuesOptions{}) assert.NoError(t, err) assert.EqualValues(t, 22, count) } @@ -392,29 +392,29 @@ func TestIssueLoadAttributes(t *testing.T) { } for _, issue := range issueList { - assert.NoError(t, issue.LoadAttributes(db.DefaultContext)) - assert.EqualValues(t, issue.RepoID, issue.Repo.ID) + assert.NoError(t, issue.LoadAttributes(t.Context())) + assert.Equal(t, issue.RepoID, issue.Repo.ID) for _, label := range issue.Labels { - assert.EqualValues(t, issue.RepoID, label.RepoID) + assert.Equal(t, issue.RepoID, label.RepoID) unittest.AssertExistsAndLoadBean(t, &issues_model.IssueLabel{IssueID: issue.ID, LabelID: label.ID}) } if issue.PosterID > 0 { - assert.EqualValues(t, issue.PosterID, issue.Poster.ID) + assert.Equal(t, issue.PosterID, issue.Poster.ID) } if issue.AssigneeID > 0 { - assert.EqualValues(t, issue.AssigneeID, issue.Assignee.ID) + assert.Equal(t, issue.AssigneeID, issue.Assignee.ID) } if issue.MilestoneID > 0 { - assert.EqualValues(t, issue.MilestoneID, issue.Milestone.ID) + assert.Equal(t, issue.MilestoneID, issue.Milestone.ID) } if issue.IsPull { - assert.EqualValues(t, issue.ID, issue.PullRequest.IssueID) + assert.Equal(t, issue.ID, issue.PullRequest.IssueID) } for _, attachment := range issue.Attachments { - assert.EqualValues(t, issue.ID, attachment.IssueID) + assert.Equal(t, issue.ID, attachment.IssueID) } for _, comment := range issue.Comments { - assert.EqualValues(t, issue.ID, comment.IssueID) + assert.Equal(t, issue.ID, comment.IssueID) } if issue.ID == int64(1) { assert.Equal(t, int64(400), issue.TotalTrackedTime) @@ -433,7 +433,7 @@ func assertCreateIssues(t *testing.T, isPull bool) { owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) label := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 1}) milestone := unittest.AssertExistsAndLoadBean(t, &issues_model.Milestone{ID: 1}) - assert.EqualValues(t, milestone.ID, 1) + assert.EqualValues(t, 1, milestone.ID) reaction := &issues_model.Reaction{ Type: "heart", UserID: owner.ID, @@ -453,7 +453,7 @@ func assertCreateIssues(t *testing.T, isPull bool) { Labels: []*issues_model.Label{label}, Reactions: []*issues_model.Reaction{reaction}, } - err := issues_model.InsertIssues(db.DefaultContext, is) + err := issues_model.InsertIssues(t.Context(), is) assert.NoError(t, err) i := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{Title: title}) diff --git a/models/issues/issue_update.go b/models/issues/issue_update.go index 5b929c9115b..1c168174918 100644 --- a/models/issues/issue_update.go +++ b/models/issues/issue_update.go @@ -5,16 +5,14 @@ package issues import ( "context" + "errors" "fmt" "strings" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/organization" - "code.gitea.io/gitea/models/perm" access_model "code.gitea.io/gitea/models/perm/access" - project_model "code.gitea.io/gitea/models/project" repo_model "code.gitea.io/gitea/models/repo" - system_model "code.gitea.io/gitea/models/system" "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" @@ -28,38 +26,40 @@ import ( // UpdateIssueCols updates cols of issue func UpdateIssueCols(ctx context.Context, issue *Issue, cols ...string) error { - if _, err := db.GetEngine(ctx).ID(issue.ID).Cols(cols...).Update(issue); err != nil { - return err - } - return nil + _, err := db.GetEngine(ctx).ID(issue.ID).Cols(cols...).Update(issue) + return err } -func changeIssueStatus(ctx context.Context, issue *Issue, doer *user_model.User, isClosed, isMergePull bool) (*Comment, error) { - // Reload the issue - currentIssue, err := GetIssueByID(ctx, issue.ID) - if err != nil { - return nil, err - } +// ErrIssueIsClosed is used when close a closed issue +type ErrIssueIsClosed struct { + ID int64 + RepoID int64 + Index int64 + IsPull bool +} - // Nothing should be performed if current status is same as target status - if currentIssue.IsClosed == isClosed { - if !issue.IsPull { - return nil, ErrIssueWasClosed{ - ID: issue.ID, - } - } - return nil, ErrPullWasClosed{ - ID: issue.ID, +// IsErrIssueIsClosed checks if an error is a ErrIssueIsClosed. +func IsErrIssueIsClosed(err error) bool { + _, ok := err.(ErrIssueIsClosed) + return ok +} + +func (err ErrIssueIsClosed) Error() string { + return fmt.Sprintf("%s [id: %d, repo_id: %d, index: %d] is already closed", util.Iif(err.IsPull, "Pull Request", "Issue"), err.ID, err.RepoID, err.Index) +} + +func SetIssueAsClosed(ctx context.Context, issue *Issue, doer *user_model.User, isMergePull bool) (*Comment, error) { + if issue.IsClosed { + return nil, ErrIssueIsClosed{ + ID: issue.ID, + RepoID: issue.RepoID, + Index: issue.Index, + IsPull: issue.IsPull, } } - issue.IsClosed = isClosed - return doChangeIssueStatus(ctx, issue, doer, isMergePull) -} - -func doChangeIssueStatus(ctx context.Context, issue *Issue, doer *user_model.User, isMergePull bool) (*Comment, error) { // Check for open dependencies - if issue.IsClosed && issue.Repo.IsDependenciesEnabled(ctx) { + if issue.Repo.IsDependenciesEnabled(ctx) { // only check if dependencies are enabled and we're about to close an issue, otherwise reopening an issue would fail when there are unsatisfied dependencies noDeps, err := IssueNoDependenciesLeft(ctx, issue) if err != nil { @@ -71,16 +71,63 @@ func doChangeIssueStatus(ctx context.Context, issue *Issue, doer *user_model.Use } } - if issue.IsClosed { - issue.ClosedUnix = timeutil.TimeStampNow() - } else { - issue.ClosedUnix = 0 - } + issue.IsClosed = true + issue.ClosedUnix = timeutil.TimeStampNow() - if err := UpdateIssueCols(ctx, issue, "is_closed", "closed_unix"); err != nil { + if cnt, err := db.GetEngine(ctx).ID(issue.ID).Cols("is_closed", "closed_unix"). + Where("is_closed = ?", false). + Update(issue); err != nil { return nil, err + } else if cnt != 1 { + return nil, ErrIssueAlreadyChanged } + return updateIssueNumbers(ctx, issue, doer, util.Iif(isMergePull, CommentTypeMergePull, CommentTypeClose)) +} + +// ErrIssueIsOpen is used when reopen an opened issue +type ErrIssueIsOpen struct { + ID int64 + RepoID int64 + IsPull bool + Index int64 +} + +// IsErrIssueIsOpen checks if an error is a ErrIssueIsOpen. +func IsErrIssueIsOpen(err error) bool { + _, ok := err.(ErrIssueIsOpen) + return ok +} + +func (err ErrIssueIsOpen) Error() string { + return fmt.Sprintf("%s [id: %d, repo_id: %d, index: %d] is already open", util.Iif(err.IsPull, "Pull Request", "Issue"), err.ID, err.RepoID, err.Index) +} + +func setIssueAsReopen(ctx context.Context, issue *Issue, doer *user_model.User) (*Comment, error) { + if !issue.IsClosed { + return nil, ErrIssueIsOpen{ + ID: issue.ID, + RepoID: issue.RepoID, + Index: issue.Index, + IsPull: issue.IsPull, + } + } + + issue.IsClosed = false + issue.ClosedUnix = 0 + + if cnt, err := db.GetEngine(ctx).ID(issue.ID).Cols("is_closed", "closed_unix"). + Where("is_closed = ?", true). + Update(issue); err != nil { + return nil, err + } else if cnt != 1 { + return nil, ErrIssueAlreadyChanged + } + + return updateIssueNumbers(ctx, issue, doer, CommentTypeReopen) +} + +func updateIssueNumbers(ctx context.Context, issue *Issue, doer *user_model.User, cmtType CommentType) (*Comment, error) { // Update issue count of labels if err := issue.LoadLabels(ctx); err != nil { return nil, err @@ -103,14 +150,6 @@ func doChangeIssueStatus(ctx context.Context, issue *Issue, doer *user_model.Use return nil, err } - // New action comment - cmtType := CommentTypeClose - if !issue.IsClosed { - cmtType = CommentTypeReopen - } else if isMergePull { - cmtType = CommentTypeMergePull - } - return CreateComment(ctx, &CreateCommentOptions{ Type: cmtType, Doer: doer, @@ -119,8 +158,8 @@ func doChangeIssueStatus(ctx context.Context, issue *Issue, doer *user_model.Use }) } -// ChangeIssueStatus changes issue status to open or closed. -func ChangeIssueStatus(ctx context.Context, issue *Issue, doer *user_model.User, isClosed bool) (*Comment, error) { +// CloseIssue changes issue status to closed. +func CloseIssue(ctx context.Context, issue *Issue, doer *user_model.User) (*Comment, error) { if err := issue.LoadRepo(ctx); err != nil { return nil, err } @@ -128,75 +167,78 @@ func ChangeIssueStatus(ctx context.Context, issue *Issue, doer *user_model.User, return nil, err } - return changeIssueStatus(ctx, issue, doer, isClosed, false) + return db.WithTx2(ctx, func(ctx context.Context) (*Comment, error) { + return SetIssueAsClosed(ctx, issue, doer, false) + }) +} + +// ReopenIssue changes issue status to open. +func ReopenIssue(ctx context.Context, issue *Issue, doer *user_model.User) (*Comment, error) { + if err := issue.LoadRepo(ctx); err != nil { + return nil, err + } + if err := issue.LoadPoster(ctx); err != nil { + return nil, err + } + + return db.WithTx2(ctx, func(ctx context.Context) (*Comment, error) { + return setIssueAsReopen(ctx, issue, doer) + }) } // ChangeIssueTitle changes the title of this issue, as the given user. func ChangeIssueTitle(ctx context.Context, issue *Issue, doer *user_model.User, oldTitle string) (err error) { - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return err - } - defer committer.Close() + return db.WithTx(ctx, func(ctx context.Context) error { + issue.Title = util.EllipsisDisplayString(issue.Title, 255) + if err = UpdateIssueCols(ctx, issue, "name"); err != nil { + return fmt.Errorf("updateIssueCols: %w", err) + } - issue.Title, _ = util.SplitStringAtByteN(issue.Title, 255) - if err = UpdateIssueCols(ctx, issue, "name"); err != nil { - return fmt.Errorf("updateIssueCols: %w", err) - } + if err = issue.LoadRepo(ctx); err != nil { + return fmt.Errorf("loadRepo: %w", err) + } - if err = issue.LoadRepo(ctx); err != nil { - return fmt.Errorf("loadRepo: %w", err) - } - - opts := &CreateCommentOptions{ - Type: CommentTypeChangeTitle, - Doer: doer, - Repo: issue.Repo, - Issue: issue, - OldTitle: oldTitle, - NewTitle: issue.Title, - } - if _, err = CreateComment(ctx, opts); err != nil { - return fmt.Errorf("createComment: %w", err) - } - if err = issue.AddCrossReferences(ctx, doer, true); err != nil { - return err - } - - return committer.Commit() + opts := &CreateCommentOptions{ + Type: CommentTypeChangeTitle, + Doer: doer, + Repo: issue.Repo, + Issue: issue, + OldTitle: oldTitle, + NewTitle: issue.Title, + } + if _, err = CreateComment(ctx, opts); err != nil { + return fmt.Errorf("createComment: %w", err) + } + return issue.AddCrossReferences(ctx, doer, true) + }) } // ChangeIssueRef changes the branch of this issue, as the given user. func ChangeIssueRef(ctx context.Context, issue *Issue, doer *user_model.User, oldRef string) (err error) { - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return err - } - defer committer.Close() + return db.WithTx(ctx, func(ctx context.Context) error { + if err = UpdateIssueCols(ctx, issue, "ref"); err != nil { + return fmt.Errorf("updateIssueCols: %w", err) + } - if err = UpdateIssueCols(ctx, issue, "ref"); err != nil { - return fmt.Errorf("updateIssueCols: %w", err) - } + if err = issue.LoadRepo(ctx); err != nil { + return fmt.Errorf("loadRepo: %w", err) + } + oldRefFriendly := strings.TrimPrefix(oldRef, git.BranchPrefix) + newRefFriendly := strings.TrimPrefix(issue.Ref, git.BranchPrefix) - if err = issue.LoadRepo(ctx); err != nil { - return fmt.Errorf("loadRepo: %w", err) - } - oldRefFriendly := strings.TrimPrefix(oldRef, git.BranchPrefix) - newRefFriendly := strings.TrimPrefix(issue.Ref, git.BranchPrefix) - - opts := &CreateCommentOptions{ - Type: CommentTypeChangeIssueRef, - Doer: doer, - Repo: issue.Repo, - Issue: issue, - OldRef: oldRefFriendly, - NewRef: newRefFriendly, - } - if _, err = CreateComment(ctx, opts); err != nil { - return fmt.Errorf("createComment: %w", err) - } - - return committer.Commit() + opts := &CreateCommentOptions{ + Type: CommentTypeChangeIssueRef, + Doer: doer, + Repo: issue.Repo, + Issue: issue, + OldRef: oldRefFriendly, + NewRef: newRefFriendly, + } + if _, err = CreateComment(ctx, opts); err != nil { + return fmt.Errorf("createComment: %w", err) + } + return nil + }) } // AddDeletePRBranchComment adds delete branch comment for pull request issue @@ -218,64 +260,56 @@ func AddDeletePRBranchComment(ctx context.Context, doer *user_model.User, repo * // UpdateIssueAttachments update attachments by UUIDs for the issue func UpdateIssueAttachments(ctx context.Context, issueID int64, uuids []string) (err error) { - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return err - } - defer committer.Close() - attachments, err := repo_model.GetAttachmentsByUUIDs(ctx, uuids) - if err != nil { - return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %w", uuids, err) - } - for i := 0; i < len(attachments); i++ { - attachments[i].IssueID = issueID - if err := repo_model.UpdateAttachment(ctx, attachments[i]); err != nil { - return fmt.Errorf("update attachment [id: %d]: %w", attachments[i].ID, err) + return db.WithTx(ctx, func(ctx context.Context) error { + attachments, err := repo_model.GetAttachmentsByUUIDs(ctx, uuids) + if err != nil { + return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %w", uuids, err) } - } - return committer.Commit() + for i := range attachments { + attachments[i].IssueID = issueID + if err := repo_model.UpdateAttachment(ctx, attachments[i]); err != nil { + return fmt.Errorf("update attachment [id: %d]: %w", attachments[i].ID, err) + } + } + return nil + }) } // ChangeIssueContent changes issue content, as the given user. func ChangeIssueContent(ctx context.Context, issue *Issue, doer *user_model.User, content string, contentVersion int) (err error) { - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return err - } - defer committer.Close() + return db.WithTx(ctx, func(ctx context.Context) error { + hasContentHistory, err := HasIssueContentHistory(ctx, issue.ID, 0) + if err != nil { + return fmt.Errorf("HasIssueContentHistory: %w", err) + } + if !hasContentHistory { + if err = SaveIssueContentHistory(ctx, issue.PosterID, issue.ID, 0, + issue.CreatedUnix, issue.Content, true); err != nil { + return fmt.Errorf("SaveIssueContentHistory: %w", err) + } + } - hasContentHistory, err := HasIssueContentHistory(ctx, issue.ID, 0) - if err != nil { - return fmt.Errorf("HasIssueContentHistory: %w", err) - } - if !hasContentHistory { - if err = SaveIssueContentHistory(ctx, issue.PosterID, issue.ID, 0, - issue.CreatedUnix, issue.Content, true); err != nil { + issue.Content = content + issue.ContentVersion = contentVersion + 1 + + affected, err := db.GetEngine(ctx).ID(issue.ID).Cols("content", "content_version").Where("content_version = ?", contentVersion).Update(issue) + if err != nil { + return err + } + if affected == 0 { + return ErrIssueAlreadyChanged + } + + if err = SaveIssueContentHistory(ctx, doer.ID, issue.ID, 0, + timeutil.TimeStampNow(), issue.Content, false); err != nil { return fmt.Errorf("SaveIssueContentHistory: %w", err) } - } - issue.Content = content - issue.ContentVersion = contentVersion + 1 - - affected, err := db.GetEngine(ctx).ID(issue.ID).Cols("content", "content_version").Where("content_version = ?", contentVersion).Update(issue) - if err != nil { - return err - } - if affected == 0 { - return ErrIssueAlreadyChanged - } - - if err = SaveIssueContentHistory(ctx, doer.ID, issue.ID, 0, - timeutil.TimeStampNow(), issue.Content, false); err != nil { - return fmt.Errorf("SaveIssueContentHistory: %w", err) - } - - if err = issue.AddCrossReferences(ctx, doer, true); err != nil { - return fmt.Errorf("addCrossReferences: %w", err) - } - - return committer.Commit() + if err = issue.AddCrossReferences(ctx, doer, true); err != nil { + return fmt.Errorf("addCrossReferences: %w", err) + } + return nil + }) } // NewIssueOptions represents the options of a new issue. @@ -307,10 +341,10 @@ func NewIssueWithIndex(ctx context.Context, doer *user_model.User, opts NewIssue } if opts.Issue.Index <= 0 { - return fmt.Errorf("no issue index provided") + return errors.New("no issue index provided") } if opts.Issue.ID > 0 { - return fmt.Errorf("issue exist") + return errors.New("issue exist") } if _, err := e.Insert(opts.Issue); err != nil { @@ -367,19 +401,10 @@ func NewIssueWithIndex(ctx context.Context, doer *user_model.User, opts NewIssue return err } - if len(opts.Attachments) > 0 { - attachments, err := repo_model.GetAttachmentsByUUIDs(ctx, opts.Attachments) - if err != nil { - return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %w", opts.Attachments, err) - } - - for i := 0; i < len(attachments); i++ { - attachments[i].IssueID = opts.Issue.ID - if _, err = e.ID(attachments[i].ID).Update(attachments[i]); err != nil { - return fmt.Errorf("update attachment [id: %d]: %w", attachments[i].ID, err) - } - } + if err := UpdateIssueAttachments(ctx, opts.Issue.ID, opts.Attachments); err != nil { + return err } + if err = opts.Issue.LoadAttributes(ctx); err != nil { return err } @@ -402,7 +427,7 @@ func NewIssue(ctx context.Context, repo *repo_model.Repository, issue *Issue, la } issue.Index = idx - issue.Title, _ = util.SplitStringAtByteN(issue.Title, 255) + issue.Title = util.EllipsisDisplayString(issue.Title, 255) if err = NewIssueWithIndex(ctx, issue.Poster, NewIssueOptions{ Repo: repo, @@ -444,23 +469,19 @@ func UpdateIssueDeadline(ctx context.Context, issue *Issue, deadlineUnix timeuti if issue.DeadlineUnix == deadlineUnix { return nil } - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return err - } - defer committer.Close() - // Update the deadline - if err = UpdateIssueCols(ctx, &Issue{ID: issue.ID, DeadlineUnix: deadlineUnix}, "deadline_unix"); err != nil { - return err - } + return db.WithTx(ctx, func(ctx context.Context) error { + // Update the deadline + if err = UpdateIssueCols(ctx, &Issue{ID: issue.ID, DeadlineUnix: deadlineUnix}, "deadline_unix"); err != nil { + return err + } - // Make the comment - if _, err = createDeadlineComment(ctx, doer, issue, deadlineUnix); err != nil { - return fmt.Errorf("createRemovedDueDateComment: %w", err) - } - - return committer.Commit() + // Make the comment + if _, err = createDeadlineComment(ctx, doer, issue, deadlineUnix); err != nil { + return fmt.Errorf("createRemovedDueDateComment: %w", err) + } + return nil + }) } // FindAndUpdateIssueMentions finds users mentioned in the given content string, and saves them in the database. @@ -541,7 +562,7 @@ func ResolveIssueMentionsByVisibility(ctx context.Context, issue *Issue, doer *u unittype = unit.TypePullRequests } for _, team := range teams { - if team.AccessMode >= perm.AccessModeAdmin { + if team.HasAdminAccess() { checked = append(checked, team.ID) resolved[issue.Repo.Owner.LowerName+"/"+team.LowerName] = true continue @@ -645,137 +666,13 @@ func UpdateReactionsMigrationsByType(ctx context.Context, gitServiceType api.Git return err } -// DeleteIssuesByRepoID deletes issues by repositories id -func DeleteIssuesByRepoID(ctx context.Context, repoID int64) (attachmentPaths []string, err error) { - // MariaDB has a performance bug: https://jira.mariadb.org/browse/MDEV-16289 - // so here it uses "DELETE ... WHERE IN" with pre-queried IDs. - sess := db.GetEngine(ctx) - - for { - issueIDs := make([]int64, 0, db.DefaultMaxInSize) - - err := sess.Table(&Issue{}).Where("repo_id = ?", repoID).OrderBy("id").Limit(db.DefaultMaxInSize).Cols("id").Find(&issueIDs) - if err != nil { - return nil, err - } - - if len(issueIDs) == 0 { - break - } - - // Delete content histories - _, err = sess.In("issue_id", issueIDs).Delete(&ContentHistory{}) - if err != nil { - return nil, err - } - - // Delete comments and attachments - _, err = sess.In("issue_id", issueIDs).Delete(&Comment{}) - if err != nil { - return nil, err - } - - // Dependencies for issues in this repository - _, err = sess.In("issue_id", issueIDs).Delete(&IssueDependency{}) - if err != nil { - return nil, err - } - - // Delete dependencies for issues in other repositories - _, err = sess.In("dependency_id", issueIDs).Delete(&IssueDependency{}) - if err != nil { - return nil, err - } - - _, err = sess.In("issue_id", issueIDs).Delete(&IssueUser{}) - if err != nil { - return nil, err - } - - _, err = sess.In("issue_id", issueIDs).Delete(&Reaction{}) - if err != nil { - return nil, err - } - - _, err = sess.In("issue_id", issueIDs).Delete(&IssueWatch{}) - if err != nil { - return nil, err - } - - _, err = sess.In("issue_id", issueIDs).Delete(&Stopwatch{}) - if err != nil { - return nil, err - } - - _, err = sess.In("issue_id", issueIDs).Delete(&TrackedTime{}) - if err != nil { - return nil, err - } - - _, err = sess.In("issue_id", issueIDs).Delete(&project_model.ProjectIssue{}) - if err != nil { - return nil, err - } - - _, err = sess.In("dependent_issue_id", issueIDs).Delete(&Comment{}) - if err != nil { - return nil, err - } - - var attachments []*repo_model.Attachment - err = sess.In("issue_id", issueIDs).Find(&attachments) - if err != nil { - return nil, err - } - - for j := range attachments { - attachmentPaths = append(attachmentPaths, attachments[j].RelativePath()) - } - - _, err = sess.In("issue_id", issueIDs).Delete(&repo_model.Attachment{}) - if err != nil { - return nil, err - } - - _, err = sess.In("id", issueIDs).Delete(&Issue{}) - if err != nil { - return nil, err - } +func GetOrphanedIssueRepoIDs(ctx context.Context) ([]int64, error) { + var repoIDs []int64 + if err := db.GetEngine(ctx).Table("issue").Distinct("issue.repo_id"). + Join("LEFT", "repository", "issue.repo_id=repository.id"). + Where(builder.IsNull{"repository.id"}). + Find(&repoIDs); err != nil { + return nil, err } - - return attachmentPaths, err -} - -// DeleteOrphanedIssues delete issues without a repo -func DeleteOrphanedIssues(ctx context.Context) error { - var attachmentPaths []string - err := db.WithTx(ctx, func(ctx context.Context) error { - var ids []int64 - - if err := db.GetEngine(ctx).Table("issue").Distinct("issue.repo_id"). - Join("LEFT", "repository", "issue.repo_id=repository.id"). - Where(builder.IsNull{"repository.id"}).GroupBy("issue.repo_id"). - Find(&ids); err != nil { - return err - } - - for i := range ids { - paths, err := DeleteIssuesByRepoID(ctx, ids[i]) - if err != nil { - return err - } - attachmentPaths = append(attachmentPaths, paths...) - } - - return nil - }) - if err != nil { - return err - } - - // Remove issue attachment files. - for i := range attachmentPaths { - system_model.RemoveAllWithNotice(ctx, "Delete issue attachment", attachmentPaths[i]) - } - return nil + return repoIDs, nil } diff --git a/models/issues/issue_user_test.go b/models/issues/issue_user_test.go index ce47adb53a0..ec6d85c2bb5 100644 --- a/models/issues/issue_user_test.go +++ b/models/issues/issue_user_test.go @@ -12,6 +12,7 @@ import ( "code.gitea.io/gitea/models/unittest" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func Test_NewIssueUsers(t *testing.T) { @@ -27,9 +28,8 @@ func Test_NewIssueUsers(t *testing.T) { } // artificially insert new issue - unittest.AssertSuccessfulInsert(t, newIssue) - - assert.NoError(t, issues_model.NewIssueUsers(db.DefaultContext, repo, newIssue)) + require.NoError(t, db.Insert(t.Context(), newIssue)) + require.NoError(t, issues_model.NewIssueUsers(t.Context(), repo, newIssue)) // issue_user table should now have entries for new issue unittest.AssertExistsAndLoadBean(t, &issues_model.IssueUser{IssueID: newIssue.ID, UID: newIssue.PosterID}) @@ -40,13 +40,13 @@ func TestUpdateIssueUserByRead(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1}) - assert.NoError(t, issues_model.UpdateIssueUserByRead(db.DefaultContext, 4, issue.ID)) + assert.NoError(t, issues_model.UpdateIssueUserByRead(t.Context(), 4, issue.ID)) unittest.AssertExistsAndLoadBean(t, &issues_model.IssueUser{IssueID: issue.ID, UID: 4}, "is_read=1") - assert.NoError(t, issues_model.UpdateIssueUserByRead(db.DefaultContext, 4, issue.ID)) + assert.NoError(t, issues_model.UpdateIssueUserByRead(t.Context(), 4, issue.ID)) unittest.AssertExistsAndLoadBean(t, &issues_model.IssueUser{IssueID: issue.ID, UID: 4}, "is_read=1") - assert.NoError(t, issues_model.UpdateIssueUserByRead(db.DefaultContext, unittest.NonexistentID, unittest.NonexistentID)) + assert.NoError(t, issues_model.UpdateIssueUserByRead(t.Context(), unittest.NonexistentID, unittest.NonexistentID)) } func TestUpdateIssueUsersByMentions(t *testing.T) { @@ -54,7 +54,7 @@ func TestUpdateIssueUsersByMentions(t *testing.T) { issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1}) uids := []int64{2, 5} - assert.NoError(t, issues_model.UpdateIssueUsersByMentions(db.DefaultContext, issue.ID, uids)) + assert.NoError(t, issues_model.UpdateIssueUsersByMentions(t.Context(), issue.ID, uids)) for _, uid := range uids { unittest.AssertExistsAndLoadBean(t, &issues_model.IssueUser{IssueID: issue.ID, UID: uid}, "is_mentioned=1") } diff --git a/models/issues/issue_watch_test.go b/models/issues/issue_watch_test.go index d4ce8d8d3d6..c860e8b8caf 100644 --- a/models/issues/issue_watch_test.go +++ b/models/issues/issue_watch_test.go @@ -16,11 +16,11 @@ import ( func TestCreateOrUpdateIssueWatch(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - assert.NoError(t, issues_model.CreateOrUpdateIssueWatch(db.DefaultContext, 3, 1, true)) + assert.NoError(t, issues_model.CreateOrUpdateIssueWatch(t.Context(), 3, 1, true)) iw := unittest.AssertExistsAndLoadBean(t, &issues_model.IssueWatch{UserID: 3, IssueID: 1}) assert.True(t, iw.IsWatching) - assert.NoError(t, issues_model.CreateOrUpdateIssueWatch(db.DefaultContext, 1, 1, false)) + assert.NoError(t, issues_model.CreateOrUpdateIssueWatch(t.Context(), 1, 1, false)) iw = unittest.AssertExistsAndLoadBean(t, &issues_model.IssueWatch{UserID: 1, IssueID: 1}) assert.False(t, iw.IsWatching) } @@ -28,16 +28,16 @@ func TestCreateOrUpdateIssueWatch(t *testing.T) { func TestGetIssueWatch(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - _, exists, err := issues_model.GetIssueWatch(db.DefaultContext, 9, 1) + _, exists, err := issues_model.GetIssueWatch(t.Context(), 9, 1) assert.True(t, exists) assert.NoError(t, err) - iw, exists, err := issues_model.GetIssueWatch(db.DefaultContext, 2, 2) + iw, exists, err := issues_model.GetIssueWatch(t.Context(), 2, 2) assert.True(t, exists) assert.NoError(t, err) assert.False(t, iw.IsWatching) - _, exists, err = issues_model.GetIssueWatch(db.DefaultContext, 3, 1) + _, exists, err = issues_model.GetIssueWatch(t.Context(), 3, 1) assert.False(t, exists) assert.NoError(t, err) } @@ -45,22 +45,22 @@ func TestGetIssueWatch(t *testing.T) { func TestGetIssueWatchers(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - iws, err := issues_model.GetIssueWatchers(db.DefaultContext, 1, db.ListOptions{}) + iws, err := issues_model.GetIssueWatchers(t.Context(), 1, db.ListOptions{}) assert.NoError(t, err) // Watcher is inactive, thus 0 - assert.Len(t, iws, 0) + assert.Empty(t, iws) - iws, err = issues_model.GetIssueWatchers(db.DefaultContext, 2, db.ListOptions{}) + iws, err = issues_model.GetIssueWatchers(t.Context(), 2, db.ListOptions{}) assert.NoError(t, err) // Watcher is explicit not watching - assert.Len(t, iws, 0) + assert.Empty(t, iws) - iws, err = issues_model.GetIssueWatchers(db.DefaultContext, 5, db.ListOptions{}) + iws, err = issues_model.GetIssueWatchers(t.Context(), 5, db.ListOptions{}) assert.NoError(t, err) // Issue has no Watchers - assert.Len(t, iws, 0) + assert.Empty(t, iws) - iws, err = issues_model.GetIssueWatchers(db.DefaultContext, 7, db.ListOptions{}) + iws, err = issues_model.GetIssueWatchers(t.Context(), 7, db.ListOptions{}) assert.NoError(t, err) // Issue has one watcher assert.Len(t, iws, 1) diff --git a/models/issues/issue_xref.go b/models/issues/issue_xref.go index e2e35859df1..f8495929cf9 100644 --- a/models/issues/issue_xref.go +++ b/models/issues/issue_xref.go @@ -235,7 +235,7 @@ func (issue *Issue) verifyReferencedIssue(stdCtx context.Context, ctx *crossRefe // AddCrossReferences add cross references func (c *Comment) AddCrossReferences(stdCtx context.Context, doer *user_model.User, removeOld bool) error { - if c.Type != CommentTypeCode && c.Type != CommentTypeComment { + if !c.Type.HasContentSupport() { return nil } if err := c.LoadIssue(stdCtx); err != nil { diff --git a/models/issues/issue_xref_test.go b/models/issues/issue_xref_test.go index f1b1bb2a6b1..b25a704bec2 100644 --- a/models/issues/issue_xref_test.go +++ b/models/issues/issue_xref_test.go @@ -83,7 +83,7 @@ func TestXRef_NeuterCrossReferences(t *testing.T) { d := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) i.Title = "title2, no mentions" - assert.NoError(t, issues_model.ChangeIssueTitle(db.DefaultContext, i, d, title)) + assert.NoError(t, issues_model.ChangeIssueTitle(t.Context(), i, d, title)) ref = unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{IssueID: itarget.ID, RefIssueID: i.ID, RefCommentID: 0}) assert.Equal(t, issues_model.CommentTypeIssueRef, ref.Type) @@ -98,7 +98,7 @@ func TestXRef_ResolveCrossReferences(t *testing.T) { i1 := testCreateIssue(t, 1, 2, "title1", "content1", false) i2 := testCreateIssue(t, 1, 2, "title2", "content2", false) i3 := testCreateIssue(t, 1, 2, "title3", "content3", false) - _, err := issues_model.ChangeIssueStatus(db.DefaultContext, i3, d, true) + _, err := issues_model.CloseIssue(t.Context(), i3, d) assert.NoError(t, err) pr := testCreatePR(t, 1, 2, "titlepr", fmt.Sprintf("closes #%d", i1.Index)) @@ -118,7 +118,7 @@ func TestXRef_ResolveCrossReferences(t *testing.T) { c4 := testCreateComment(t, 2, pr.Issue.ID, fmt.Sprintf("closes #%d", i3.Index)) r4 := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{IssueID: i3.ID, RefIssueID: pr.Issue.ID, RefCommentID: c4.ID}) - refs, err := pr.ResolveCrossReferences(db.DefaultContext) + refs, err := pr.ResolveCrossReferences(t.Context()) assert.NoError(t, err) assert.Len(t, refs, 3) assert.Equal(t, rp.ID, refs[0].ID, "bad ref rp: %+v", refs[0]) @@ -130,7 +130,7 @@ func testCreateIssue(t *testing.T, repo, doer int64, title, content string, ispu r := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repo}) d := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: doer}) - ctx, committer, err := db.TxContext(db.DefaultContext) + ctx, committer, err := db.TxContext(t.Context()) assert.NoError(t, err) defer committer.Close() @@ -163,7 +163,7 @@ func testCreatePR(t *testing.T, repo, doer int64, title, content string) *issues d := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: doer}) i := &issues_model.Issue{RepoID: r.ID, PosterID: d.ID, Poster: d, Title: title, Content: content, IsPull: true} pr := &issues_model.PullRequest{HeadRepoID: repo, BaseRepoID: repo, HeadBranch: "head", BaseBranch: "base", Status: issues_model.PullRequestStatusMergeable} - assert.NoError(t, issues_model.NewPullRequest(db.DefaultContext, r, i, nil, nil, pr)) + assert.NoError(t, issues_model.NewPullRequest(t.Context(), r, i, nil, nil, pr)) pr.Issue = i return pr } @@ -173,7 +173,7 @@ func testCreateComment(t *testing.T, doer, issue int64, content string) *issues_ i := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: issue}) c := &issues_model.Comment{Type: issues_model.CommentTypeComment, PosterID: doer, Poster: d, IssueID: issue, Issue: i, Content: content} - ctx, committer, err := db.TxContext(db.DefaultContext) + ctx, committer, err := db.TxContext(t.Context()) assert.NoError(t, err) defer committer.Close() err = db.Insert(ctx, c) diff --git a/models/issues/label.go b/models/issues/label.go index d80578193e5..25d6f1303e8 100644 --- a/models/issues/label.go +++ b/models/issues/label.go @@ -87,6 +87,7 @@ type Label struct { OrgID int64 `xorm:"INDEX"` Name string Exclusive bool + ExclusiveOrder int `xorm:"DEFAULT 0"` // 0 means no exclusive order Description string Color string `xorm:"VARCHAR(7)"` NumIssues int @@ -208,24 +209,20 @@ func NewLabel(ctx context.Context, l *Label) error { // NewLabels creates new labels func NewLabels(ctx context.Context, labels ...*Label) error { - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return err - } - defer committer.Close() + return db.WithTx(ctx, func(ctx context.Context) error { + for _, l := range labels { + color, err := label.NormalizeColor(l.Color) + if err != nil { + return err + } + l.Color = color - for _, l := range labels { - color, err := label.NormalizeColor(l.Color) - if err != nil { - return err + if err := db.Insert(ctx, l); err != nil { + return err + } } - l.Color = color - - if err := db.Insert(ctx, l); err != nil { - return err - } - } - return committer.Commit() + return nil + }) } // UpdateLabel updates label information. @@ -236,7 +233,7 @@ func UpdateLabel(ctx context.Context, l *Label) error { } l.Color = color - return updateLabelCols(ctx, l, "name", "description", "color", "exclusive", "archived_unix") + return updateLabelCols(ctx, l, "name", "description", "color", "exclusive", "exclusive_order", "archived_unix") } // DeleteLabel delete a label @@ -249,35 +246,26 @@ func DeleteLabel(ctx context.Context, id, labelID int64) error { return err } - ctx, committer, err := db.TxContext(ctx) - if err != nil { + return db.WithTx(ctx, func(ctx context.Context) error { + if l.BelongsToOrg() && l.OrgID != id { + return nil + } + if l.BelongsToRepo() && l.RepoID != id { + return nil + } + + if _, err = db.DeleteByID[Label](ctx, labelID); err != nil { + return err + } else if _, err = db.GetEngine(ctx). + Where("label_id = ?", labelID). + Delete(new(IssueLabel)); err != nil { + return err + } + + // delete comments about now deleted label_id + _, err = db.GetEngine(ctx).Where("label_id = ?", labelID).Cols("label_id").Delete(&Comment{}) return err - } - defer committer.Close() - - sess := db.GetEngine(ctx) - - if l.BelongsToOrg() && l.OrgID != id { - return nil - } - if l.BelongsToRepo() && l.RepoID != id { - return nil - } - - if _, err = db.DeleteByID[Label](ctx, labelID); err != nil { - return err - } else if _, err = sess. - Where("label_id = ?", labelID). - Delete(new(IssueLabel)); err != nil { - return err - } - - // delete comments about now deleted label_id - if _, err = sess.Where("label_id = ?", labelID).Cols("label_id").Delete(&Comment{}); err != nil { - return err - } - - return committer.Commit() + }) } // GetLabelByID returns a label by given ID. @@ -299,6 +287,9 @@ func GetLabelByID(ctx context.Context, labelID int64) (*Label, error) { // GetLabelsByIDs returns a list of labels by IDs func GetLabelsByIDs(ctx context.Context, labelIDs []int64, cols ...string) ([]*Label, error) { labels := make([]*Label, 0, len(labelIDs)) + if len(labelIDs) == 0 { + return labels, nil + } return labels, db.GetEngine(ctx).Table("label"). In("id", labelIDs). Asc("name"). @@ -349,6 +340,17 @@ func GetLabelIDsInRepoByNames(ctx context.Context, repoID int64, labelNames []st Find(&labelIDs) } +// GetLabelIDsInOrgByNames returns a list of labelIDs by names in a given org. +func GetLabelIDsInOrgByNames(ctx context.Context, orgID int64, labelNames []string) ([]int64, error) { + labelIDs := make([]int64, 0, len(labelNames)) + return labelIDs, db.GetEngine(ctx).Table("label"). + Where("org_id = ?", orgID). + In("name", labelNames). + Asc("name"). + Cols("id"). + Find(&labelIDs) +} + // BuildLabelNamesIssueIDsCondition returns a builder where get issue ids match label names func BuildLabelNamesIssueIDsCondition(labelNames []string) *builder.Builder { return builder.Select("issue_label.issue_id"). @@ -364,6 +366,9 @@ func BuildLabelNamesIssueIDsCondition(labelNames []string) *builder.Builder { // it silently ignores label IDs that do not belong to the repository. func GetLabelsInRepoByIDs(ctx context.Context, repoID int64, labelIDs []int64) ([]*Label, error) { labels := make([]*Label, 0, len(labelIDs)) + if len(labelIDs) == 0 { + return labels, nil + } return labels, db.GetEngine(ctx). Where("repo_id = ?", repoID). In("id", labelIDs). @@ -436,6 +441,9 @@ func GetLabelInOrgByID(ctx context.Context, orgID, labelID int64) (*Label, error // it silently ignores label IDs that do not belong to the organization. func GetLabelsInOrgByIDs(ctx context.Context, orgID int64, labelIDs []int64) ([]*Label, error) { labels := make([]*Label, 0, len(labelIDs)) + if len(labelIDs) == 0 { + return labels, nil + } return labels, db.GetEngine(ctx). Where("org_id = ?", orgID). In("id", labelIDs). diff --git a/models/issues/label_test.go b/models/issues/label_test.go index c2ff084c236..50393855d97 100644 --- a/models/issues/label_test.go +++ b/models/issues/label_test.go @@ -20,7 +20,7 @@ func TestLabel_CalOpenIssues(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) label := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 1}) label.CalOpenIssues() - assert.EqualValues(t, 2, label.NumOpenIssues) + assert.Equal(t, 2, label.NumOpenIssues) } func TestLabel_LoadSelectedLabelsAfterClick(t *testing.T) { @@ -31,12 +31,12 @@ func TestLabel_LoadSelectedLabelsAfterClick(t *testing.T) { // First test : with negative and scope label.LoadSelectedLabelsAfterClick([]int64{1, -8}, []string{"", "scope"}) assert.Equal(t, "1", label.QueryString) - assert.Equal(t, true, label.IsSelected) + assert.True(t, label.IsSelected) // Second test : with duplicates label.LoadSelectedLabelsAfterClick([]int64{1, 7, 1, 7, 7}, []string{"", "scope", "", "scope", "scope"}) assert.Equal(t, "1,8", label.QueryString) - assert.Equal(t, false, label.IsSelected) + assert.False(t, label.IsSelected) // Third test : empty set label.LoadSelectedLabelsAfterClick([]int64{}, []string{}) @@ -61,15 +61,15 @@ func TestNewLabels(t *testing.T) { {RepoID: 4, Name: "labelName4", Color: "ABCDEF"}, {RepoID: 5, Name: "labelName5", Color: "DEF"}, } - assert.Error(t, issues_model.NewLabel(db.DefaultContext, &issues_model.Label{RepoID: 3, Name: "invalid Color", Color: ""})) - assert.Error(t, issues_model.NewLabel(db.DefaultContext, &issues_model.Label{RepoID: 3, Name: "invalid Color", Color: "#45G"})) - assert.Error(t, issues_model.NewLabel(db.DefaultContext, &issues_model.Label{RepoID: 3, Name: "invalid Color", Color: "#12345G"})) - assert.Error(t, issues_model.NewLabel(db.DefaultContext, &issues_model.Label{RepoID: 3, Name: "invalid Color", Color: "45G"})) - assert.Error(t, issues_model.NewLabel(db.DefaultContext, &issues_model.Label{RepoID: 3, Name: "invalid Color", Color: "12345G"})) + assert.Error(t, issues_model.NewLabel(t.Context(), &issues_model.Label{RepoID: 3, Name: "invalid Color", Color: ""})) + assert.Error(t, issues_model.NewLabel(t.Context(), &issues_model.Label{RepoID: 3, Name: "invalid Color", Color: "#45G"})) + assert.Error(t, issues_model.NewLabel(t.Context(), &issues_model.Label{RepoID: 3, Name: "invalid Color", Color: "#12345G"})) + assert.Error(t, issues_model.NewLabel(t.Context(), &issues_model.Label{RepoID: 3, Name: "invalid Color", Color: "45G"})) + assert.Error(t, issues_model.NewLabel(t.Context(), &issues_model.Label{RepoID: 3, Name: "invalid Color", Color: "12345G"})) for _, label := range labels { unittest.AssertNotExistsBean(t, label) } - assert.NoError(t, issues_model.NewLabels(db.DefaultContext, labels...)) + assert.NoError(t, issues_model.NewLabels(t.Context(), labels...)) for _, label := range labels { unittest.AssertExistsAndLoadBean(t, label, unittest.Cond("id = ?", label.ID)) } @@ -78,31 +78,31 @@ func TestNewLabels(t *testing.T) { func TestGetLabelByID(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - label, err := issues_model.GetLabelByID(db.DefaultContext, 1) + label, err := issues_model.GetLabelByID(t.Context(), 1) assert.NoError(t, err) assert.EqualValues(t, 1, label.ID) - _, err = issues_model.GetLabelByID(db.DefaultContext, unittest.NonexistentID) + _, err = issues_model.GetLabelByID(t.Context(), unittest.NonexistentID) assert.True(t, issues_model.IsErrLabelNotExist(err)) } func TestGetLabelInRepoByName(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - label, err := issues_model.GetLabelInRepoByName(db.DefaultContext, 1, "label1") + label, err := issues_model.GetLabelInRepoByName(t.Context(), 1, "label1") assert.NoError(t, err) assert.EqualValues(t, 1, label.ID) assert.Equal(t, "label1", label.Name) - _, err = issues_model.GetLabelInRepoByName(db.DefaultContext, 1, "") + _, err = issues_model.GetLabelInRepoByName(t.Context(), 1, "") assert.True(t, issues_model.IsErrRepoLabelNotExist(err)) - _, err = issues_model.GetLabelInRepoByName(db.DefaultContext, unittest.NonexistentID, "nonexistent") + _, err = issues_model.GetLabelInRepoByName(t.Context(), unittest.NonexistentID, "nonexistent") assert.True(t, issues_model.IsErrRepoLabelNotExist(err)) } func TestGetLabelInRepoByNames(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - labelIDs, err := issues_model.GetLabelIDsInRepoByNames(db.DefaultContext, 1, []string{"label1", "label2"}) + labelIDs, err := issues_model.GetLabelIDsInRepoByNames(t.Context(), 1, []string{"label1", "label2"}) assert.NoError(t, err) assert.Len(t, labelIDs, 2) @@ -114,7 +114,7 @@ func TestGetLabelInRepoByNames(t *testing.T) { func TestGetLabelInRepoByNamesDiscardsNonExistentLabels(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) // label3 doesn't exists.. See labels.yml - labelIDs, err := issues_model.GetLabelIDsInRepoByNames(db.DefaultContext, 1, []string{"label1", "label2", "label3"}) + labelIDs, err := issues_model.GetLabelIDsInRepoByNames(t.Context(), 1, []string{"label1", "label2", "label3"}) assert.NoError(t, err) assert.Len(t, labelIDs, 2) @@ -126,20 +126,20 @@ func TestGetLabelInRepoByNamesDiscardsNonExistentLabels(t *testing.T) { func TestGetLabelInRepoByID(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - label, err := issues_model.GetLabelInRepoByID(db.DefaultContext, 1, 1) + label, err := issues_model.GetLabelInRepoByID(t.Context(), 1, 1) assert.NoError(t, err) assert.EqualValues(t, 1, label.ID) - _, err = issues_model.GetLabelInRepoByID(db.DefaultContext, 1, -1) + _, err = issues_model.GetLabelInRepoByID(t.Context(), 1, -1) assert.True(t, issues_model.IsErrRepoLabelNotExist(err)) - _, err = issues_model.GetLabelInRepoByID(db.DefaultContext, unittest.NonexistentID, unittest.NonexistentID) + _, err = issues_model.GetLabelInRepoByID(t.Context(), unittest.NonexistentID, unittest.NonexistentID) assert.True(t, issues_model.IsErrRepoLabelNotExist(err)) } func TestGetLabelsInRepoByIDs(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - labels, err := issues_model.GetLabelsInRepoByIDs(db.DefaultContext, 1, []int64{1, 2, unittest.NonexistentID}) + labels, err := issues_model.GetLabelsInRepoByIDs(t.Context(), 1, []int64{1, 2, unittest.NonexistentID}) assert.NoError(t, err) if assert.Len(t, labels, 2) { assert.EqualValues(t, 1, labels[0].ID) @@ -150,11 +150,11 @@ func TestGetLabelsInRepoByIDs(t *testing.T) { func TestGetLabelsByRepoID(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) testSuccess := func(repoID int64, sortType string, expectedIssueIDs []int64) { - labels, err := issues_model.GetLabelsByRepoID(db.DefaultContext, repoID, sortType, db.ListOptions{}) + labels, err := issues_model.GetLabelsByRepoID(t.Context(), repoID, sortType, db.ListOptions{}) assert.NoError(t, err) assert.Len(t, labels, len(expectedIssueIDs)) for i, label := range labels { - assert.EqualValues(t, expectedIssueIDs[i], label.ID) + assert.Equal(t, expectedIssueIDs[i], label.ID) } } testSuccess(1, "leastissues", []int64{2, 1}) @@ -167,46 +167,46 @@ func TestGetLabelsByRepoID(t *testing.T) { func TestGetLabelInOrgByName(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - label, err := issues_model.GetLabelInOrgByName(db.DefaultContext, 3, "orglabel3") + label, err := issues_model.GetLabelInOrgByName(t.Context(), 3, "orglabel3") assert.NoError(t, err) assert.EqualValues(t, 3, label.ID) assert.Equal(t, "orglabel3", label.Name) - _, err = issues_model.GetLabelInOrgByName(db.DefaultContext, 3, "") + _, err = issues_model.GetLabelInOrgByName(t.Context(), 3, "") assert.True(t, issues_model.IsErrOrgLabelNotExist(err)) - _, err = issues_model.GetLabelInOrgByName(db.DefaultContext, 0, "orglabel3") + _, err = issues_model.GetLabelInOrgByName(t.Context(), 0, "orglabel3") assert.True(t, issues_model.IsErrOrgLabelNotExist(err)) - _, err = issues_model.GetLabelInOrgByName(db.DefaultContext, -1, "orglabel3") + _, err = issues_model.GetLabelInOrgByName(t.Context(), -1, "orglabel3") assert.True(t, issues_model.IsErrOrgLabelNotExist(err)) - _, err = issues_model.GetLabelInOrgByName(db.DefaultContext, unittest.NonexistentID, "nonexistent") + _, err = issues_model.GetLabelInOrgByName(t.Context(), unittest.NonexistentID, "nonexistent") assert.True(t, issues_model.IsErrOrgLabelNotExist(err)) } func TestGetLabelInOrgByID(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - label, err := issues_model.GetLabelInOrgByID(db.DefaultContext, 3, 3) + label, err := issues_model.GetLabelInOrgByID(t.Context(), 3, 3) assert.NoError(t, err) assert.EqualValues(t, 3, label.ID) - _, err = issues_model.GetLabelInOrgByID(db.DefaultContext, 3, -1) + _, err = issues_model.GetLabelInOrgByID(t.Context(), 3, -1) assert.True(t, issues_model.IsErrOrgLabelNotExist(err)) - _, err = issues_model.GetLabelInOrgByID(db.DefaultContext, 0, 3) + _, err = issues_model.GetLabelInOrgByID(t.Context(), 0, 3) assert.True(t, issues_model.IsErrOrgLabelNotExist(err)) - _, err = issues_model.GetLabelInOrgByID(db.DefaultContext, -1, 3) + _, err = issues_model.GetLabelInOrgByID(t.Context(), -1, 3) assert.True(t, issues_model.IsErrOrgLabelNotExist(err)) - _, err = issues_model.GetLabelInOrgByID(db.DefaultContext, unittest.NonexistentID, unittest.NonexistentID) + _, err = issues_model.GetLabelInOrgByID(t.Context(), unittest.NonexistentID, unittest.NonexistentID) assert.True(t, issues_model.IsErrOrgLabelNotExist(err)) } func TestGetLabelsInOrgByIDs(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - labels, err := issues_model.GetLabelsInOrgByIDs(db.DefaultContext, 3, []int64{3, 4, unittest.NonexistentID}) + labels, err := issues_model.GetLabelsInOrgByIDs(t.Context(), 3, []int64{3, 4, unittest.NonexistentID}) assert.NoError(t, err) if assert.Len(t, labels, 2) { assert.EqualValues(t, 3, labels[0].ID) @@ -217,11 +217,11 @@ func TestGetLabelsInOrgByIDs(t *testing.T) { func TestGetLabelsByOrgID(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) testSuccess := func(orgID int64, sortType string, expectedIssueIDs []int64) { - labels, err := issues_model.GetLabelsByOrgID(db.DefaultContext, orgID, sortType, db.ListOptions{}) + labels, err := issues_model.GetLabelsByOrgID(t.Context(), orgID, sortType, db.ListOptions{}) assert.NoError(t, err) assert.Len(t, labels, len(expectedIssueIDs)) for i, label := range labels { - assert.EqualValues(t, expectedIssueIDs[i], label.ID) + assert.Equal(t, expectedIssueIDs[i], label.ID) } } testSuccess(3, "leastissues", []int64{3, 4}) @@ -229,10 +229,10 @@ func TestGetLabelsByOrgID(t *testing.T) { testSuccess(3, "reversealphabetically", []int64{4, 3}) testSuccess(3, "default", []int64{3, 4}) - _, err := issues_model.GetLabelsByOrgID(db.DefaultContext, 0, "leastissues", db.ListOptions{}) + _, err := issues_model.GetLabelsByOrgID(t.Context(), 0, "leastissues", db.ListOptions{}) assert.True(t, issues_model.IsErrOrgLabelNotExist(err)) - _, err = issues_model.GetLabelsByOrgID(db.DefaultContext, -1, "leastissues", db.ListOptions{}) + _, err = issues_model.GetLabelsByOrgID(t.Context(), -1, "leastissues", db.ListOptions{}) assert.True(t, issues_model.IsErrOrgLabelNotExist(err)) } @@ -240,15 +240,15 @@ func TestGetLabelsByOrgID(t *testing.T) { func TestGetLabelsByIssueID(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - labels, err := issues_model.GetLabelsByIssueID(db.DefaultContext, 1) + labels, err := issues_model.GetLabelsByIssueID(t.Context(), 1) assert.NoError(t, err) if assert.Len(t, labels, 1) { assert.EqualValues(t, 1, labels[0].ID) } - labels, err = issues_model.GetLabelsByIssueID(db.DefaultContext, unittest.NonexistentID) + labels, err = issues_model.GetLabelsByIssueID(t.Context(), unittest.NonexistentID) assert.NoError(t, err) - assert.Len(t, labels, 0) + assert.Empty(t, labels) } func TestUpdateLabel(t *testing.T) { @@ -265,34 +265,34 @@ func TestUpdateLabel(t *testing.T) { } label.Color = update.Color label.Name = update.Name - assert.NoError(t, issues_model.UpdateLabel(db.DefaultContext, update)) + assert.NoError(t, issues_model.UpdateLabel(t.Context(), update)) newLabel := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 1}) - assert.EqualValues(t, label.ID, newLabel.ID) - assert.EqualValues(t, label.Color, newLabel.Color) - assert.EqualValues(t, label.Name, newLabel.Name) - assert.EqualValues(t, label.Description, newLabel.Description) - assert.EqualValues(t, newLabel.ArchivedUnix, 0) + assert.Equal(t, label.ID, newLabel.ID) + assert.Equal(t, label.Color, newLabel.Color) + assert.Equal(t, label.Name, newLabel.Name) + assert.Equal(t, label.Description, newLabel.Description) + assert.EqualValues(t, 0, newLabel.ArchivedUnix) unittest.CheckConsistencyFor(t, &issues_model.Label{}, &repo_model.Repository{}) } func TestDeleteLabel(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) label := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 1}) - assert.NoError(t, issues_model.DeleteLabel(db.DefaultContext, label.RepoID, label.ID)) + assert.NoError(t, issues_model.DeleteLabel(t.Context(), label.RepoID, label.ID)) unittest.AssertNotExistsBean(t, &issues_model.Label{ID: label.ID, RepoID: label.RepoID}) - assert.NoError(t, issues_model.DeleteLabel(db.DefaultContext, label.RepoID, label.ID)) + assert.NoError(t, issues_model.DeleteLabel(t.Context(), label.RepoID, label.ID)) unittest.AssertNotExistsBean(t, &issues_model.Label{ID: label.ID}) - assert.NoError(t, issues_model.DeleteLabel(db.DefaultContext, unittest.NonexistentID, unittest.NonexistentID)) + assert.NoError(t, issues_model.DeleteLabel(t.Context(), unittest.NonexistentID, unittest.NonexistentID)) unittest.CheckConsistencyFor(t, &issues_model.Label{}, &repo_model.Repository{}) } func TestHasIssueLabel(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - assert.True(t, issues_model.HasIssueLabel(db.DefaultContext, 1, 1)) - assert.False(t, issues_model.HasIssueLabel(db.DefaultContext, 1, 2)) - assert.False(t, issues_model.HasIssueLabel(db.DefaultContext, unittest.NonexistentID, unittest.NonexistentID)) + assert.True(t, issues_model.HasIssueLabel(t.Context(), 1, 1)) + assert.False(t, issues_model.HasIssueLabel(t.Context(), 1, 2)) + assert.False(t, issues_model.HasIssueLabel(t.Context(), unittest.NonexistentID, unittest.NonexistentID)) } func TestNewIssueLabel(t *testing.T) { @@ -303,7 +303,7 @@ func TestNewIssueLabel(t *testing.T) { // add new IssueLabel prevNumIssues := label.NumIssues - assert.NoError(t, issues_model.NewIssueLabel(db.DefaultContext, issue, label, doer)) + assert.NoError(t, issues_model.NewIssueLabel(t.Context(), issue, label, doer)) unittest.AssertExistsAndLoadBean(t, &issues_model.IssueLabel{IssueID: issue.ID, LabelID: label.ID}) unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ Type: issues_model.CommentTypeLabel, @@ -313,10 +313,10 @@ func TestNewIssueLabel(t *testing.T) { Content: "1", }) label = unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 2}) - assert.EqualValues(t, prevNumIssues+1, label.NumIssues) + assert.Equal(t, prevNumIssues+1, label.NumIssues) // re-add existing IssueLabel - assert.NoError(t, issues_model.NewIssueLabel(db.DefaultContext, issue, label, doer)) + assert.NoError(t, issues_model.NewIssueLabel(t.Context(), issue, label, doer)) unittest.CheckConsistencyFor(t, &issues_model.Issue{}, &issues_model.Label{}) } @@ -330,19 +330,19 @@ func TestNewIssueExclusiveLabel(t *testing.T) { exclusiveLabelB := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 8}) // coexisting regular and exclusive label - assert.NoError(t, issues_model.NewIssueLabel(db.DefaultContext, issue, otherLabel, doer)) - assert.NoError(t, issues_model.NewIssueLabel(db.DefaultContext, issue, exclusiveLabelA, doer)) + assert.NoError(t, issues_model.NewIssueLabel(t.Context(), issue, otherLabel, doer)) + assert.NoError(t, issues_model.NewIssueLabel(t.Context(), issue, exclusiveLabelA, doer)) unittest.AssertExistsAndLoadBean(t, &issues_model.IssueLabel{IssueID: issue.ID, LabelID: otherLabel.ID}) unittest.AssertExistsAndLoadBean(t, &issues_model.IssueLabel{IssueID: issue.ID, LabelID: exclusiveLabelA.ID}) // exclusive label replaces existing one - assert.NoError(t, issues_model.NewIssueLabel(db.DefaultContext, issue, exclusiveLabelB, doer)) + assert.NoError(t, issues_model.NewIssueLabel(t.Context(), issue, exclusiveLabelB, doer)) unittest.AssertExistsAndLoadBean(t, &issues_model.IssueLabel{IssueID: issue.ID, LabelID: otherLabel.ID}) unittest.AssertExistsAndLoadBean(t, &issues_model.IssueLabel{IssueID: issue.ID, LabelID: exclusiveLabelB.ID}) unittest.AssertNotExistsBean(t, &issues_model.IssueLabel{IssueID: issue.ID, LabelID: exclusiveLabelA.ID}) // exclusive label replaces existing one again - assert.NoError(t, issues_model.NewIssueLabel(db.DefaultContext, issue, exclusiveLabelA, doer)) + assert.NoError(t, issues_model.NewIssueLabel(t.Context(), issue, exclusiveLabelA, doer)) unittest.AssertExistsAndLoadBean(t, &issues_model.IssueLabel{IssueID: issue.ID, LabelID: otherLabel.ID}) unittest.AssertExistsAndLoadBean(t, &issues_model.IssueLabel{IssueID: issue.ID, LabelID: exclusiveLabelA.ID}) unittest.AssertNotExistsBean(t, &issues_model.IssueLabel{IssueID: issue.ID, LabelID: exclusiveLabelB.ID}) @@ -355,7 +355,7 @@ func TestNewIssueLabels(t *testing.T) { issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 5}) doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) - assert.NoError(t, issues_model.NewIssueLabels(db.DefaultContext, issue, []*issues_model.Label{label1, label2}, doer)) + assert.NoError(t, issues_model.NewIssueLabels(t.Context(), issue, []*issues_model.Label{label1, label2}, doer)) unittest.AssertExistsAndLoadBean(t, &issues_model.IssueLabel{IssueID: issue.ID, LabelID: label1.ID}) unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ Type: issues_model.CommentTypeLabel, @@ -366,14 +366,14 @@ func TestNewIssueLabels(t *testing.T) { }) unittest.AssertExistsAndLoadBean(t, &issues_model.IssueLabel{IssueID: issue.ID, LabelID: label1.ID}) label1 = unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 1}) - assert.EqualValues(t, 3, label1.NumIssues) - assert.EqualValues(t, 1, label1.NumClosedIssues) + assert.Equal(t, 3, label1.NumIssues) + assert.Equal(t, 1, label1.NumClosedIssues) label2 = unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 2}) - assert.EqualValues(t, 1, label2.NumIssues) - assert.EqualValues(t, 1, label2.NumClosedIssues) + assert.Equal(t, 1, label2.NumIssues) + assert.Equal(t, 1, label2.NumClosedIssues) // corner case: test empty slice - assert.NoError(t, issues_model.NewIssueLabels(db.DefaultContext, issue, []*issues_model.Label{}, doer)) + assert.NoError(t, issues_model.NewIssueLabels(t.Context(), issue, []*issues_model.Label{}, doer)) unittest.CheckConsistencyFor(t, &issues_model.Issue{}, &issues_model.Label{}) } @@ -387,14 +387,14 @@ func TestDeleteIssueLabel(t *testing.T) { expectedNumIssues := label.NumIssues expectedNumClosedIssues := label.NumClosedIssues - if unittest.BeanExists(t, &issues_model.IssueLabel{IssueID: issueID, LabelID: labelID}) { + if unittest.GetBean(t, &issues_model.IssueLabel{IssueID: issueID, LabelID: labelID}) != nil { expectedNumIssues-- if issue.IsClosed { expectedNumClosedIssues-- } } - ctx, committer, err := db.TxContext(db.DefaultContext) + ctx, committer, err := db.TxContext(t.Context()) defer committer.Close() assert.NoError(t, err) assert.NoError(t, issues_model.DeleteIssueLabel(ctx, issue, label, doer)) @@ -408,8 +408,8 @@ func TestDeleteIssueLabel(t *testing.T) { LabelID: labelID, }, `content=''`) label = unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: labelID}) - assert.EqualValues(t, expectedNumIssues, label.NumIssues) - assert.EqualValues(t, expectedNumClosedIssues, label.NumClosedIssues) + assert.Equal(t, expectedNumIssues, label.NumIssues) + assert.Equal(t, expectedNumClosedIssues, label.NumClosedIssues) } testSuccess(1, 1, 2) testSuccess(2, 5, 2) diff --git a/models/issues/milestone.go b/models/issues/milestone.go index 4c9bae58f7d..373f39f4ffe 100644 --- a/models/issues/milestone.go +++ b/models/issues/milestone.go @@ -105,22 +105,16 @@ func (m *Milestone) State() api.StateType { // NewMilestone creates new milestone of repository. func NewMilestone(ctx context.Context, m *Milestone) (err error) { - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return err - } - defer committer.Close() + return db.WithTx(ctx, func(ctx context.Context) error { + m.Name = strings.TrimSpace(m.Name) - m.Name = strings.TrimSpace(m.Name) + if err = db.Insert(ctx, m); err != nil { + return err + } - if err = db.Insert(ctx, m); err != nil { + _, err = db.Exec(ctx, "UPDATE `repository` SET num_milestones = num_milestones + 1 WHERE id = ?", m.RepoID) return err - } - - if _, err = db.Exec(ctx, "UPDATE `repository` SET num_milestones = num_milestones + 1 WHERE id = ?", m.RepoID); err != nil { - return err - } - return committer.Commit() + }) } // HasMilestoneByRepoID returns if the milestone exists in the repository. @@ -155,28 +149,23 @@ func GetMilestoneByRepoIDANDName(ctx context.Context, repoID int64, name string) // UpdateMilestone updates information of given milestone. func UpdateMilestone(ctx context.Context, m *Milestone, oldIsClosed bool) error { - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return err - } - defer committer.Close() + return db.WithTx(ctx, func(ctx context.Context) error { + if m.IsClosed && !oldIsClosed { + m.ClosedDateUnix = timeutil.TimeStampNow() + } - if m.IsClosed && !oldIsClosed { - m.ClosedDateUnix = timeutil.TimeStampNow() - } - - if err := updateMilestone(ctx, m); err != nil { - return err - } - - // if IsClosed changed, update milestone numbers of repository - if oldIsClosed != m.IsClosed { - if err := updateRepoMilestoneNum(ctx, m.RepoID); err != nil { + if err := updateMilestone(ctx, m); err != nil { return err } - } - return committer.Commit() + // if IsClosed changed, update milestone numbers of repository + if oldIsClosed != m.IsClosed { + if err := updateRepoMilestoneNum(ctx, m.RepoID); err != nil { + return err + } + } + return nil + }) } func updateMilestone(ctx context.Context, m *Milestone) error { @@ -213,44 +202,28 @@ func UpdateMilestoneCounters(ctx context.Context, id int64) error { // ChangeMilestoneStatusByRepoIDAndID changes a milestone open/closed status if the milestone ID is in the repo. func ChangeMilestoneStatusByRepoIDAndID(ctx context.Context, repoID, milestoneID int64, isClosed bool) error { - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return err - } - defer committer.Close() + return db.WithTx(ctx, func(ctx context.Context) error { + m := &Milestone{ + ID: milestoneID, + RepoID: repoID, + } - m := &Milestone{ - ID: milestoneID, - RepoID: repoID, - } + has, err := db.GetEngine(ctx).ID(milestoneID).Where("repo_id = ?", repoID).Get(m) + if err != nil { + return err + } else if !has { + return ErrMilestoneNotExist{ID: milestoneID, RepoID: repoID} + } - has, err := db.GetEngine(ctx).ID(milestoneID).Where("repo_id = ?", repoID).Get(m) - if err != nil { - return err - } else if !has { - return ErrMilestoneNotExist{ID: milestoneID, RepoID: repoID} - } - - if err := changeMilestoneStatus(ctx, m, isClosed); err != nil { - return err - } - - return committer.Commit() + return changeMilestoneStatus(ctx, m, isClosed) + }) } // ChangeMilestoneStatus changes the milestone open/closed status. func ChangeMilestoneStatus(ctx context.Context, m *Milestone, isClosed bool) (err error) { - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return err - } - defer committer.Close() - - if err := changeMilestoneStatus(ctx, m, isClosed); err != nil { - return err - } - - return committer.Commit() + return db.WithTx(ctx, func(ctx context.Context) error { + return changeMilestoneStatus(ctx, m, isClosed) + }) } func changeMilestoneStatus(ctx context.Context, m *Milestone, isClosed bool) error { @@ -284,40 +257,34 @@ func DeleteMilestoneByRepoID(ctx context.Context, repoID, id int64) error { return err } - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return err - } - defer committer.Close() + return db.WithTx(ctx, func(ctx context.Context) error { + if _, err = db.DeleteByID[Milestone](ctx, m.ID); err != nil { + return err + } - if _, err = db.DeleteByID[Milestone](ctx, m.ID); err != nil { - return err - } + numMilestones, err := db.Count[Milestone](ctx, FindMilestoneOptions{ + RepoID: repo.ID, + }) + if err != nil { + return err + } + numClosedMilestones, err := db.Count[Milestone](ctx, FindMilestoneOptions{ + RepoID: repo.ID, + IsClosed: optional.Some(true), + }) + if err != nil { + return err + } + repo.NumMilestones = int(numMilestones) + repo.NumClosedMilestones = int(numClosedMilestones) - numMilestones, err := db.Count[Milestone](ctx, FindMilestoneOptions{ - RepoID: repo.ID, + if _, err = db.GetEngine(ctx).ID(repo.ID).Cols("num_milestones, num_closed_milestones").Update(repo); err != nil { + return err + } + + _, err = db.Exec(ctx, "UPDATE `issue` SET milestone_id = 0 WHERE milestone_id = ?", m.ID) + return err }) - if err != nil { - return err - } - numClosedMilestones, err := db.Count[Milestone](ctx, FindMilestoneOptions{ - RepoID: repo.ID, - IsClosed: optional.Some(true), - }) - if err != nil { - return err - } - repo.NumMilestones = int(numMilestones) - repo.NumClosedMilestones = int(numClosedMilestones) - - if _, err = db.GetEngine(ctx).ID(repo.ID).Cols("num_milestones, num_closed_milestones").Update(repo); err != nil { - return err - } - - if _, err = db.Exec(ctx, "UPDATE `issue` SET milestone_id = 0 WHERE milestone_id = ?", m.ID); err != nil { - return err - } - return committer.Commit() } func updateRepoMilestoneNum(ctx context.Context, repoID int64) error { @@ -360,22 +327,15 @@ func InsertMilestones(ctx context.Context, ms ...*Milestone) (err error) { return nil } - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return err - } - defer committer.Close() - sess := db.GetEngine(ctx) - - // to return the id, so we should not use batch insert - for _, m := range ms { - if _, err = sess.NoAutoTime().Insert(m); err != nil { - return err + return db.WithTx(ctx, func(ctx context.Context) error { + // to return the id, so we should not use batch insert + for _, m := range ms { + if _, err = db.GetEngine(ctx).NoAutoTime().Insert(m); err != nil { + return err + } } - } - if _, err = db.Exec(ctx, "UPDATE `repository` SET num_milestones = num_milestones + ? WHERE id = ?", len(ms), ms[0].RepoID); err != nil { + _, err = db.Exec(ctx, "UPDATE `repository` SET num_milestones = num_milestones + ? WHERE id = ?", len(ms), ms[0].RepoID) return err - } - return committer.Commit() + }) } diff --git a/models/issues/milestone_test.go b/models/issues/milestone_test.go index e5f6f15ca2a..107ad305d48 100644 --- a/models/issues/milestone_test.go +++ b/models/issues/milestone_test.go @@ -27,12 +27,12 @@ func TestMilestone_State(t *testing.T) { func TestGetMilestoneByRepoID(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - milestone, err := issues_model.GetMilestoneByRepoID(db.DefaultContext, 1, 1) + milestone, err := issues_model.GetMilestoneByRepoID(t.Context(), 1, 1) assert.NoError(t, err) assert.EqualValues(t, 1, milestone.ID) assert.EqualValues(t, 1, milestone.RepoID) - _, err = issues_model.GetMilestoneByRepoID(db.DefaultContext, unittest.NonexistentID, unittest.NonexistentID) + _, err = issues_model.GetMilestoneByRepoID(t.Context(), unittest.NonexistentID, unittest.NonexistentID) assert.True(t, issues_model.IsErrMilestoneNotExist(err)) } @@ -45,7 +45,7 @@ func TestGetMilestonesByRepoID(t *testing.T) { isClosed = optional.Some(state == api.StateClosed) } repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID}) - milestones, err := db.Find[issues_model.Milestone](db.DefaultContext, issues_model.FindMilestoneOptions{ + milestones, err := db.Find[issues_model.Milestone](t.Context(), issues_model.FindMilestoneOptions{ RepoID: repo.ID, IsClosed: isClosed, }) @@ -69,7 +69,7 @@ func TestGetMilestonesByRepoID(t *testing.T) { assert.Len(t, milestones, n) for _, milestone := range milestones { - assert.EqualValues(t, repoID, milestone.RepoID) + assert.Equal(t, repoID, milestone.RepoID) } } test(1, api.StateOpen) @@ -82,12 +82,12 @@ func TestGetMilestonesByRepoID(t *testing.T) { test(3, api.StateClosed) test(3, api.StateAll) - milestones, err := db.Find[issues_model.Milestone](db.DefaultContext, issues_model.FindMilestoneOptions{ + milestones, err := db.Find[issues_model.Milestone](t.Context(), issues_model.FindMilestoneOptions{ RepoID: unittest.NonexistentID, IsClosed: optional.Some(false), }) assert.NoError(t, err) - assert.Len(t, milestones, 0) + assert.Empty(t, milestones) } func TestGetMilestones(t *testing.T) { @@ -95,7 +95,7 @@ func TestGetMilestones(t *testing.T) { repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) test := func(sortType string, sortCond func(*issues_model.Milestone) int) { for _, page := range []int{0, 1} { - milestones, err := db.Find[issues_model.Milestone](db.DefaultContext, issues_model.FindMilestoneOptions{ + milestones, err := db.Find[issues_model.Milestone](t.Context(), issues_model.FindMilestoneOptions{ ListOptions: db.ListOptions{ Page: page, PageSize: setting.UI.IssuePagingNum, @@ -112,7 +112,7 @@ func TestGetMilestones(t *testing.T) { } assert.True(t, sort.IntsAreSorted(values)) - milestones, err = db.Find[issues_model.Milestone](db.DefaultContext, issues_model.FindMilestoneOptions{ + milestones, err = db.Find[issues_model.Milestone](t.Context(), issues_model.FindMilestoneOptions{ ListOptions: db.ListOptions{ Page: page, PageSize: setting.UI.IssuePagingNum, @@ -155,7 +155,7 @@ func TestCountRepoMilestones(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) test := func(repoID int64) { repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID}) - count, err := db.Count[issues_model.Milestone](db.DefaultContext, issues_model.FindMilestoneOptions{ + count, err := db.Count[issues_model.Milestone](t.Context(), issues_model.FindMilestoneOptions{ RepoID: repoID, }) assert.NoError(t, err) @@ -165,7 +165,7 @@ func TestCountRepoMilestones(t *testing.T) { test(2) test(3) - count, err := db.Count[issues_model.Milestone](db.DefaultContext, issues_model.FindMilestoneOptions{ + count, err := db.Count[issues_model.Milestone](t.Context(), issues_model.FindMilestoneOptions{ RepoID: unittest.NonexistentID, }) assert.NoError(t, err) @@ -176,7 +176,7 @@ func TestCountRepoClosedMilestones(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) test := func(repoID int64) { repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID}) - count, err := db.Count[issues_model.Milestone](db.DefaultContext, issues_model.FindMilestoneOptions{ + count, err := db.Count[issues_model.Milestone](t.Context(), issues_model.FindMilestoneOptions{ RepoID: repoID, IsClosed: optional.Some(true), }) @@ -187,7 +187,7 @@ func TestCountRepoClosedMilestones(t *testing.T) { test(2) test(3) - count, err := db.Count[issues_model.Milestone](db.DefaultContext, issues_model.FindMilestoneOptions{ + count, err := db.Count[issues_model.Milestone](t.Context(), issues_model.FindMilestoneOptions{ RepoID: unittest.NonexistentID, IsClosed: optional.Some(true), }) @@ -204,7 +204,7 @@ func TestCountMilestonesByRepoIDs(t *testing.T) { repo1OpenCount, repo1ClosedCount := milestonesCount(1) repo2OpenCount, repo2ClosedCount := milestonesCount(2) - openCounts, err := issues_model.CountMilestonesMap(db.DefaultContext, issues_model.FindMilestoneOptions{ + openCounts, err := issues_model.CountMilestonesMap(t.Context(), issues_model.FindMilestoneOptions{ RepoIDs: []int64{1, 2}, IsClosed: optional.Some(false), }) @@ -212,7 +212,7 @@ func TestCountMilestonesByRepoIDs(t *testing.T) { assert.EqualValues(t, repo1OpenCount, openCounts[1]) assert.EqualValues(t, repo2OpenCount, openCounts[2]) - closedCounts, err := issues_model.CountMilestonesMap(db.DefaultContext, + closedCounts, err := issues_model.CountMilestonesMap(t.Context(), issues_model.FindMilestoneOptions{ RepoIDs: []int64{1, 2}, IsClosed: optional.Some(true), @@ -228,7 +228,7 @@ func TestGetMilestonesByRepoIDs(t *testing.T) { repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}) test := func(sortType string, sortCond func(*issues_model.Milestone) int) { for _, page := range []int{0, 1} { - openMilestones, err := db.Find[issues_model.Milestone](db.DefaultContext, issues_model.FindMilestoneOptions{ + openMilestones, err := db.Find[issues_model.Milestone](t.Context(), issues_model.FindMilestoneOptions{ ListOptions: db.ListOptions{ Page: page, PageSize: setting.UI.IssuePagingNum, @@ -245,7 +245,7 @@ func TestGetMilestonesByRepoIDs(t *testing.T) { } assert.True(t, sort.IntsAreSorted(values)) - closedMilestones, err := db.Find[issues_model.Milestone](db.DefaultContext, + closedMilestones, err := db.Find[issues_model.Milestone](t.Context(), issues_model.FindMilestoneOptions{ ListOptions: db.ListOptions{ Page: page, @@ -292,7 +292,7 @@ func TestNewMilestone(t *testing.T) { Content: "milestoneContent", } - assert.NoError(t, issues_model.NewMilestone(db.DefaultContext, milestone)) + assert.NoError(t, issues_model.NewMilestone(t.Context(), milestone)) unittest.AssertExistsAndLoadBean(t, milestone) unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: milestone.RepoID}, &issues_model.Milestone{}) } @@ -301,22 +301,22 @@ func TestChangeMilestoneStatus(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) milestone := unittest.AssertExistsAndLoadBean(t, &issues_model.Milestone{ID: 1}) - assert.NoError(t, issues_model.ChangeMilestoneStatus(db.DefaultContext, milestone, true)) + assert.NoError(t, issues_model.ChangeMilestoneStatus(t.Context(), milestone, true)) unittest.AssertExistsAndLoadBean(t, &issues_model.Milestone{ID: 1}, "is_closed=1") unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: milestone.RepoID}, &issues_model.Milestone{}) - assert.NoError(t, issues_model.ChangeMilestoneStatus(db.DefaultContext, milestone, false)) + assert.NoError(t, issues_model.ChangeMilestoneStatus(t.Context(), milestone, false)) unittest.AssertExistsAndLoadBean(t, &issues_model.Milestone{ID: 1}, "is_closed=0") unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: milestone.RepoID}, &issues_model.Milestone{}) } func TestDeleteMilestoneByRepoID(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - assert.NoError(t, issues_model.DeleteMilestoneByRepoID(db.DefaultContext, 1, 1)) + assert.NoError(t, issues_model.DeleteMilestoneByRepoID(t.Context(), 1, 1)) unittest.AssertNotExistsBean(t, &issues_model.Milestone{ID: 1}) unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: 1}) - assert.NoError(t, issues_model.DeleteMilestoneByRepoID(db.DefaultContext, unittest.NonexistentID, unittest.NonexistentID)) + assert.NoError(t, issues_model.DeleteMilestoneByRepoID(t.Context(), unittest.NonexistentID, unittest.NonexistentID)) } func TestUpdateMilestone(t *testing.T) { @@ -325,9 +325,9 @@ func TestUpdateMilestone(t *testing.T) { milestone := unittest.AssertExistsAndLoadBean(t, &issues_model.Milestone{ID: 1}) milestone.Name = " newMilestoneName " milestone.Content = "newMilestoneContent" - assert.NoError(t, issues_model.UpdateMilestone(db.DefaultContext, milestone, milestone.IsClosed)) + assert.NoError(t, issues_model.UpdateMilestone(t.Context(), milestone, milestone.IsClosed)) milestone = unittest.AssertExistsAndLoadBean(t, &issues_model.Milestone{ID: 1}) - assert.EqualValues(t, "newMilestoneName", milestone.Name) + assert.Equal(t, "newMilestoneName", milestone.Name) unittest.CheckConsistencyFor(t, &issues_model.Milestone{}) } @@ -338,16 +338,16 @@ func TestUpdateMilestoneCounters(t *testing.T) { issue.IsClosed = true issue.ClosedUnix = timeutil.TimeStampNow() - _, err := db.GetEngine(db.DefaultContext).ID(issue.ID).Cols("is_closed", "closed_unix").Update(issue) + _, err := db.GetEngine(t.Context()).ID(issue.ID).Cols("is_closed", "closed_unix").Update(issue) assert.NoError(t, err) - assert.NoError(t, issues_model.UpdateMilestoneCounters(db.DefaultContext, issue.MilestoneID)) + assert.NoError(t, issues_model.UpdateMilestoneCounters(t.Context(), issue.MilestoneID)) unittest.CheckConsistencyFor(t, &issues_model.Milestone{}) issue.IsClosed = false issue.ClosedUnix = 0 - _, err = db.GetEngine(db.DefaultContext).ID(issue.ID).Cols("is_closed", "closed_unix").Update(issue) + _, err = db.GetEngine(t.Context()).ID(issue.ID).Cols("is_closed", "closed_unix").Update(issue) assert.NoError(t, err) - assert.NoError(t, issues_model.UpdateMilestoneCounters(db.DefaultContext, issue.MilestoneID)) + assert.NoError(t, issues_model.UpdateMilestoneCounters(t.Context(), issue.MilestoneID)) unittest.CheckConsistencyFor(t, &issues_model.Milestone{}) } @@ -360,11 +360,11 @@ func TestMigrate_InsertMilestones(t *testing.T) { RepoID: repo.ID, Name: name, } - err := issues_model.InsertMilestones(db.DefaultContext, ms) + err := issues_model.InsertMilestones(t.Context(), ms) assert.NoError(t, err) unittest.AssertExistsAndLoadBean(t, ms) repoModified := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repo.ID}) - assert.EqualValues(t, repo.NumMilestones+1, repoModified.NumMilestones) + assert.Equal(t, repo.NumMilestones+1, repoModified.NumMilestones) unittest.CheckConsistencyFor(t, &issues_model.Milestone{}) } diff --git a/models/issues/pull.go b/models/issues/pull.go index 853e2a69e62..7a37b627e1b 100644 --- a/models/issues/pull.go +++ b/models/issues/pull.go @@ -6,10 +6,10 @@ package issues import ( "context" + "errors" "fmt" "io" "regexp" - "strconv" "strings" "code.gitea.io/gitea/models/db" @@ -29,6 +29,8 @@ import ( var ErrMustCollaborator = util.NewPermissionDeniedErrorf("user must be a collaborator") +const reviewedBy = "Reviewed-by: " + // ErrPullRequestNotExist represents a "PullRequestNotExist" kind of error. type ErrPullRequestNotExist struct { ID int64 @@ -80,22 +82,6 @@ func (err ErrPullRequestAlreadyExists) Unwrap() error { return util.ErrAlreadyExist } -// ErrPullWasClosed is used close a closed pull request -type ErrPullWasClosed struct { - ID int64 - Index int64 -} - -// IsErrPullWasClosed checks if an error is a ErrErrPullWasClosed. -func IsErrPullWasClosed(err error) bool { - _, ok := err.(ErrPullWasClosed) - return ok -} - -func (err ErrPullWasClosed) Error() string { - return fmt.Sprintf("Pull request [%d] %d was already closed", err.ID, err.Index) -} - // PullRequestType defines pull request type type PullRequestType int @@ -119,27 +105,6 @@ const ( PullRequestStatusAncestor ) -func (status PullRequestStatus) String() string { - switch status { - case PullRequestStatusConflict: - return "CONFLICT" - case PullRequestStatusChecking: - return "CHECKING" - case PullRequestStatusMergeable: - return "MERGEABLE" - case PullRequestStatusManuallyMerged: - return "MANUALLY_MERGED" - case PullRequestStatusError: - return "ERROR" - case PullRequestStatusEmpty: - return "EMPTY" - case PullRequestStatusAncestor: - return "ANCESTOR" - default: - return strconv.Itoa(int(status)) - } -} - // PullRequestFlow the flow of pull request type PullRequestFlow int @@ -301,7 +266,7 @@ func (pr *PullRequest) LoadRequestedReviewers(ctx context.Context) error { return nil } - reviews, err := GetReviewsByIssueID(ctx, pr.Issue.ID) + reviews, _, err := GetReviewsByIssueID(ctx, pr.Issue.ID) if err != nil { return err } @@ -320,7 +285,7 @@ func (pr *PullRequest) LoadRequestedReviewers(ctx context.Context) error { // LoadRequestedReviewersTeams loads the requested reviewers teams. func (pr *PullRequest) LoadRequestedReviewersTeams(ctx context.Context) error { - reviews, err := GetReviewsByIssueID(ctx, pr.Issue.ID) + reviews, _, err := GetReviewsByIssueID(ctx, pr.Issue.ID) if err != nil { return err } @@ -385,7 +350,11 @@ type ReviewCount struct { func (pr *PullRequest) GetApprovalCounts(ctx context.Context) ([]*ReviewCount, error) { rCounts := make([]*ReviewCount, 0, 6) sess := db.GetEngine(ctx).Where("issue_id = ?", pr.IssueID) - return rCounts, sess.Select("issue_id, type, count(id) as `count`").Where("official = ? AND dismissed = ?", true, false).GroupBy("issue_id, type").Table("review").Find(&rCounts) + return rCounts, sess.Select("issue_id, type, count(id) as `count`"). + Where(builder.Eq{"official": true, "dismissed": false}). + GroupBy("issue_id, type"). + Table("review"). + Find(&rCounts) } // GetApprovers returns the approvers of the pull request @@ -401,17 +370,10 @@ func (pr *PullRequest) GetApprovers(ctx context.Context) string { func (pr *PullRequest) getReviewedByLines(ctx context.Context, writer io.Writer) error { maxReviewers := setting.Repository.PullRequest.DefaultMergeMessageMaxApprovers - if maxReviewers == 0 { return nil } - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return err - } - defer committer.Close() - // Note: This doesn't page as we only expect a very limited number of reviews reviews, err := FindLatestReviews(ctx, FindReviewOptions{ Types: []ReviewType{ReviewTypeApprove}, @@ -436,7 +398,7 @@ func (pr *PullRequest) getReviewedByLines(ctx context.Context, writer io.Writer) } else if review.Reviewer == nil { continue } - if _, err := writer.Write([]byte("Reviewed-by: ")); err != nil { + if _, err := writer.Write([]byte(reviewedBy)); err != nil { return err } if _, err := writer.Write([]byte(review.Reviewer.NewGitSig().String())); err != nil { @@ -447,11 +409,11 @@ func (pr *PullRequest) getReviewedByLines(ctx context.Context, writer io.Writer) } reviewersWritten++ } - return committer.Commit() + return nil } -// GetGitRefName returns git ref for hidden pull request branch -func (pr *PullRequest) GetGitRefName() string { +// GetGitHeadRefName returns git ref for hidden pull request branch +func (pr *PullRequest) GetGitHeadRefName() string { return fmt.Sprintf("%s%d/head", git.PullPrefix, pr.Index) } @@ -499,106 +461,38 @@ func (pr *PullRequest) IsFromFork() bool { return pr.HeadRepoID != pr.BaseRepoID } -// SetMerged sets a pull request to merged and closes the corresponding issue -func (pr *PullRequest) SetMerged(ctx context.Context) (bool, error) { - if pr.HasMerged { - return false, fmt.Errorf("PullRequest[%d] already merged", pr.Index) - } - if pr.MergedCommitID == "" || pr.MergedUnix == 0 || pr.Merger == nil { - return false, fmt.Errorf("Unable to merge PullRequest[%d], some required fields are empty", pr.Index) - } - - pr.HasMerged = true - sess := db.GetEngine(ctx) - - if _, err := sess.Exec("UPDATE `issue` SET `repo_id` = `repo_id` WHERE `id` = ?", pr.IssueID); err != nil { - return false, err - } - - if _, err := sess.Exec("UPDATE `pull_request` SET `issue_id` = `issue_id` WHERE `id` = ?", pr.ID); err != nil { - return false, err - } - - pr.Issue = nil - if err := pr.LoadIssue(ctx); err != nil { - return false, err - } - - if tmpPr, err := GetPullRequestByID(ctx, pr.ID); err != nil { - return false, err - } else if tmpPr.HasMerged { - if pr.Issue.IsClosed { - return false, nil - } - return false, fmt.Errorf("PullRequest[%d] already merged but it's associated issue [%d] is not closed", pr.Index, pr.IssueID) - } else if pr.Issue.IsClosed { - return false, fmt.Errorf("PullRequest[%d] already closed", pr.Index) - } - - if err := pr.Issue.LoadRepo(ctx); err != nil { - return false, err - } - - if err := pr.Issue.Repo.LoadOwner(ctx); err != nil { - return false, err - } - - if _, err := changeIssueStatus(ctx, pr.Issue, pr.Merger, true, true); err != nil { - return false, fmt.Errorf("Issue.changeStatus: %w", err) - } - - // reset the conflicted files as there cannot be any if we're merged - pr.ConflictedFiles = []string{} - - // We need to save all of the data used to compute this merge as it may have already been changed by TestPatch. FIXME: need to set some state to prevent TestPatch from running whilst we are merging. - if _, err := sess.Where("id = ?", pr.ID).Cols("has_merged, status, merge_base, merged_commit_id, merger_id, merged_unix, conflicted_files").Update(pr); err != nil { - return false, fmt.Errorf("Failed to update pr[%d]: %w", pr.ID, err) - } - - return true, nil -} - // NewPullRequest creates new pull request with labels for repository. func NewPullRequest(ctx context.Context, repo *repo_model.Repository, issue *Issue, labelIDs []int64, uuids []string, pr *PullRequest) (err error) { - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return err - } - defer committer.Close() - - idx, err := db.GetNextResourceIndex(ctx, "issue_index", repo.ID) - if err != nil { - return fmt.Errorf("generate pull request index failed: %w", err) - } - - issue.Index = idx - issue.Title, _ = util.SplitStringAtByteN(issue.Title, 255) - - if err = NewIssueWithIndex(ctx, issue.Poster, NewIssueOptions{ - Repo: repo, - Issue: issue, - LabelIDs: labelIDs, - Attachments: uuids, - IsPull: true, - }); err != nil { - if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) || IsErrNewIssueInsert(err) { - return err + return db.WithTx(ctx, func(ctx context.Context) error { + idx, err := db.GetNextResourceIndex(ctx, "issue_index", repo.ID) + if err != nil { + return fmt.Errorf("generate pull request index failed: %w", err) } - return fmt.Errorf("newIssue: %w", err) - } - pr.Index = issue.Index - pr.BaseRepo = repo - pr.IssueID = issue.ID - if err = db.Insert(ctx, pr); err != nil { - return fmt.Errorf("insert pull repo: %w", err) - } + issue.Index = idx + issue.Title = util.EllipsisDisplayString(issue.Title, 255) - if err = committer.Commit(); err != nil { - return fmt.Errorf("Commit: %w", err) - } + if err = NewIssueWithIndex(ctx, issue.Poster, NewIssueOptions{ + Repo: repo, + Issue: issue, + LabelIDs: labelIDs, + Attachments: uuids, + IsPull: true, + }); err != nil { + if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) || IsErrNewIssueInsert(err) { + return err + } + return fmt.Errorf("newIssue: %w", err) + } - return nil + pr.Index = issue.Index + pr.BaseRepo = repo + pr.IssueID = issue.ID + if err = db.Insert(ctx, pr); err != nil { + return fmt.Errorf("insert pull repo: %w", err) + } + return nil + }) } // ErrUserMustCollaborator represents an error that the user must be a collaborator to a given repo. @@ -745,12 +639,6 @@ func GetAllUnmergedAgitPullRequestByPoster(ctx context.Context, uid int64) ([]*P return pulls, err } -// Update updates all fields of pull request. -func (pr *PullRequest) Update(ctx context.Context) error { - _, err := db.GetEngine(ctx).ID(pr.ID).AllCols().Update(pr) - return err -} - // UpdateCols updates specific fields of pull request. func (pr *PullRequest) UpdateCols(ctx context.Context, cols ...string) error { _, err := db.GetEngine(ctx).ID(pr.ID).Cols(cols...).Update(pr) @@ -807,7 +695,7 @@ func (pr *PullRequest) GetWorkInProgressPrefix(ctx context.Context) string { // UpdateCommitDivergence update Divergence of a pull request func (pr *PullRequest) UpdateCommitDivergence(ctx context.Context, ahead, behind int) error { if pr.ID == 0 { - return fmt.Errorf("pull ID is 0") + return errors.New("pull ID is 0") } pr.CommitsAhead = ahead pr.CommitsBehind = behind @@ -1000,7 +888,7 @@ func ParseCodeOwnersLine(ctx context.Context, tokens []string) (*CodeOwnerRule, if strings.Contains(user, "/") { s := strings.Split(user, "/") if len(s) != 2 { - warnings = append(warnings, fmt.Sprintf("incorrect codeowner group: %s", user)) + warnings = append(warnings, "incorrect codeowner group: "+user) continue } orgName := s[0] @@ -1008,12 +896,12 @@ func ParseCodeOwnersLine(ctx context.Context, tokens []string) (*CodeOwnerRule, org, err := org_model.GetOrgByName(ctx, orgName) if err != nil { - warnings = append(warnings, fmt.Sprintf("incorrect codeowner organization: %s", user)) + warnings = append(warnings, "incorrect codeowner organization: "+user) continue } teams, err := org.LoadTeams(ctx) if err != nil { - warnings = append(warnings, fmt.Sprintf("incorrect codeowner team: %s", user)) + warnings = append(warnings, "incorrect codeowner team: "+user) continue } @@ -1025,7 +913,7 @@ func ParseCodeOwnersLine(ctx context.Context, tokens []string) (*CodeOwnerRule, } else { u, err := user_model.GetUserByName(ctx, user) if err != nil { - warnings = append(warnings, fmt.Sprintf("incorrect codeowner user: %s", user)) + warnings = append(warnings, "incorrect codeowner user: "+user) continue } rule.Users = append(rule.Users, u) @@ -1079,22 +967,18 @@ func TokenizeCodeOwnersLine(line string) []string { // InsertPullRequests inserted pull requests func InsertPullRequests(ctx context.Context, prs ...*PullRequest) error { - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return err - } - defer committer.Close() - sess := db.GetEngine(ctx) - for _, pr := range prs { - if err := insertIssue(ctx, pr.Issue); err != nil { - return err + return db.WithTx(ctx, func(ctx context.Context) error { + for _, pr := range prs { + if err := insertIssue(ctx, pr.Issue); err != nil { + return err + } + pr.IssueID = pr.Issue.ID + if _, err := db.GetEngine(ctx).NoAutoTime().Insert(pr); err != nil { + return err + } } - pr.IssueID = pr.Issue.ID - if _, err := sess.NoAutoTime().Insert(pr); err != nil { - return err - } - } - return committer.Commit() + return nil + }) } // GetPullRequestByMergedCommit returns a merged pull request by the given commit diff --git a/models/issues/pull_list.go b/models/issues/pull_list.go index 9155ea08346..84f9f6166d1 100644 --- a/models/issues/pull_list.go +++ b/models/issues/pull_list.go @@ -16,6 +16,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/util" + "xorm.io/builder" "xorm.io/xorm" ) @@ -27,11 +28,16 @@ type PullRequestsOptions struct { Labels []int64 MilestoneID int64 PosterID int64 + BaseBranch string } func listPullRequestStatement(ctx context.Context, baseRepoID int64, opts *PullRequestsOptions) *xorm.Session { sess := db.GetEngine(ctx).Where("pull_request.base_repo_id=?", baseRepoID) + if opts.BaseBranch != "" { + sess.And("pull_request.base_branch=?", opts.BaseBranch) + } + sess.Join("INNER", "issue", "pull_request.issue_id = issue.id") switch opts.State { case "closed", "open": @@ -55,7 +61,7 @@ func listPullRequestStatement(ctx context.Context, baseRepoID int64, opts *PullR } // GetUnmergedPullRequestsByHeadInfo returns all pull requests that are open and has not been merged -func GetUnmergedPullRequestsByHeadInfo(ctx context.Context, repoID int64, branch string) ([]*PullRequest, error) { +func GetUnmergedPullRequestsByHeadInfo(ctx context.Context, repoID int64, branch string) (PullRequestList, error) { prs := make([]*PullRequest, 0, 2) sess := db.GetEngine(ctx). Join("INNER", "issue", "issue.id = pull_request.issue_id"). @@ -110,7 +116,7 @@ func HasUnmergedPullRequestsByHeadInfo(ctx context.Context, repoID int64, branch // GetUnmergedPullRequestsByBaseInfo returns all pull requests that are open and has not been merged // by given base information (repo and branch). -func GetUnmergedPullRequestsByBaseInfo(ctx context.Context, repoID int64, branch string) ([]*PullRequest, error) { +func GetUnmergedPullRequestsByBaseInfo(ctx context.Context, repoID int64, branch string) (PullRequestList, error) { prs := make([]*PullRequest, 0, 2) return prs, db.GetEngine(ctx). Where("base_repo_id=? AND base_branch=? AND has_merged=? AND issue.is_closed=?", @@ -146,7 +152,8 @@ func PullRequests(ctx context.Context, baseRepoID int64, opts *PullRequestsOptio applySorts(findSession, opts.SortType, 0) findSession = db.SetSessionPagination(findSession, opts) prs := make([]*PullRequest, 0, opts.PageSize) - return prs, maxResults, findSession.Find(&prs) + found := findSession.Find(&prs) + return prs, maxResults, found } // PullRequestList defines a list of pull requests @@ -165,6 +172,23 @@ func (prs PullRequestList) getRepositoryIDs() []int64 { return repoIDs.Values() } +func (prs PullRequestList) SetBaseRepo(baseRepo *repo_model.Repository) { + for _, pr := range prs { + if pr.BaseRepo == nil { + pr.BaseRepo = baseRepo + } + } +} + +func (prs PullRequestList) SetHeadRepo(headRepo *repo_model.Repository) { + for _, pr := range prs { + if pr.HeadRepo == nil { + pr.HeadRepo = headRepo + pr.isHeadRepoLoaded = true + } + } +} + func (prs PullRequestList) LoadRepositories(ctx context.Context) error { repoIDs := prs.getRepositoryIDs() reposMap := make(map[int64]*repo_model.Repository, len(repoIDs)) @@ -240,6 +264,64 @@ func (prs PullRequestList) GetIssueIDs() []int64 { }) } +func (prs PullRequestList) LoadReviewCommentsCounts(ctx context.Context) (map[int64]int, error) { + issueIDs := prs.GetIssueIDs() + countsMap := make(map[int64]int, len(issueIDs)) + counts := make([]struct { + IssueID int64 + Count int + }, 0, len(issueIDs)) + if err := db.GetEngine(ctx).Select("issue_id, count(*) as count"). + Table("comment").In("issue_id", issueIDs).And("type = ?", CommentTypeReview). + GroupBy("issue_id").Find(&counts); err != nil { + return nil, err + } + for _, c := range counts { + countsMap[c.IssueID] = c.Count + } + return countsMap, nil +} + +func (prs PullRequestList) LoadReviews(ctx context.Context) (ReviewList, error) { + issueIDs := prs.GetIssueIDs() + reviews := make([]*Review, 0, len(issueIDs)) + + subQuery := builder.Select("max(id) as id"). + From("review"). + Where(builder.In("issue_id", issueIDs)). + And(builder.In("`type`", ReviewTypeApprove, ReviewTypeReject, ReviewTypeRequest)). + And(builder.Eq{ + "dismissed": false, + "original_author_id": 0, + "reviewer_team_id": 0, + }). + GroupBy("issue_id, reviewer_id") + // Get latest review of each reviewer, sorted in order they were made + if err := db.GetEngine(ctx).In("id", subQuery).OrderBy("review.updated_unix ASC").Find(&reviews); err != nil { + return nil, err + } + + teamReviewRequests := make([]*Review, 0, 5) + subQueryTeam := builder.Select("max(id) as id"). + From("review"). + Where(builder.In("issue_id", issueIDs)). + And(builder.Eq{ + "original_author_id": 0, + }).And(builder.Neq{ + "reviewer_team_id": 0, + }). + GroupBy("issue_id, reviewer_team_id") + if err := db.GetEngine(ctx).In("id", subQueryTeam).OrderBy("review.updated_unix ASC").Find(&teamReviewRequests); err != nil { + return nil, err + } + + if len(teamReviewRequests) > 0 { + reviews = append(reviews, teamReviewRequests...) + } + + return reviews, nil +} + // HasMergedPullRequestInRepo returns whether the user(poster) has merged pull-request in the repo func HasMergedPullRequestInRepo(ctx context.Context, repoID, posterID int64) (bool, error) { return db.GetEngine(ctx). diff --git a/models/issues/pull_list_test.go b/models/issues/pull_list_test.go new file mode 100644 index 00000000000..437830701c6 --- /dev/null +++ b/models/issues/pull_list_test.go @@ -0,0 +1,63 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package issues_test + +import ( + "testing" + + issues_model "code.gitea.io/gitea/models/issues" + "code.gitea.io/gitea/models/unittest" + + "github.com/stretchr/testify/assert" +) + +func TestPullRequestList_LoadAttributes(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + prs := issues_model.PullRequestList{ + unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1}), + unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2}), + } + assert.NoError(t, prs.LoadAttributes(t.Context())) + for _, pr := range prs { + assert.NotNil(t, pr.Issue) + assert.Equal(t, pr.IssueID, pr.Issue.ID) + } + + assert.NoError(t, issues_model.PullRequestList([]*issues_model.PullRequest{}).LoadAttributes(t.Context())) +} + +func TestPullRequestList_LoadReviewCommentsCounts(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + prs := issues_model.PullRequestList{ + unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1}), + unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2}), + } + reviewComments, err := prs.LoadReviewCommentsCounts(t.Context()) + assert.NoError(t, err) + assert.Len(t, reviewComments, 2) + for _, pr := range prs { + assert.Equal(t, 1, reviewComments[pr.IssueID]) + } +} + +func TestPullRequestList_LoadReviews(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + prs := issues_model.PullRequestList{ + unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1}), + unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2}), + } + reviewList, err := prs.LoadReviews(t.Context()) + assert.NoError(t, err) + // 1, 7, 8, 9, 10, 22 + assert.Len(t, reviewList, 6) + assert.EqualValues(t, 1, reviewList[0].ID) + assert.EqualValues(t, 7, reviewList[1].ID) + assert.EqualValues(t, 8, reviewList[2].ID) + assert.EqualValues(t, 9, reviewList[3].ID) + assert.EqualValues(t, 10, reviewList[4].ID) + assert.EqualValues(t, 22, reviewList[5].ID) +} diff --git a/models/issues/pull_test.go b/models/issues/pull_test.go index 675c90527d8..7089af253b7 100644 --- a/models/issues/pull_test.go +++ b/models/issues/pull_test.go @@ -14,12 +14,13 @@ import ( "code.gitea.io/gitea/modules/setting" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestPullRequest_LoadAttributes(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1}) - assert.NoError(t, pr.LoadAttributes(db.DefaultContext)) + assert.NoError(t, pr.LoadAttributes(t.Context())) assert.NotNil(t, pr.Merger) assert.Equal(t, pr.MergerID, pr.Merger.ID) } @@ -27,10 +28,10 @@ func TestPullRequest_LoadAttributes(t *testing.T) { func TestPullRequest_LoadIssue(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1}) - assert.NoError(t, pr.LoadIssue(db.DefaultContext)) + assert.NoError(t, pr.LoadIssue(t.Context())) assert.NotNil(t, pr.Issue) assert.Equal(t, int64(2), pr.Issue.ID) - assert.NoError(t, pr.LoadIssue(db.DefaultContext)) + assert.NoError(t, pr.LoadIssue(t.Context())) assert.NotNil(t, pr.Issue) assert.Equal(t, int64(2), pr.Issue.ID) } @@ -38,10 +39,10 @@ func TestPullRequest_LoadIssue(t *testing.T) { func TestPullRequest_LoadBaseRepo(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1}) - assert.NoError(t, pr.LoadBaseRepo(db.DefaultContext)) + assert.NoError(t, pr.LoadBaseRepo(t.Context())) assert.NotNil(t, pr.BaseRepo) assert.Equal(t, pr.BaseRepoID, pr.BaseRepo.ID) - assert.NoError(t, pr.LoadBaseRepo(db.DefaultContext)) + assert.NoError(t, pr.LoadBaseRepo(t.Context())) assert.NotNil(t, pr.BaseRepo) assert.Equal(t, pr.BaseRepoID, pr.BaseRepo.ID) } @@ -49,7 +50,7 @@ func TestPullRequest_LoadBaseRepo(t *testing.T) { func TestPullRequest_LoadHeadRepo(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1}) - assert.NoError(t, pr.LoadHeadRepo(db.DefaultContext)) + assert.NoError(t, pr.LoadHeadRepo(t.Context())) assert.NotNil(t, pr.HeadRepo) assert.Equal(t, pr.HeadRepoID, pr.HeadRepo.ID) } @@ -60,7 +61,7 @@ func TestPullRequest_LoadHeadRepo(t *testing.T) { func TestPullRequestsNewest(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - prs, count, err := issues_model.PullRequests(db.DefaultContext, 1, &issues_model.PullRequestsOptions{ + prs, count, err := issues_model.PullRequests(t.Context(), 1, &issues_model.PullRequestsOptions{ ListOptions: db.ListOptions{ Page: 1, }, @@ -76,37 +77,78 @@ func TestPullRequestsNewest(t *testing.T) { } } +func TestPullRequests_Closed_RecentSortType(t *testing.T) { + // Issue ID | Closed At. | Updated At + // 2 | 1707270001 | 1707270001 + // 3 | 1707271000 | 1707279999 + // 11 | 1707279999 | 1707275555 + tests := []struct { + sortType string + expectedIssueIDOrder []int64 + }{ + {"recentupdate", []int64{3, 11, 2}}, + {"recentclose", []int64{11, 3, 2}}, + } + + assert.NoError(t, unittest.PrepareTestDatabase()) + _, err := db.Exec(t.Context(), "UPDATE issue SET closed_unix = 1707270001, updated_unix = 1707270001, is_closed = true WHERE id = 2") + require.NoError(t, err) + _, err = db.Exec(t.Context(), "UPDATE issue SET closed_unix = 1707271000, updated_unix = 1707279999, is_closed = true WHERE id = 3") + require.NoError(t, err) + _, err = db.Exec(t.Context(), "UPDATE issue SET closed_unix = 1707279999, updated_unix = 1707275555, is_closed = true WHERE id = 11") + require.NoError(t, err) + + for _, test := range tests { + t.Run(test.sortType, func(t *testing.T) { + prs, _, err := issues_model.PullRequests(t.Context(), 1, &issues_model.PullRequestsOptions{ + ListOptions: db.ListOptions{ + Page: 1, + }, + State: "closed", + SortType: test.sortType, + }) + require.NoError(t, err) + + if assert.Len(t, prs, len(test.expectedIssueIDOrder)) { + for i := range test.expectedIssueIDOrder { + assert.Equal(t, test.expectedIssueIDOrder[i], prs[i].IssueID) + } + } + }) + } +} + func TestLoadRequestedReviewers(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - pull := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1}) - assert.NoError(t, pull.LoadIssue(db.DefaultContext)) + pull := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2}) + assert.NoError(t, pull.LoadIssue(t.Context())) issue := pull.Issue - assert.NoError(t, issue.LoadRepo(db.DefaultContext)) - assert.Len(t, pull.RequestedReviewers, 0) + assert.NoError(t, issue.LoadRepo(t.Context())) + assert.Empty(t, pull.RequestedReviewers) - user1, err := user_model.GetUserByID(db.DefaultContext, 1) + user1, err := user_model.GetUserByID(t.Context(), 1) assert.NoError(t, err) - comment, err := issues_model.AddReviewRequest(db.DefaultContext, issue, user1, &user_model.User{}) + comment, err := issues_model.AddReviewRequest(t.Context(), issue, user1, &user_model.User{}) assert.NoError(t, err) assert.NotNil(t, comment) - assert.NoError(t, pull.LoadRequestedReviewers(db.DefaultContext)) - assert.Len(t, pull.RequestedReviewers, 1) + assert.NoError(t, pull.LoadRequestedReviewers(t.Context())) + assert.Len(t, pull.RequestedReviewers, 6) - comment, err = issues_model.RemoveReviewRequest(db.DefaultContext, issue, user1, &user_model.User{}) + comment, err = issues_model.RemoveReviewRequest(t.Context(), issue, user1, &user_model.User{}) assert.NoError(t, err) assert.NotNil(t, comment) pull.RequestedReviewers = nil - assert.NoError(t, pull.LoadRequestedReviewers(db.DefaultContext)) + assert.NoError(t, pull.LoadRequestedReviewers(t.Context())) assert.Empty(t, pull.RequestedReviewers) } func TestPullRequestsOldest(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - prs, count, err := issues_model.PullRequests(db.DefaultContext, 1, &issues_model.PullRequestsOptions{ + prs, count, err := issues_model.PullRequests(t.Context(), 1, &issues_model.PullRequestsOptions{ ListOptions: db.ListOptions{ Page: 1, }, @@ -124,11 +166,11 @@ func TestPullRequestsOldest(t *testing.T) { func TestGetUnmergedPullRequest(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - pr, err := issues_model.GetUnmergedPullRequest(db.DefaultContext, 1, 1, "branch2", "master", issues_model.PullRequestFlowGithub) + pr, err := issues_model.GetUnmergedPullRequest(t.Context(), 1, 1, "branch2", "master", issues_model.PullRequestFlowGithub) assert.NoError(t, err) assert.Equal(t, int64(2), pr.ID) - _, err = issues_model.GetUnmergedPullRequest(db.DefaultContext, 1, 9223372036854775807, "branch1", "master", issues_model.PullRequestFlowGithub) + _, err = issues_model.GetUnmergedPullRequest(t.Context(), 1, 9223372036854775807, "branch1", "master", issues_model.PullRequestFlowGithub) assert.Error(t, err) assert.True(t, issues_model.IsErrPullRequestNotExist(err)) } @@ -136,18 +178,18 @@ func TestGetUnmergedPullRequest(t *testing.T) { func TestHasUnmergedPullRequestsByHeadInfo(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - exist, err := issues_model.HasUnmergedPullRequestsByHeadInfo(db.DefaultContext, 1, "branch2") + exist, err := issues_model.HasUnmergedPullRequestsByHeadInfo(t.Context(), 1, "branch2") assert.NoError(t, err) assert.True(t, exist) - exist, err = issues_model.HasUnmergedPullRequestsByHeadInfo(db.DefaultContext, 1, "not_exist_branch") + exist, err = issues_model.HasUnmergedPullRequestsByHeadInfo(t.Context(), 1, "not_exist_branch") assert.NoError(t, err) assert.False(t, exist) } func TestGetUnmergedPullRequestsByHeadInfo(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - prs, err := issues_model.GetUnmergedPullRequestsByHeadInfo(db.DefaultContext, 1, "branch2") + prs, err := issues_model.GetUnmergedPullRequestsByHeadInfo(t.Context(), 1, "branch2") assert.NoError(t, err) assert.Len(t, prs, 1) for _, pr := range prs { @@ -158,7 +200,7 @@ func TestGetUnmergedPullRequestsByHeadInfo(t *testing.T) { func TestGetUnmergedPullRequestsByBaseInfo(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - prs, err := issues_model.GetUnmergedPullRequestsByBaseInfo(db.DefaultContext, 1, "master") + prs, err := issues_model.GetUnmergedPullRequestsByBaseInfo(t.Context(), 1, "master") assert.NoError(t, err) assert.Len(t, prs, 1) pr := prs[0] @@ -169,56 +211,43 @@ func TestGetUnmergedPullRequestsByBaseInfo(t *testing.T) { func TestGetPullRequestByIndex(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - pr, err := issues_model.GetPullRequestByIndex(db.DefaultContext, 1, 2) + pr, err := issues_model.GetPullRequestByIndex(t.Context(), 1, 2) assert.NoError(t, err) assert.Equal(t, int64(1), pr.BaseRepoID) assert.Equal(t, int64(2), pr.Index) - _, err = issues_model.GetPullRequestByIndex(db.DefaultContext, 9223372036854775807, 9223372036854775807) + _, err = issues_model.GetPullRequestByIndex(t.Context(), 9223372036854775807, 9223372036854775807) assert.Error(t, err) assert.True(t, issues_model.IsErrPullRequestNotExist(err)) - _, err = issues_model.GetPullRequestByIndex(db.DefaultContext, 1, 0) + _, err = issues_model.GetPullRequestByIndex(t.Context(), 1, 0) assert.Error(t, err) assert.True(t, issues_model.IsErrPullRequestNotExist(err)) } func TestGetPullRequestByID(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - pr, err := issues_model.GetPullRequestByID(db.DefaultContext, 1) + pr, err := issues_model.GetPullRequestByID(t.Context(), 1) assert.NoError(t, err) assert.Equal(t, int64(1), pr.ID) assert.Equal(t, int64(2), pr.IssueID) - _, err = issues_model.GetPullRequestByID(db.DefaultContext, 9223372036854775807) + _, err = issues_model.GetPullRequestByID(t.Context(), 9223372036854775807) assert.Error(t, err) assert.True(t, issues_model.IsErrPullRequestNotExist(err)) } func TestGetPullRequestByIssueID(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - pr, err := issues_model.GetPullRequestByIssueID(db.DefaultContext, 2) + pr, err := issues_model.GetPullRequestByIssueID(t.Context(), 2) assert.NoError(t, err) assert.Equal(t, int64(2), pr.IssueID) - _, err = issues_model.GetPullRequestByIssueID(db.DefaultContext, 9223372036854775807) + _, err = issues_model.GetPullRequestByIssueID(t.Context(), 9223372036854775807) assert.Error(t, err) assert.True(t, issues_model.IsErrPullRequestNotExist(err)) } -func TestPullRequest_Update(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1}) - pr.BaseBranch = "baseBranch" - pr.HeadBranch = "headBranch" - pr.Update(db.DefaultContext) - - pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: pr.ID}) - assert.Equal(t, "baseBranch", pr.BaseBranch) - assert.Equal(t, "headBranch", pr.HeadBranch) - unittest.CheckConsistencyFor(t, pr) -} - func TestPullRequest_UpdateCols(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) pr := &issues_model.PullRequest{ @@ -226,7 +255,7 @@ func TestPullRequest_UpdateCols(t *testing.T) { BaseBranch: "baseBranch", HeadBranch: "headBranch", } - assert.NoError(t, pr.UpdateCols(db.DefaultContext, "head_branch")) + assert.NoError(t, pr.UpdateCols(t.Context(), "head_branch")) pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1}) assert.Equal(t, "master", pr.BaseBranch) @@ -234,74 +263,58 @@ func TestPullRequest_UpdateCols(t *testing.T) { unittest.CheckConsistencyFor(t, pr) } -func TestPullRequestList_LoadAttributes(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - prs := []*issues_model.PullRequest{ - unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1}), - unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2}), - } - assert.NoError(t, issues_model.PullRequestList(prs).LoadAttributes(db.DefaultContext)) - for _, pr := range prs { - assert.NotNil(t, pr.Issue) - assert.Equal(t, pr.IssueID, pr.Issue.ID) - } - - assert.NoError(t, issues_model.PullRequestList([]*issues_model.PullRequest{}).LoadAttributes(db.DefaultContext)) -} - // TODO TestAddTestPullRequestTask func TestPullRequest_IsWorkInProgress(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2}) - pr.LoadIssue(db.DefaultContext) + pr.LoadIssue(t.Context()) - assert.False(t, pr.IsWorkInProgress(db.DefaultContext)) + assert.False(t, pr.IsWorkInProgress(t.Context())) pr.Issue.Title = "WIP: " + pr.Issue.Title - assert.True(t, pr.IsWorkInProgress(db.DefaultContext)) + assert.True(t, pr.IsWorkInProgress(t.Context())) pr.Issue.Title = "[wip]: " + pr.Issue.Title - assert.True(t, pr.IsWorkInProgress(db.DefaultContext)) + assert.True(t, pr.IsWorkInProgress(t.Context())) } func TestPullRequest_GetWorkInProgressPrefixWorkInProgress(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2}) - pr.LoadIssue(db.DefaultContext) + pr.LoadIssue(t.Context()) - assert.Empty(t, pr.GetWorkInProgressPrefix(db.DefaultContext)) + assert.Empty(t, pr.GetWorkInProgressPrefix(t.Context())) original := pr.Issue.Title pr.Issue.Title = "WIP: " + original - assert.Equal(t, "WIP:", pr.GetWorkInProgressPrefix(db.DefaultContext)) + assert.Equal(t, "WIP:", pr.GetWorkInProgressPrefix(t.Context())) pr.Issue.Title = "[wip] " + original - assert.Equal(t, "[wip]", pr.GetWorkInProgressPrefix(db.DefaultContext)) + assert.Equal(t, "[wip]", pr.GetWorkInProgressPrefix(t.Context())) } func TestDeleteOrphanedObjects(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - countBefore, err := db.GetEngine(db.DefaultContext).Count(&issues_model.PullRequest{}) + countBefore, err := db.GetEngine(t.Context()).Count(&issues_model.PullRequest{}) assert.NoError(t, err) - _, err = db.GetEngine(db.DefaultContext).Insert(&issues_model.PullRequest{IssueID: 1000}, &issues_model.PullRequest{IssueID: 1001}, &issues_model.PullRequest{IssueID: 1003}) + _, err = db.GetEngine(t.Context()).Insert(&issues_model.PullRequest{IssueID: 1000}, &issues_model.PullRequest{IssueID: 1001}, &issues_model.PullRequest{IssueID: 1003}) assert.NoError(t, err) - orphaned, err := db.CountOrphanedObjects(db.DefaultContext, "pull_request", "issue", "pull_request.issue_id=issue.id") + orphaned, err := db.CountOrphanedObjects(t.Context(), "pull_request", "issue", "pull_request.issue_id=issue.id") assert.NoError(t, err) assert.EqualValues(t, 3, orphaned) - err = db.DeleteOrphanedObjects(db.DefaultContext, "pull_request", "issue", "pull_request.issue_id=issue.id") + err = db.DeleteOrphanedObjects(t.Context(), "pull_request", "issue", "pull_request.issue_id=issue.id") assert.NoError(t, err) - countAfter, err := db.GetEngine(db.DefaultContext).Count(&issues_model.PullRequest{}) + countAfter, err := db.GetEngine(t.Context()).Count(&issues_model.PullRequest{}) assert.NoError(t, err) - assert.EqualValues(t, countBefore, countAfter) + assert.Equal(t, countBefore, countAfter) } func TestParseCodeOwnersLine(t *testing.T) { @@ -332,20 +345,20 @@ func TestGetApprovers(t *testing.T) { // Official reviews are already deduplicated. Allow unofficial reviews // to assert that there are no duplicated approvers. setting.Repository.PullRequest.DefaultMergeMessageOfficialApproversOnly = false - approvers := pr.GetApprovers(db.DefaultContext) + approvers := pr.GetApprovers(t.Context()) expected := "Reviewed-by: User Five \nReviewed-by: Org Six \n" - assert.EqualValues(t, expected, approvers) + assert.Equal(t, expected, approvers) } func TestGetPullRequestByMergedCommit(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - pr, err := issues_model.GetPullRequestByMergedCommit(db.DefaultContext, 1, "1a8823cd1a9549fde083f992f6b9b87a7ab74fb3") + pr, err := issues_model.GetPullRequestByMergedCommit(t.Context(), 1, "1a8823cd1a9549fde083f992f6b9b87a7ab74fb3") assert.NoError(t, err) assert.EqualValues(t, 1, pr.ID) - _, err = issues_model.GetPullRequestByMergedCommit(db.DefaultContext, 0, "1a8823cd1a9549fde083f992f6b9b87a7ab74fb3") + _, err = issues_model.GetPullRequestByMergedCommit(t.Context(), 0, "1a8823cd1a9549fde083f992f6b9b87a7ab74fb3") assert.ErrorAs(t, err, &issues_model.ErrPullRequestNotExist{}) - _, err = issues_model.GetPullRequestByMergedCommit(db.DefaultContext, 1, "") + _, err = issues_model.GetPullRequestByMergedCommit(t.Context(), 1, "") assert.ErrorAs(t, err, &issues_model.ErrPullRequestNotExist{}) } @@ -369,7 +382,7 @@ func TestMigrate_InsertPullRequests(t *testing.T) { Issue: i, } - err := issues_model.InsertPullRequests(db.DefaultContext, p) + err := issues_model.InsertPullRequests(t.Context(), p) assert.NoError(t, err) _ = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{IssueID: i.ID}) diff --git a/models/issues/reaction.go b/models/issues/reaction.go index 11b3c6be203..3b5ad6d7ab9 100644 --- a/models/issues/reaction.go +++ b/models/issues/reaction.go @@ -7,6 +7,7 @@ import ( "bytes" "context" "fmt" + "strings" "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" @@ -223,21 +224,9 @@ func CreateReaction(ctx context.Context, opts *ReactionOptions) (*Reaction, erro return nil, ErrForbiddenIssueReaction{opts.Type} } - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return nil, err - } - defer committer.Close() - - reaction, err := createReaction(ctx, opts) - if err != nil { - return reaction, err - } - - if err := committer.Commit(); err != nil { - return nil, err - } - return reaction, nil + return db.WithTx2(ctx, func(ctx context.Context) (*Reaction, error) { + return createReaction(ctx, opts) + }) } // DeleteReaction deletes reaction for issue or comment. @@ -321,6 +310,11 @@ func valuesUser(m map[int64]*user_model.User) []*user_model.User { return values } +// newMigrationOriginalUser creates and returns a fake user for external user +func newMigrationOriginalUser(name string) *user_model.User { + return &user_model.User{ID: 0, Name: name, LowerName: strings.ToLower(name)} +} + // LoadUsers loads reactions' all users func (list ReactionList) LoadUsers(ctx context.Context, repo *repo_model.Repository) ([]*user_model.User, error) { if len(list) == 0 { @@ -338,7 +332,7 @@ func (list ReactionList) LoadUsers(ctx context.Context, repo *repo_model.Reposit for _, reaction := range list { if reaction.OriginalAuthor != "" { - reaction.User = user_model.NewReplaceUser(fmt.Sprintf("%s(%s)", reaction.OriginalAuthor, repo.OriginalServiceType.Name())) + reaction.User = newMigrationOriginalUser(fmt.Sprintf("%s(%s)", reaction.OriginalAuthor, repo.OriginalServiceType.Name())) } else if user, ok := userMaps[reaction.UserID]; ok { reaction.User = user } else { diff --git a/models/issues/review.go b/models/issues/review.go index 8b345e5fd80..b758fa5ffac 100644 --- a/models/issues/review.go +++ b/models/issues/review.go @@ -5,6 +5,7 @@ package issues import ( "context" + "errors" "fmt" "slices" "strings" @@ -333,54 +334,51 @@ func IsOfficialReviewerTeam(ctx context.Context, issue *Issue, team *organizatio // CreateReview creates a new review based on opts func CreateReview(ctx context.Context, opts CreateReviewOptions) (*Review, error) { - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return nil, err - } - defer committer.Close() - sess := db.GetEngine(ctx) + return db.WithTx2(ctx, func(ctx context.Context) (*Review, error) { + sess := db.GetEngine(ctx) - review := &Review{ - Issue: opts.Issue, - IssueID: opts.Issue.ID, - Reviewer: opts.Reviewer, - ReviewerTeam: opts.ReviewerTeam, - Content: opts.Content, - Official: opts.Official, - CommitID: opts.CommitID, - Stale: opts.Stale, - } - - if opts.Reviewer != nil { - review.Type = opts.Type - review.ReviewerID = opts.Reviewer.ID - - reviewCond := builder.Eq{"reviewer_id": opts.Reviewer.ID, "issue_id": opts.Issue.ID} - // make sure user review requests are cleared - if opts.Type != ReviewTypePending { - if _, err := sess.Where(reviewCond.And(builder.Eq{"type": ReviewTypeRequest})).Delete(new(Review)); err != nil { - return nil, err - } + review := &Review{ + Issue: opts.Issue, + IssueID: opts.Issue.ID, + Reviewer: opts.Reviewer, + ReviewerTeam: opts.ReviewerTeam, + Content: opts.Content, + Official: opts.Official, + CommitID: opts.CommitID, + Stale: opts.Stale, } - // make sure if the created review gets dismissed no old review surface - // other types can be ignored, as they don't affect branch protection - if opts.Type == ReviewTypeApprove || opts.Type == ReviewTypeReject { - if _, err := sess.Where(reviewCond.And(builder.In("type", ReviewTypeApprove, ReviewTypeReject))). - Cols("dismissed").Update(&Review{Dismissed: true}); err != nil { - return nil, err - } - } - } else if opts.ReviewerTeam != nil { - review.Type = ReviewTypeRequest - review.ReviewerTeamID = opts.ReviewerTeam.ID - } else { - return nil, fmt.Errorf("provide either reviewer or reviewer team") - } - if _, err := sess.Insert(review); err != nil { - return nil, err - } - return review, committer.Commit() + if opts.Reviewer != nil { + review.Type = opts.Type + review.ReviewerID = opts.Reviewer.ID + + reviewCond := builder.Eq{"reviewer_id": opts.Reviewer.ID, "issue_id": opts.Issue.ID} + // make sure user review requests are cleared + if opts.Type != ReviewTypePending { + if _, err := sess.Where(reviewCond.And(builder.Eq{"type": ReviewTypeRequest})).Delete(new(Review)); err != nil { + return nil, err + } + } + // make sure if the created review gets dismissed no old review surface + // other types can be ignored, as they don't affect branch protection + if opts.Type == ReviewTypeApprove || opts.Type == ReviewTypeReject { + if _, err := sess.Where(reviewCond.And(builder.In("type", ReviewTypeApprove, ReviewTypeReject))). + Cols("dismissed").Update(&Review{Dismissed: true}); err != nil { + return nil, err + } + } + } else if opts.ReviewerTeam != nil { + review.Type = ReviewTypeRequest + review.ReviewerTeamID = opts.ReviewerTeam.ID + } else { + return nil, errors.New("provide either reviewer or reviewer team") + } + + if _, err := sess.Insert(review); err != nil { + return nil, err + } + return review, nil + }) } // GetCurrentReview returns the current pending review of reviewer for given issue @@ -604,164 +602,152 @@ func DismissReview(ctx context.Context, review *Review, isDismiss bool) (err err // InsertReviews inserts review and review comments func InsertReviews(ctx context.Context, reviews []*Review) error { - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return err - } - defer committer.Close() - sess := db.GetEngine(ctx) + return db.WithTx(ctx, func(ctx context.Context) error { + sess := db.GetEngine(ctx) - for _, review := range reviews { - if _, err := sess.NoAutoTime().Insert(review); err != nil { - return err - } + for _, review := range reviews { + if _, err := sess.NoAutoTime().Insert(review); err != nil { + return err + } - if _, err := sess.NoAutoTime().Insert(&Comment{ - Type: CommentTypeReview, - Content: review.Content, - PosterID: review.ReviewerID, - OriginalAuthor: review.OriginalAuthor, - OriginalAuthorID: review.OriginalAuthorID, - IssueID: review.IssueID, - ReviewID: review.ID, - CreatedUnix: review.CreatedUnix, - UpdatedUnix: review.UpdatedUnix, - }); err != nil { - return err - } + if _, err := sess.NoAutoTime().Insert(&Comment{ + Type: CommentTypeReview, + Content: review.Content, + PosterID: review.ReviewerID, + OriginalAuthor: review.OriginalAuthor, + OriginalAuthorID: review.OriginalAuthorID, + IssueID: review.IssueID, + ReviewID: review.ID, + CreatedUnix: review.CreatedUnix, + UpdatedUnix: review.UpdatedUnix, + }); err != nil { + return err + } - for _, c := range review.Comments { - c.ReviewID = review.ID - } + for _, c := range review.Comments { + c.ReviewID = review.ID + } - if len(review.Comments) > 0 { - if _, err := sess.NoAutoTime().Insert(review.Comments); err != nil { + if len(review.Comments) > 0 { + if _, err := sess.NoAutoTime().Insert(review.Comments); err != nil { + return err + } + } + + if err := UpdateIssueNumComments(ctx, review.IssueID); err != nil { return err } } - } - - return committer.Commit() + return nil + }) } // AddReviewRequest add a review request from one reviewer func AddReviewRequest(ctx context.Context, issue *Issue, reviewer, doer *user_model.User) (*Comment, error) { - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return nil, err - } - defer committer.Close() - sess := db.GetEngine(ctx) + return db.WithTx2(ctx, func(ctx context.Context) (*Comment, error) { + sess := db.GetEngine(ctx) - review, err := GetReviewByIssueIDAndUserID(ctx, issue.ID, reviewer.ID) - if err != nil && !IsErrReviewNotExist(err) { - return nil, err - } - - if review != nil { - // skip it when reviewer hase been request to review - if review.Type == ReviewTypeRequest { - return nil, committer.Commit() // still commit the transaction, or committer.Close() will rollback it, even if it's a reused transaction. - } - - if issue.IsClosed { - return nil, ErrReviewRequestOnClosedPR{} - } - - if issue.IsPull { - if err := issue.LoadPullRequest(ctx); err != nil { - return nil, err - } - if issue.PullRequest.HasMerged { - return nil, ErrReviewRequestOnClosedPR{} - } - } - } - - // if the reviewer is an official reviewer, - // remove the official flag in the all previous reviews - official, err := IsOfficialReviewer(ctx, issue, reviewer) - if err != nil { - return nil, err - } else if official { - if _, err := sess.Exec("UPDATE `review` SET official=? WHERE issue_id=? AND reviewer_id=?", false, issue.ID, reviewer.ID); err != nil { + review, err := GetReviewByIssueIDAndUserID(ctx, issue.ID, reviewer.ID) + if err != nil && !IsErrReviewNotExist(err) { return nil, err } - } - review, err = CreateReview(ctx, CreateReviewOptions{ - Type: ReviewTypeRequest, - Issue: issue, - Reviewer: reviewer, - Official: official, - Stale: false, + if review != nil { + // skip it when reviewer has been request to review + if review.Type == ReviewTypeRequest { + return nil, nil // still commit the transaction, or committer.Close() will rollback it, even if it's a reused transaction. + } + + if issue.IsClosed { + return nil, ErrReviewRequestOnClosedPR{} + } + + if issue.IsPull { + if err := issue.LoadPullRequest(ctx); err != nil { + return nil, err + } + if issue.PullRequest.HasMerged { + return nil, ErrReviewRequestOnClosedPR{} + } + } + } + + // if the reviewer is an official reviewer, + // remove the official flag in the all previous reviews + official, err := IsOfficialReviewer(ctx, issue, reviewer) + if err != nil { + return nil, err + } else if official { + if _, err := sess.Exec("UPDATE `review` SET official=? WHERE issue_id=? AND reviewer_id=?", false, issue.ID, reviewer.ID); err != nil { + return nil, err + } + } + + review, err = CreateReview(ctx, CreateReviewOptions{ + Type: ReviewTypeRequest, + Issue: issue, + Reviewer: reviewer, + Official: official, + Stale: false, + }) + if err != nil { + return nil, err + } + + comment, err := CreateComment(ctx, &CreateCommentOptions{ + Type: CommentTypeReviewRequest, + Doer: doer, + Repo: issue.Repo, + Issue: issue, + RemovedAssignee: false, // Use RemovedAssignee as !isRequest + AssigneeID: reviewer.ID, // Use AssigneeID as reviewer ID + ReviewID: review.ID, + }) + if err != nil { + return nil, err + } + + // func caller use the created comment to retrieve created review too. + comment.Review = review + + return comment, nil }) - if err != nil { - return nil, err - } - - comment, err := CreateComment(ctx, &CreateCommentOptions{ - Type: CommentTypeReviewRequest, - Doer: doer, - Repo: issue.Repo, - Issue: issue, - RemovedAssignee: false, // Use RemovedAssignee as !isRequest - AssigneeID: reviewer.ID, // Use AssigneeID as reviewer ID - ReviewID: review.ID, - }) - if err != nil { - return nil, err - } - - // func caller use the created comment to retrieve created review too. - comment.Review = review - - return comment, committer.Commit() } // RemoveReviewRequest remove a review request from one reviewer func RemoveReviewRequest(ctx context.Context, issue *Issue, reviewer, doer *user_model.User) (*Comment, error) { - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return nil, err - } - defer committer.Close() - - review, err := GetReviewByIssueIDAndUserID(ctx, issue.ID, reviewer.ID) - if err != nil && !IsErrReviewNotExist(err) { - return nil, err - } - - if review == nil || review.Type != ReviewTypeRequest { - return nil, nil - } - - if _, err = db.DeleteByBean(ctx, review); err != nil { - return nil, err - } - - official, err := IsOfficialReviewer(ctx, issue, reviewer) - if err != nil { - return nil, err - } else if official { - if err := restoreLatestOfficialReview(ctx, issue.ID, reviewer.ID); err != nil { + return db.WithTx2(ctx, func(ctx context.Context) (*Comment, error) { + review, err := GetReviewByIssueIDAndUserID(ctx, issue.ID, reviewer.ID) + if err != nil && !IsErrReviewNotExist(err) { return nil, err } - } - comment, err := CreateComment(ctx, &CreateCommentOptions{ - Type: CommentTypeReviewRequest, - Doer: doer, - Repo: issue.Repo, - Issue: issue, - RemovedAssignee: true, // Use RemovedAssignee as !isRequest - AssigneeID: reviewer.ID, // Use AssigneeID as reviewer ID + if review == nil || review.Type != ReviewTypeRequest { + return nil, nil + } + + if _, err = db.DeleteByBean(ctx, review); err != nil { + return nil, err + } + + official, err := IsOfficialReviewer(ctx, issue, reviewer) + if err != nil { + return nil, err + } else if official { + if err := restoreLatestOfficialReview(ctx, issue.ID, reviewer.ID); err != nil { + return nil, err + } + } + + return CreateComment(ctx, &CreateCommentOptions{ + Type: CommentTypeReviewRequest, + Doer: doer, + Repo: issue.Repo, + Issue: issue, + RemovedAssignee: true, // Use RemovedAssignee as !isRequest + AssigneeID: reviewer.ID, // Use AssigneeID as reviewer ID + }) }) - if err != nil { - return nil, err - } - - return comment, committer.Commit() } // Recalculate the latest official review for reviewer @@ -782,120 +768,112 @@ func restoreLatestOfficialReview(ctx context.Context, issueID, reviewerID int64) // AddTeamReviewRequest add a review request from one team func AddTeamReviewRequest(ctx context.Context, issue *Issue, reviewer *organization.Team, doer *user_model.User) (*Comment, error) { - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return nil, err - } - defer committer.Close() - - review, err := GetTeamReviewerByIssueIDAndTeamID(ctx, issue.ID, reviewer.ID) - if err != nil && !IsErrReviewNotExist(err) { - return nil, err - } - - // This team already has been requested to review - therefore skip this. - if review != nil { - return nil, nil - } - - official, err := IsOfficialReviewerTeam(ctx, issue, reviewer) - if err != nil { - return nil, fmt.Errorf("isOfficialReviewerTeam(): %w", err) - } else if !official { - if official, err = IsOfficialReviewer(ctx, issue, doer); err != nil { - return nil, fmt.Errorf("isOfficialReviewer(): %w", err) - } - } - - if review, err = CreateReview(ctx, CreateReviewOptions{ - Type: ReviewTypeRequest, - Issue: issue, - ReviewerTeam: reviewer, - Official: official, - Stale: false, - }); err != nil { - return nil, err - } - - if official { - if _, err := db.Exec(ctx, "UPDATE `review` SET official=? WHERE issue_id=? AND reviewer_team_id=?", false, issue.ID, reviewer.ID); err != nil { - return nil, err - } - } - - comment, err := CreateComment(ctx, &CreateCommentOptions{ - Type: CommentTypeReviewRequest, - Doer: doer, - Repo: issue.Repo, - Issue: issue, - RemovedAssignee: false, // Use RemovedAssignee as !isRequest - AssigneeTeamID: reviewer.ID, // Use AssigneeTeamID as reviewer team ID - ReviewID: review.ID, - }) - if err != nil { - return nil, fmt.Errorf("CreateComment(): %w", err) - } - - return comment, committer.Commit() -} - -// RemoveTeamReviewRequest remove a review request from one team -func RemoveTeamReviewRequest(ctx context.Context, issue *Issue, reviewer *organization.Team, doer *user_model.User) (*Comment, error) { - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return nil, err - } - defer committer.Close() - - review, err := GetTeamReviewerByIssueIDAndTeamID(ctx, issue.ID, reviewer.ID) - if err != nil && !IsErrReviewNotExist(err) { - return nil, err - } - - if review == nil { - return nil, nil - } - - if _, err = db.DeleteByBean(ctx, review); err != nil { - return nil, err - } - - official, err := IsOfficialReviewerTeam(ctx, issue, reviewer) - if err != nil { - return nil, fmt.Errorf("isOfficialReviewerTeam(): %w", err) - } - - if official { - // recalculate which is the latest official review from that team - review, err := GetReviewByIssueIDAndUserID(ctx, issue.ID, -reviewer.ID) + return db.WithTx2(ctx, func(ctx context.Context) (*Comment, error) { + review, err := GetTeamReviewerByIssueIDAndTeamID(ctx, issue.ID, reviewer.ID) if err != nil && !IsErrReviewNotExist(err) { return nil, err } + // This team already has been requested to review - therefore skip this. if review != nil { - if _, err := db.Exec(ctx, "UPDATE `review` SET official=? WHERE id=?", true, review.ID); err != nil { + return nil, nil + } + + official, err := IsOfficialReviewerTeam(ctx, issue, reviewer) + if err != nil { + return nil, fmt.Errorf("isOfficialReviewerTeam(): %w", err) + } else if !official { + if official, err = IsOfficialReviewer(ctx, issue, doer); err != nil { + return nil, fmt.Errorf("isOfficialReviewer(): %w", err) + } + } + + if review, err = CreateReview(ctx, CreateReviewOptions{ + Type: ReviewTypeRequest, + Issue: issue, + ReviewerTeam: reviewer, + Official: official, + Stale: false, + }); err != nil { + return nil, err + } + + if official { + if _, err := db.Exec(ctx, "UPDATE `review` SET official=? WHERE issue_id=? AND reviewer_team_id=?", false, issue.ID, reviewer.ID); err != nil { return nil, err } } - } - if doer == nil { - return nil, committer.Commit() - } + comment, err := CreateComment(ctx, &CreateCommentOptions{ + Type: CommentTypeReviewRequest, + Doer: doer, + Repo: issue.Repo, + Issue: issue, + RemovedAssignee: false, // Use RemovedAssignee as !isRequest + AssigneeTeamID: reviewer.ID, // Use AssigneeTeamID as reviewer team ID + ReviewID: review.ID, + }) + if err != nil { + return nil, fmt.Errorf("CreateComment(): %w", err) + } - comment, err := CreateComment(ctx, &CreateCommentOptions{ - Type: CommentTypeReviewRequest, - Doer: doer, - Repo: issue.Repo, - Issue: issue, - RemovedAssignee: true, // Use RemovedAssignee as !isRequest - AssigneeTeamID: reviewer.ID, // Use AssigneeTeamID as reviewer team ID + return comment, nil }) - if err != nil { - return nil, fmt.Errorf("CreateComment(): %w", err) - } +} - return comment, committer.Commit() +// RemoveTeamReviewRequest remove a review request from one team +func RemoveTeamReviewRequest(ctx context.Context, issue *Issue, reviewer *organization.Team, doer *user_model.User) (*Comment, error) { + return db.WithTx2(ctx, func(ctx context.Context) (*Comment, error) { + review, err := GetTeamReviewerByIssueIDAndTeamID(ctx, issue.ID, reviewer.ID) + if err != nil && !IsErrReviewNotExist(err) { + return nil, err + } + + if review == nil { + return nil, nil + } + + if _, err = db.DeleteByBean(ctx, review); err != nil { + return nil, err + } + + official, err := IsOfficialReviewerTeam(ctx, issue, reviewer) + if err != nil { + return nil, fmt.Errorf("isOfficialReviewerTeam(): %w", err) + } + + if official { + // recalculate which is the latest official review from that team + review, err := GetReviewByIssueIDAndUserID(ctx, issue.ID, -reviewer.ID) + if err != nil && !IsErrReviewNotExist(err) { + return nil, err + } + + if review != nil { + if _, err := db.Exec(ctx, "UPDATE `review` SET official=? WHERE id=?", true, review.ID); err != nil { + return nil, err + } + } + } + + if doer == nil { + return nil, nil + } + + comment, err := CreateComment(ctx, &CreateCommentOptions{ + Type: CommentTypeReviewRequest, + Doer: doer, + Repo: issue.Repo, + Issue: issue, + RemovedAssignee: true, // Use RemovedAssignee as !isRequest + AssigneeTeamID: reviewer.ID, // Use AssigneeTeamID as reviewer team ID + }) + if err != nil { + return nil, fmt.Errorf("CreateComment(): %w", err) + } + + return comment, nil + }) } // MarkConversation Add or remove Conversation mark for a code comment @@ -926,17 +904,19 @@ func MarkConversation(ctx context.Context, comment *Comment, doer *user_model.Us } // CanMarkConversation Add or remove Conversation mark for a code comment permission check -// the PR writer , offfcial reviewer and poster can do it +// the PR writer , official reviewer and poster can do it func CanMarkConversation(ctx context.Context, issue *Issue, doer *user_model.User) (permResult bool, err error) { if doer == nil || issue == nil { - return false, fmt.Errorf("issue or doer is nil") + return false, errors.New("issue or doer is nil") } + if err = issue.LoadRepo(ctx); err != nil { + return false, err + } + if issue.Repo.IsArchived { + return false, nil + } if doer.ID != issue.PosterID { - if err = issue.LoadRepo(ctx); err != nil { - return false, err - } - p, err := access_model.GetUserRepoPermission(ctx, issue.Repo, doer) if err != nil { return false, err @@ -959,61 +939,56 @@ func CanMarkConversation(ctx context.Context, issue *Issue, doer *user_model.Use // DeleteReview delete a review and it's code comments func DeleteReview(ctx context.Context, r *Review) error { - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return err - } - defer committer.Close() + return db.WithTx(ctx, func(ctx context.Context) error { + if r.ID == 0 { + return errors.New("review is not allowed to be 0") + } - if r.ID == 0 { - return fmt.Errorf("review is not allowed to be 0") - } + if r.Type == ReviewTypeRequest { + return errors.New("review request can not be deleted using this method") + } - if r.Type == ReviewTypeRequest { - return fmt.Errorf("review request can not be deleted using this method") - } + opts := FindCommentsOptions{ + Type: CommentTypeCode, + IssueID: r.IssueID, + ReviewID: r.ID, + } - opts := FindCommentsOptions{ - Type: CommentTypeCode, - IssueID: r.IssueID, - ReviewID: r.ID, - } - - if _, err := db.Delete[Comment](ctx, opts); err != nil { - return err - } - - opts = FindCommentsOptions{ - Type: CommentTypeReview, - IssueID: r.IssueID, - ReviewID: r.ID, - } - - if _, err := db.Delete[Comment](ctx, opts); err != nil { - return err - } - - opts = FindCommentsOptions{ - Type: CommentTypeDismissReview, - IssueID: r.IssueID, - ReviewID: r.ID, - } - - if _, err := db.Delete[Comment](ctx, opts); err != nil { - return err - } - - if _, err := db.DeleteByID[Review](ctx, r.ID); err != nil { - return err - } - - if r.Official { - if err := restoreLatestOfficialReview(ctx, r.IssueID, r.ReviewerID); err != nil { + if _, err := db.Delete[Comment](ctx, opts); err != nil { return err } - } - return committer.Commit() + opts = FindCommentsOptions{ + Type: CommentTypeReview, + IssueID: r.IssueID, + ReviewID: r.ID, + } + + if _, err := db.Delete[Comment](ctx, opts); err != nil { + return err + } + + opts = FindCommentsOptions{ + Type: CommentTypeDismissReview, + IssueID: r.IssueID, + ReviewID: r.ID, + } + + if _, err := db.Delete[Comment](ctx, opts); err != nil { + return err + } + + if _, err := db.DeleteByID[Review](ctx, r.ID); err != nil { + return err + } + + if r.Official { + if err := restoreLatestOfficialReview(ctx, r.IssueID, r.ReviewerID); err != nil { + return err + } + } + return nil + }) } // GetCodeCommentsCount return count of CodeComments a Review has diff --git a/models/issues/review_list.go b/models/issues/review_list.go index a5ceb217911..bbb8c489fa1 100644 --- a/models/issues/review_list.go +++ b/models/issues/review_list.go @@ -5,6 +5,8 @@ package issues import ( "context" + "slices" + "sort" "code.gitea.io/gitea/models/db" organization_model "code.gitea.io/gitea/models/organization" @@ -20,7 +22,7 @@ type ReviewList []*Review // LoadReviewers loads reviewers func (reviews ReviewList) LoadReviewers(ctx context.Context) error { reviewerIDs := make([]int64, len(reviews)) - for i := 0; i < len(reviews); i++ { + for i := range reviews { reviewerIDs[i] = reviews[i].ReviewerID } reviewers, err := user_model.GetPossibleUserByIDs(ctx, reviewerIDs) @@ -47,14 +49,9 @@ func (reviews ReviewList) LoadReviewersTeams(ctx context.Context) error { } } - teamsMap := make(map[int64]*organization_model.Team, 0) - for _, teamID := range reviewersTeamsIDs { - team, err := organization_model.GetTeamByID(ctx, teamID) - if err != nil { - return err - } - - teamsMap[teamID] = team + teamsMap, err := organization_model.GetTeamsByIDs(ctx, reviewersTeamsIDs) + if err != nil { + return err } for _, review := range reviews { @@ -158,43 +155,60 @@ func CountReviews(ctx context.Context, opts FindReviewOptions) (int64, error) { return db.GetEngine(ctx).Where(opts.toCond()).Count(&Review{}) } -// GetReviewersFromOriginalAuthorsByIssueID gets the latest review of each original authors for a pull request -func GetReviewersFromOriginalAuthorsByIssueID(ctx context.Context, issueID int64) (ReviewList, error) { - reviews := make([]*Review, 0, 10) - - // Get latest review of each reviewer, sorted in order they were made - if err := db.GetEngine(ctx).SQL("SELECT * FROM review WHERE id IN (SELECT max(id) as id FROM review WHERE issue_id = ? AND reviewer_team_id = 0 AND type in (?, ?, ?) AND original_author_id <> 0 GROUP BY issue_id, original_author_id) ORDER BY review.updated_unix ASC", - issueID, ReviewTypeApprove, ReviewTypeReject, ReviewTypeRequest). - Find(&reviews); err != nil { - return nil, err - } - - return reviews, nil -} - // GetReviewsByIssueID gets the latest review of each reviewer for a pull request -func GetReviewsByIssueID(ctx context.Context, issueID int64) (ReviewList, error) { +// The first returned parameter is the latest review of each individual reviewer or team +// The second returned parameter is the latest review of each original author which is migrated from other systems +// The reviews are sorted by updated time +func GetReviewsByIssueID(ctx context.Context, issueID int64) (latestReviews, migratedOriginalReviews ReviewList, err error) { reviews := make([]*Review, 0, 10) - sess := db.GetEngine(ctx) - - // Get latest review of each reviewer, sorted in order they were made - if err := sess.SQL("SELECT * FROM review WHERE id IN (SELECT max(id) as id FROM review WHERE issue_id = ? AND reviewer_team_id = 0 AND type in (?, ?, ?) AND dismissed = ? AND original_author_id = 0 GROUP BY issue_id, reviewer_id) ORDER BY review.updated_unix ASC", - issueID, ReviewTypeApprove, ReviewTypeReject, ReviewTypeRequest, false). - Find(&reviews); err != nil { - return nil, err + // Get all reviews for the issue id + if err := db.GetEngine(ctx).Where("issue_id=?", issueID).OrderBy("updated_unix ASC").Find(&reviews); err != nil { + return nil, nil, err } + // filter them in memory to get the latest review of each reviewer + // Since the reviews should not be too many for one issue, less than 100 commonly, it's acceptable to do this in memory + // And since there are too less indexes in review table, it will be very slow to filter in the database + reviewersMap := make(map[int64][]*Review) // key is reviewer id + originalReviewersMap := make(map[int64][]*Review) // key is original author id + reviewTeamsMap := make(map[int64][]*Review) // key is reviewer team id + countedReivewTypes := []ReviewType{ReviewTypeApprove, ReviewTypeReject, ReviewTypeRequest} + for _, review := range reviews { + if review.ReviewerTeamID == 0 && slices.Contains(countedReivewTypes, review.Type) && !review.Dismissed { + if review.OriginalAuthorID != 0 { + originalReviewersMap[review.OriginalAuthorID] = append(originalReviewersMap[review.OriginalAuthorID], review) + } else { + reviewersMap[review.ReviewerID] = append(reviewersMap[review.ReviewerID], review) + } + } else if review.ReviewerTeamID != 0 && review.OriginalAuthorID == 0 { + reviewTeamsMap[review.ReviewerTeamID] = append(reviewTeamsMap[review.ReviewerTeamID], review) + } + } + + individualReviews := make([]*Review, 0, 10) + for _, reviews := range reviewersMap { + individualReviews = append(individualReviews, reviews[len(reviews)-1]) + } + sort.Slice(individualReviews, func(i, j int) bool { + return individualReviews[i].UpdatedUnix < individualReviews[j].UpdatedUnix + }) + + originalReviews := make([]*Review, 0, 10) + for _, reviews := range originalReviewersMap { + originalReviews = append(originalReviews, reviews[len(reviews)-1]) + } + sort.Slice(originalReviews, func(i, j int) bool { + return originalReviews[i].UpdatedUnix < originalReviews[j].UpdatedUnix + }) + teamReviewRequests := make([]*Review, 0, 5) - if err := sess.SQL("SELECT * FROM review WHERE id IN (SELECT max(id) as id FROM review WHERE issue_id = ? AND reviewer_team_id <> 0 AND original_author_id = 0 GROUP BY issue_id, reviewer_team_id) ORDER BY review.updated_unix ASC", - issueID). - Find(&teamReviewRequests); err != nil { - return nil, err + for _, reviews := range reviewTeamsMap { + teamReviewRequests = append(teamReviewRequests, reviews[len(reviews)-1]) } + sort.Slice(teamReviewRequests, func(i, j int) bool { + return teamReviewRequests[i].UpdatedUnix < teamReviewRequests[j].UpdatedUnix + }) - if len(teamReviewRequests) > 0 { - reviews = append(reviews, teamReviewRequests...) - } - - return reviews, nil + return append(individualReviews, teamReviewRequests...), originalReviews, nil } diff --git a/models/issues/review_test.go b/models/issues/review_test.go index 942121fd8f2..7b8537cc7df 100644 --- a/models/issues/review_test.go +++ b/models/issues/review_test.go @@ -6,7 +6,6 @@ package issues_test import ( "testing" - "code.gitea.io/gitea/models/db" issues_model "code.gitea.io/gitea/models/issues" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" @@ -17,12 +16,12 @@ import ( func TestGetReviewByID(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - review, err := issues_model.GetReviewByID(db.DefaultContext, 1) + review, err := issues_model.GetReviewByID(t.Context(), 1) assert.NoError(t, err) assert.Equal(t, "Demo Review", review.Content) assert.Equal(t, issues_model.ReviewTypeApprove, review.Type) - _, err = issues_model.GetReviewByID(db.DefaultContext, 23892) + _, err = issues_model.GetReviewByID(t.Context(), 23892) assert.Error(t, err) assert.True(t, issues_model.IsErrReviewNotExist(err), "IsErrReviewNotExist") } @@ -30,23 +29,23 @@ func TestGetReviewByID(t *testing.T) { func TestReview_LoadAttributes(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) review := unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: 1}) - assert.NoError(t, review.LoadAttributes(db.DefaultContext)) + assert.NoError(t, review.LoadAttributes(t.Context())) assert.NotNil(t, review.Issue) assert.NotNil(t, review.Reviewer) invalidReview1 := unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: 2}) - assert.Error(t, invalidReview1.LoadAttributes(db.DefaultContext)) + assert.Error(t, invalidReview1.LoadAttributes(t.Context())) invalidReview2 := unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: 3}) - assert.Error(t, invalidReview2.LoadAttributes(db.DefaultContext)) + assert.Error(t, invalidReview2.LoadAttributes(t.Context())) } func TestReview_LoadCodeComments(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) review := unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: 4}) - assert.NoError(t, review.LoadAttributes(db.DefaultContext)) - assert.NoError(t, review.LoadCodeComments(db.DefaultContext)) + assert.NoError(t, review.LoadAttributes(t.Context())) + assert.NoError(t, review.LoadCodeComments(t.Context())) assert.Len(t, review.CodeComments, 1) assert.Equal(t, int64(4), review.CodeComments["README.md"][int64(4)][0].Line) } @@ -62,7 +61,7 @@ func TestReviewType_Icon(t *testing.T) { func TestFindReviews(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - reviews, err := issues_model.FindReviews(db.DefaultContext, issues_model.FindReviewOptions{ + reviews, err := issues_model.FindReviews(t.Context(), issues_model.FindReviewOptions{ Types: []issues_model.ReviewType{issues_model.ReviewTypeApprove}, IssueID: 2, ReviewerID: 1, @@ -74,7 +73,7 @@ func TestFindReviews(t *testing.T) { func TestFindLatestReviews(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - reviews, err := issues_model.FindLatestReviews(db.DefaultContext, issues_model.FindReviewOptions{ + reviews, err := issues_model.FindLatestReviews(t.Context(), issues_model.FindReviewOptions{ Types: []issues_model.ReviewType{issues_model.ReviewTypeApprove}, IssueID: 11, }) @@ -89,14 +88,14 @@ func TestGetCurrentReview(t *testing.T) { issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2}) user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) - review, err := issues_model.GetCurrentReview(db.DefaultContext, user, issue) + review, err := issues_model.GetCurrentReview(t.Context(), user, issue) assert.NoError(t, err) assert.NotNil(t, review) assert.Equal(t, issues_model.ReviewTypePending, review.Type) assert.Equal(t, "Pending Review", review.Content) user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 7}) - review2, err := issues_model.GetCurrentReview(db.DefaultContext, user2, issue) + review2, err := issues_model.GetCurrentReview(t.Context(), user2, issue) assert.Error(t, err) assert.True(t, issues_model.IsErrReviewNotExist(err)) assert.Nil(t, review2) @@ -108,7 +107,7 @@ func TestCreateReview(t *testing.T) { issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2}) user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) - review, err := issues_model.CreateReview(db.DefaultContext, issues_model.CreateReviewOptions{ + review, err := issues_model.CreateReview(t.Context(), issues_model.CreateReviewOptions{ Content: "New Review", Type: issues_model.ReviewTypePending, Issue: issue, @@ -126,42 +125,49 @@ func TestGetReviewersByIssueID(t *testing.T) { user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}) user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) + user5 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5}) expectedReviews := []*issues_model.Review{} expectedReviews = append(expectedReviews, &issues_model.Review{ + ID: 7, Reviewer: org3, Type: issues_model.ReviewTypeReject, UpdatedUnix: 946684812, }, &issues_model.Review{ + ID: 8, Reviewer: user4, Type: issues_model.ReviewTypeApprove, UpdatedUnix: 946684813, }, &issues_model.Review{ + ID: 9, Reviewer: user2, Type: issues_model.ReviewTypeReject, UpdatedUnix: 946684814, - }) + }, + &issues_model.Review{ + ID: 10, + Reviewer: user_model.NewGhostUser(), + Type: issues_model.ReviewTypeReject, + UpdatedUnix: 946684815, + }, + &issues_model.Review{ + ID: 22, + Reviewer: user5, + Type: issues_model.ReviewTypeRequest, + UpdatedUnix: 946684817, + }, + ) - allReviews, err := issues_model.GetReviewsByIssueID(db.DefaultContext, issue.ID) + allReviews, migratedReviews, err := issues_model.GetReviewsByIssueID(t.Context(), issue.ID) assert.NoError(t, err) + assert.Empty(t, migratedReviews) for _, review := range allReviews { - assert.NoError(t, review.LoadReviewer(db.DefaultContext)) + assert.NoError(t, review.LoadReviewer(t.Context())) } - if assert.Len(t, allReviews, 3) { - for i, review := range allReviews { - assert.Equal(t, expectedReviews[i].Reviewer, review.Reviewer) - assert.Equal(t, expectedReviews[i].Type, review.Type) - assert.Equal(t, expectedReviews[i].UpdatedUnix, review.UpdatedUnix) - } - } - - allReviews, err = issues_model.GetReviewsByIssueID(db.DefaultContext, issue.ID) - assert.NoError(t, err) - assert.NoError(t, allReviews.LoadReviewers(db.DefaultContext)) - if assert.Len(t, allReviews, 3) { + if assert.Len(t, allReviews, 5) { for i, review := range allReviews { assert.Equal(t, expectedReviews[i].Reviewer, review.Reviewer) assert.Equal(t, expectedReviews[i].Type, review.Type) @@ -180,46 +186,46 @@ func TestDismissReview(t *testing.T) { assert.False(t, requestReviewExample.Dismissed) assert.False(t, approveReviewExample.Dismissed) - assert.NoError(t, issues_model.DismissReview(db.DefaultContext, rejectReviewExample, true)) + assert.NoError(t, issues_model.DismissReview(t.Context(), rejectReviewExample, true)) rejectReviewExample = unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: 9}) requestReviewExample = unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: 11}) assert.True(t, rejectReviewExample.Dismissed) assert.False(t, requestReviewExample.Dismissed) - assert.NoError(t, issues_model.DismissReview(db.DefaultContext, requestReviewExample, true)) + assert.NoError(t, issues_model.DismissReview(t.Context(), requestReviewExample, true)) rejectReviewExample = unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: 9}) requestReviewExample = unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: 11}) assert.True(t, rejectReviewExample.Dismissed) assert.False(t, requestReviewExample.Dismissed) assert.False(t, approveReviewExample.Dismissed) - assert.NoError(t, issues_model.DismissReview(db.DefaultContext, requestReviewExample, true)) + assert.NoError(t, issues_model.DismissReview(t.Context(), requestReviewExample, true)) rejectReviewExample = unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: 9}) requestReviewExample = unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: 11}) assert.True(t, rejectReviewExample.Dismissed) assert.False(t, requestReviewExample.Dismissed) assert.False(t, approveReviewExample.Dismissed) - assert.NoError(t, issues_model.DismissReview(db.DefaultContext, requestReviewExample, false)) + assert.NoError(t, issues_model.DismissReview(t.Context(), requestReviewExample, false)) rejectReviewExample = unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: 9}) requestReviewExample = unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: 11}) assert.True(t, rejectReviewExample.Dismissed) assert.False(t, requestReviewExample.Dismissed) assert.False(t, approveReviewExample.Dismissed) - assert.NoError(t, issues_model.DismissReview(db.DefaultContext, requestReviewExample, false)) + assert.NoError(t, issues_model.DismissReview(t.Context(), requestReviewExample, false)) rejectReviewExample = unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: 9}) requestReviewExample = unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: 11}) assert.True(t, rejectReviewExample.Dismissed) assert.False(t, requestReviewExample.Dismissed) assert.False(t, approveReviewExample.Dismissed) - assert.NoError(t, issues_model.DismissReview(db.DefaultContext, rejectReviewExample, false)) + assert.NoError(t, issues_model.DismissReview(t.Context(), rejectReviewExample, false)) assert.False(t, rejectReviewExample.Dismissed) assert.False(t, requestReviewExample.Dismissed) assert.False(t, approveReviewExample.Dismissed) - assert.NoError(t, issues_model.DismissReview(db.DefaultContext, approveReviewExample, true)) + assert.NoError(t, issues_model.DismissReview(t.Context(), approveReviewExample, true)) assert.False(t, rejectReviewExample.Dismissed) assert.False(t, requestReviewExample.Dismissed) assert.True(t, approveReviewExample.Dismissed) @@ -231,7 +237,7 @@ func TestDeleteReview(t *testing.T) { issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2}) user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) - review1, err := issues_model.CreateReview(db.DefaultContext, issues_model.CreateReviewOptions{ + review1, err := issues_model.CreateReview(t.Context(), issues_model.CreateReviewOptions{ Content: "Official rejection", Type: issues_model.ReviewTypeReject, Official: false, @@ -240,7 +246,7 @@ func TestDeleteReview(t *testing.T) { }) assert.NoError(t, err) - review2, err := issues_model.CreateReview(db.DefaultContext, issues_model.CreateReviewOptions{ + review2, err := issues_model.CreateReview(t.Context(), issues_model.CreateReviewOptions{ Content: "Official approval", Type: issues_model.ReviewTypeApprove, Official: true, @@ -249,13 +255,13 @@ func TestDeleteReview(t *testing.T) { }) assert.NoError(t, err) - assert.NoError(t, issues_model.DeleteReview(db.DefaultContext, review2)) + assert.NoError(t, issues_model.DeleteReview(t.Context(), review2)) - _, err = issues_model.GetReviewByID(db.DefaultContext, review2.ID) + _, err = issues_model.GetReviewByID(t.Context(), review2.ID) assert.Error(t, err) assert.True(t, issues_model.IsErrReviewNotExist(err), "IsErrReviewNotExist") - review1, err = issues_model.GetReviewByID(db.DefaultContext, review1.ID) + review1, err = issues_model.GetReviewByID(t.Context(), review1.ID) assert.NoError(t, err) assert.True(t, review1.Official) } @@ -266,7 +272,7 @@ func TestDeleteDismissedReview(t *testing.T) { issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2}) user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID}) - review, err := issues_model.CreateReview(db.DefaultContext, issues_model.CreateReviewOptions{ + review, err := issues_model.CreateReview(t.Context(), issues_model.CreateReviewOptions{ Content: "reject", Type: issues_model.ReviewTypeReject, Official: false, @@ -274,8 +280,8 @@ func TestDeleteDismissedReview(t *testing.T) { Reviewer: user, }) assert.NoError(t, err) - assert.NoError(t, issues_model.DismissReview(db.DefaultContext, review, true)) - comment, err := issues_model.CreateComment(db.DefaultContext, &issues_model.CreateCommentOptions{ + assert.NoError(t, issues_model.DismissReview(t.Context(), review, true)) + comment, err := issues_model.CreateComment(t.Context(), &issues_model.CreateCommentOptions{ Type: issues_model.CommentTypeDismissReview, Doer: user, Repo: repo, @@ -285,7 +291,7 @@ func TestDeleteDismissedReview(t *testing.T) { }) assert.NoError(t, err) unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: comment.ID}) - assert.NoError(t, issues_model.DeleteReview(db.DefaultContext, review)) + assert.NoError(t, issues_model.DeleteReview(t.Context(), review)) unittest.AssertNotExistsBean(t, &issues_model.Comment{ID: comment.ID}) } @@ -293,11 +299,11 @@ func TestAddReviewRequest(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) pull := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1}) - assert.NoError(t, pull.LoadIssue(db.DefaultContext)) + assert.NoError(t, pull.LoadIssue(t.Context())) issue := pull.Issue - assert.NoError(t, issue.LoadRepo(db.DefaultContext)) + assert.NoError(t, issue.LoadRepo(t.Context())) reviewer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) - _, err := issues_model.CreateReview(db.DefaultContext, issues_model.CreateReviewOptions{ + _, err := issues_model.CreateReview(t.Context(), issues_model.CreateReviewOptions{ Issue: issue, Reviewer: reviewer, Type: issues_model.ReviewTypeReject, @@ -305,16 +311,16 @@ func TestAddReviewRequest(t *testing.T) { assert.NoError(t, err) pull.HasMerged = false - assert.NoError(t, pull.UpdateCols(db.DefaultContext, "has_merged")) + assert.NoError(t, pull.UpdateCols(t.Context(), "has_merged")) issue.IsClosed = true - _, err = issues_model.AddReviewRequest(db.DefaultContext, issue, reviewer, &user_model.User{}) + _, err = issues_model.AddReviewRequest(t.Context(), issue, reviewer, &user_model.User{}) assert.Error(t, err) assert.True(t, issues_model.IsErrReviewRequestOnClosedPR(err)) pull.HasMerged = true - assert.NoError(t, pull.UpdateCols(db.DefaultContext, "has_merged")) + assert.NoError(t, pull.UpdateCols(t.Context(), "has_merged")) issue.IsClosed = false - _, err = issues_model.AddReviewRequest(db.DefaultContext, issue, reviewer, &user_model.User{}) + _, err = issues_model.AddReviewRequest(t.Context(), issue, reviewer, &user_model.User{}) assert.Error(t, err) assert.True(t, issues_model.IsErrReviewRequestOnClosedPR(err)) } diff --git a/models/issues/stopwatch.go b/models/issues/stopwatch.go index 629af95b577..761b8f91a01 100644 --- a/models/issues/stopwatch.go +++ b/models/issues/stopwatch.go @@ -5,7 +5,6 @@ package issues import ( "context" - "fmt" "time" "code.gitea.io/gitea/models/db" @@ -15,20 +14,6 @@ import ( "code.gitea.io/gitea/modules/util" ) -// ErrIssueStopwatchNotExist represents an error that stopwatch is not exist -type ErrIssueStopwatchNotExist struct { - UserID int64 - IssueID int64 -} - -func (err ErrIssueStopwatchNotExist) Error() string { - return fmt.Sprintf("issue stopwatch doesn't exist[uid: %d, issue_id: %d", err.UserID, err.IssueID) -} - -func (err ErrIssueStopwatchNotExist) Unwrap() error { - return util.ErrNotExist -} - // Stopwatch represents a stopwatch for time tracking. type Stopwatch struct { ID int64 `xorm:"pk autoincr"` @@ -46,11 +31,6 @@ func (s Stopwatch) Seconds() int64 { return int64(timeutil.TimeStampNow() - s.CreatedUnix) } -// Duration returns a human-readable duration string based on local server time -func (s Stopwatch) Duration() string { - return util.SecToTime(s.Seconds()) -} - func getStopwatch(ctx context.Context, userID, issueID int64) (sw *Stopwatch, exists bool, err error) { sw = new(Stopwatch) exists, err = db.GetEngine(ctx). @@ -60,13 +40,11 @@ func getStopwatch(ctx context.Context, userID, issueID int64) (sw *Stopwatch, ex return sw, exists, err } -// UserIDCount is a simple coalition of UserID and Count type UserStopwatch struct { UserID int64 StopWatches []*Stopwatch } -// GetUIDsAndNotificationCounts between the two provided times func GetUIDsAndStopwatch(ctx context.Context) ([]*UserStopwatch, error) { sws := []*Stopwatch{} if err := db.GetEngine(ctx).Where("issue_id != 0").Find(&sws); err != nil { @@ -92,7 +70,7 @@ func GetUIDsAndStopwatch(ctx context.Context) ([]*UserStopwatch, error) { return res, nil } -// GetUserStopwatches return list of all stopwatches of a user +// GetUserStopwatches return list of the user's all stopwatches func GetUserStopwatches(ctx context.Context, userID int64, listOptions db.ListOptions) ([]*Stopwatch, error) { sws := make([]*Stopwatch, 0, 8) sess := db.GetEngine(ctx).Where("stopwatch.user_id = ?", userID) @@ -107,7 +85,7 @@ func GetUserStopwatches(ctx context.Context, userID int64, listOptions db.ListOp return sws, nil } -// CountUserStopwatches return count of all stopwatches of a user +// CountUserStopwatches return count of the user's all stopwatches func CountUserStopwatches(ctx context.Context, userID int64) (int64, error) { return db.GetEngine(ctx).Where("user_id = ?", userID).Count(&Stopwatch{}) } @@ -141,43 +119,21 @@ func HasUserStopwatch(ctx context.Context, userID int64) (exists bool, sw *Stopw return exists, sw, issue, err } -// FinishIssueStopwatchIfPossible if stopwatch exist then finish it otherwise ignore -func FinishIssueStopwatchIfPossible(ctx context.Context, user *user_model.User, issue *Issue) error { - _, exists, err := getStopwatch(ctx, user.ID, issue.ID) - if err != nil { - return err - } - if !exists { - return nil - } - return FinishIssueStopwatch(ctx, user, issue) -} - -// CreateOrStopIssueStopwatch create an issue stopwatch if it's not exist, otherwise finish it -func CreateOrStopIssueStopwatch(ctx context.Context, user *user_model.User, issue *Issue) error { - _, exists, err := getStopwatch(ctx, user.ID, issue.ID) - if err != nil { - return err - } - if exists { - return FinishIssueStopwatch(ctx, user, issue) - } - return CreateIssueStopwatch(ctx, user, issue) -} - -// FinishIssueStopwatch if stopwatch exist then finish it otherwise return an error -func FinishIssueStopwatch(ctx context.Context, user *user_model.User, issue *Issue) error { +// FinishIssueStopwatch if stopwatch exists, then finish it. +func FinishIssueStopwatch(ctx context.Context, user *user_model.User, issue *Issue) (ok bool, err error) { sw, exists, err := getStopwatch(ctx, user.ID, issue.ID) if err != nil { - return err + return false, err + } else if !exists { + return false, nil } - if !exists { - return ErrIssueStopwatchNotExist{ - UserID: user.ID, - IssueID: issue.ID, - } + if err = finishIssueStopwatch(ctx, user, issue, sw); err != nil { + return false, err } + return true, nil +} +func finishIssueStopwatch(ctx context.Context, user *user_model.User, issue *Issue, sw *Stopwatch) error { // Create tracked time out of the time difference between start date and actual date timediff := time.Now().Unix() - int64(sw.CreatedUnix) @@ -189,101 +145,81 @@ func FinishIssueStopwatch(ctx context.Context, user *user_model.User, issue *Iss Time: timediff, } - if err := db.Insert(ctx, tt); err != nil { - return err - } - if err := issue.LoadRepo(ctx); err != nil { return err } - + if err := db.Insert(ctx, tt); err != nil { + return err + } if _, err := CreateComment(ctx, &CreateCommentOptions{ Doer: user, Issue: issue, Repo: issue.Repo, - Content: util.SecToTime(timediff), + Content: util.SecToHours(timediff), Type: CommentTypeStopTracking, TimeID: tt.ID, }); err != nil { return err } - _, err = db.DeleteByBean(ctx, sw) + _, err := db.DeleteByBean(ctx, sw) return err } -// CreateIssueStopwatch creates a stopwatch if not exist, otherwise return an error -func CreateIssueStopwatch(ctx context.Context, user *user_model.User, issue *Issue) error { - if err := issue.LoadRepo(ctx); err != nil { - return err - } - - // if another stopwatch is running: stop it - exists, _, otherIssue, err := HasUserStopwatch(ctx, user.ID) - if err != nil { - return err - } - if exists { - if err := FinishIssueStopwatch(ctx, user, otherIssue); err != nil { - return err +// CreateIssueStopwatch creates a stopwatch if the issue doesn't have the user's stopwatch. +// It also stops any other stopwatch that might be running for the user. +func CreateIssueStopwatch(ctx context.Context, user *user_model.User, issue *Issue) (ok bool, err error) { + { // if another issue's stopwatch is running: stop it; if this issue has a stopwatch: return an error. + exists, otherStopWatch, otherIssue, err := HasUserStopwatch(ctx, user.ID) + if err != nil { + return false, err + } + if exists { + if otherStopWatch.IssueID == issue.ID { + // don't allow starting stopwatch for the same issue + return false, nil + } + // stop the other issue's stopwatch + if err = finishIssueStopwatch(ctx, user, otherIssue, otherStopWatch); err != nil { + return false, err + } } } - // Create stopwatch - sw := &Stopwatch{ - UserID: user.ID, - IssueID: issue.ID, + if err = issue.LoadRepo(ctx); err != nil { + return false, err } - - if err := db.Insert(ctx, sw); err != nil { - return err + if err = db.Insert(ctx, &Stopwatch{UserID: user.ID, IssueID: issue.ID}); err != nil { + return false, err } - - if err := issue.LoadRepo(ctx); err != nil { - return err - } - - if _, err := CreateComment(ctx, &CreateCommentOptions{ + if _, err = CreateComment(ctx, &CreateCommentOptions{ Doer: user, Issue: issue, Repo: issue.Repo, Type: CommentTypeStartTracking, }); err != nil { - return err + return false, err } - - return nil + return true, nil } // CancelStopwatch removes the given stopwatch and logs it into issue's timeline. -func CancelStopwatch(ctx context.Context, user *user_model.User, issue *Issue) error { - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return err - } - defer committer.Close() - if err := cancelStopwatch(ctx, user, issue); err != nil { - return err - } - return committer.Commit() -} - -func cancelStopwatch(ctx context.Context, user *user_model.User, issue *Issue) error { - e := db.GetEngine(ctx) - sw, exists, err := getStopwatch(ctx, user.ID, issue.ID) - if err != nil { - return err - } - - if exists { - if _, err := e.Delete(sw); err != nil { +func CancelStopwatch(ctx context.Context, user *user_model.User, issue *Issue) (ok bool, err error) { + err = db.WithTx(ctx, func(ctx context.Context) error { + e := db.GetEngine(ctx) + sw, exists, err := getStopwatch(ctx, user.ID, issue.ID) + if err != nil { return err + } else if !exists { + return nil } - if err := issue.LoadRepo(ctx); err != nil { + if err = issue.LoadRepo(ctx); err != nil { return err } - - if _, err := CreateComment(ctx, &CreateCommentOptions{ + if _, err = e.Delete(sw); err != nil { + return err + } + if _, err = CreateComment(ctx, &CreateCommentOptions{ Doer: user, Issue: issue, Repo: issue.Repo, @@ -291,6 +227,8 @@ func cancelStopwatch(ctx context.Context, user *user_model.User, issue *Issue) e }); err != nil { return err } - } - return nil + ok = true + return nil + }) + return ok, err } diff --git a/models/issues/stopwatch_test.go b/models/issues/stopwatch_test.go index 39958a7f36b..684ec6cd319 100644 --- a/models/issues/stopwatch_test.go +++ b/models/issues/stopwatch_test.go @@ -6,11 +6,9 @@ package issues_test import ( "testing" - "code.gitea.io/gitea/models/db" issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/modules/timeutil" "github.com/stretchr/testify/assert" ) @@ -18,39 +16,35 @@ import ( func TestCancelStopwatch(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - user1, err := user_model.GetUserByID(db.DefaultContext, 1) - assert.NoError(t, err) + user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) + issue1 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1}) - issue1, err := issues_model.GetIssueByID(db.DefaultContext, 1) - assert.NoError(t, err) - issue2, err := issues_model.GetIssueByID(db.DefaultContext, 2) - assert.NoError(t, err) - - err = issues_model.CancelStopwatch(db.DefaultContext, user1, issue1) + ok, err := issues_model.CancelStopwatch(t.Context(), user1, issue1) assert.NoError(t, err) + assert.True(t, ok) unittest.AssertNotExistsBean(t, &issues_model.Stopwatch{UserID: user1.ID, IssueID: issue1.ID}) + unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{Type: issues_model.CommentTypeCancelTracking, PosterID: user1.ID, IssueID: issue1.ID}) - _ = unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{Type: issues_model.CommentTypeCancelTracking, PosterID: user1.ID, IssueID: issue1.ID}) - - assert.Nil(t, issues_model.CancelStopwatch(db.DefaultContext, user1, issue2)) + ok, err = issues_model.CancelStopwatch(t.Context(), user1, issue1) + assert.NoError(t, err) + assert.False(t, ok) } func TestStopwatchExists(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - - assert.True(t, issues_model.StopwatchExists(db.DefaultContext, 1, 1)) - assert.False(t, issues_model.StopwatchExists(db.DefaultContext, 1, 2)) + assert.True(t, issues_model.StopwatchExists(t.Context(), 1, 1)) + assert.False(t, issues_model.StopwatchExists(t.Context(), 1, 2)) } func TestHasUserStopwatch(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - exists, sw, _, err := issues_model.HasUserStopwatch(db.DefaultContext, 1) + exists, sw, _, err := issues_model.HasUserStopwatch(t.Context(), 1) assert.NoError(t, err) assert.True(t, exists) assert.Equal(t, int64(1), sw.ID) - exists, _, _, err = issues_model.HasUserStopwatch(db.DefaultContext, 3) + exists, _, _, err = issues_model.HasUserStopwatch(t.Context(), 3) assert.NoError(t, err) assert.False(t, exists) } @@ -58,21 +52,35 @@ func TestHasUserStopwatch(t *testing.T) { func TestCreateOrStopIssueStopwatch(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - user2, err := user_model.GetUserByID(db.DefaultContext, 2) - assert.NoError(t, err) - org3, err := user_model.GetUserByID(db.DefaultContext, 3) - assert.NoError(t, err) + user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) + issue1 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1}) + issue3 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 3}) - issue1, err := issues_model.GetIssueByID(db.DefaultContext, 1) + // create a new stopwatch + ok, err := issues_model.CreateIssueStopwatch(t.Context(), user4, issue1) assert.NoError(t, err) - issue2, err := issues_model.GetIssueByID(db.DefaultContext, 2) + assert.True(t, ok) + unittest.AssertExistsAndLoadBean(t, &issues_model.Stopwatch{UserID: user4.ID, IssueID: issue1.ID}) + // should not create a second stopwatch for the same issue + ok, err = issues_model.CreateIssueStopwatch(t.Context(), user4, issue1) assert.NoError(t, err) + assert.False(t, ok) + // on a different issue, it will finish the existing stopwatch and create a new one + ok, err = issues_model.CreateIssueStopwatch(t.Context(), user4, issue3) + assert.NoError(t, err) + assert.True(t, ok) + unittest.AssertNotExistsBean(t, &issues_model.Stopwatch{UserID: user4.ID, IssueID: issue1.ID}) + unittest.AssertExistsAndLoadBean(t, &issues_model.Stopwatch{UserID: user4.ID, IssueID: issue3.ID}) - assert.NoError(t, issues_model.CreateOrStopIssueStopwatch(db.DefaultContext, org3, issue1)) - sw := unittest.AssertExistsAndLoadBean(t, &issues_model.Stopwatch{UserID: 3, IssueID: 1}) - assert.LessOrEqual(t, sw.CreatedUnix, timeutil.TimeStampNow()) - - assert.NoError(t, issues_model.CreateOrStopIssueStopwatch(db.DefaultContext, user2, issue2)) - unittest.AssertNotExistsBean(t, &issues_model.Stopwatch{UserID: 2, IssueID: 2}) - unittest.AssertExistsAndLoadBean(t, &issues_model.TrackedTime{UserID: 2, IssueID: 2}) + // user2 already has a stopwatch in test fixture + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + issue2 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2}) + ok, err = issues_model.FinishIssueStopwatch(t.Context(), user2, issue2) + assert.NoError(t, err) + assert.True(t, ok) + unittest.AssertNotExistsBean(t, &issues_model.Stopwatch{UserID: user2.ID, IssueID: issue2.ID}) + unittest.AssertExistsAndLoadBean(t, &issues_model.TrackedTime{UserID: user2.ID, IssueID: issue2.ID}) + ok, err = issues_model.FinishIssueStopwatch(t.Context(), user2, issue2) + assert.NoError(t, err) + assert.False(t, ok) } diff --git a/models/issues/tracked_time.go b/models/issues/tracked_time.go index ea404d36cd1..9c11881e442 100644 --- a/models/issues/tracked_time.go +++ b/models/issues/tracked_time.go @@ -168,35 +168,31 @@ func GetTrackedSeconds(ctx context.Context, opts FindTrackedTimesOptions) (track // AddTime will add the given time (in seconds) to the issue func AddTime(ctx context.Context, user *user_model.User, issue *Issue, amount int64, created time.Time) (*TrackedTime, error) { - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return nil, err - } - defer committer.Close() + return db.WithTx2(ctx, func(ctx context.Context) (*TrackedTime, error) { + t, err := addTime(ctx, user, issue, amount, created) + if err != nil { + return nil, err + } - t, err := addTime(ctx, user, issue, amount, created) - if err != nil { - return nil, err - } + if err := issue.LoadRepo(ctx); err != nil { + return nil, err + } - if err := issue.LoadRepo(ctx); err != nil { - return nil, err - } + if _, err := CreateComment(ctx, &CreateCommentOptions{ + Issue: issue, + Repo: issue.Repo, + Doer: user, + // Content before v1.21 did store the formatted string instead of seconds, + // so use "|" as delimiter to mark the new format + Content: fmt.Sprintf("|%d", amount), + Type: CommentTypeAddTimeManual, + TimeID: t.ID, + }); err != nil { + return nil, err + } - if _, err := CreateComment(ctx, &CreateCommentOptions{ - Issue: issue, - Repo: issue.Repo, - Doer: user, - // Content before v1.21 did store the formatted string instead of seconds, - // so use "|" as delimiter to mark the new format - Content: fmt.Sprintf("|%d", amount), - Type: CommentTypeAddTimeManual, - TimeID: t.ID, - }); err != nil { - return nil, err - } - - return t, committer.Commit() + return t, nil + }) } func addTime(ctx context.Context, user *user_model.User, issue *Issue, amount int64, created time.Time) (*TrackedTime, error) { @@ -241,72 +237,58 @@ func TotalTimesForEachUser(ctx context.Context, options *FindTrackedTimesOptions // DeleteIssueUserTimes deletes times for issue func DeleteIssueUserTimes(ctx context.Context, issue *Issue, user *user_model.User) error { - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return err - } - defer committer.Close() + return db.WithTx(ctx, func(ctx context.Context) error { + opts := FindTrackedTimesOptions{ + IssueID: issue.ID, + UserID: user.ID, + } - opts := FindTrackedTimesOptions{ - IssueID: issue.ID, - UserID: user.ID, - } + removedTime, err := deleteTimes(ctx, opts) + if err != nil { + return err + } + if removedTime == 0 { + return db.ErrNotExist{Resource: "tracked_time"} + } - removedTime, err := deleteTimes(ctx, opts) - if err != nil { + if err := issue.LoadRepo(ctx); err != nil { + return err + } + _, err = CreateComment(ctx, &CreateCommentOptions{ + Issue: issue, + Repo: issue.Repo, + Doer: user, + // Content before v1.21 did store the formatted string instead of seconds, + // so use "|" as delimiter to mark the new format + Content: fmt.Sprintf("|%d", removedTime), + Type: CommentTypeDeleteTimeManual, + }) return err - } - if removedTime == 0 { - return db.ErrNotExist{Resource: "tracked_time"} - } - - if err := issue.LoadRepo(ctx); err != nil { - return err - } - if _, err := CreateComment(ctx, &CreateCommentOptions{ - Issue: issue, - Repo: issue.Repo, - Doer: user, - // Content before v1.21 did store the formatted string instead of seconds, - // so use "|" as delimiter to mark the new format - Content: fmt.Sprintf("|%d", removedTime), - Type: CommentTypeDeleteTimeManual, - }); err != nil { - return err - } - - return committer.Commit() + }) } // DeleteTime delete a specific Time func DeleteTime(ctx context.Context, t *TrackedTime) error { - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return err - } - defer committer.Close() + return db.WithTx(ctx, func(ctx context.Context) error { + if err := t.LoadAttributes(ctx); err != nil { + return err + } - if err := t.LoadAttributes(ctx); err != nil { - return err - } + if err := deleteTime(ctx, t); err != nil { + return err + } - if err := deleteTime(ctx, t); err != nil { + _, err := CreateComment(ctx, &CreateCommentOptions{ + Issue: t.Issue, + Repo: t.Issue.Repo, + Doer: t.User, + // Content before v1.21 did store the formatted string instead of seconds, + // so use "|" as delimiter to mark the new format + Content: fmt.Sprintf("|%d", t.Time), + Type: CommentTypeDeleteTimeManual, + }) return err - } - - if _, err := CreateComment(ctx, &CreateCommentOptions{ - Issue: t.Issue, - Repo: t.Issue.Repo, - Doer: t.User, - // Content before v1.21 did store the formatted string instead of seconds, - // so use "|" as delimiter to mark the new format - Content: fmt.Sprintf("|%d", t.Time), - Type: CommentTypeDeleteTimeManual, - }); err != nil { - return err - } - - return committer.Commit() + }) } func deleteTimes(ctx context.Context, opts FindTrackedTimesOptions) (removedTime int64, err error) { @@ -350,10 +332,7 @@ func GetIssueTotalTrackedTime(ctx context.Context, opts *IssuesOptions, isClosed // we get the statistics in smaller chunks and get accumulates var accum int64 for i := 0; i < len(opts.IssueIDs); { - chunk := i + MaxQueryParameters - if chunk > len(opts.IssueIDs) { - chunk = len(opts.IssueIDs) - } + chunk := min(i+MaxQueryParameters, len(opts.IssueIDs)) time, err := getIssueTotalTrackedTimeChunk(ctx, opts, isClosed, opts.IssueIDs[i:chunk]) if err != nil { return 0, err diff --git a/models/issues/tracked_time_test.go b/models/issues/tracked_time_test.go index d82bff967a9..ef7c72958fc 100644 --- a/models/issues/tracked_time_test.go +++ b/models/issues/tracked_time_test.go @@ -7,7 +7,6 @@ import ( "testing" "time" - "code.gitea.io/gitea/models/db" issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" @@ -19,14 +18,14 @@ import ( func TestAddTime(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - org3, err := user_model.GetUserByID(db.DefaultContext, 3) + org3, err := user_model.GetUserByID(t.Context(), 3) assert.NoError(t, err) - issue1, err := issues_model.GetIssueByID(db.DefaultContext, 1) + issue1, err := issues_model.GetIssueByID(t.Context(), 1) assert.NoError(t, err) // 3661 = 1h 1min 1s - trackedTime, err := issues_model.AddTime(db.DefaultContext, org3, issue1, 3661, time.Now()) + trackedTime, err := issues_model.AddTime(t.Context(), org3, issue1, 3661, time.Now()) assert.NoError(t, err) assert.Equal(t, int64(3), trackedTime.UserID) assert.Equal(t, int64(1), trackedTime.IssueID) @@ -43,47 +42,47 @@ func TestGetTrackedTimes(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) // by Issue - times, err := issues_model.GetTrackedTimes(db.DefaultContext, &issues_model.FindTrackedTimesOptions{IssueID: 1}) + times, err := issues_model.GetTrackedTimes(t.Context(), &issues_model.FindTrackedTimesOptions{IssueID: 1}) assert.NoError(t, err) assert.Len(t, times, 1) assert.Equal(t, int64(400), times[0].Time) - times, err = issues_model.GetTrackedTimes(db.DefaultContext, &issues_model.FindTrackedTimesOptions{IssueID: -1}) + times, err = issues_model.GetTrackedTimes(t.Context(), &issues_model.FindTrackedTimesOptions{IssueID: -1}) assert.NoError(t, err) - assert.Len(t, times, 0) + assert.Empty(t, times) // by User - times, err = issues_model.GetTrackedTimes(db.DefaultContext, &issues_model.FindTrackedTimesOptions{UserID: 1}) + times, err = issues_model.GetTrackedTimes(t.Context(), &issues_model.FindTrackedTimesOptions{UserID: 1}) assert.NoError(t, err) assert.Len(t, times, 3) assert.Equal(t, int64(400), times[0].Time) - times, err = issues_model.GetTrackedTimes(db.DefaultContext, &issues_model.FindTrackedTimesOptions{UserID: 3}) + times, err = issues_model.GetTrackedTimes(t.Context(), &issues_model.FindTrackedTimesOptions{UserID: 3}) assert.NoError(t, err) - assert.Len(t, times, 0) + assert.Empty(t, times) // by Repo - times, err = issues_model.GetTrackedTimes(db.DefaultContext, &issues_model.FindTrackedTimesOptions{RepositoryID: 2}) + times, err = issues_model.GetTrackedTimes(t.Context(), &issues_model.FindTrackedTimesOptions{RepositoryID: 2}) assert.NoError(t, err) assert.Len(t, times, 3) assert.Equal(t, int64(1), times[0].Time) - issue, err := issues_model.GetIssueByID(db.DefaultContext, times[0].IssueID) + issue, err := issues_model.GetIssueByID(t.Context(), times[0].IssueID) assert.NoError(t, err) - assert.Equal(t, issue.RepoID, int64(2)) + assert.Equal(t, int64(2), issue.RepoID) - times, err = issues_model.GetTrackedTimes(db.DefaultContext, &issues_model.FindTrackedTimesOptions{RepositoryID: 1}) + times, err = issues_model.GetTrackedTimes(t.Context(), &issues_model.FindTrackedTimesOptions{RepositoryID: 1}) assert.NoError(t, err) assert.Len(t, times, 5) - times, err = issues_model.GetTrackedTimes(db.DefaultContext, &issues_model.FindTrackedTimesOptions{RepositoryID: 10}) + times, err = issues_model.GetTrackedTimes(t.Context(), &issues_model.FindTrackedTimesOptions{RepositoryID: 10}) assert.NoError(t, err) - assert.Len(t, times, 0) + assert.Empty(t, times) } func TestTotalTimesForEachUser(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - total, err := issues_model.TotalTimesForEachUser(db.DefaultContext, &issues_model.FindTrackedTimesOptions{IssueID: 1}) + total, err := issues_model.TotalTimesForEachUser(t.Context(), &issues_model.FindTrackedTimesOptions{IssueID: 1}) assert.NoError(t, err) assert.Len(t, total, 1) for user, time := range total { @@ -91,7 +90,7 @@ func TestTotalTimesForEachUser(t *testing.T) { assert.EqualValues(t, 400, time) } - total, err = issues_model.TotalTimesForEachUser(db.DefaultContext, &issues_model.FindTrackedTimesOptions{IssueID: 2}) + total, err = issues_model.TotalTimesForEachUser(t.Context(), &issues_model.FindTrackedTimesOptions{IssueID: 2}) assert.NoError(t, err) assert.Len(t, total, 2) for user, time := range total { @@ -104,7 +103,7 @@ func TestTotalTimesForEachUser(t *testing.T) { } } - total, err = issues_model.TotalTimesForEachUser(db.DefaultContext, &issues_model.FindTrackedTimesOptions{IssueID: 5}) + total, err = issues_model.TotalTimesForEachUser(t.Context(), &issues_model.FindTrackedTimesOptions{IssueID: 5}) assert.NoError(t, err) assert.Len(t, total, 1) for user, time := range total { @@ -112,7 +111,7 @@ func TestTotalTimesForEachUser(t *testing.T) { assert.EqualValues(t, 1, time) } - total, err = issues_model.TotalTimesForEachUser(db.DefaultContext, &issues_model.FindTrackedTimesOptions{IssueID: 4}) + total, err = issues_model.TotalTimesForEachUser(t.Context(), &issues_model.FindTrackedTimesOptions{IssueID: 4}) assert.NoError(t, err) assert.Len(t, total, 2) } @@ -120,15 +119,15 @@ func TestTotalTimesForEachUser(t *testing.T) { func TestGetIssueTotalTrackedTime(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - ttt, err := issues_model.GetIssueTotalTrackedTime(db.DefaultContext, &issues_model.IssuesOptions{MilestoneIDs: []int64{1}}, optional.Some(false)) + ttt, err := issues_model.GetIssueTotalTrackedTime(t.Context(), &issues_model.IssuesOptions{MilestoneIDs: []int64{1}}, optional.Some(false)) assert.NoError(t, err) assert.EqualValues(t, 3682, ttt) - ttt, err = issues_model.GetIssueTotalTrackedTime(db.DefaultContext, &issues_model.IssuesOptions{MilestoneIDs: []int64{1}}, optional.Some(true)) + ttt, err = issues_model.GetIssueTotalTrackedTime(t.Context(), &issues_model.IssuesOptions{MilestoneIDs: []int64{1}}, optional.Some(true)) assert.NoError(t, err) assert.EqualValues(t, 0, ttt) - ttt, err = issues_model.GetIssueTotalTrackedTime(db.DefaultContext, &issues_model.IssuesOptions{MilestoneIDs: []int64{1}}, optional.None[bool]()) + ttt, err = issues_model.GetIssueTotalTrackedTime(t.Context(), &issues_model.IssuesOptions{MilestoneIDs: []int64{1}}, optional.None[bool]()) assert.NoError(t, err) assert.EqualValues(t, 3682, ttt) } diff --git a/models/migrations/base/db.go b/models/migrations/base/db.go index eb1c44a79e6..479a46379c2 100644 --- a/models/migrations/base/db.go +++ b/models/migrations/base/db.go @@ -52,7 +52,7 @@ func RecreateTable(sess *xorm.Session, bean any) error { // TODO: This will not work if there are foreign keys tableName := sess.Engine().TableName(bean) - tempTableName := fmt.Sprintf("tmp_recreate__%s", tableName) + tempTableName := "tmp_recreate__" + tableName // We need to move the old table away and create a new one with the correct columns // We will need to do this in stages to prevent data loss @@ -82,7 +82,7 @@ func RecreateTable(sess *xorm.Session, bean any) error { } newTableColumns := table.Columns() if len(newTableColumns) == 0 { - return fmt.Errorf("no columns in new table") + return errors.New("no columns in new table") } hasID := false for _, column := range newTableColumns { @@ -518,7 +518,7 @@ func ModifyColumn(x *xorm.Engine, tableName string, col *schemas.Column) error { func removeAllWithRetry(dir string) error { var err error - for i := 0; i < 20; i++ { + for range 20 { err = os.RemoveAll(dir) if err == nil { break @@ -552,11 +552,11 @@ func deleteDB() error { } defer db.Close() - if _, err = db.Exec(fmt.Sprintf("DROP DATABASE IF EXISTS %s", setting.Database.Name)); err != nil { + if _, err = db.Exec("DROP DATABASE IF EXISTS " + setting.Database.Name); err != nil { return err } - if _, err = db.Exec(fmt.Sprintf("CREATE DATABASE IF NOT EXISTS %s", setting.Database.Name)); err != nil { + if _, err = db.Exec("CREATE DATABASE IF NOT EXISTS " + setting.Database.Name); err != nil { return err } return nil @@ -568,11 +568,11 @@ func deleteDB() error { } defer db.Close() - if _, err = db.Exec(fmt.Sprintf("DROP DATABASE IF EXISTS %s", setting.Database.Name)); err != nil { + if _, err = db.Exec("DROP DATABASE IF EXISTS " + setting.Database.Name); err != nil { return err } - if _, err = db.Exec(fmt.Sprintf("CREATE DATABASE %s", setting.Database.Name)); err != nil { + if _, err = db.Exec("CREATE DATABASE " + setting.Database.Name); err != nil { return err } db.Close() @@ -594,7 +594,7 @@ func deleteDB() error { if !schrows.Next() { // Create and setup a DB schema - _, err = db.Exec(fmt.Sprintf("CREATE SCHEMA %s", setting.Database.Schema)) + _, err = db.Exec("CREATE SCHEMA " + setting.Database.Schema) if err != nil { return err } diff --git a/models/migrations/base/tests.go b/models/migrations/base/tests.go index c2134f702a5..3b52a5e7c72 100644 --- a/models/migrations/base/tests.go +++ b/models/migrations/base/tests.go @@ -1,11 +1,9 @@ // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -//nolint:forbidigo package base import ( - "context" "fmt" "os" "path/filepath" @@ -13,9 +11,10 @@ import ( "testing" "code.gitea.io/gitea/models/unittest" - "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/tempdir" + "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/modules/testlogger" "github.com/stretchr/testify/require" @@ -76,7 +75,7 @@ func PrepareTestEnv(t *testing.T, skip int, syncModels ...any) (*xorm.Engine, fu t.Errorf("error whilst initializing fixtures from %s: %v", fixturesDir, err) return x, deferFn } - if err := unittest.LoadFixtures(x); err != nil { + if err := unittest.LoadFixtures(); err != nil { t.Errorf("error whilst loading fixtures from %s: %v", fixturesDir, err) return x, deferFn } @@ -92,10 +91,7 @@ func PrepareTestEnv(t *testing.T, skip int, syncModels ...any) (*xorm.Engine, fu func MainTest(m *testing.M) { testlogger.Init() - giteaRoot := base.SetupGiteaRoot() - if giteaRoot == "" { - testlogger.Fatalf("Environment variable $GITEA_ROOT not set\n") - } + giteaRoot := test.SetupGiteaRoot() giteaBinary := "gitea" if runtime.GOOS == "windows" { giteaBinary += ".exe" @@ -108,7 +104,7 @@ func MainTest(m *testing.M) { giteaConf := os.Getenv("GITEA_CONF") if giteaConf == "" { giteaConf = filepath.Join(filepath.Dir(setting.AppPath), "tests/sqlite.ini") - fmt.Printf("Environment variable $GITEA_CONF not set - defaulting to %s\n", giteaConf) + _, _ = fmt.Fprintf(os.Stderr, "Environment variable $GITEA_CONF not set - defaulting to %s\n", giteaConf) } if !filepath.IsAbs(giteaConf) { @@ -117,16 +113,17 @@ func MainTest(m *testing.M) { setting.CustomConf = giteaConf } - tmpDataPath, err := os.MkdirTemp("", "data") + tmpDataPath, cleanup, err := tempdir.OsTempDir("gitea-test").MkdirTempRandom("data") if err != nil { testlogger.Fatalf("Unable to create temporary data path %v\n", err) } + defer cleanup() setting.CustomPath = filepath.Join(setting.AppWorkPath, "custom") setting.AppDataPath = tmpDataPath - unittest.InitSettings() - if err = git.InitFull(context.Background()); err != nil { + unittest.InitSettingsForTesting() + if err = git.InitFull(); err != nil { testlogger.Fatalf("Unable to InitFull: %v\n", err) } setting.LoadDBSetting() @@ -135,10 +132,7 @@ func MainTest(m *testing.M) { exitStatus := m.Run() if err := removeAllWithRetry(setting.RepoRootPath); err != nil { - fmt.Fprintf(os.Stderr, "os.RemoveAll: %v\n", err) - } - if err := removeAllWithRetry(tmpDataPath); err != nil { - fmt.Fprintf(os.Stderr, "os.RemoveAll: %v\n", err) + _, _ = fmt.Fprintf(os.Stderr, "os.RemoveAll: %v\n", err) } os.Exit(exitStatus) } diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index 4c3cefde7bf..88967a8b870 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -6,6 +6,7 @@ package migrations import ( "context" + "errors" "fmt" "code.gitea.io/gitea/models/migrations/v1_10" @@ -22,6 +23,8 @@ import ( "code.gitea.io/gitea/models/migrations/v1_21" "code.gitea.io/gitea/models/migrations/v1_22" "code.gitea.io/gitea/models/migrations/v1_23" + "code.gitea.io/gitea/models/migrations/v1_24" + "code.gitea.io/gitea/models/migrations/v1_25" "code.gitea.io/gitea/models/migrations/v1_6" "code.gitea.io/gitea/models/migrations/v1_7" "code.gitea.io/gitea/models/migrations/v1_8" @@ -39,17 +42,24 @@ const minDBVersion = 70 // Gitea 1.5.3 type migration struct { idNumber int64 // DB version is "the last migration's idNumber" + 1 description string - migrate func(*xorm.Engine) error + migrate func(context.Context, *xorm.Engine) error } // newMigration creates a new migration -func newMigration(idNumber int64, desc string, fn func(*xorm.Engine) error) *migration { - return &migration{idNumber, desc, fn} +func newMigration[T func(*xorm.Engine) error | func(context.Context, *xorm.Engine) error](idNumber int64, desc string, fn T) *migration { + m := &migration{idNumber: idNumber, description: desc} + var ok bool + if m.migrate, ok = any(fn).(func(context.Context, *xorm.Engine) error); !ok { + m.migrate = func(ctx context.Context, x *xorm.Engine) error { + return any(fn).(func(*xorm.Engine) error)(x) + } + } + return m } // Migrate executes the migration -func (m *migration) Migrate(x *xorm.Engine) error { - return m.migrate(x) +func (m *migration) Migrate(ctx context.Context, x *xorm.Engine) error { + return m.migrate(ctx, x) } // Version describes the version table. Should have only one row with id==1 @@ -368,6 +378,21 @@ func prepareMigrationTasks() []*migration { newMigration(308, "Add index(user_id, is_deleted) for action table", v1_23.AddNewIndexForUserDashboard), newMigration(309, "Improve Notification table indices", v1_23.ImproveNotificationTableIndices), newMigration(310, "Add Priority to ProtectedBranch", v1_23.AddPriorityToProtectedBranch), + newMigration(311, "Add TimeEstimate to Issue table", v1_23.AddTimeEstimateColumnToIssueTable), + + // Gitea 1.23.0-rc0 ends at migration ID number 311 (database version 312) + newMigration(312, "Add DeleteBranchAfterMerge to AutoMerge", v1_24.AddDeleteBranchAfterMergeForAutoMerge), + newMigration(313, "Move PinOrder from issue table to a new table issue_pin", v1_24.MovePinOrderToTableIssuePin), + newMigration(314, "Update OwnerID as zero for repository level action tables", v1_24.UpdateOwnerIDOfRepoLevelActionsTables), + newMigration(315, "Add Ephemeral to ActionRunner", v1_24.AddEphemeralToActionRunner), + newMigration(316, "Add description for secrets and variables", v1_24.AddDescriptionForSecretsAndVariables), + newMigration(317, "Add new index for action for heatmap", v1_24.AddNewIndexForUserDashboard), + newMigration(318, "Add anonymous_access_mode for repo_unit", v1_24.AddRepoUnitAnonymousAccessMode), + newMigration(319, "Add ExclusiveOrder to Label table", v1_24.AddExclusiveOrderColumnToLabelTable), + newMigration(320, "Migrate two_factor_policy to login_source table", v1_24.MigrateSkipTwoFactor), + + // Gitea 1.24.0 ends at database version 321 + newMigration(321, "Use LONGTEXT for some columns and fix review_state.updated_files column", v1_25.UseLongTextInSomeColumnsAndFixBugs), } return preparedMigrations } @@ -406,14 +431,14 @@ func ExpectedDBVersion() int64 { } // EnsureUpToDate will check if the db is at the correct version -func EnsureUpToDate(x *xorm.Engine) error { +func EnsureUpToDate(ctx context.Context, x *xorm.Engine) error { currentDB, err := GetCurrentDBVersion(x) if err != nil { return err } if currentDB < 0 { - return fmt.Errorf("database has not been initialized") + return errors.New("database has not been initialized") } if minDBVersion > currentDB { @@ -438,7 +463,7 @@ func migrationIDNumberToDBVersion(idNumber int64) int64 { } // Migrate database to current version -func Migrate(x *xorm.Engine) error { +func Migrate(ctx context.Context, x *xorm.Engine) error { migrations := prepareMigrationTasks() maxDBVer := calcDBVersion(migrations) @@ -482,10 +507,8 @@ Please try upgrading to a lower version first (suggested v1.6.4), then upgrade t } // Some migration tasks depend on the git command - if git.DefaultContext == nil { - if err = git.InitSimple(context.Background()); err != nil { - return err - } + if err = git.InitSimple(); err != nil { + return err } // Migrate @@ -493,7 +516,7 @@ Please try upgrading to a lower version first (suggested v1.6.4), then upgrade t log.Info("Migration[%d]: %s", m.idNumber, m.description) // Reset the mapper between each migration - migrations are not supposed to depend on each other x.SetMapper(names.GonicMapper{}) - if err = m.Migrate(x); err != nil { + if err = m.Migrate(ctx, x); err != nil { return fmt.Errorf("migration[%d]: %s failed: %w", m.idNumber, m.description, err) } currentVersion.Version = migrationIDNumberToDBVersion(m.idNumber) diff --git a/models/migrations/migrations_test.go b/models/migrations/migrations_test.go index e66b015b3d0..8649d116f58 100644 --- a/models/migrations/migrations_test.go +++ b/models/migrations/migrations_test.go @@ -22,7 +22,7 @@ func TestMigrations(t *testing.T) { assert.EqualValues(t, 71, migrationIDNumberToDBVersion(70)) - assert.EqualValues(t, []*migration{{idNumber: 70}, {idNumber: 71}}, getPendingMigrations(70, preparedMigrations)) - assert.EqualValues(t, []*migration{{idNumber: 71}}, getPendingMigrations(71, preparedMigrations)) - assert.EqualValues(t, []*migration{}, getPendingMigrations(72, preparedMigrations)) + assert.Equal(t, []*migration{{idNumber: 70}, {idNumber: 71}}, getPendingMigrations(70, preparedMigrations)) + assert.Equal(t, []*migration{{idNumber: 71}}, getPendingMigrations(71, preparedMigrations)) + assert.Equal(t, []*migration{}, getPendingMigrations(72, preparedMigrations)) } diff --git a/models/migrations/v1_10/v100.go b/models/migrations/v1_10/v100.go index 5d2fd8e2449..1742bea2963 100644 --- a/models/migrations/v1_10/v100.go +++ b/models/migrations/v1_10/v100.go @@ -1,7 +1,7 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_10 //nolint +package v1_10 import ( "net/url" diff --git a/models/migrations/v1_10/v101.go b/models/migrations/v1_10/v101.go index f023a2a0e77..6c8dfe24860 100644 --- a/models/migrations/v1_10/v101.go +++ b/models/migrations/v1_10/v101.go @@ -1,7 +1,7 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_10 //nolint +package v1_10 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_10/v88.go b/models/migrations/v1_10/v88.go index 7e86ac364f6..eb8e81c19ee 100644 --- a/models/migrations/v1_10/v88.go +++ b/models/migrations/v1_10/v88.go @@ -1,7 +1,7 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_10 //nolint +package v1_10 import ( "crypto/sha1" diff --git a/models/migrations/v1_10/v89.go b/models/migrations/v1_10/v89.go index d5f27ffdc65..0df2a6e17b6 100644 --- a/models/migrations/v1_10/v89.go +++ b/models/migrations/v1_10/v89.go @@ -1,7 +1,7 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_10 //nolint +package v1_10 import "xorm.io/xorm" diff --git a/models/migrations/v1_10/v90.go b/models/migrations/v1_10/v90.go index 295d4b1c1ba..5521a97e327 100644 --- a/models/migrations/v1_10/v90.go +++ b/models/migrations/v1_10/v90.go @@ -1,7 +1,7 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_10 //nolint +package v1_10 import "xorm.io/xorm" diff --git a/models/migrations/v1_10/v91.go b/models/migrations/v1_10/v91.go index 48cac2de707..08db6c27421 100644 --- a/models/migrations/v1_10/v91.go +++ b/models/migrations/v1_10/v91.go @@ -1,7 +1,7 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_10 //nolint +package v1_10 import "xorm.io/xorm" diff --git a/models/migrations/v1_10/v92.go b/models/migrations/v1_10/v92.go index 9080108594c..b6c04a92348 100644 --- a/models/migrations/v1_10/v92.go +++ b/models/migrations/v1_10/v92.go @@ -1,7 +1,7 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_10 //nolint +package v1_10 import ( "xorm.io/builder" diff --git a/models/migrations/v1_10/v93.go b/models/migrations/v1_10/v93.go index ee59a8db394..c131be9a8d8 100644 --- a/models/migrations/v1_10/v93.go +++ b/models/migrations/v1_10/v93.go @@ -1,7 +1,7 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_10 //nolint +package v1_10 import "xorm.io/xorm" diff --git a/models/migrations/v1_10/v94.go b/models/migrations/v1_10/v94.go index c131af162b3..13b7d7b303e 100644 --- a/models/migrations/v1_10/v94.go +++ b/models/migrations/v1_10/v94.go @@ -1,7 +1,7 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_10 //nolint +package v1_10 import "xorm.io/xorm" diff --git a/models/migrations/v1_10/v95.go b/models/migrations/v1_10/v95.go index 3b1f67fd9c9..86b52026bfa 100644 --- a/models/migrations/v1_10/v95.go +++ b/models/migrations/v1_10/v95.go @@ -1,7 +1,7 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_10 //nolint +package v1_10 import "xorm.io/xorm" diff --git a/models/migrations/v1_10/v96.go b/models/migrations/v1_10/v96.go index 34c8240031c..ca35a169c4a 100644 --- a/models/migrations/v1_10/v96.go +++ b/models/migrations/v1_10/v96.go @@ -1,7 +1,7 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_10 //nolint +package v1_10 import ( "path/filepath" diff --git a/models/migrations/v1_10/v97.go b/models/migrations/v1_10/v97.go index dee45b32e35..5872bb63e57 100644 --- a/models/migrations/v1_10/v97.go +++ b/models/migrations/v1_10/v97.go @@ -1,7 +1,7 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_10 //nolint +package v1_10 import "xorm.io/xorm" diff --git a/models/migrations/v1_10/v98.go b/models/migrations/v1_10/v98.go index bdd9aed0891..d21c3264595 100644 --- a/models/migrations/v1_10/v98.go +++ b/models/migrations/v1_10/v98.go @@ -1,7 +1,7 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_10 //nolint +package v1_10 import "xorm.io/xorm" diff --git a/models/migrations/v1_10/v99.go b/models/migrations/v1_10/v99.go index ebe6597f7c9..223c188057b 100644 --- a/models/migrations/v1_10/v99.go +++ b/models/migrations/v1_10/v99.go @@ -1,7 +1,7 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_10 //nolint +package v1_10 import ( "code.gitea.io/gitea/modules/timeutil" diff --git a/models/migrations/v1_11/v102.go b/models/migrations/v1_11/v102.go index 9358e4cef33..e52290afb0a 100644 --- a/models/migrations/v1_11/v102.go +++ b/models/migrations/v1_11/v102.go @@ -1,7 +1,7 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_11 //nolint +package v1_11 import ( "code.gitea.io/gitea/models/migrations/base" diff --git a/models/migrations/v1_11/v103.go b/models/migrations/v1_11/v103.go index 53527dac586..a5157101605 100644 --- a/models/migrations/v1_11/v103.go +++ b/models/migrations/v1_11/v103.go @@ -1,7 +1,7 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_11 //nolint +package v1_11 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_11/v104.go b/models/migrations/v1_11/v104.go index 3e8ee64bc11..3b0d3c64b2f 100644 --- a/models/migrations/v1_11/v104.go +++ b/models/migrations/v1_11/v104.go @@ -1,7 +1,7 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_11 //nolint +package v1_11 import ( "code.gitea.io/gitea/models/migrations/base" diff --git a/models/migrations/v1_11/v105.go b/models/migrations/v1_11/v105.go index b91340c30a9..d86973a0f68 100644 --- a/models/migrations/v1_11/v105.go +++ b/models/migrations/v1_11/v105.go @@ -1,7 +1,7 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_11 //nolint +package v1_11 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_11/v106.go b/models/migrations/v1_11/v106.go index ecb11cdd1e3..edffe186831 100644 --- a/models/migrations/v1_11/v106.go +++ b/models/migrations/v1_11/v106.go @@ -1,7 +1,7 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_11 //nolint +package v1_11 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_11/v107.go b/models/migrations/v1_11/v107.go index f0bfe5862c9..a158e3bb502 100644 --- a/models/migrations/v1_11/v107.go +++ b/models/migrations/v1_11/v107.go @@ -1,7 +1,7 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_11 //nolint +package v1_11 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_11/v108.go b/models/migrations/v1_11/v108.go index a85096234d2..8f14504cebf 100644 --- a/models/migrations/v1_11/v108.go +++ b/models/migrations/v1_11/v108.go @@ -1,7 +1,7 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_11 //nolint +package v1_11 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_11/v109.go b/models/migrations/v1_11/v109.go index ea565ccda37..f7616aec7b5 100644 --- a/models/migrations/v1_11/v109.go +++ b/models/migrations/v1_11/v109.go @@ -1,7 +1,7 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_11 //nolint +package v1_11 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_11/v110.go b/models/migrations/v1_11/v110.go index 81afa1331d4..512f728c035 100644 --- a/models/migrations/v1_11/v110.go +++ b/models/migrations/v1_11/v110.go @@ -1,7 +1,7 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_11 //nolint +package v1_11 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_11/v111.go b/models/migrations/v1_11/v111.go index ff108479a9d..c27465f0518 100644 --- a/models/migrations/v1_11/v111.go +++ b/models/migrations/v1_11/v111.go @@ -1,10 +1,11 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_11 //nolint +package v1_11 import ( "fmt" + "slices" "xorm.io/xorm" ) @@ -344,10 +345,8 @@ func AddBranchProtectionCanPushAndEnableWhitelist(x *xorm.Engine) error { } return AccessModeWrite <= perm.UnitsMode[UnitTypeCode], nil } - for _, id := range protectedBranch.ApprovalsWhitelistUserIDs { - if id == reviewer.ID { - return true, nil - } + if slices.Contains(protectedBranch.ApprovalsWhitelistUserIDs, reviewer.ID) { + return true, nil } // isUserInTeams diff --git a/models/migrations/v1_11/v112.go b/models/migrations/v1_11/v112.go index 08576631195..fe45cf92221 100644 --- a/models/migrations/v1_11/v112.go +++ b/models/migrations/v1_11/v112.go @@ -1,12 +1,12 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_11 //nolint +package v1_11 import ( - "fmt" "path/filepath" + "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" @@ -31,7 +31,7 @@ func RemoveAttachmentMissedRepo(x *xorm.Engine) error { for i := 0; i < len(attachments); i++ { uuid := attachments[i].UUID if err = util.RemoveAll(filepath.Join(setting.Attachment.Storage.Path, uuid[0:1], uuid[1:2], uuid)); err != nil { - fmt.Printf("Error: %v", err) //nolint:forbidigo + log.Warn("Unable to remove attachment file by UUID %s: %v", uuid, err) } } diff --git a/models/migrations/v1_11/v113.go b/models/migrations/v1_11/v113.go index dea344a44f2..a4d54f66fb4 100644 --- a/models/migrations/v1_11/v113.go +++ b/models/migrations/v1_11/v113.go @@ -1,7 +1,7 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_11 //nolint +package v1_11 import ( "fmt" diff --git a/models/migrations/v1_11/v114.go b/models/migrations/v1_11/v114.go index 95adcee989c..9467a8a90c9 100644 --- a/models/migrations/v1_11/v114.go +++ b/models/migrations/v1_11/v114.go @@ -1,7 +1,7 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_11 //nolint +package v1_11 import ( "net/url" diff --git a/models/migrations/v1_11/v115.go b/models/migrations/v1_11/v115.go index 8c631cfd0bb..5933c0520f6 100644 --- a/models/migrations/v1_11/v115.go +++ b/models/migrations/v1_11/v115.go @@ -1,7 +1,7 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_11 //nolint +package v1_11 import ( "crypto/md5" @@ -146,7 +146,7 @@ func copyOldAvatarToNewLocation(userID int64, oldAvatar string) (string, error) return "", fmt.Errorf("io.ReadAll: %w", err) } - newAvatar := fmt.Sprintf("%x", md5.Sum([]byte(fmt.Sprintf("%d-%x", userID, md5.Sum(data))))) + newAvatar := fmt.Sprintf("%x", md5.Sum(fmt.Appendf(nil, "%d-%x", userID, md5.Sum(data)))) if newAvatar == oldAvatar { return newAvatar, nil } diff --git a/models/migrations/v1_11/v116.go b/models/migrations/v1_11/v116.go index 85aa76c1e02..729fbad18b8 100644 --- a/models/migrations/v1_11/v116.go +++ b/models/migrations/v1_11/v116.go @@ -1,7 +1,7 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_11 //nolint +package v1_11 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_12/v117.go b/models/migrations/v1_12/v117.go index 8eadcdef2b3..73b58ca34b2 100644 --- a/models/migrations/v1_12/v117.go +++ b/models/migrations/v1_12/v117.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_12 //nolint +package v1_12 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_12/v118.go b/models/migrations/v1_12/v118.go index eb022dc5e48..e8b4249743d 100644 --- a/models/migrations/v1_12/v118.go +++ b/models/migrations/v1_12/v118.go @@ -1,7 +1,7 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_12 //nolint +package v1_12 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_12/v119.go b/models/migrations/v1_12/v119.go index 60bfe6a57da..b4bf29a9359 100644 --- a/models/migrations/v1_12/v119.go +++ b/models/migrations/v1_12/v119.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_12 //nolint +package v1_12 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_12/v120.go b/models/migrations/v1_12/v120.go index 3f7ed8d3731..14d515f5a7f 100644 --- a/models/migrations/v1_12/v120.go +++ b/models/migrations/v1_12/v120.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_12 //nolint +package v1_12 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_12/v121.go b/models/migrations/v1_12/v121.go index 175ec9164dd..a28ae4e1c9f 100644 --- a/models/migrations/v1_12/v121.go +++ b/models/migrations/v1_12/v121.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_12 //nolint +package v1_12 import "xorm.io/xorm" diff --git a/models/migrations/v1_12/v122.go b/models/migrations/v1_12/v122.go index 6e31d863a12..bc1b175f6ae 100644 --- a/models/migrations/v1_12/v122.go +++ b/models/migrations/v1_12/v122.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_12 //nolint +package v1_12 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_12/v123.go b/models/migrations/v1_12/v123.go index b0c3af07a3a..52b10bb8506 100644 --- a/models/migrations/v1_12/v123.go +++ b/models/migrations/v1_12/v123.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_12 //nolint +package v1_12 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_12/v124.go b/models/migrations/v1_12/v124.go index d2ba03ffe03..9a93f436d43 100644 --- a/models/migrations/v1_12/v124.go +++ b/models/migrations/v1_12/v124.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_12 //nolint +package v1_12 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_12/v125.go b/models/migrations/v1_12/v125.go index ec4ffaab254..7f582ecff5e 100644 --- a/models/migrations/v1_12/v125.go +++ b/models/migrations/v1_12/v125.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_12 //nolint +package v1_12 import ( "fmt" diff --git a/models/migrations/v1_12/v126.go b/models/migrations/v1_12/v126.go index ca9ec3aa3f3..64fd7f74787 100644 --- a/models/migrations/v1_12/v126.go +++ b/models/migrations/v1_12/v126.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_12 //nolint +package v1_12 import ( "xorm.io/builder" diff --git a/models/migrations/v1_12/v127.go b/models/migrations/v1_12/v127.go index 00e391dc875..9bd78db95e4 100644 --- a/models/migrations/v1_12/v127.go +++ b/models/migrations/v1_12/v127.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_12 //nolint +package v1_12 import ( "fmt" diff --git a/models/migrations/v1_12/v128.go b/models/migrations/v1_12/v128.go index 44d44a26c54..d2e8b86a3d9 100644 --- a/models/migrations/v1_12/v128.go +++ b/models/migrations/v1_12/v128.go @@ -1,9 +1,10 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_12 //nolint +package v1_12 import ( + "context" "fmt" "math" "path/filepath" @@ -17,7 +18,7 @@ import ( "xorm.io/xorm" ) -func FixMergeBase(x *xorm.Engine) error { +func FixMergeBase(ctx context.Context, x *xorm.Engine) error { type Repository struct { ID int64 `xorm:"pk autoincr"` OwnerID int64 `xorm:"UNIQUE(s) index"` @@ -82,17 +83,17 @@ func FixMergeBase(x *xorm.Engine) error { if !pr.HasMerged { var err error - pr.MergeBase, _, err = git.NewCommand(git.DefaultContext, "merge-base").AddDashesAndList(pr.BaseBranch, gitRefName).RunStdString(&git.RunOpts{Dir: repoPath}) + pr.MergeBase, _, err = git.NewCommand("merge-base").AddDashesAndList(pr.BaseBranch, gitRefName).RunStdString(ctx, &git.RunOpts{Dir: repoPath}) if err != nil { var err2 error - pr.MergeBase, _, err2 = git.NewCommand(git.DefaultContext, "rev-parse").AddDynamicArguments(git.BranchPrefix + pr.BaseBranch).RunStdString(&git.RunOpts{Dir: repoPath}) + pr.MergeBase, _, err2 = git.NewCommand("rev-parse").AddDynamicArguments(git.BranchPrefix+pr.BaseBranch).RunStdString(ctx, &git.RunOpts{Dir: repoPath}) if err2 != nil { log.Error("Unable to get merge base for PR ID %d, Index %d in %s/%s. Error: %v & %v", pr.ID, pr.Index, baseRepo.OwnerName, baseRepo.Name, err, err2) continue } } } else { - parentsString, _, err := git.NewCommand(git.DefaultContext, "rev-list", "--parents", "-n", "1").AddDynamicArguments(pr.MergedCommitID).RunStdString(&git.RunOpts{Dir: repoPath}) + parentsString, _, err := git.NewCommand("rev-list", "--parents", "-n", "1").AddDynamicArguments(pr.MergedCommitID).RunStdString(ctx, &git.RunOpts{Dir: repoPath}) if err != nil { log.Error("Unable to get parents for merged PR ID %d, Index %d in %s/%s. Error: %v", pr.ID, pr.Index, baseRepo.OwnerName, baseRepo.Name, err) continue @@ -104,9 +105,9 @@ func FixMergeBase(x *xorm.Engine) error { refs := append([]string{}, parents[1:]...) refs = append(refs, gitRefName) - cmd := git.NewCommand(git.DefaultContext, "merge-base").AddDashesAndList(refs...) + cmd := git.NewCommand("merge-base").AddDashesAndList(refs...) - pr.MergeBase, _, err = cmd.RunStdString(&git.RunOpts{Dir: repoPath}) + pr.MergeBase, _, err = cmd.RunStdString(ctx, &git.RunOpts{Dir: repoPath}) if err != nil { log.Error("Unable to get merge base for merged PR ID %d, Index %d in %s/%s. Error: %v", pr.ID, pr.Index, baseRepo.OwnerName, baseRepo.Name, err) continue diff --git a/models/migrations/v1_12/v129.go b/models/migrations/v1_12/v129.go index cf228242b9d..3e4d3aca685 100644 --- a/models/migrations/v1_12/v129.go +++ b/models/migrations/v1_12/v129.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_12 //nolint +package v1_12 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_12/v130.go b/models/migrations/v1_12/v130.go index 391810c7cad..107bb756fd6 100644 --- a/models/migrations/v1_12/v130.go +++ b/models/migrations/v1_12/v130.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_12 //nolint +package v1_12 import ( "code.gitea.io/gitea/modules/json" diff --git a/models/migrations/v1_12/v131.go b/models/migrations/v1_12/v131.go index 5184bc35903..1266c2f1859 100644 --- a/models/migrations/v1_12/v131.go +++ b/models/migrations/v1_12/v131.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_12 //nolint +package v1_12 import ( "fmt" diff --git a/models/migrations/v1_12/v132.go b/models/migrations/v1_12/v132.go index 3b2b28f7abb..8b1ae6db935 100644 --- a/models/migrations/v1_12/v132.go +++ b/models/migrations/v1_12/v132.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_12 //nolint +package v1_12 import ( "fmt" diff --git a/models/migrations/v1_12/v133.go b/models/migrations/v1_12/v133.go index c9087fc8c14..69e20597d85 100644 --- a/models/migrations/v1_12/v133.go +++ b/models/migrations/v1_12/v133.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_12 //nolint +package v1_12 import "xorm.io/xorm" diff --git a/models/migrations/v1_12/v134.go b/models/migrations/v1_12/v134.go index 3d1c82f09e1..189d80cc523 100644 --- a/models/migrations/v1_12/v134.go +++ b/models/migrations/v1_12/v134.go @@ -1,9 +1,10 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_12 //nolint +package v1_12 import ( + "context" "fmt" "math" "path/filepath" @@ -17,7 +18,7 @@ import ( "xorm.io/xorm" ) -func RefixMergeBase(x *xorm.Engine) error { +func RefixMergeBase(ctx context.Context, x *xorm.Engine) error { type Repository struct { ID int64 `xorm:"pk autoincr"` OwnerID int64 `xorm:"UNIQUE(s) index"` @@ -79,7 +80,7 @@ func RefixMergeBase(x *xorm.Engine) error { gitRefName := fmt.Sprintf("refs/pull/%d/head", pr.Index) - parentsString, _, err := git.NewCommand(git.DefaultContext, "rev-list", "--parents", "-n", "1").AddDynamicArguments(pr.MergedCommitID).RunStdString(&git.RunOpts{Dir: repoPath}) + parentsString, _, err := git.NewCommand("rev-list", "--parents", "-n", "1").AddDynamicArguments(pr.MergedCommitID).RunStdString(ctx, &git.RunOpts{Dir: repoPath}) if err != nil { log.Error("Unable to get parents for merged PR ID %d, Index %d in %s/%s. Error: %v", pr.ID, pr.Index, baseRepo.OwnerName, baseRepo.Name, err) continue @@ -92,9 +93,9 @@ func RefixMergeBase(x *xorm.Engine) error { // we should recalculate refs := append([]string{}, parents[1:]...) refs = append(refs, gitRefName) - cmd := git.NewCommand(git.DefaultContext, "merge-base").AddDashesAndList(refs...) + cmd := git.NewCommand("merge-base").AddDashesAndList(refs...) - pr.MergeBase, _, err = cmd.RunStdString(&git.RunOpts{Dir: repoPath}) + pr.MergeBase, _, err = cmd.RunStdString(ctx, &git.RunOpts{Dir: repoPath}) if err != nil { log.Error("Unable to get merge base for merged PR ID %d, Index %d in %s/%s. Error: %v", pr.ID, pr.Index, baseRepo.OwnerName, baseRepo.Name, err) continue diff --git a/models/migrations/v1_12/v135.go b/models/migrations/v1_12/v135.go index 8898011df56..5df0ad7fc4f 100644 --- a/models/migrations/v1_12/v135.go +++ b/models/migrations/v1_12/v135.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_12 //nolint +package v1_12 import ( "fmt" diff --git a/models/migrations/v1_12/v136.go b/models/migrations/v1_12/v136.go index d91ff92feb0..0f53278b462 100644 --- a/models/migrations/v1_12/v136.go +++ b/models/migrations/v1_12/v136.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_12 //nolint +package v1_12 import ( "fmt" diff --git a/models/migrations/v1_12/v137.go b/models/migrations/v1_12/v137.go index 0d86b720109..9d384834882 100644 --- a/models/migrations/v1_12/v137.go +++ b/models/migrations/v1_12/v137.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_12 //nolint +package v1_12 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_12/v138.go b/models/migrations/v1_12/v138.go index 8c8d353f405..4485adeb2dc 100644 --- a/models/migrations/v1_12/v138.go +++ b/models/migrations/v1_12/v138.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_12 //nolint +package v1_12 import ( "fmt" diff --git a/models/migrations/v1_12/v139.go b/models/migrations/v1_12/v139.go index 279aa7df87d..a3799841ac5 100644 --- a/models/migrations/v1_12/v139.go +++ b/models/migrations/v1_12/v139.go @@ -1,7 +1,7 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_12 //nolint +package v1_12 import ( "code.gitea.io/gitea/modules/setting" diff --git a/models/migrations/v1_13/v140.go b/models/migrations/v1_13/v140.go index 2d3337012dd..a9a047bca9d 100644 --- a/models/migrations/v1_13/v140.go +++ b/models/migrations/v1_13/v140.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_13 //nolint +package v1_13 import ( "fmt" @@ -21,12 +21,7 @@ func FixLanguageStatsToSaveSize(x *xorm.Engine) error { // RepoIndexerType specifies the repository indexer type type RepoIndexerType int - const ( - // RepoIndexerTypeCode code indexer - 0 - RepoIndexerTypeCode RepoIndexerType = iota //nolint:unused - // RepoIndexerTypeStats repository stats indexer - 1 - RepoIndexerTypeStats - ) + const RepoIndexerTypeStats RepoIndexerType = 1 // RepoIndexerStatus see models/repo_indexer.go type RepoIndexerStatus struct { @@ -46,7 +41,7 @@ func FixLanguageStatsToSaveSize(x *xorm.Engine) error { } // Delete language stats - if _, err := x.Exec(fmt.Sprintf("%s language_stat", truncExpr)); err != nil { + if _, err := x.Exec(truncExpr + " language_stat"); err != nil { return err } diff --git a/models/migrations/v1_13/v141.go b/models/migrations/v1_13/v141.go index ae211e0e44b..b54bc1727cb 100644 --- a/models/migrations/v1_13/v141.go +++ b/models/migrations/v1_13/v141.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_13 //nolint +package v1_13 import ( "fmt" diff --git a/models/migrations/v1_13/v142.go b/models/migrations/v1_13/v142.go index 7c7c01ad47d..d08a0ae0bf4 100644 --- a/models/migrations/v1_13/v142.go +++ b/models/migrations/v1_13/v142.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_13 //nolint +package v1_13 import ( "code.gitea.io/gitea/modules/log" diff --git a/models/migrations/v1_13/v143.go b/models/migrations/v1_13/v143.go index 885768dff37..b9a856ed0fa 100644 --- a/models/migrations/v1_13/v143.go +++ b/models/migrations/v1_13/v143.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_13 //nolint +package v1_13 import ( "code.gitea.io/gitea/modules/log" diff --git a/models/migrations/v1_13/v144.go b/models/migrations/v1_13/v144.go index f5a0bc57510..9352d78bc88 100644 --- a/models/migrations/v1_13/v144.go +++ b/models/migrations/v1_13/v144.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_13 //nolint +package v1_13 import ( "code.gitea.io/gitea/modules/log" diff --git a/models/migrations/v1_13/v145.go b/models/migrations/v1_13/v145.go index 8acb29bf337..86ebb4f9d95 100644 --- a/models/migrations/v1_13/v145.go +++ b/models/migrations/v1_13/v145.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_13 //nolint +package v1_13 import ( "fmt" @@ -42,7 +42,7 @@ func IncreaseLanguageField(x *xorm.Engine) error { switch { case setting.Database.Type.IsMySQL(): - if _, err := sess.Exec(fmt.Sprintf("ALTER TABLE language_stat MODIFY COLUMN language %s", sqlType)); err != nil { + if _, err := sess.Exec("ALTER TABLE language_stat MODIFY COLUMN language " + sqlType); err != nil { return err } case setting.Database.Type.IsMSSQL(): @@ -64,7 +64,7 @@ func IncreaseLanguageField(x *xorm.Engine) error { return fmt.Errorf("Drop table `language_stat` constraint `%s`: %w", constraint, err) } } - if _, err := sess.Exec(fmt.Sprintf("ALTER TABLE language_stat ALTER COLUMN language %s", sqlType)); err != nil { + if _, err := sess.Exec("ALTER TABLE language_stat ALTER COLUMN language " + sqlType); err != nil { return err } // Finally restore the constraint @@ -72,7 +72,7 @@ func IncreaseLanguageField(x *xorm.Engine) error { return err } case setting.Database.Type.IsPostgreSQL(): - if _, err := sess.Exec(fmt.Sprintf("ALTER TABLE language_stat ALTER COLUMN language TYPE %s", sqlType)); err != nil { + if _, err := sess.Exec("ALTER TABLE language_stat ALTER COLUMN language TYPE " + sqlType); err != nil { return err } } diff --git a/models/migrations/v1_13/v146.go b/models/migrations/v1_13/v146.go index 7d9a8787047..355c772c268 100644 --- a/models/migrations/v1_13/v146.go +++ b/models/migrations/v1_13/v146.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_13 //nolint +package v1_13 import ( "code.gitea.io/gitea/modules/timeutil" diff --git a/models/migrations/v1_13/v147.go b/models/migrations/v1_13/v147.go index 510ef39b286..0059c062203 100644 --- a/models/migrations/v1_13/v147.go +++ b/models/migrations/v1_13/v147.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_13 //nolint +package v1_13 import ( "code.gitea.io/gitea/modules/timeutil" diff --git a/models/migrations/v1_13/v148.go b/models/migrations/v1_13/v148.go index 7bb8ab700b6..d276db3d61d 100644 --- a/models/migrations/v1_13/v148.go +++ b/models/migrations/v1_13/v148.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_13 //nolint +package v1_13 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_13/v149.go b/models/migrations/v1_13/v149.go index 2a1db04cbb4..a96b8e5ac7d 100644 --- a/models/migrations/v1_13/v149.go +++ b/models/migrations/v1_13/v149.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_13 //nolint +package v1_13 import ( "fmt" diff --git a/models/migrations/v1_13/v150.go b/models/migrations/v1_13/v150.go index d5ba4895665..590ea729030 100644 --- a/models/migrations/v1_13/v150.go +++ b/models/migrations/v1_13/v150.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_13 //nolint +package v1_13 import ( "code.gitea.io/gitea/models/migrations/base" diff --git a/models/migrations/v1_13/v151.go b/models/migrations/v1_13/v151.go index 25af1d03ec9..454929534fe 100644 --- a/models/migrations/v1_13/v151.go +++ b/models/migrations/v1_13/v151.go @@ -1,10 +1,11 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_13 //nolint +package v1_13 import ( "context" + "errors" "fmt" "strings" @@ -113,7 +114,7 @@ func SetDefaultPasswordToArgon2(x *xorm.Engine) error { newTableColumns := table.Columns() if len(newTableColumns) == 0 { - return fmt.Errorf("no columns in new table") + return errors.New("no columns in new table") } hasID := false for _, column := range newTableColumns { diff --git a/models/migrations/v1_13/v152.go b/models/migrations/v1_13/v152.go index 502c82a40de..648e26446fe 100644 --- a/models/migrations/v1_13/v152.go +++ b/models/migrations/v1_13/v152.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_13 //nolint +package v1_13 import "xorm.io/xorm" diff --git a/models/migrations/v1_13/v153.go b/models/migrations/v1_13/v153.go index 0b2dd3eb62e..e5462fc1624 100644 --- a/models/migrations/v1_13/v153.go +++ b/models/migrations/v1_13/v153.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_13 //nolint +package v1_13 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_13/v154.go b/models/migrations/v1_13/v154.go index 60cc56713e5..5477d1b8891 100644 --- a/models/migrations/v1_13/v154.go +++ b/models/migrations/v1_13/v154.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_13 //nolint +package v1_13 import ( "code.gitea.io/gitea/modules/timeutil" diff --git a/models/migrations/v1_14/main_test.go b/models/migrations/v1_14/main_test.go index 7a091b9b9ac..978f88577c3 100644 --- a/models/migrations/v1_14/main_test.go +++ b/models/migrations/v1_14/main_test.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_14 //nolint +package v1_14 import ( "testing" diff --git a/models/migrations/v1_14/v155.go b/models/migrations/v1_14/v155.go index e814f59938d..505a9ae0334 100644 --- a/models/migrations/v1_14/v155.go +++ b/models/migrations/v1_14/v155.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_14 //nolint +package v1_14 import ( "fmt" diff --git a/models/migrations/v1_14/v156.go b/models/migrations/v1_14/v156.go index 2cf4954a15f..593d3f9c70c 100644 --- a/models/migrations/v1_14/v156.go +++ b/models/migrations/v1_14/v156.go @@ -1,9 +1,10 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_14 //nolint +package v1_14 import ( + "context" "fmt" "path/filepath" "strings" @@ -24,7 +25,7 @@ func userPath(userName string) string { return filepath.Join(setting.RepoRootPath, strings.ToLower(userName)) } -func FixPublisherIDforTagReleases(x *xorm.Engine) error { +func FixPublisherIDforTagReleases(ctx context.Context, x *xorm.Engine) error { type Release struct { ID int64 RepoID int64 @@ -108,7 +109,7 @@ func FixPublisherIDforTagReleases(x *xorm.Engine) error { return err } } - gitRepo, err = git.OpenRepository(git.DefaultContext, repoPath(repo.OwnerName, repo.Name)) + gitRepo, err = git.OpenRepository(ctx, repoPath(repo.OwnerName, repo.Name)) if err != nil { log.Error("Error whilst opening git repo for [%d]%s/%s. Error: %v", repo.ID, repo.OwnerName, repo.Name, err) return err diff --git a/models/migrations/v1_14/v157.go b/models/migrations/v1_14/v157.go index 7187278d294..2c5625ebbd4 100644 --- a/models/migrations/v1_14/v157.go +++ b/models/migrations/v1_14/v157.go @@ -1,24 +1,13 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_14 //nolint +package v1_14 import ( "xorm.io/xorm" ) func FixRepoTopics(x *xorm.Engine) error { - type Topic struct { //nolint:unused - ID int64 `xorm:"pk autoincr"` - Name string `xorm:"UNIQUE VARCHAR(25)"` - RepoCount int - } - - type RepoTopic struct { //nolint:unused - RepoID int64 `xorm:"pk"` - TopicID int64 `xorm:"pk"` - } - type Repository struct { ID int64 `xorm:"pk autoincr"` Topics []string `xorm:"TEXT JSON"` diff --git a/models/migrations/v1_14/v158.go b/models/migrations/v1_14/v158.go index 1094d8abf79..3c57e8e3daa 100644 --- a/models/migrations/v1_14/v158.go +++ b/models/migrations/v1_14/v158.go @@ -1,10 +1,10 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_14 //nolint +package v1_14 import ( - "fmt" + "errors" "strconv" "code.gitea.io/gitea/modules/log" @@ -82,7 +82,7 @@ func UpdateCodeCommentReplies(x *xorm.Engine) error { sqlCmd = "SELECT TOP " + strconv.Itoa(batchSize) + " * FROM #temp_comments WHERE " + "(id NOT IN ( SELECT TOP " + strconv.Itoa(start) + " id FROM #temp_comments ORDER BY id )) ORDER BY id" default: - return fmt.Errorf("Unsupported database type") + return errors.New("Unsupported database type") } if err := sess.SQL(sqlCmd).Find(&comments); err != nil { diff --git a/models/migrations/v1_14/v159.go b/models/migrations/v1_14/v159.go index 149ae0f6a8e..e6f6f0f061a 100644 --- a/models/migrations/v1_14/v159.go +++ b/models/migrations/v1_14/v159.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_14 //nolint +package v1_14 import ( "code.gitea.io/gitea/models/migrations/base" diff --git a/models/migrations/v1_14/v160.go b/models/migrations/v1_14/v160.go index 4dea91b5148..73f37989547 100644 --- a/models/migrations/v1_14/v160.go +++ b/models/migrations/v1_14/v160.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_14 //nolint +package v1_14 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_14/v161.go b/models/migrations/v1_14/v161.go index ac7e821a804..eb92dee77cb 100644 --- a/models/migrations/v1_14/v161.go +++ b/models/migrations/v1_14/v161.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_14 //nolint +package v1_14 import ( "context" diff --git a/models/migrations/v1_14/v162.go b/models/migrations/v1_14/v162.go index 2e4e0b8eb05..a0ddd36d55f 100644 --- a/models/migrations/v1_14/v162.go +++ b/models/migrations/v1_14/v162.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_14 //nolint +package v1_14 import ( "code.gitea.io/gitea/models/migrations/base" diff --git a/models/migrations/v1_14/v163.go b/models/migrations/v1_14/v163.go index 0cd8ba68c8e..84c35190b7b 100644 --- a/models/migrations/v1_14/v163.go +++ b/models/migrations/v1_14/v163.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_14 //nolint +package v1_14 import ( "code.gitea.io/gitea/models/migrations/base" diff --git a/models/migrations/v1_14/v164.go b/models/migrations/v1_14/v164.go index 54f6951427e..d2fd9b8464a 100644 --- a/models/migrations/v1_14/v164.go +++ b/models/migrations/v1_14/v164.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_14 //nolint +package v1_14 import ( "fmt" diff --git a/models/migrations/v1_14/v165.go b/models/migrations/v1_14/v165.go index 926350cdf78..6e1b34156b8 100644 --- a/models/migrations/v1_14/v165.go +++ b/models/migrations/v1_14/v165.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_14 //nolint +package v1_14 import ( "code.gitea.io/gitea/models/migrations/base" @@ -16,10 +16,7 @@ func ConvertHookTaskTypeToVarcharAndTrim(x *xorm.Engine) error { return nil } - type HookTask struct { //nolint:unused - Typ string `xorm:"VARCHAR(16) index"` - } - + // HookTask: Typ string `xorm:"VARCHAR(16) index"` if err := base.ModifyColumn(x, "hook_task", &schemas.Column{ Name: "typ", SQLType: schemas.SQLType{ @@ -42,10 +39,7 @@ func ConvertHookTaskTypeToVarcharAndTrim(x *xorm.Engine) error { return err } - type Webhook struct { //nolint:unused - Type string `xorm:"VARCHAR(16) index"` - } - + // Webhook: Type string `xorm:"VARCHAR(16) index"` if err := base.ModifyColumn(x, "webhook", &schemas.Column{ Name: "type", SQLType: schemas.SQLType{ diff --git a/models/migrations/v1_14/v166.go b/models/migrations/v1_14/v166.go index e5731582fdd..4c106bd7daf 100644 --- a/models/migrations/v1_14/v166.go +++ b/models/migrations/v1_14/v166.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_14 //nolint +package v1_14 import ( "crypto/sha256" diff --git a/models/migrations/v1_14/v167.go b/models/migrations/v1_14/v167.go index 9d416f6a32d..d77bbc401e5 100644 --- a/models/migrations/v1_14/v167.go +++ b/models/migrations/v1_14/v167.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_14 //nolint +package v1_14 import ( "fmt" diff --git a/models/migrations/v1_14/v168.go b/models/migrations/v1_14/v168.go index a30a8859f7f..aa93eec19b0 100644 --- a/models/migrations/v1_14/v168.go +++ b/models/migrations/v1_14/v168.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_14 //nolint +package v1_14 import "xorm.io/xorm" diff --git a/models/migrations/v1_14/v169.go b/models/migrations/v1_14/v169.go index 5b81bb58b19..4f9df0d96f2 100644 --- a/models/migrations/v1_14/v169.go +++ b/models/migrations/v1_14/v169.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_14 //nolint +package v1_14 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_14/v170.go b/models/migrations/v1_14/v170.go index 7b6498a3e9b..a2ff4623e14 100644 --- a/models/migrations/v1_14/v170.go +++ b/models/migrations/v1_14/v170.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_14 //nolint +package v1_14 import ( "fmt" diff --git a/models/migrations/v1_14/v171.go b/models/migrations/v1_14/v171.go index 51a35a02add..7b200e960ad 100644 --- a/models/migrations/v1_14/v171.go +++ b/models/migrations/v1_14/v171.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_14 //nolint +package v1_14 import ( "fmt" diff --git a/models/migrations/v1_14/v172.go b/models/migrations/v1_14/v172.go index 0f9bef902a3..bbd61d87b28 100644 --- a/models/migrations/v1_14/v172.go +++ b/models/migrations/v1_14/v172.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_14 //nolint +package v1_14 import ( "code.gitea.io/gitea/modules/timeutil" diff --git a/models/migrations/v1_14/v173.go b/models/migrations/v1_14/v173.go index 2d9eee9197f..7752fbe9664 100644 --- a/models/migrations/v1_14/v173.go +++ b/models/migrations/v1_14/v173.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_14 //nolint +package v1_14 import ( "fmt" diff --git a/models/migrations/v1_14/v174.go b/models/migrations/v1_14/v174.go index c839e15db85..4049e43070d 100644 --- a/models/migrations/v1_14/v174.go +++ b/models/migrations/v1_14/v174.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_14 //nolint +package v1_14 import ( "fmt" diff --git a/models/migrations/v1_14/v175.go b/models/migrations/v1_14/v175.go index 70d72b26003..92ed1304734 100644 --- a/models/migrations/v1_14/v175.go +++ b/models/migrations/v1_14/v175.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_14 //nolint +package v1_14 import ( "fmt" diff --git a/models/migrations/v1_14/v176.go b/models/migrations/v1_14/v176.go index 1ed49f75fac..ef5dce9a027 100644 --- a/models/migrations/v1_14/v176.go +++ b/models/migrations/v1_14/v176.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_14 //nolint +package v1_14 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_14/v176_test.go b/models/migrations/v1_14/v176_test.go index ea3e750d7f9..5c1db4db71c 100644 --- a/models/migrations/v1_14/v176_test.go +++ b/models/migrations/v1_14/v176_test.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_14 //nolint +package v1_14 import ( "testing" diff --git a/models/migrations/v1_14/v177.go b/models/migrations/v1_14/v177.go index 6e1838f3696..96676bf8d94 100644 --- a/models/migrations/v1_14/v177.go +++ b/models/migrations/v1_14/v177.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_14 //nolint +package v1_14 import ( "fmt" diff --git a/models/migrations/v1_14/v177_test.go b/models/migrations/v1_14/v177_test.go index 5568a18fec0..263f69f3380 100644 --- a/models/migrations/v1_14/v177_test.go +++ b/models/migrations/v1_14/v177_test.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_14 //nolint +package v1_14 import ( "testing" diff --git a/models/migrations/v1_15/main_test.go b/models/migrations/v1_15/main_test.go index 366f19788ec..d01585e9978 100644 --- a/models/migrations/v1_15/main_test.go +++ b/models/migrations/v1_15/main_test.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_15 //nolint +package v1_15 import ( "testing" diff --git a/models/migrations/v1_15/v178.go b/models/migrations/v1_15/v178.go index 6d236eb0498..ca3a5c262e4 100644 --- a/models/migrations/v1_15/v178.go +++ b/models/migrations/v1_15/v178.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_15 //nolint +package v1_15 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_15/v179.go b/models/migrations/v1_15/v179.go index f6b142eb42d..d6fb86ffecc 100644 --- a/models/migrations/v1_15/v179.go +++ b/models/migrations/v1_15/v179.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_15 //nolint +package v1_15 import ( "code.gitea.io/gitea/models/migrations/base" diff --git a/models/migrations/v1_15/v180.go b/models/migrations/v1_15/v180.go index c71e7718617..dd132f83306 100644 --- a/models/migrations/v1_15/v180.go +++ b/models/migrations/v1_15/v180.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_15 //nolint +package v1_15 import ( "code.gitea.io/gitea/modules/json" diff --git a/models/migrations/v1_15/v181.go b/models/migrations/v1_15/v181.go index 2185ed02134..fb1d3d7a75f 100644 --- a/models/migrations/v1_15/v181.go +++ b/models/migrations/v1_15/v181.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_15 //nolint +package v1_15 import ( "strings" diff --git a/models/migrations/v1_15/v181_test.go b/models/migrations/v1_15/v181_test.go index 1b075be7a09..73b5c1f3d6c 100644 --- a/models/migrations/v1_15/v181_test.go +++ b/models/migrations/v1_15/v181_test.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_15 //nolint +package v1_15 import ( "strings" @@ -49,7 +49,7 @@ func Test_AddPrimaryEmail2EmailAddress(t *testing.T) { assert.NoError(t, err) assert.True(t, has) assert.True(t, emailAddress.IsPrimary) - assert.EqualValues(t, user.IsActive, emailAddress.IsActivated) - assert.EqualValues(t, user.ID, emailAddress.UID) + assert.Equal(t, user.IsActive, emailAddress.IsActivated) + assert.Equal(t, user.ID, emailAddress.UID) } } diff --git a/models/migrations/v1_15/v182.go b/models/migrations/v1_15/v182.go index 9ca500c0f96..f53ff11df9c 100644 --- a/models/migrations/v1_15/v182.go +++ b/models/migrations/v1_15/v182.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_15 //nolint +package v1_15 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_15/v182_test.go b/models/migrations/v1_15/v182_test.go index 75ef8e1cd83..5fc6a0c467e 100644 --- a/models/migrations/v1_15/v182_test.go +++ b/models/migrations/v1_15/v182_test.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_15 //nolint +package v1_15 import ( "testing" diff --git a/models/migrations/v1_15/v183.go b/models/migrations/v1_15/v183.go index effad1b467c..5d0582f03d4 100644 --- a/models/migrations/v1_15/v183.go +++ b/models/migrations/v1_15/v183.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_15 //nolint +package v1_15 import ( "fmt" diff --git a/models/migrations/v1_15/v184.go b/models/migrations/v1_15/v184.go index 4b3dd1467a8..2823bc1f7af 100644 --- a/models/migrations/v1_15/v184.go +++ b/models/migrations/v1_15/v184.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_15 //nolint +package v1_15 import ( "context" diff --git a/models/migrations/v1_15/v185.go b/models/migrations/v1_15/v185.go index e5878ec1938..60af59edca4 100644 --- a/models/migrations/v1_15/v185.go +++ b/models/migrations/v1_15/v185.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_15 //nolint +package v1_15 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_15/v186.go b/models/migrations/v1_15/v186.go index 01aab3add5c..67dc97d13d7 100644 --- a/models/migrations/v1_15/v186.go +++ b/models/migrations/v1_15/v186.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_15 //nolint +package v1_15 import ( "code.gitea.io/gitea/modules/timeutil" diff --git a/models/migrations/v1_15/v187.go b/models/migrations/v1_15/v187.go index 21cd6772b7e..5fd90c65fbf 100644 --- a/models/migrations/v1_15/v187.go +++ b/models/migrations/v1_15/v187.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_15 //nolint +package v1_15 import ( "code.gitea.io/gitea/models/migrations/base" diff --git a/models/migrations/v1_15/v188.go b/models/migrations/v1_15/v188.go index 71e45cab0e3..4494e6ff055 100644 --- a/models/migrations/v1_15/v188.go +++ b/models/migrations/v1_15/v188.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_15 //nolint +package v1_15 import "xorm.io/xorm" diff --git a/models/migrations/v1_16/main_test.go b/models/migrations/v1_16/main_test.go index 817a0c13a45..7f93d6e9e5e 100644 --- a/models/migrations/v1_16/main_test.go +++ b/models/migrations/v1_16/main_test.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_16 //nolint +package v1_16 import ( "testing" diff --git a/models/migrations/v1_16/v189.go b/models/migrations/v1_16/v189.go index 56496450519..6bc99e58ab7 100644 --- a/models/migrations/v1_16/v189.go +++ b/models/migrations/v1_16/v189.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_16 //nolint +package v1_16 import ( "encoding/binary" diff --git a/models/migrations/v1_16/v189_test.go b/models/migrations/v1_16/v189_test.go index 32ef821d278..fb56ac8e116 100644 --- a/models/migrations/v1_16/v189_test.go +++ b/models/migrations/v1_16/v189_test.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_16 //nolint +package v1_16 import ( "testing" @@ -75,8 +75,8 @@ func Test_UnwrapLDAPSourceCfg(t *testing.T) { return } - assert.EqualValues(t, expected, converted, "UnwrapLDAPSourceCfg failed for %d", source.ID) - assert.EqualValues(t, source.ID%2 == 0, source.IsActive, "UnwrapLDAPSourceCfg failed for %d", source.ID) + assert.Equal(t, expected, converted, "UnwrapLDAPSourceCfg failed for %d", source.ID) + assert.Equal(t, source.ID%2 == 0, source.IsActive, "UnwrapLDAPSourceCfg failed for %d", source.ID) } } } diff --git a/models/migrations/v1_16/v190.go b/models/migrations/v1_16/v190.go index 5953802849a..1eb6b6ddb4f 100644 --- a/models/migrations/v1_16/v190.go +++ b/models/migrations/v1_16/v190.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_16 //nolint +package v1_16 import ( "fmt" diff --git a/models/migrations/v1_16/v191.go b/models/migrations/v1_16/v191.go index c618783c08e..957c82e484c 100644 --- a/models/migrations/v1_16/v191.go +++ b/models/migrations/v1_16/v191.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_16 //nolint +package v1_16 import ( "code.gitea.io/gitea/modules/setting" diff --git a/models/migrations/v1_16/v192.go b/models/migrations/v1_16/v192.go index 2d5d158a09e..9d03fbe3c8f 100644 --- a/models/migrations/v1_16/v192.go +++ b/models/migrations/v1_16/v192.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_16 //nolint +package v1_16 import ( "code.gitea.io/gitea/models/migrations/base" diff --git a/models/migrations/v1_16/v193.go b/models/migrations/v1_16/v193.go index 8d3ce7a5587..a5af2de3805 100644 --- a/models/migrations/v1_16/v193.go +++ b/models/migrations/v1_16/v193.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_16 //nolint +package v1_16 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_16/v193_test.go b/models/migrations/v1_16/v193_test.go index d99bbc29620..2e827f0550b 100644 --- a/models/migrations/v1_16/v193_test.go +++ b/models/migrations/v1_16/v193_test.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_16 //nolint +package v1_16 import ( "testing" @@ -56,25 +56,25 @@ func Test_AddRepoIDForAttachment(t *testing.T) { err := x.Table("attachment").Where("issue_id > 0").Find(&issueAttachments) assert.NoError(t, err) for _, attach := range issueAttachments { - assert.Greater(t, attach.RepoID, int64(0)) - assert.Greater(t, attach.IssueID, int64(0)) + assert.Positive(t, attach.RepoID) + assert.Positive(t, attach.IssueID) var issue Issue has, err := x.ID(attach.IssueID).Get(&issue) assert.NoError(t, err) assert.True(t, has) - assert.EqualValues(t, attach.RepoID, issue.RepoID) + assert.Equal(t, attach.RepoID, issue.RepoID) } var releaseAttachments []*NewAttachment err = x.Table("attachment").Where("release_id > 0").Find(&releaseAttachments) assert.NoError(t, err) for _, attach := range releaseAttachments { - assert.Greater(t, attach.RepoID, int64(0)) - assert.Greater(t, attach.ReleaseID, int64(0)) + assert.Positive(t, attach.RepoID) + assert.Positive(t, attach.ReleaseID) var release Release has, err := x.ID(attach.ReleaseID).Get(&release) assert.NoError(t, err) assert.True(t, has) - assert.EqualValues(t, attach.RepoID, release.RepoID) + assert.Equal(t, attach.RepoID, release.RepoID) } } diff --git a/models/migrations/v1_16/v194.go b/models/migrations/v1_16/v194.go index 6aa13c50cf8..2e4ed8340e6 100644 --- a/models/migrations/v1_16/v194.go +++ b/models/migrations/v1_16/v194.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_16 //nolint +package v1_16 import ( "fmt" diff --git a/models/migrations/v1_16/v195.go b/models/migrations/v1_16/v195.go index 6d7e94141e4..4fd42b7bd23 100644 --- a/models/migrations/v1_16/v195.go +++ b/models/migrations/v1_16/v195.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_16 //nolint +package v1_16 import ( "fmt" diff --git a/models/migrations/v1_16/v195_test.go b/models/migrations/v1_16/v195_test.go index 742397bf32a..946e06e3997 100644 --- a/models/migrations/v1_16/v195_test.go +++ b/models/migrations/v1_16/v195_test.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_16 //nolint +package v1_16 import ( "testing" diff --git a/models/migrations/v1_16/v196.go b/models/migrations/v1_16/v196.go index 7cbafc61e56..6c9caa100f1 100644 --- a/models/migrations/v1_16/v196.go +++ b/models/migrations/v1_16/v196.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_16 //nolint +package v1_16 import ( "fmt" diff --git a/models/migrations/v1_16/v197.go b/models/migrations/v1_16/v197.go index 97888b28479..862bdfdcbdb 100644 --- a/models/migrations/v1_16/v197.go +++ b/models/migrations/v1_16/v197.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_16 //nolint +package v1_16 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_16/v198.go b/models/migrations/v1_16/v198.go index 115bb313a06..f35ede138a0 100644 --- a/models/migrations/v1_16/v198.go +++ b/models/migrations/v1_16/v198.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_16 //nolint +package v1_16 import ( "fmt" diff --git a/models/migrations/v1_16/v199.go b/models/migrations/v1_16/v199.go index 6adcf890afb..4020352f2b5 100644 --- a/models/migrations/v1_16/v199.go +++ b/models/migrations/v1_16/v199.go @@ -1,6 +1,6 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_16 //nolint +package v1_16 // We used to use a table `remote_version` to store information for updater, now we use `AppState`, so this migration task is a no-op now. diff --git a/models/migrations/v1_16/v200.go b/models/migrations/v1_16/v200.go index c08c20e51de..de57fad8fe7 100644 --- a/models/migrations/v1_16/v200.go +++ b/models/migrations/v1_16/v200.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_16 //nolint +package v1_16 import ( "fmt" diff --git a/models/migrations/v1_16/v201.go b/models/migrations/v1_16/v201.go index 35e0c9f2fbe..2c43698b0c9 100644 --- a/models/migrations/v1_16/v201.go +++ b/models/migrations/v1_16/v201.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_16 //nolint +package v1_16 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_16/v202.go b/models/migrations/v1_16/v202.go index 6ba36152f1f..d8c8fdcadc3 100644 --- a/models/migrations/v1_16/v202.go +++ b/models/migrations/v1_16/v202.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_16 //nolint +package v1_16 import ( "fmt" diff --git a/models/migrations/v1_16/v203.go b/models/migrations/v1_16/v203.go index e8e6b524538..c3241cba576 100644 --- a/models/migrations/v1_16/v203.go +++ b/models/migrations/v1_16/v203.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_16 //nolint +package v1_16 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_16/v204.go b/models/migrations/v1_16/v204.go index ece03e13052..4d375307e76 100644 --- a/models/migrations/v1_16/v204.go +++ b/models/migrations/v1_16/v204.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_16 //nolint +package v1_16 import "xorm.io/xorm" diff --git a/models/migrations/v1_16/v205.go b/models/migrations/v1_16/v205.go index d6c577083cd..78241bad5b2 100644 --- a/models/migrations/v1_16/v205.go +++ b/models/migrations/v1_16/v205.go @@ -1,7 +1,7 @@ // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_16 //nolint +package v1_16 import ( "code.gitea.io/gitea/models/migrations/base" diff --git a/models/migrations/v1_16/v206.go b/models/migrations/v1_16/v206.go index 581a7d76e9e..01a9c386eb2 100644 --- a/models/migrations/v1_16/v206.go +++ b/models/migrations/v1_16/v206.go @@ -1,7 +1,7 @@ // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_16 //nolint +package v1_16 import ( "fmt" diff --git a/models/migrations/v1_16/v207.go b/models/migrations/v1_16/v207.go index 91208f066ca..19126ead1f4 100644 --- a/models/migrations/v1_16/v207.go +++ b/models/migrations/v1_16/v207.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_16 //nolint +package v1_16 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_16/v208.go b/models/migrations/v1_16/v208.go index 1a11ef096ad..fb643324f48 100644 --- a/models/migrations/v1_16/v208.go +++ b/models/migrations/v1_16/v208.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_16 //nolint +package v1_16 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_16/v209.go b/models/migrations/v1_16/v209.go index be3100e02a0..230838647bb 100644 --- a/models/migrations/v1_16/v209.go +++ b/models/migrations/v1_16/v209.go @@ -1,7 +1,7 @@ // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_16 //nolint +package v1_16 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_16/v210.go b/models/migrations/v1_16/v210.go index 51b7d81e998..0b94baf8e3d 100644 --- a/models/migrations/v1_16/v210.go +++ b/models/migrations/v1_16/v210.go @@ -1,7 +1,7 @@ // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_16 //nolint +package v1_16 import ( "encoding/base32" diff --git a/models/migrations/v1_16/v210_test.go b/models/migrations/v1_16/v210_test.go index d43fb031061..3b4ac7aa4b1 100644 --- a/models/migrations/v1_16/v210_test.go +++ b/models/migrations/v1_16/v210_test.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_16 //nolint +package v1_16 import ( "testing" @@ -71,5 +71,5 @@ func Test_RemigrateU2FCredentials(t *testing.T) { return } - assert.EqualValues(t, expected, got) + assert.Equal(t, expected, got) } diff --git a/models/migrations/v1_17/main_test.go b/models/migrations/v1_17/main_test.go index 79cb3fa0788..571a4f55a34 100644 --- a/models/migrations/v1_17/main_test.go +++ b/models/migrations/v1_17/main_test.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_17 //nolint +package v1_17 import ( "testing" diff --git a/models/migrations/v1_17/v211.go b/models/migrations/v1_17/v211.go index 9b72c8610b6..517cf19388d 100644 --- a/models/migrations/v1_17/v211.go +++ b/models/migrations/v1_17/v211.go @@ -1,7 +1,7 @@ // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_17 //nolint +package v1_17 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_17/v212.go b/models/migrations/v1_17/v212.go index e3f94371212..788792211f5 100644 --- a/models/migrations/v1_17/v212.go +++ b/models/migrations/v1_17/v212.go @@ -1,7 +1,7 @@ // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_17 //nolint +package v1_17 import ( "code.gitea.io/gitea/modules/timeutil" diff --git a/models/migrations/v1_17/v213.go b/models/migrations/v1_17/v213.go index bb3f466e528..b2bbdf72795 100644 --- a/models/migrations/v1_17/v213.go +++ b/models/migrations/v1_17/v213.go @@ -1,7 +1,7 @@ // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_17 //nolint +package v1_17 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_17/v214.go b/models/migrations/v1_17/v214.go index 2268164919d..1925324f0f1 100644 --- a/models/migrations/v1_17/v214.go +++ b/models/migrations/v1_17/v214.go @@ -1,7 +1,7 @@ // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_17 //nolint +package v1_17 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_17/v215.go b/models/migrations/v1_17/v215.go index b338f854178..748539225d3 100644 --- a/models/migrations/v1_17/v215.go +++ b/models/migrations/v1_17/v215.go @@ -1,7 +1,7 @@ // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_17 //nolint +package v1_17 import ( "code.gitea.io/gitea/models/pull" diff --git a/models/migrations/v1_17/v216.go b/models/migrations/v1_17/v216.go index 268f472a425..37aeacb6fca 100644 --- a/models/migrations/v1_17/v216.go +++ b/models/migrations/v1_17/v216.go @@ -1,7 +1,7 @@ // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_17 //nolint +package v1_17 // This migration added non-ideal indices to the action table which on larger datasets slowed things down // it has been superseded by v218.go diff --git a/models/migrations/v1_17/v217.go b/models/migrations/v1_17/v217.go index 3f970b68a54..04626bcbc59 100644 --- a/models/migrations/v1_17/v217.go +++ b/models/migrations/v1_17/v217.go @@ -1,7 +1,7 @@ // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_17 //nolint +package v1_17 import ( "code.gitea.io/gitea/modules/setting" diff --git a/models/migrations/v1_17/v218.go b/models/migrations/v1_17/v218.go index 4c05a9b5392..17d4cd89d4e 100644 --- a/models/migrations/v1_17/v218.go +++ b/models/migrations/v1_17/v218.go @@ -1,7 +1,7 @@ // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_17 //nolint +package v1_17 import ( "code.gitea.io/gitea/modules/setting" diff --git a/models/migrations/v1_17/v219.go b/models/migrations/v1_17/v219.go index d266029fd9f..6e335cb813a 100644 --- a/models/migrations/v1_17/v219.go +++ b/models/migrations/v1_17/v219.go @@ -1,7 +1,7 @@ // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_17 //nolint +package v1_17 import ( "time" diff --git a/models/migrations/v1_17/v220.go b/models/migrations/v1_17/v220.go index 904ddc51929..4ac8c58e1ec 100644 --- a/models/migrations/v1_17/v220.go +++ b/models/migrations/v1_17/v220.go @@ -1,7 +1,7 @@ // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_17 //nolint +package v1_17 import ( packages_model "code.gitea.io/gitea/models/packages" diff --git a/models/migrations/v1_17/v221.go b/models/migrations/v1_17/v221.go index 9e159388bdc..9e6a67eb183 100644 --- a/models/migrations/v1_17/v221.go +++ b/models/migrations/v1_17/v221.go @@ -1,7 +1,7 @@ // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_17 //nolint +package v1_17 import ( "encoding/base32" diff --git a/models/migrations/v1_17/v221_test.go b/models/migrations/v1_17/v221_test.go index 9ca54142e2d..a2dc0fae554 100644 --- a/models/migrations/v1_17/v221_test.go +++ b/models/migrations/v1_17/v221_test.go @@ -1,7 +1,7 @@ // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_17 //nolint +package v1_17 import ( "encoding/base32" diff --git a/models/migrations/v1_17/v222.go b/models/migrations/v1_17/v222.go index 2ffb94eb1ca..a5ea537d8a4 100644 --- a/models/migrations/v1_17/v222.go +++ b/models/migrations/v1_17/v222.go @@ -1,10 +1,11 @@ // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_17 //nolint +package v1_17 import ( "context" + "errors" "fmt" "code.gitea.io/gitea/models/migrations/base" @@ -29,7 +30,7 @@ func DropOldCredentialIDColumn(x *xorm.Engine) error { } if !credentialIDBytesExists { // looks like 221 hasn't properly run - return fmt.Errorf("webauthn_credential does not have a credential_id_bytes column... it is not safe to run this migration") + return errors.New("webauthn_credential does not have a credential_id_bytes column... it is not safe to run this migration") } // Create webauthnCredential table diff --git a/models/migrations/v1_17/v223.go b/models/migrations/v1_17/v223.go index 018451ee4c3..b2bfb76551d 100644 --- a/models/migrations/v1_17/v223.go +++ b/models/migrations/v1_17/v223.go @@ -1,7 +1,7 @@ // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_17 //nolint +package v1_17 import ( "context" diff --git a/models/migrations/v1_18/main_test.go b/models/migrations/v1_18/main_test.go index f71a21d1fb2..ebcfb45a941 100644 --- a/models/migrations/v1_18/main_test.go +++ b/models/migrations/v1_18/main_test.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_18 //nolint +package v1_18 import ( "testing" diff --git a/models/migrations/v1_18/v224.go b/models/migrations/v1_18/v224.go index f3d522b91a1..6dc12020eaa 100644 --- a/models/migrations/v1_18/v224.go +++ b/models/migrations/v1_18/v224.go @@ -1,7 +1,7 @@ // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_18 //nolint +package v1_18 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_18/v225.go b/models/migrations/v1_18/v225.go index b0ac3777fc2..bc6117e38f5 100644 --- a/models/migrations/v1_18/v225.go +++ b/models/migrations/v1_18/v225.go @@ -1,7 +1,7 @@ // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_18 //nolint +package v1_18 import ( "code.gitea.io/gitea/modules/setting" diff --git a/models/migrations/v1_18/v226.go b/models/migrations/v1_18/v226.go index f87e24b11de..8ed9761476d 100644 --- a/models/migrations/v1_18/v226.go +++ b/models/migrations/v1_18/v226.go @@ -1,7 +1,7 @@ // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_18 //nolint +package v1_18 import ( "xorm.io/builder" diff --git a/models/migrations/v1_18/v227.go b/models/migrations/v1_18/v227.go index 5fe5dcd0c95..3aca686d597 100644 --- a/models/migrations/v1_18/v227.go +++ b/models/migrations/v1_18/v227.go @@ -1,7 +1,7 @@ // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_18 //nolint +package v1_18 import ( "code.gitea.io/gitea/modules/timeutil" diff --git a/models/migrations/v1_18/v228.go b/models/migrations/v1_18/v228.go index 3e7a36de15e..b13f6461bd8 100644 --- a/models/migrations/v1_18/v228.go +++ b/models/migrations/v1_18/v228.go @@ -1,7 +1,7 @@ // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_18 //nolint +package v1_18 import ( "code.gitea.io/gitea/modules/timeutil" diff --git a/models/migrations/v1_18/v229.go b/models/migrations/v1_18/v229.go index 10d9f350979..bc15e013908 100644 --- a/models/migrations/v1_18/v229.go +++ b/models/migrations/v1_18/v229.go @@ -1,7 +1,7 @@ // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_18 //nolint +package v1_18 import ( "fmt" diff --git a/models/migrations/v1_18/v229_test.go b/models/migrations/v1_18/v229_test.go index d489328c000..5722dd35574 100644 --- a/models/migrations/v1_18/v229_test.go +++ b/models/migrations/v1_18/v229_test.go @@ -1,7 +1,7 @@ // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_18 //nolint +package v1_18 import ( "testing" diff --git a/models/migrations/v1_18/v230.go b/models/migrations/v1_18/v230.go index ea5b4d02e1f..078fce7643d 100644 --- a/models/migrations/v1_18/v230.go +++ b/models/migrations/v1_18/v230.go @@ -1,7 +1,7 @@ // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_18 //nolint +package v1_18 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_18/v230_test.go b/models/migrations/v1_18/v230_test.go index 40db4c2ffe2..25b2f6525da 100644 --- a/models/migrations/v1_18/v230_test.go +++ b/models/migrations/v1_18/v230_test.go @@ -1,7 +1,7 @@ // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_18 //nolint +package v1_18 import ( "testing" diff --git a/models/migrations/v1_19/main_test.go b/models/migrations/v1_19/main_test.go index 59f42af1116..87e807be6e1 100644 --- a/models/migrations/v1_19/main_test.go +++ b/models/migrations/v1_19/main_test.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_19 //nolint +package v1_19 import ( "testing" diff --git a/models/migrations/v1_19/v231.go b/models/migrations/v1_19/v231.go index 79e46132f0a..8ef1e4e7438 100644 --- a/models/migrations/v1_19/v231.go +++ b/models/migrations/v1_19/v231.go @@ -1,7 +1,7 @@ // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_19 //nolint +package v1_19 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_19/v232.go b/models/migrations/v1_19/v232.go index 9caf587c1e9..493dbc6df83 100644 --- a/models/migrations/v1_19/v232.go +++ b/models/migrations/v1_19/v232.go @@ -1,7 +1,7 @@ // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_19 //nolint +package v1_19 import ( "code.gitea.io/gitea/modules/setting" diff --git a/models/migrations/v1_19/v233.go b/models/migrations/v1_19/v233.go index ba4cd8e20b9..9eb6d405099 100644 --- a/models/migrations/v1_19/v233.go +++ b/models/migrations/v1_19/v233.go @@ -1,7 +1,7 @@ // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_19 //nolint +package v1_19 import ( "fmt" diff --git a/models/migrations/v1_19/v233_test.go b/models/migrations/v1_19/v233_test.go index 32c10ab0f45..7436ff7483c 100644 --- a/models/migrations/v1_19/v233_test.go +++ b/models/migrations/v1_19/v233_test.go @@ -1,7 +1,7 @@ // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_19 //nolint +package v1_19 import ( "testing" @@ -64,7 +64,7 @@ func Test_AddHeaderAuthorizationEncryptedColWebhook(t *testing.T) { assert.Equal(t, e.Meta, got[i].Meta) if e.HeaderAuthorization == "" { - assert.Equal(t, "", got[i].HeaderAuthorizationEncrypted) + assert.Empty(t, got[i].HeaderAuthorizationEncrypted) } else { cipherhex := got[i].HeaderAuthorizationEncrypted cleartext, err := secret.DecryptSecret(setting.SecretKey, cipherhex) diff --git a/models/migrations/v1_19/v234.go b/models/migrations/v1_19/v234.go index 728a580807b..3475384d6f4 100644 --- a/models/migrations/v1_19/v234.go +++ b/models/migrations/v1_19/v234.go @@ -1,7 +1,7 @@ // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_19 //nolint +package v1_19 import ( "code.gitea.io/gitea/modules/timeutil" diff --git a/models/migrations/v1_19/v235.go b/models/migrations/v1_19/v235.go index 3715de3920c..297d90f65a2 100644 --- a/models/migrations/v1_19/v235.go +++ b/models/migrations/v1_19/v235.go @@ -1,7 +1,7 @@ // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_19 //nolint +package v1_19 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_19/v236.go b/models/migrations/v1_19/v236.go index f172a85b1fc..0ed4d97a272 100644 --- a/models/migrations/v1_19/v236.go +++ b/models/migrations/v1_19/v236.go @@ -1,7 +1,7 @@ // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_19 //nolint +package v1_19 import ( "code.gitea.io/gitea/modules/timeutil" diff --git a/models/migrations/v1_19/v237.go b/models/migrations/v1_19/v237.go index b23c765aa5a..cf30226ccd8 100644 --- a/models/migrations/v1_19/v237.go +++ b/models/migrations/v1_19/v237.go @@ -1,7 +1,7 @@ // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_19 //nolint +package v1_19 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_19/v238.go b/models/migrations/v1_19/v238.go index 266e6cea58a..de681bfc7a4 100644 --- a/models/migrations/v1_19/v238.go +++ b/models/migrations/v1_19/v238.go @@ -1,7 +1,7 @@ // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_19 //nolint +package v1_19 import ( "code.gitea.io/gitea/modules/timeutil" diff --git a/models/migrations/v1_19/v239.go b/models/migrations/v1_19/v239.go index 10076f24016..8f4a65be95d 100644 --- a/models/migrations/v1_19/v239.go +++ b/models/migrations/v1_19/v239.go @@ -1,7 +1,7 @@ // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_19 //nolint +package v1_19 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_19/v240.go b/models/migrations/v1_19/v240.go index 4505f862995..7fdbaeb9dc9 100644 --- a/models/migrations/v1_19/v240.go +++ b/models/migrations/v1_19/v240.go @@ -1,7 +1,7 @@ // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_19 //nolint +package v1_19 import ( "code.gitea.io/gitea/models/db" diff --git a/models/migrations/v1_19/v241.go b/models/migrations/v1_19/v241.go index a617d6fd2f6..e35801a0572 100644 --- a/models/migrations/v1_19/v241.go +++ b/models/migrations/v1_19/v241.go @@ -1,7 +1,7 @@ // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_19 //nolint +package v1_19 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_19/v242.go b/models/migrations/v1_19/v242.go index 4470835214f..e9e759eaaa9 100644 --- a/models/migrations/v1_19/v242.go +++ b/models/migrations/v1_19/v242.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_19 //nolint +package v1_19 import ( "code.gitea.io/gitea/modules/setting" diff --git a/models/migrations/v1_19/v243.go b/models/migrations/v1_19/v243.go index 55bbfafb2fa..9c3f3725949 100644 --- a/models/migrations/v1_19/v243.go +++ b/models/migrations/v1_19/v243.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_19 //nolint +package v1_19 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_20/main_test.go b/models/migrations/v1_20/main_test.go index 92a1a9f6226..2fd63a7118e 100644 --- a/models/migrations/v1_20/main_test.go +++ b/models/migrations/v1_20/main_test.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_20 //nolint +package v1_20 import ( "testing" diff --git a/models/migrations/v1_20/v244.go b/models/migrations/v1_20/v244.go index 977566ad7dc..76cdccaca5b 100644 --- a/models/migrations/v1_20/v244.go +++ b/models/migrations/v1_20/v244.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_20 //nolint +package v1_20 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_20/v245.go b/models/migrations/v1_20/v245.go index ab58d128806..4acb11416c3 100644 --- a/models/migrations/v1_20/v245.go +++ b/models/migrations/v1_20/v245.go @@ -1,11 +1,10 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_20 //nolint +package v1_20 import ( "context" - "fmt" "code.gitea.io/gitea/models/migrations/base" "code.gitea.io/gitea/modules/setting" @@ -57,7 +56,7 @@ func RenameWebhookOrgToOwner(x *xorm.Engine) error { return err } sqlType := x.Dialect().SQLType(inferredTable.GetColumn("org_id")) - if _, err := sess.Exec(fmt.Sprintf("ALTER TABLE `webhook` CHANGE org_id owner_id %s", sqlType)); err != nil { + if _, err := sess.Exec("ALTER TABLE `webhook` CHANGE org_id owner_id " + sqlType); err != nil { return err } case setting.Database.Type.IsMSSQL(): diff --git a/models/migrations/v1_20/v246.go b/models/migrations/v1_20/v246.go index e6340ef079d..22bf7234043 100644 --- a/models/migrations/v1_20/v246.go +++ b/models/migrations/v1_20/v246.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_20 //nolint +package v1_20 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_20/v247.go b/models/migrations/v1_20/v247.go index 59fc5c46b5d..4f82937e185 100644 --- a/models/migrations/v1_20/v247.go +++ b/models/migrations/v1_20/v247.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_20 //nolint +package v1_20 import ( "code.gitea.io/gitea/modules/log" diff --git a/models/migrations/v1_20/v248.go b/models/migrations/v1_20/v248.go index 40555210e7e..4f2091e4bca 100644 --- a/models/migrations/v1_20/v248.go +++ b/models/migrations/v1_20/v248.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_20 //nolint +package v1_20 import "xorm.io/xorm" diff --git a/models/migrations/v1_20/v249.go b/models/migrations/v1_20/v249.go index 02951a74d6d..c6d3a177ca3 100644 --- a/models/migrations/v1_20/v249.go +++ b/models/migrations/v1_20/v249.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_20 //nolint +package v1_20 import ( "code.gitea.io/gitea/modules/timeutil" diff --git a/models/migrations/v1_20/v250.go b/models/migrations/v1_20/v250.go index 86388ef0b80..ec45e6e5c31 100644 --- a/models/migrations/v1_20/v250.go +++ b/models/migrations/v1_20/v250.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_20 //nolint +package v1_20 import ( "strings" diff --git a/models/migrations/v1_20/v251.go b/models/migrations/v1_20/v251.go index 7743248a3f1..a274c22a731 100644 --- a/models/migrations/v1_20/v251.go +++ b/models/migrations/v1_20/v251.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_20 //nolint +package v1_20 import ( "code.gitea.io/gitea/modules/log" diff --git a/models/migrations/v1_20/v252.go b/models/migrations/v1_20/v252.go index ab61cd9b8b3..d6aa6027534 100644 --- a/models/migrations/v1_20/v252.go +++ b/models/migrations/v1_20/v252.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_20 //nolint +package v1_20 import ( "code.gitea.io/gitea/modules/log" diff --git a/models/migrations/v1_20/v253.go b/models/migrations/v1_20/v253.go index 96c494bd8d9..c96454dbf92 100644 --- a/models/migrations/v1_20/v253.go +++ b/models/migrations/v1_20/v253.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_20 //nolint +package v1_20 import ( "code.gitea.io/gitea/modules/log" diff --git a/models/migrations/v1_20/v254.go b/models/migrations/v1_20/v254.go index 1e26979a5b2..9cdbfb39164 100644 --- a/models/migrations/v1_20/v254.go +++ b/models/migrations/v1_20/v254.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_20 //nolint +package v1_20 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_20/v255.go b/models/migrations/v1_20/v255.go index 14b70f8f962..caf198700e0 100644 --- a/models/migrations/v1_20/v255.go +++ b/models/migrations/v1_20/v255.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_20 //nolint +package v1_20 import ( "code.gitea.io/gitea/modules/timeutil" diff --git a/models/migrations/v1_20/v256.go b/models/migrations/v1_20/v256.go index 822153b93e5..7b84c1e1544 100644 --- a/models/migrations/v1_20/v256.go +++ b/models/migrations/v1_20/v256.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_20 //nolint +package v1_20 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_20/v257.go b/models/migrations/v1_20/v257.go index 6c6ca4c7486..9d5f7c07dfc 100644 --- a/models/migrations/v1_20/v257.go +++ b/models/migrations/v1_20/v257.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_20 //nolint +package v1_20 import ( "code.gitea.io/gitea/modules/timeutil" diff --git a/models/migrations/v1_20/v258.go b/models/migrations/v1_20/v258.go index 47174ce8051..1d3faffdaed 100644 --- a/models/migrations/v1_20/v258.go +++ b/models/migrations/v1_20/v258.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_20 //nolint +package v1_20 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_20/v259.go b/models/migrations/v1_20/v259.go index 5b8ced4ad7b..9e0dc9b61d9 100644 --- a/models/migrations/v1_20/v259.go +++ b/models/migrations/v1_20/v259.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_20 //nolint +package v1_20 import ( "fmt" @@ -329,7 +329,7 @@ func ConvertScopedAccessTokens(x *xorm.Engine) error { for _, token := range tokens { var scopes []string allNewScopesMap := make(map[AccessTokenScope]bool) - for _, oldScope := range strings.Split(token.Scope, ",") { + for oldScope := range strings.SplitSeq(token.Scope, ",") { if newScopes, exists := accessTokenScopeMap[OldAccessTokenScope(oldScope)]; exists { for _, newScope := range newScopes { allNewScopesMap[newScope] = true diff --git a/models/migrations/v1_20/v259_test.go b/models/migrations/v1_20/v259_test.go index 5bc9a71391e..0bf63719e5e 100644 --- a/models/migrations/v1_20/v259_test.go +++ b/models/migrations/v1_20/v259_test.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_20 //nolint +package v1_20 import ( "sort" @@ -96,7 +96,7 @@ func Test_ConvertScopedAccessTokens(t *testing.T) { tokens := make([]AccessToken, 0) err = x.Find(&tokens) assert.NoError(t, err) - assert.Equal(t, len(tests), len(tokens)) + assert.Len(t, tokens, len(tests)) // sort the tokens (insertion order by auto-incrementing primary key) sort.Slice(tokens, func(i, j int) bool { diff --git a/models/migrations/v1_21/main_test.go b/models/migrations/v1_21/main_test.go index 9afdea16775..536a7ade088 100644 --- a/models/migrations/v1_21/main_test.go +++ b/models/migrations/v1_21/main_test.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_21 //nolint +package v1_21 import ( "testing" diff --git a/models/migrations/v1_21/v260.go b/models/migrations/v1_21/v260.go index 6ca52c5998d..8540c58ae86 100644 --- a/models/migrations/v1_21/v260.go +++ b/models/migrations/v1_21/v260.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_21 //nolint +package v1_21 import ( "code.gitea.io/gitea/models/migrations/base" diff --git a/models/migrations/v1_21/v261.go b/models/migrations/v1_21/v261.go index 4ec1160d0b3..122b98eb93b 100644 --- a/models/migrations/v1_21/v261.go +++ b/models/migrations/v1_21/v261.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_21 //nolint +package v1_21 import ( "code.gitea.io/gitea/modules/timeutil" diff --git a/models/migrations/v1_21/v262.go b/models/migrations/v1_21/v262.go index 23e900572a2..6e88e29b9db 100644 --- a/models/migrations/v1_21/v262.go +++ b/models/migrations/v1_21/v262.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_21 //nolint +package v1_21 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_21/v263.go b/models/migrations/v1_21/v263.go index 2c7cbadf0d8..55c418bde0d 100644 --- a/models/migrations/v1_21/v263.go +++ b/models/migrations/v1_21/v263.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_21 //nolint +package v1_21 import ( "fmt" diff --git a/models/migrations/v1_21/v264.go b/models/migrations/v1_21/v264.go index e81a17ad6d1..7fc0ec60240 100644 --- a/models/migrations/v1_21/v264.go +++ b/models/migrations/v1_21/v264.go @@ -1,11 +1,11 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_21 //nolint +package v1_21 import ( "context" - "fmt" + "errors" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/modules/timeutil" @@ -57,7 +57,7 @@ func AddBranchTable(x *xorm.Engine) error { if err != nil { return err } else if !has { - return fmt.Errorf("no admin user found") + return errors.New("no admin user found") } branches := make([]Branch, 0, 100) diff --git a/models/migrations/v1_21/v265.go b/models/migrations/v1_21/v265.go index 800eb95f72c..b6892acc277 100644 --- a/models/migrations/v1_21/v265.go +++ b/models/migrations/v1_21/v265.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_21 //nolint +package v1_21 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_21/v266.go b/models/migrations/v1_21/v266.go index 79a5f5e14c5..440549e868b 100644 --- a/models/migrations/v1_21/v266.go +++ b/models/migrations/v1_21/v266.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_21 //nolint +package v1_21 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_21/v267.go b/models/migrations/v1_21/v267.go index bc0e954bdcc..394139a17e2 100644 --- a/models/migrations/v1_21/v267.go +++ b/models/migrations/v1_21/v267.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_21 //nolint +package v1_21 import ( "code.gitea.io/gitea/modules/timeutil" diff --git a/models/migrations/v1_21/v268.go b/models/migrations/v1_21/v268.go index 332793ff073..b677d2383e9 100644 --- a/models/migrations/v1_21/v268.go +++ b/models/migrations/v1_21/v268.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_21 //nolint +package v1_21 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_21/v269.go b/models/migrations/v1_21/v269.go index 475ec023804..042040927d4 100644 --- a/models/migrations/v1_21/v269.go +++ b/models/migrations/v1_21/v269.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_21 //nolint +package v1_21 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_21/v270.go b/models/migrations/v1_21/v270.go index b9cc84d3ac4..ab7c5660bad 100644 --- a/models/migrations/v1_21/v270.go +++ b/models/migrations/v1_21/v270.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_21 //nolint +package v1_21 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_21/v271.go b/models/migrations/v1_21/v271.go index 098f6499d57..05e1af1351d 100644 --- a/models/migrations/v1_21/v271.go +++ b/models/migrations/v1_21/v271.go @@ -1,7 +1,8 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_21 //nolint +package v1_21 + import ( "code.gitea.io/gitea/modules/timeutil" diff --git a/models/migrations/v1_21/v272.go b/models/migrations/v1_21/v272.go index a729c49f1bc..14c1e0c4b0f 100644 --- a/models/migrations/v1_21/v272.go +++ b/models/migrations/v1_21/v272.go @@ -1,7 +1,8 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_21 //nolint +package v1_21 + import ( "xorm.io/xorm" ) diff --git a/models/migrations/v1_21/v273.go b/models/migrations/v1_21/v273.go index 61c79f4a763..e614a56a7dc 100644 --- a/models/migrations/v1_21/v273.go +++ b/models/migrations/v1_21/v273.go @@ -1,7 +1,8 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_21 //nolint +package v1_21 + import ( "code.gitea.io/gitea/modules/timeutil" diff --git a/models/migrations/v1_21/v274.go b/models/migrations/v1_21/v274.go index df5994f159f..d0b557a1519 100644 --- a/models/migrations/v1_21/v274.go +++ b/models/migrations/v1_21/v274.go @@ -1,7 +1,8 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_21 //nolint +package v1_21 + import ( "time" diff --git a/models/migrations/v1_21/v275.go b/models/migrations/v1_21/v275.go index 78804a59d62..2bfe5c72fa9 100644 --- a/models/migrations/v1_21/v275.go +++ b/models/migrations/v1_21/v275.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_21 //nolint +package v1_21 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_21/v276.go b/models/migrations/v1_21/v276.go index 15177bf0406..3ab7e22cd05 100644 --- a/models/migrations/v1_21/v276.go +++ b/models/migrations/v1_21/v276.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_21 //nolint +package v1_21 import ( "context" @@ -172,7 +172,7 @@ func getRemoteAddress(ownerName, repoName, remoteName string) (string, error) { return "", fmt.Errorf("get remote %s's address of %s/%s failed: %v", remoteName, ownerName, repoName, err) } - u, err := giturl.Parse(remoteURL) + u, err := giturl.ParseGitURL(remoteURL) if err != nil { return "", err } diff --git a/models/migrations/v1_21/v277.go b/models/migrations/v1_21/v277.go index 12529160b75..0c102edddec 100644 --- a/models/migrations/v1_21/v277.go +++ b/models/migrations/v1_21/v277.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_21 //nolint +package v1_21 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_21/v278.go b/models/migrations/v1_21/v278.go index d6a462d1e7e..846f2286780 100644 --- a/models/migrations/v1_21/v278.go +++ b/models/migrations/v1_21/v278.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_21 //nolint +package v1_21 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_21/v279.go b/models/migrations/v1_21/v279.go index 2abd1bbe84b..beb39effe1a 100644 --- a/models/migrations/v1_21/v279.go +++ b/models/migrations/v1_21/v279.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_21 //nolint +package v1_21 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_22/main_test.go b/models/migrations/v1_22/main_test.go index efd8dbaa8c6..ac8facd6aa0 100644 --- a/models/migrations/v1_22/main_test.go +++ b/models/migrations/v1_22/main_test.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_22 //nolint +package v1_22 import ( "testing" diff --git a/models/migrations/v1_22/v280.go b/models/migrations/v1_22/v280.go index a8ee4a3bf7d..2271cb60896 100644 --- a/models/migrations/v1_22/v280.go +++ b/models/migrations/v1_22/v280.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_22 //nolint +package v1_22 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_22/v281.go b/models/migrations/v1_22/v281.go index fc1866aa835..129ec2cba09 100644 --- a/models/migrations/v1_22/v281.go +++ b/models/migrations/v1_22/v281.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_22 //nolint +package v1_22 import ( "code.gitea.io/gitea/modules/timeutil" diff --git a/models/migrations/v1_22/v282.go b/models/migrations/v1_22/v282.go index baad9e09168..eed64c30f79 100644 --- a/models/migrations/v1_22/v282.go +++ b/models/migrations/v1_22/v282.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_22 //nolint +package v1_22 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_22/v283.go b/models/migrations/v1_22/v283.go index 0a45c512459..0eca031b654 100644 --- a/models/migrations/v1_22/v283.go +++ b/models/migrations/v1_22/v283.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_22 //nolint +package v1_22 import ( "fmt" diff --git a/models/migrations/v1_22/v283_test.go b/models/migrations/v1_22/v283_test.go index e89a7cbfc2c..743f860466f 100644 --- a/models/migrations/v1_22/v283_test.go +++ b/models/migrations/v1_22/v283_test.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_22 //nolint +package v1_22 import ( "testing" diff --git a/models/migrations/v1_22/v284.go b/models/migrations/v1_22/v284.go index 2b950789802..31b38f6aed8 100644 --- a/models/migrations/v1_22/v284.go +++ b/models/migrations/v1_22/v284.go @@ -1,7 +1,8 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_22 //nolint +package v1_22 + import ( "xorm.io/xorm" ) diff --git a/models/migrations/v1_22/v285.go b/models/migrations/v1_22/v285.go index a55cc17c04f..fed89f670e0 100644 --- a/models/migrations/v1_22/v285.go +++ b/models/migrations/v1_22/v285.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_22 //nolint +package v1_22 import ( "time" diff --git a/models/migrations/v1_22/v286.go b/models/migrations/v1_22/v286.go index 1fcde33202a..f3ba50dbb63 100644 --- a/models/migrations/v1_22/v286.go +++ b/models/migrations/v1_22/v286.go @@ -1,6 +1,6 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_22 //nolint +package v1_22 import ( "errors" diff --git a/models/migrations/v1_22/v286_test.go b/models/migrations/v1_22/v286_test.go index a19c9396e2e..b4a50f6fcb4 100644 --- a/models/migrations/v1_22/v286_test.go +++ b/models/migrations/v1_22/v286_test.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_22 //nolint +package v1_22 import ( "testing" @@ -107,12 +107,12 @@ func Test_RepositoryFormat(t *testing.T) { repo = new(Repository) ok, err := x.ID(2).Get(repo) assert.NoError(t, err) - assert.EqualValues(t, true, ok) - assert.EqualValues(t, "sha1", repo.ObjectFormatName) + assert.True(t, ok) + assert.Equal(t, "sha1", repo.ObjectFormatName) repo = new(Repository) ok, err = x.ID(id).Get(repo) assert.NoError(t, err) - assert.EqualValues(t, true, ok) - assert.EqualValues(t, "sha256", repo.ObjectFormatName) + assert.True(t, ok) + assert.Equal(t, "sha256", repo.ObjectFormatName) } diff --git a/models/migrations/v1_22/v287.go b/models/migrations/v1_22/v287.go index c8b15932869..5fd901f9ded 100644 --- a/models/migrations/v1_22/v287.go +++ b/models/migrations/v1_22/v287.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_22 //nolint +package v1_22 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_22/v287_test.go b/models/migrations/v1_22/v287_test.go index 9c7b10947df..2b42a33c389 100644 --- a/models/migrations/v1_22/v287_test.go +++ b/models/migrations/v1_22/v287_test.go @@ -1,10 +1,10 @@ // Copyright 2024 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_22 //nolint +package v1_22 import ( - "fmt" + "strconv" "testing" "code.gitea.io/gitea/models/migrations/base" @@ -50,7 +50,7 @@ func Test_UpdateBadgeColName(t *testing.T) { for i, e := range oldBadges { got := got[i+1] // 1 is in the badge.yml assert.Equal(t, e.ID, got.ID) - assert.Equal(t, fmt.Sprintf("%d", e.ID), got.Slug) + assert.Equal(t, strconv.FormatInt(e.ID, 10), got.Slug) } // TODO: check if badges have been updated diff --git a/models/migrations/v1_22/v288.go b/models/migrations/v1_22/v288.go index 7c93bfcc663..26c850c2188 100644 --- a/models/migrations/v1_22/v288.go +++ b/models/migrations/v1_22/v288.go @@ -1,7 +1,7 @@ // Copyright 2024 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_22 //nolint +package v1_22 import ( "code.gitea.io/gitea/modules/timeutil" diff --git a/models/migrations/v1_22/v289.go b/models/migrations/v1_22/v289.go index b9941aadd90..78689a4ffae 100644 --- a/models/migrations/v1_22/v289.go +++ b/models/migrations/v1_22/v289.go @@ -1,7 +1,7 @@ // Copyright 2024 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_22 //nolint +package v1_22 import "xorm.io/xorm" diff --git a/models/migrations/v1_22/v290.go b/models/migrations/v1_22/v290.go index 9c54d4e87c8..0f4d78410c1 100644 --- a/models/migrations/v1_22/v290.go +++ b/models/migrations/v1_22/v290.go @@ -1,7 +1,7 @@ // Copyright 2024 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_22 //nolint +package v1_22 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_22/v291.go b/models/migrations/v1_22/v291.go index 74726fae966..823a644a955 100644 --- a/models/migrations/v1_22/v291.go +++ b/models/migrations/v1_22/v291.go @@ -1,7 +1,7 @@ // Copyright 2024 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_22 //nolint +package v1_22 import "xorm.io/xorm" diff --git a/models/migrations/v1_22/v292.go b/models/migrations/v1_22/v292.go index beca556aee2..440f48ce809 100644 --- a/models/migrations/v1_22/v292.go +++ b/models/migrations/v1_22/v292.go @@ -1,7 +1,7 @@ // Copyright 2024 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_22 //nolint +package v1_22 // NOTE: noop the original migration has bug which some projects will be skip, so // these projects will have no default board. diff --git a/models/migrations/v1_22/v293.go b/models/migrations/v1_22/v293.go index 53cc719294b..5299b8618f0 100644 --- a/models/migrations/v1_22/v293.go +++ b/models/migrations/v1_22/v293.go @@ -1,7 +1,7 @@ // Copyright 2024 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_22 //nolint +package v1_22 import ( "code.gitea.io/gitea/modules/setting" diff --git a/models/migrations/v1_22/v293_test.go b/models/migrations/v1_22/v293_test.go index cfe4345143e..c7b643c7e08 100644 --- a/models/migrations/v1_22/v293_test.go +++ b/models/migrations/v1_22/v293_test.go @@ -1,12 +1,11 @@ // Copyright 2024 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_22 //nolint +package v1_22 import ( "testing" - "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/migrations/base" "code.gitea.io/gitea/models/project" @@ -32,12 +31,12 @@ func Test_CheckProjectColumnsConsistency(t *testing.T) { assert.True(t, defaultColumn.Default) // check if multiple defaults, previous were removed and last will be kept - expectDefaultColumn, err := project.GetColumn(db.DefaultContext, 2) + expectDefaultColumn, err := project.GetColumn(t.Context(), 2) assert.NoError(t, err) assert.Equal(t, int64(2), expectDefaultColumn.ProjectID) assert.False(t, expectDefaultColumn.Default) - expectNonDefaultColumn, err := project.GetColumn(db.DefaultContext, 3) + expectNonDefaultColumn, err := project.GetColumn(t.Context(), 3) assert.NoError(t, err) assert.Equal(t, int64(2), expectNonDefaultColumn.ProjectID) assert.True(t, expectNonDefaultColumn.Default) diff --git a/models/migrations/v1_22/v294.go b/models/migrations/v1_22/v294.go index 20e261fb1b8..8776e51a165 100644 --- a/models/migrations/v1_22/v294.go +++ b/models/migrations/v1_22/v294.go @@ -1,7 +1,7 @@ // Copyright 2024 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_22 //nolint +package v1_22 import ( "fmt" diff --git a/models/migrations/v1_22/v294_test.go b/models/migrations/v1_22/v294_test.go index 82a3bcd602e..1cf03d61201 100644 --- a/models/migrations/v1_22/v294_test.go +++ b/models/migrations/v1_22/v294_test.go @@ -1,10 +1,9 @@ // Copyright 2024 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_22 //nolint +package v1_22 import ( - "slices" "testing" "code.gitea.io/gitea/models/migrations/base" @@ -39,12 +38,12 @@ func Test_AddUniqueIndexForProjectIssue(t *testing.T) { tables, err := x.DBMetas() assert.NoError(t, err) - assert.EqualValues(t, 1, len(tables)) + assert.Len(t, tables, 1) found := false for _, index := range tables[0].Indexes { if index.Type == schemas.UniqueType { found = true - slices.Equal(index.Cols, []string{"project_id", "issue_id"}) + assert.ElementsMatch(t, index.Cols, []string{"project_id", "issue_id"}) break } } diff --git a/models/migrations/v1_22/v295.go b/models/migrations/v1_22/v295.go index 17bdadb4ad3..319b1a399b3 100644 --- a/models/migrations/v1_22/v295.go +++ b/models/migrations/v1_22/v295.go @@ -1,7 +1,7 @@ // Copyright 2024 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_22 //nolint +package v1_22 import "xorm.io/xorm" diff --git a/models/migrations/v1_22/v296.go b/models/migrations/v1_22/v296.go index 1ecacab95f2..75350f9f654 100644 --- a/models/migrations/v1_22/v296.go +++ b/models/migrations/v1_22/v296.go @@ -1,7 +1,7 @@ // Copyright 2024 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_22 //nolint +package v1_22 import "xorm.io/xorm" diff --git a/models/migrations/v1_22/v297.go b/models/migrations/v1_22/v297.go index 7d4b5069258..9a4405f2666 100644 --- a/models/migrations/v1_22/v297.go +++ b/models/migrations/v1_22/v297.go @@ -1,7 +1,7 @@ // Copyright 2024 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_22 //nolint +package v1_22 import ( "code.gitea.io/gitea/models/perm" diff --git a/models/migrations/v1_22/v298.go b/models/migrations/v1_22/v298.go index b9f3b95ade8..7700173a004 100644 --- a/models/migrations/v1_22/v298.go +++ b/models/migrations/v1_22/v298.go @@ -1,7 +1,7 @@ // Copyright 2024 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_22 //nolint +package v1_22 import "xorm.io/xorm" diff --git a/models/migrations/v1_23/main_test.go b/models/migrations/v1_23/main_test.go index b7948bd4dd2..f7b2caed83d 100644 --- a/models/migrations/v1_23/main_test.go +++ b/models/migrations/v1_23/main_test.go @@ -1,7 +1,7 @@ // Copyright 2024 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_23 //nolint +package v1_23 import ( "testing" diff --git a/models/migrations/v1_23/v299.go b/models/migrations/v1_23/v299.go index f6db960c3b4..11021d8855f 100644 --- a/models/migrations/v1_23/v299.go +++ b/models/migrations/v1_23/v299.go @@ -1,7 +1,7 @@ // Copyright 2024 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_23 //nolint +package v1_23 import "xorm.io/xorm" @@ -14,5 +14,9 @@ func AddContentVersionToIssueAndComment(x *xorm.Engine) error { ContentVersion int `xorm:"NOT NULL DEFAULT 0"` } - return x.Sync(new(Comment), new(Issue)) + _, err := x.SyncWithOptions(xorm.SyncOptions{ + IgnoreConstrains: true, + IgnoreIndices: true, + }, new(Comment), new(Issue)) + return err } diff --git a/models/migrations/v1_23/v300.go b/models/migrations/v1_23/v300.go index f1f1cccdbfb..13c6489c5e4 100644 --- a/models/migrations/v1_23/v300.go +++ b/models/migrations/v1_23/v300.go @@ -1,7 +1,7 @@ // Copyright 2024 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_23 //nolint +package v1_23 import "xorm.io/xorm" @@ -13,5 +13,9 @@ func AddForcePushBranchProtection(x *xorm.Engine) error { ForcePushAllowlistTeamIDs []int64 `xorm:"JSON TEXT"` ForcePushAllowlistDeployKeys bool `xorm:"NOT NULL DEFAULT false"` } - return x.Sync(new(ProtectedBranch)) + _, err := x.SyncWithOptions(xorm.SyncOptions{ + IgnoreConstrains: true, + IgnoreIndices: true, + }, new(ProtectedBranch)) + return err } diff --git a/models/migrations/v1_23/v301.go b/models/migrations/v1_23/v301.go index b7797f6c6b5..ed8e9ef0599 100644 --- a/models/migrations/v1_23/v301.go +++ b/models/migrations/v1_23/v301.go @@ -1,7 +1,7 @@ // Copyright 2024 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_23 //nolint +package v1_23 import "xorm.io/xorm" @@ -10,5 +10,9 @@ func AddSkipSecondaryAuthColumnToOAuth2ApplicationTable(x *xorm.Engine) error { type oauth2Application struct { SkipSecondaryAuthorization bool `xorm:"NOT NULL DEFAULT FALSE"` } - return x.Sync(new(oauth2Application)) + _, err := x.SyncWithOptions(xorm.SyncOptions{ + IgnoreConstrains: true, + IgnoreIndices: true, + }, new(oauth2Application)) + return err } diff --git a/models/migrations/v1_23/v302.go b/models/migrations/v1_23/v302.go index d7ea03eb3da..e4a50b3ec88 100644 --- a/models/migrations/v1_23/v302.go +++ b/models/migrations/v1_23/v302.go @@ -1,7 +1,7 @@ // Copyright 2024 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_23 //nolint +package v1_23 import ( "code.gitea.io/gitea/modules/timeutil" @@ -14,5 +14,8 @@ func AddIndexToActionTaskStoppedLogExpired(x *xorm.Engine) error { Stopped timeutil.TimeStamp `xorm:"index(stopped_log_expired)"` LogExpired bool `xorm:"index(stopped_log_expired)"` } - return x.Sync(new(ActionTask)) + _, err := x.SyncWithOptions(xorm.SyncOptions{ + IgnoreDropIndices: true, + }, new(ActionTask)) + return err } diff --git a/models/migrations/v1_23/v302_test.go b/models/migrations/v1_23/v302_test.go new file mode 100644 index 00000000000..b008b6fc03d --- /dev/null +++ b/models/migrations/v1_23/v302_test.go @@ -0,0 +1,51 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_23 + +import ( + "testing" + + "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/modules/timeutil" + + "github.com/stretchr/testify/assert" +) + +func Test_AddIndexToActionTaskStoppedLogExpired(t *testing.T) { + type ActionTask struct { + ID int64 + JobID int64 + Attempt int64 + RunnerID int64 `xorm:"index"` + Status int `xorm:"index"` + Started timeutil.TimeStamp `xorm:"index"` + Stopped timeutil.TimeStamp `xorm:"index(stopped_log_expired)"` + + RepoID int64 `xorm:"index"` + OwnerID int64 `xorm:"index"` + CommitSHA string `xorm:"index"` + IsForkPullRequest bool + + Token string `xorm:"-"` + TokenHash string `xorm:"UNIQUE"` // sha256 of token + TokenSalt string + TokenLastEight string `xorm:"index token_last_eight"` + + LogFilename string // file name of log + LogInStorage bool // read log from database or from storage + LogLength int64 // lines count + LogSize int64 // blob size + LogIndexes []int64 `xorm:"LONGBLOB"` // line number to offset + LogExpired bool `xorm:"index(stopped_log_expired)"` // files that are too old will be deleted + + Created timeutil.TimeStamp `xorm:"created"` + Updated timeutil.TimeStamp `xorm:"updated index"` + } + + // Prepare and load the testing database + x, deferable := base.PrepareTestEnv(t, 0, new(ActionTask)) + defer deferable() + + assert.NoError(t, AddIndexToActionTaskStoppedLogExpired(x)) +} diff --git a/models/migrations/v1_23/v303.go b/models/migrations/v1_23/v303.go index adfe917d3f2..dc541a9535a 100644 --- a/models/migrations/v1_23/v303.go +++ b/models/migrations/v1_23/v303.go @@ -1,7 +1,7 @@ // Copyright 2024 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_23 //nolint +package v1_23 import ( "xorm.io/xorm" @@ -19,5 +19,9 @@ func AddCommentMetaDataColumn(x *xorm.Engine) error { CommentMetaData *CommentMetaData `xorm:"JSON TEXT"` // put all non-index metadata in a single field } - return x.Sync(new(Comment)) + _, err := x.SyncWithOptions(xorm.SyncOptions{ + IgnoreConstrains: true, + IgnoreIndices: true, + }, new(Comment)) + return err } diff --git a/models/migrations/v1_23/v304.go b/models/migrations/v1_23/v304.go index 65cffedbd99..35d4d4881a9 100644 --- a/models/migrations/v1_23/v304.go +++ b/models/migrations/v1_23/v304.go @@ -1,7 +1,7 @@ // Copyright 2024 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_23 //nolint +package v1_23 import "xorm.io/xorm" @@ -9,5 +9,8 @@ func AddIndexForReleaseSha1(x *xorm.Engine) error { type Release struct { Sha1 string `xorm:"INDEX VARCHAR(64)"` } - return x.Sync(new(Release)) + _, err := x.SyncWithOptions(xorm.SyncOptions{ + IgnoreDropIndices: true, + }, new(Release)) + return err } diff --git a/models/migrations/v1_23/v304_test.go b/models/migrations/v1_23/v304_test.go new file mode 100644 index 00000000000..c3dfa5e7e7e --- /dev/null +++ b/models/migrations/v1_23/v304_test.go @@ -0,0 +1,40 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_23 + +import ( + "testing" + + "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/modules/timeutil" + + "github.com/stretchr/testify/assert" +) + +func Test_AddIndexForReleaseSha1(t *testing.T) { + type Release struct { + ID int64 `xorm:"pk autoincr"` + RepoID int64 `xorm:"INDEX UNIQUE(n)"` + PublisherID int64 `xorm:"INDEX"` + TagName string `xorm:"INDEX UNIQUE(n)"` + OriginalAuthor string + OriginalAuthorID int64 `xorm:"index"` + LowerTagName string + Target string + Title string + Sha1 string `xorm:"VARCHAR(64)"` + NumCommits int64 + Note string `xorm:"TEXT"` + IsDraft bool `xorm:"NOT NULL DEFAULT false"` + IsPrerelease bool `xorm:"NOT NULL DEFAULT false"` + IsTag bool `xorm:"NOT NULL DEFAULT false"` // will be true only if the record is a tag and has no related releases + CreatedUnix timeutil.TimeStamp `xorm:"INDEX"` + } + + // Prepare and load the testing database + x, deferable := base.PrepareTestEnv(t, 0, new(Release)) + defer deferable() + + assert.NoError(t, AddIndexForReleaseSha1(x)) +} diff --git a/models/migrations/v1_23/v305.go b/models/migrations/v1_23/v305.go index 4d881192b27..3762279de1b 100644 --- a/models/migrations/v1_23/v305.go +++ b/models/migrations/v1_23/v305.go @@ -1,7 +1,7 @@ // Copyright 2024 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_23 //nolint +package v1_23 import ( "code.gitea.io/gitea/modules/timeutil" diff --git a/models/migrations/v1_23/v306.go b/models/migrations/v1_23/v306.go index 276b438e95b..c5c89dbeb8f 100644 --- a/models/migrations/v1_23/v306.go +++ b/models/migrations/v1_23/v306.go @@ -1,7 +1,7 @@ // Copyright 2024 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_23 //nolint +package v1_23 import "xorm.io/xorm" @@ -9,5 +9,9 @@ func AddBlockAdminMergeOverrideBranchProtection(x *xorm.Engine) error { type ProtectedBranch struct { BlockAdminMergeOverride bool `xorm:"NOT NULL DEFAULT false"` } - return x.Sync(new(ProtectedBranch)) + _, err := x.SyncWithOptions(xorm.SyncOptions{ + IgnoreConstrains: true, + IgnoreIndices: true, + }, new(ProtectedBranch)) + return err } diff --git a/models/migrations/v1_23/v307.go b/models/migrations/v1_23/v307.go index ef7f5f2c3f4..54a69d250b7 100644 --- a/models/migrations/v1_23/v307.go +++ b/models/migrations/v1_23/v307.go @@ -1,7 +1,7 @@ // Copyright 2024 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_23 //nolint +package v1_23 import ( "code.gitea.io/gitea/modules/timeutil" diff --git a/models/migrations/v1_23/v308.go b/models/migrations/v1_23/v308.go index 1e8a9b0af24..695fdfcc2de 100644 --- a/models/migrations/v1_23/v308.go +++ b/models/migrations/v1_23/v308.go @@ -1,7 +1,7 @@ // Copyright 2024 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_23 //nolint +package v1_23 import ( "code.gitea.io/gitea/modules/timeutil" diff --git a/models/migrations/v1_23/v309.go b/models/migrations/v1_23/v309.go index 5b39398443f..e629b718a80 100644 --- a/models/migrations/v1_23/v309.go +++ b/models/migrations/v1_23/v309.go @@ -1,7 +1,7 @@ // Copyright 2024 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_23 //nolint +package v1_23 import ( "code.gitea.io/gitea/modules/timeutil" diff --git a/models/migrations/v1_23/v310.go b/models/migrations/v1_23/v310.go index 394417f5a01..074b1c54d35 100644 --- a/models/migrations/v1_23/v310.go +++ b/models/migrations/v1_23/v310.go @@ -1,7 +1,7 @@ // Copyright 2024 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_23 //nolint +package v1_23 import ( "xorm.io/xorm" @@ -12,5 +12,9 @@ func AddPriorityToProtectedBranch(x *xorm.Engine) error { Priority int64 `xorm:"NOT NULL DEFAULT 0"` } - return x.Sync(new(ProtectedBranch)) + _, err := x.SyncWithOptions(xorm.SyncOptions{ + IgnoreConstrains: true, + IgnoreIndices: true, + }, new(ProtectedBranch)) + return err } diff --git a/models/migrations/v1_23/v311.go b/models/migrations/v1_23/v311.go new file mode 100644 index 00000000000..ef48085c79b --- /dev/null +++ b/models/migrations/v1_23/v311.go @@ -0,0 +1,19 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_23 + +import ( + "xorm.io/xorm" +) + +func AddTimeEstimateColumnToIssueTable(x *xorm.Engine) error { + type Issue struct { + TimeEstimate int64 `xorm:"NOT NULL DEFAULT 0"` + } + _, err := x.SyncWithOptions(xorm.SyncOptions{ + IgnoreConstrains: true, + IgnoreIndices: true, + }, new(Issue)) + return err +} diff --git a/models/migrations/v1_24/v312.go b/models/migrations/v1_24/v312.go new file mode 100644 index 00000000000..823b0eae403 --- /dev/null +++ b/models/migrations/v1_24/v312.go @@ -0,0 +1,25 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_24 + +import ( + "xorm.io/xorm" +) + +type pullAutoMerge struct { + DeleteBranchAfterMerge bool +} + +// TableName return database table name for xorm +func (pullAutoMerge) TableName() string { + return "pull_auto_merge" +} + +func AddDeleteBranchAfterMergeForAutoMerge(x *xorm.Engine) error { + _, err := x.SyncWithOptions(xorm.SyncOptions{ + IgnoreConstrains: true, + IgnoreIndices: true, + }, new(pullAutoMerge)) + return err +} diff --git a/models/migrations/v1_24/v313.go b/models/migrations/v1_24/v313.go new file mode 100644 index 00000000000..7e6cda6bfd3 --- /dev/null +++ b/models/migrations/v1_24/v313.go @@ -0,0 +1,31 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_24 + +import ( + "code.gitea.io/gitea/models/migrations/base" + + "xorm.io/xorm" +) + +func MovePinOrderToTableIssuePin(x *xorm.Engine) error { + type IssuePin struct { + ID int64 `xorm:"pk autoincr"` + RepoID int64 `xorm:"UNIQUE(s) NOT NULL"` + IssueID int64 `xorm:"UNIQUE(s) NOT NULL"` + IsPull bool `xorm:"NOT NULL"` + PinOrder int `xorm:"DEFAULT 0"` + } + + if err := x.Sync(new(IssuePin)); err != nil { + return err + } + + if _, err := x.Exec("INSERT INTO issue_pin (repo_id, issue_id, is_pull, pin_order) SELECT repo_id, id, is_pull, pin_order FROM issue WHERE pin_order > 0"); err != nil { + return err + } + sess := x.NewSession() + defer sess.Close() + return base.DropTableColumns(sess, "issue", "pin_order") +} diff --git a/models/migrations/v1_24/v314.go b/models/migrations/v1_24/v314.go new file mode 100644 index 00000000000..51cb2e34aa9 --- /dev/null +++ b/models/migrations/v1_24/v314.go @@ -0,0 +1,19 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_24 + +import ( + "xorm.io/xorm" +) + +func UpdateOwnerIDOfRepoLevelActionsTables(x *xorm.Engine) error { + if _, err := x.Exec("UPDATE `action_runner` SET `owner_id` = 0 WHERE `repo_id` > 0 AND `owner_id` > 0"); err != nil { + return err + } + if _, err := x.Exec("UPDATE `action_variable` SET `owner_id` = 0 WHERE `repo_id` > 0 AND `owner_id` > 0"); err != nil { + return err + } + _, err := x.Exec("UPDATE `secret` SET `owner_id` = 0 WHERE `repo_id` > 0 AND `owner_id` > 0") + return err +} diff --git a/models/migrations/v1_24/v315.go b/models/migrations/v1_24/v315.go new file mode 100644 index 00000000000..52b9b447856 --- /dev/null +++ b/models/migrations/v1_24/v315.go @@ -0,0 +1,19 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_24 + +import ( + "xorm.io/xorm" +) + +func AddEphemeralToActionRunner(x *xorm.Engine) error { + type ActionRunner struct { + Ephemeral bool `xorm:"ephemeral NOT NULL DEFAULT false"` + } + _, err := x.SyncWithOptions(xorm.SyncOptions{ + IgnoreConstrains: true, + IgnoreIndices: true, + }, new(ActionRunner)) + return err +} diff --git a/models/migrations/v1_24/v316.go b/models/migrations/v1_24/v316.go new file mode 100644 index 00000000000..14e888f9eea --- /dev/null +++ b/models/migrations/v1_24/v316.go @@ -0,0 +1,24 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_24 + +import ( + "xorm.io/xorm" +) + +func AddDescriptionForSecretsAndVariables(x *xorm.Engine) error { + type Secret struct { + Description string `xorm:"TEXT"` + } + + type ActionVariable struct { + Description string `xorm:"TEXT"` + } + + _, err := x.SyncWithOptions(xorm.SyncOptions{ + IgnoreConstrains: true, + IgnoreIndices: true, + }, new(Secret), new(ActionVariable)) + return err +} diff --git a/models/migrations/v1_24/v317.go b/models/migrations/v1_24/v317.go new file mode 100644 index 00000000000..a13db2dd27e --- /dev/null +++ b/models/migrations/v1_24/v317.go @@ -0,0 +1,56 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_24 + +import ( + "code.gitea.io/gitea/modules/timeutil" + + "xorm.io/xorm" + "xorm.io/xorm/schemas" +) + +type improveActionTableIndicesAction struct { + ID int64 `xorm:"pk autoincr"` + UserID int64 `xorm:"INDEX"` // Receiver user id. + OpType int + ActUserID int64 // Action user id. + RepoID int64 + CommentID int64 `xorm:"INDEX"` + IsDeleted bool `xorm:"NOT NULL DEFAULT false"` + RefName string + IsPrivate bool `xorm:"NOT NULL DEFAULT false"` + Content string `xorm:"TEXT"` + CreatedUnix timeutil.TimeStamp `xorm:"created"` +} + +// TableName sets the name of this table +func (*improveActionTableIndicesAction) TableName() string { + return "action" +} + +// TableIndices implements xorm's TableIndices interface +func (a *improveActionTableIndicesAction) TableIndices() []*schemas.Index { + repoIndex := schemas.NewIndex("r_u_d", schemas.IndexType) + repoIndex.AddColumn("repo_id", "user_id", "is_deleted") + + actUserIndex := schemas.NewIndex("au_r_c_u_d", schemas.IndexType) + actUserIndex.AddColumn("act_user_id", "repo_id", "created_unix", "user_id", "is_deleted") + + cudIndex := schemas.NewIndex("c_u_d", schemas.IndexType) + cudIndex.AddColumn("created_unix", "user_id", "is_deleted") + + cuIndex := schemas.NewIndex("c_u", schemas.IndexType) + cuIndex.AddColumn("user_id", "is_deleted") + + actUserUserIndex := schemas.NewIndex("au_c_u", schemas.IndexType) + actUserUserIndex.AddColumn("act_user_id", "created_unix", "user_id") + + indices := []*schemas.Index{actUserIndex, repoIndex, cudIndex, cuIndex, actUserUserIndex} + + return indices +} + +func AddNewIndexForUserDashboard(x *xorm.Engine) error { + return x.Sync(new(improveActionTableIndicesAction)) +} diff --git a/models/migrations/v1_24/v318.go b/models/migrations/v1_24/v318.go new file mode 100644 index 00000000000..9b4a5409609 --- /dev/null +++ b/models/migrations/v1_24/v318.go @@ -0,0 +1,21 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_24 + +import ( + "code.gitea.io/gitea/models/perm" + + "xorm.io/xorm" +) + +func AddRepoUnitAnonymousAccessMode(x *xorm.Engine) error { + type RepoUnit struct { //revive:disable-line:exported + AnonymousAccessMode perm.AccessMode `xorm:"NOT NULL DEFAULT 0"` + } + _, err := x.SyncWithOptions(xorm.SyncOptions{ + IgnoreConstrains: true, + IgnoreIndices: true, + }, new(RepoUnit)) + return err +} diff --git a/models/migrations/v1_24/v319.go b/models/migrations/v1_24/v319.go new file mode 100644 index 00000000000..648081f74e0 --- /dev/null +++ b/models/migrations/v1_24/v319.go @@ -0,0 +1,19 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_24 + +import ( + "xorm.io/xorm" +) + +func AddExclusiveOrderColumnToLabelTable(x *xorm.Engine) error { + type Label struct { + ExclusiveOrder int `xorm:"DEFAULT 0"` + } + _, err := x.SyncWithOptions(xorm.SyncOptions{ + IgnoreConstrains: true, + IgnoreIndices: true, + }, new(Label)) + return err +} diff --git a/models/migrations/v1_24/v320.go b/models/migrations/v1_24/v320.go new file mode 100644 index 00000000000..ebef71939c2 --- /dev/null +++ b/models/migrations/v1_24/v320.go @@ -0,0 +1,57 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_24 + +import ( + "code.gitea.io/gitea/modules/json" + + "xorm.io/xorm" +) + +func MigrateSkipTwoFactor(x *xorm.Engine) error { + type LoginSource struct { + TwoFactorPolicy string `xorm:"two_factor_policy NOT NULL DEFAULT ''"` + } + _, err := x.SyncWithOptions( + xorm.SyncOptions{ + IgnoreConstrains: true, + IgnoreIndices: true, + }, + new(LoginSource), + ) + if err != nil { + return err + } + + type LoginSourceSimple struct { + ID int64 + Cfg string + } + + var loginSources []LoginSourceSimple + err = x.Table("login_source").Find(&loginSources) + if err != nil { + return err + } + + for _, source := range loginSources { + if source.Cfg == "" { + continue + } + + var cfg map[string]any + err = json.Unmarshal([]byte(source.Cfg), &cfg) + if err != nil { + return err + } + + if cfg["SkipLocalTwoFA"] == true { + _, err = x.Exec("UPDATE login_source SET two_factor_policy = 'skip' WHERE id = ?", source.ID) + if err != nil { + return err + } + } + } + return nil +} diff --git a/models/migrations/v1_25/main_test.go b/models/migrations/v1_25/main_test.go new file mode 100644 index 00000000000..d2c4a4105d3 --- /dev/null +++ b/models/migrations/v1_25/main_test.go @@ -0,0 +1,14 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_25 + +import ( + "testing" + + "code.gitea.io/gitea/models/migrations/base" +) + +func TestMain(m *testing.M) { + base.MainTest(m) +} diff --git a/models/migrations/v1_25/v321.go b/models/migrations/v1_25/v321.go new file mode 100644 index 00000000000..73ef180f485 --- /dev/null +++ b/models/migrations/v1_25/v321.go @@ -0,0 +1,52 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_25 + +import ( + "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/modules/setting" + + "xorm.io/xorm" + "xorm.io/xorm/schemas" +) + +func UseLongTextInSomeColumnsAndFixBugs(x *xorm.Engine) error { + if !setting.Database.Type.IsMySQL() { + return nil // Only mysql need to change from text to long text, for other databases, they are the same + } + + if err := base.ModifyColumn(x, "review_state", &schemas.Column{ + Name: "updated_files", + SQLType: schemas.SQLType{ + Name: "LONGTEXT", + }, + Length: 0, + Nullable: false, + DefaultIsEmpty: true, + }); err != nil { + return err + } + + if err := base.ModifyColumn(x, "package_property", &schemas.Column{ + Name: "value", + SQLType: schemas.SQLType{ + Name: "LONGTEXT", + }, + Length: 0, + Nullable: false, + DefaultIsEmpty: true, + }); err != nil { + return err + } + + return base.ModifyColumn(x, "notice", &schemas.Column{ + Name: "description", + SQLType: schemas.SQLType{ + Name: "LONGTEXT", + }, + Length: 0, + Nullable: false, + DefaultIsEmpty: true, + }) +} diff --git a/models/migrations/v1_25/v321_test.go b/models/migrations/v1_25/v321_test.go new file mode 100644 index 00000000000..4897783fd3b --- /dev/null +++ b/models/migrations/v1_25/v321_test.go @@ -0,0 +1,70 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_25 + +import ( + "testing" + + "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/timeutil" + + "github.com/stretchr/testify/assert" +) + +func Test_UseLongTextInSomeColumnsAndFixBugs(t *testing.T) { + if !setting.Database.Type.IsMySQL() { + t.Skip("Only MySQL needs to change from TEXT to LONGTEXT") + } + + type ReviewState struct { + ID int64 `xorm:"pk autoincr"` + UserID int64 `xorm:"NOT NULL UNIQUE(pull_commit_user)"` + PullID int64 `xorm:"NOT NULL INDEX UNIQUE(pull_commit_user) DEFAULT 0"` // Which PR was the review on? + CommitSHA string `xorm:"NOT NULL VARCHAR(64) UNIQUE(pull_commit_user)"` // Which commit was the head commit for the review? + UpdatedFiles map[string]int `xorm:"NOT NULL TEXT JSON"` // Stores for each of the changed files of a PR whether they have been viewed, changed since last viewed, or not viewed + UpdatedUnix timeutil.TimeStamp `xorm:"updated"` // Is an accurate indicator of the order of commits as we do not expect it to be possible to make reviews on previous commits + } + + type PackageProperty struct { + ID int64 `xorm:"pk autoincr"` + RefType int `xorm:"INDEX NOT NULL"` + RefID int64 `xorm:"INDEX NOT NULL"` + Name string `xorm:"INDEX NOT NULL"` + Value string `xorm:"TEXT NOT NULL"` + } + + type Notice struct { + ID int64 `xorm:"pk autoincr"` + Type int + Description string `xorm:"LONGTEXT"` + CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` + } + + // Prepare and load the testing database + x, deferable := base.PrepareTestEnv(t, 0, new(ReviewState), new(PackageProperty), new(Notice)) + defer deferable() + + assert.NoError(t, UseLongTextInSomeColumnsAndFixBugs(x)) + + tables, err := x.DBMetas() + assert.NoError(t, err) + + for _, table := range tables { + switch table.Name { + case "review_state": + column := table.GetColumn("updated_files") + assert.NotNil(t, column) + assert.Equal(t, "LONGTEXT", column.SQLType.Name) + case "package_property": + column := table.GetColumn("value") + assert.NotNil(t, column) + assert.Equal(t, "LONGTEXT", column.SQLType.Name) + case "notice": + column := table.GetColumn("description") + assert.NotNil(t, column) + assert.Equal(t, "LONGTEXT", column.SQLType.Name) + } + } +} diff --git a/models/migrations/v1_6/v70.go b/models/migrations/v1_6/v70.go index 74434a84a14..41f09669423 100644 --- a/models/migrations/v1_6/v70.go +++ b/models/migrations/v1_6/v70.go @@ -1,7 +1,7 @@ // Copyright 2018 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_6 //nolint +package v1_6 import ( "fmt" diff --git a/models/migrations/v1_6/v71.go b/models/migrations/v1_6/v71.go index 586187228b3..2b11f57c92f 100644 --- a/models/migrations/v1_6/v71.go +++ b/models/migrations/v1_6/v71.go @@ -1,7 +1,7 @@ // Copyright 2018 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_6 //nolint +package v1_6 import ( "fmt" diff --git a/models/migrations/v1_6/v72.go b/models/migrations/v1_6/v72.go index 04cef9a1707..9fad88a1b61 100644 --- a/models/migrations/v1_6/v72.go +++ b/models/migrations/v1_6/v72.go @@ -1,7 +1,7 @@ // Copyright 2018 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_6 //nolint +package v1_6 import ( "fmt" diff --git a/models/migrations/v1_7/v73.go b/models/migrations/v1_7/v73.go index b5a748aae3a..e0b7a285375 100644 --- a/models/migrations/v1_7/v73.go +++ b/models/migrations/v1_7/v73.go @@ -1,7 +1,7 @@ // Copyright 2018 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_7 //nolint +package v1_7 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_7/v74.go b/models/migrations/v1_7/v74.go index f0567e3c9b9..376be37a246 100644 --- a/models/migrations/v1_7/v74.go +++ b/models/migrations/v1_7/v74.go @@ -1,7 +1,7 @@ // Copyright 2018 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_7 //nolint +package v1_7 import "xorm.io/xorm" diff --git a/models/migrations/v1_7/v75.go b/models/migrations/v1_7/v75.go index fa7430970c9..ef115754664 100644 --- a/models/migrations/v1_7/v75.go +++ b/models/migrations/v1_7/v75.go @@ -1,7 +1,7 @@ // Copyright 2018 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_7 //nolint +package v1_7 import ( "xorm.io/builder" diff --git a/models/migrations/v1_8/v76.go b/models/migrations/v1_8/v76.go index d3fbd94deb1..81e93075497 100644 --- a/models/migrations/v1_8/v76.go +++ b/models/migrations/v1_8/v76.go @@ -1,7 +1,7 @@ // Copyright 2018 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_8 //nolint +package v1_8 import ( "fmt" diff --git a/models/migrations/v1_8/v77.go b/models/migrations/v1_8/v77.go index 8b199939245..4fe5ebe6350 100644 --- a/models/migrations/v1_8/v77.go +++ b/models/migrations/v1_8/v77.go @@ -1,7 +1,7 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_8 //nolint +package v1_8 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_8/v78.go b/models/migrations/v1_8/v78.go index 8f041c14849..e67f4641312 100644 --- a/models/migrations/v1_8/v78.go +++ b/models/migrations/v1_8/v78.go @@ -1,7 +1,7 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_8 //nolint +package v1_8 import ( "code.gitea.io/gitea/models/migrations/base" diff --git a/models/migrations/v1_8/v79.go b/models/migrations/v1_8/v79.go index eb3a9ed0f4d..3f50114d5a5 100644 --- a/models/migrations/v1_8/v79.go +++ b/models/migrations/v1_8/v79.go @@ -1,7 +1,7 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_8 //nolint +package v1_8 import ( "code.gitea.io/gitea/modules/setting" diff --git a/models/migrations/v1_8/v80.go b/models/migrations/v1_8/v80.go index cebbbead28b..6f9df47a933 100644 --- a/models/migrations/v1_8/v80.go +++ b/models/migrations/v1_8/v80.go @@ -1,7 +1,7 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_8 //nolint +package v1_8 import "xorm.io/xorm" diff --git a/models/migrations/v1_8/v81.go b/models/migrations/v1_8/v81.go index a100dc1ef71..3c2acc64584 100644 --- a/models/migrations/v1_8/v81.go +++ b/models/migrations/v1_8/v81.go @@ -1,7 +1,7 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_8 //nolint +package v1_8 import ( "fmt" diff --git a/models/migrations/v1_9/v82.go b/models/migrations/v1_9/v82.go index 26806dd6450..f0307bf07aa 100644 --- a/models/migrations/v1_9/v82.go +++ b/models/migrations/v1_9/v82.go @@ -1,9 +1,10 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_9 //nolint +package v1_9 import ( + "context" "fmt" "path/filepath" "strings" @@ -14,7 +15,7 @@ import ( "xorm.io/xorm" ) -func FixReleaseSha1OnReleaseTable(x *xorm.Engine) error { +func FixReleaseSha1OnReleaseTable(ctx context.Context, x *xorm.Engine) error { type Release struct { ID int64 RepoID int64 @@ -98,7 +99,7 @@ func FixReleaseSha1OnReleaseTable(x *xorm.Engine) error { userCache[repo.OwnerID] = user } - gitRepo, err = git.OpenRepository(git.DefaultContext, RepoPath(user.Name, repo.Name)) + gitRepo, err = git.OpenRepository(ctx, RepoPath(user.Name, repo.Name)) if err != nil { return err } diff --git a/models/migrations/v1_9/v83.go b/models/migrations/v1_9/v83.go index 10e6c458757..a0cd57f7c5f 100644 --- a/models/migrations/v1_9/v83.go +++ b/models/migrations/v1_9/v83.go @@ -1,7 +1,7 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_9 //nolint +package v1_9 import ( "code.gitea.io/gitea/modules/timeutil" diff --git a/models/migrations/v1_9/v84.go b/models/migrations/v1_9/v84.go index c7155fe9cff..423915ae57c 100644 --- a/models/migrations/v1_9/v84.go +++ b/models/migrations/v1_9/v84.go @@ -1,7 +1,7 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_9 //nolint +package v1_9 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_9/v85.go b/models/migrations/v1_9/v85.go index a23d7c5d6ef..48e1cd5dc4e 100644 --- a/models/migrations/v1_9/v85.go +++ b/models/migrations/v1_9/v85.go @@ -1,7 +1,7 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_9 //nolint +package v1_9 import ( "fmt" diff --git a/models/migrations/v1_9/v86.go b/models/migrations/v1_9/v86.go index cf2725d1585..9464ff0cf68 100644 --- a/models/migrations/v1_9/v86.go +++ b/models/migrations/v1_9/v86.go @@ -1,7 +1,7 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_9 //nolint +package v1_9 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_9/v87.go b/models/migrations/v1_9/v87.go index fa01b6e5e36..81a4ebf80d0 100644 --- a/models/migrations/v1_9/v87.go +++ b/models/migrations/v1_9/v87.go @@ -1,7 +1,7 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_9 //nolint +package v1_9 import ( "xorm.io/xorm" diff --git a/models/organization/org.go b/models/organization/org.go index 725a99356e9..9ece044d6c0 100644 --- a/models/organization/org.go +++ b/models/organization/org.go @@ -9,11 +9,8 @@ import ( "fmt" "strings" - actions_model "code.gitea.io/gitea/models/actions" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/perm" - repo_model "code.gitea.io/gitea/models/repo" - secret_model "code.gitea.io/gitea/models/secret" "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/log" @@ -162,8 +159,8 @@ func (org *Organization) AvatarLink(ctx context.Context) string { } // HTMLURL returns the organization's full link. -func (org *Organization) HTMLURL() string { - return org.AsUser().HTMLURL() +func (org *Organization) HTMLURL(ctx context.Context) string { + return org.AsUser().HTMLURL(ctx) } // OrganisationLink returns the organization sub page link. @@ -181,12 +178,6 @@ func (org *Organization) HomeLink() string { return org.AsUser().HomeLink() } -// CanCreateRepo returns if user login can create a repository -// NOTE: functions calling this assume a failure due to repository count limit; if new checks are added, those functions should be revised -func (org *Organization) CanCreateRepo() bool { - return org.AsUser().CanCreateRepo() -} - // FindOrgMembersOpts represensts find org members conditions type FindOrgMembersOpts struct { db.ListOptions @@ -319,74 +310,69 @@ func CreateOrganization(ctx context.Context, org *Organization, owner *user_mode org.NumMembers = 1 org.Type = user_model.UserTypeOrganization - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return err - } - defer committer.Close() - - if err = user_model.DeleteUserRedirect(ctx, org.Name); err != nil { - return err - } - - if err = db.Insert(ctx, org); err != nil { - return fmt.Errorf("insert organization: %w", err) - } - if err = user_model.GenerateRandomAvatar(ctx, org.AsUser()); err != nil { - return fmt.Errorf("generate random avatar: %w", err) - } - - // Add initial creator to organization and owner team. - if err = db.Insert(ctx, &OrgUser{ - UID: owner.ID, - OrgID: org.ID, - IsPublic: setting.Service.DefaultOrgMemberVisible, - }); err != nil { - return fmt.Errorf("insert org-user relation: %w", err) - } - - // Create default owner team. - t := &Team{ - OrgID: org.ID, - LowerName: strings.ToLower(OwnerTeamName), - Name: OwnerTeamName, - AccessMode: perm.AccessModeOwner, - NumMembers: 1, - IncludesAllRepositories: true, - CanCreateOrgRepo: true, - } - if err = db.Insert(ctx, t); err != nil { - return fmt.Errorf("insert owner team: %w", err) - } - - // insert units for team - units := make([]TeamUnit, 0, len(unit.AllRepoUnitTypes)) - for _, tp := range unit.AllRepoUnitTypes { - up := perm.AccessModeOwner - if tp == unit.TypeExternalTracker || tp == unit.TypeExternalWiki { - up = perm.AccessModeRead + return db.WithTx(ctx, func(ctx context.Context) error { + if err = user_model.DeleteUserRedirect(ctx, org.Name); err != nil { + return err } - units = append(units, TeamUnit{ - OrgID: org.ID, - TeamID: t.ID, - Type: tp, - AccessMode: up, - }) - } - if err = db.Insert(ctx, &units); err != nil { - return err - } + if err = db.Insert(ctx, org); err != nil { + return fmt.Errorf("insert organization: %w", err) + } + if err = user_model.GenerateRandomAvatar(ctx, org.AsUser()); err != nil { + return fmt.Errorf("generate random avatar: %w", err) + } - if err = db.Insert(ctx, &TeamUser{ - UID: owner.ID, - OrgID: org.ID, - TeamID: t.ID, - }); err != nil { - return fmt.Errorf("insert team-user relation: %w", err) - } + // Add initial creator to organization and owner team. + if err = db.Insert(ctx, &OrgUser{ + UID: owner.ID, + OrgID: org.ID, + IsPublic: setting.Service.DefaultOrgMemberVisible, + }); err != nil { + return fmt.Errorf("insert org-user relation: %w", err) + } - return committer.Commit() + // Create default owner team. + t := &Team{ + OrgID: org.ID, + LowerName: strings.ToLower(OwnerTeamName), + Name: OwnerTeamName, + AccessMode: perm.AccessModeOwner, + NumMembers: 1, + IncludesAllRepositories: true, + CanCreateOrgRepo: true, + } + if err = db.Insert(ctx, t); err != nil { + return fmt.Errorf("insert owner team: %w", err) + } + + // insert units for team + units := make([]TeamUnit, 0, len(unit.AllRepoUnitTypes)) + for _, tp := range unit.AllRepoUnitTypes { + up := perm.AccessModeOwner + if tp == unit.TypeExternalTracker || tp == unit.TypeExternalWiki { + up = perm.AccessModeRead + } + units = append(units, TeamUnit{ + OrgID: org.ID, + TeamID: t.ID, + Type: tp, + AccessMode: up, + }) + } + + if err = db.Insert(ctx, &units); err != nil { + return err + } + + if err = db.Insert(ctx, &TeamUser{ + UID: owner.ID, + OrgID: org.ID, + TeamID: t.ID, + }); err != nil { + return fmt.Errorf("insert team-user relation: %w", err) + } + return nil + }) } // GetOrgByName returns organization by given name. @@ -407,33 +393,6 @@ func GetOrgByName(ctx context.Context, name string) (*Organization, error) { 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 func (org *Organization) GetOrgUserMaxAuthorizeLevel(ctx context.Context, uid int64) (perm.AccessMode, error) { var authorize perm.AccessMode @@ -535,31 +494,26 @@ func AddOrgUser(ctx context.Context, orgID, uid int64) error { return err } - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return err - } - defer committer.Close() + return db.WithTx(ctx, func(ctx context.Context) error { + // check in transaction + isAlreadyMember, err = IsOrganizationMember(ctx, orgID, uid) + if err != nil || isAlreadyMember { + return err + } - // check in transaction - isAlreadyMember, err = IsOrganizationMember(ctx, orgID, uid) - if err != nil || isAlreadyMember { - return err - } + ou := &OrgUser{ + UID: uid, + OrgID: orgID, + IsPublic: setting.Service.DefaultOrgMemberVisible, + } - ou := &OrgUser{ - UID: uid, - OrgID: orgID, - IsPublic: setting.Service.DefaultOrgMemberVisible, - } - - if err := db.Insert(ctx, ou); err != nil { - return err - } else if _, err = db.Exec(ctx, "UPDATE `user` SET num_members = num_members + 1 WHERE id = ?", orgID); err != nil { - return err - } - - return committer.Commit() + if err := db.Insert(ctx, ou); err != nil { + return err + } else if _, err = db.Exec(ctx, "UPDATE `user` SET num_members = num_members + 1 WHERE id = ?", orgID); err != nil { + return err + } + return nil + }) } // GetOrgByID returns the user object by given ID if exists. @@ -604,7 +558,9 @@ func RemoveOrgRepo(ctx context.Context, orgID, repoID int64) error { 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) return teams, db.GetEngine(ctx). Where("`team_user`.org_id = ?", org.ID). @@ -616,7 +572,8 @@ func (org *Organization) getUserTeams(ctx context.Context, userID int64, cols .. 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) return teamIDs, db.GetEngine(ctx). Table("team"). @@ -635,180 +592,3 @@ func getUserTeamIDsQueryBuilder(orgID, userID int64) *builder.Builder { "team_user.uid": userID, }) } - -// TeamsWithAccessToRepo returns all teams that have given access level to the repository. -func (org *Organization) TeamsWithAccessToRepo(ctx context.Context, repoID int64, mode perm.AccessMode) ([]*Team, error) { - return GetTeamsWithAccessToRepo(ctx, org.ID, repoID, mode) -} - -// 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 -} diff --git a/models/organization/org_list.go b/models/organization/org_list.go index 4c4168af1f8..f37961b5f62 100644 --- a/models/organization/org_list.go +++ b/models/organization/org_list.go @@ -50,8 +50,8 @@ type SearchOrganizationsOptions struct { // FindOrgOptions finds orgs options type FindOrgOptions struct { db.ListOptions - UserID int64 - IncludePrivate bool + UserID int64 + IncludeVisibility structs.VisibleType } func queryUserOrgIDs(userID int64, includePrivate bool) *builder.Builder { @@ -65,11 +65,10 @@ func queryUserOrgIDs(userID int64, includePrivate bool) *builder.Builder { func (opts FindOrgOptions) ToConds() builder.Cond { var cond builder.Cond = builder.Eq{"`user`.`type`": user_model.UserTypeOrganization} if opts.UserID > 0 { - cond = cond.And(builder.In("`user`.`id`", queryUserOrgIDs(opts.UserID, opts.IncludePrivate))) - } - if !opts.IncludePrivate { - cond = cond.And(builder.Eq{"`user`.visibility": structs.VisibleTypePublic}) + cond = cond.And(builder.In("`user`.`id`", queryUserOrgIDs(opts.UserID, opts.IncludeVisibility == structs.VisibleTypePrivate))) } + // public=0, limited=1, private=2 + cond = cond.And(builder.Lte{"`user`.visibility": opts.IncludeVisibility}) return cond } @@ -77,6 +76,16 @@ func (opts FindOrgOptions) ToOrders() string { return "`user`.lower_name ASC" } +func DoerViewOtherVisibility(doer, other *user_model.User) structs.VisibleType { + if doer == nil || other == nil { + return structs.VisibleTypePublic + } + if doer.IsAdmin || doer.ID == other.ID { + return structs.VisibleTypePrivate + } + return structs.VisibleTypeLimited +} + // GetOrgsCanCreateRepoByUserID returns a list of organizations where given user ID // are allowed to create repos. func GetOrgsCanCreateRepoByUserID(ctx context.Context, userID int64) ([]*Organization, error) { @@ -96,11 +105,6 @@ type MinimalOrg = Organization // GetUserOrgsList returns all organizations the given user has access to func GetUserOrgsList(ctx context.Context, user *user_model.User) ([]*MinimalOrg, error) { - schema, err := db.TableInfo(new(user_model.User)) - if err != nil { - return nil, err - } - outputCols := []string{ "id", "name", @@ -113,7 +117,7 @@ func GetUserOrgsList(ctx context.Context, user *user_model.User) ([]*MinimalOrg, selectColumns := &strings.Builder{} for i, col := range outputCols { - fmt.Fprintf(selectColumns, "`%s`.%s", schema.Name, col) + _, _ = fmt.Fprintf(selectColumns, "`user`.%s", col) if i < len(outputCols)-1 { selectColumns.WriteString(", ") } @@ -124,6 +128,7 @@ func GetUserOrgsList(ctx context.Context, user *user_model.User) ([]*MinimalOrg, if err := db.GetEngine(ctx).Select(columnsStr). Table("user"). Where(builder.In("`user`.`id`", queryUserOrgIDs(user.ID, true))). + OrderBy("`user`.lower_name ASC"). Find(&orgs); err != nil { return nil, err } diff --git a/models/organization/org_list_test.go b/models/organization/org_list_test.go index edc8996f3ec..b2a10444225 100644 --- a/models/organization/org_list_test.go +++ b/models/organization/org_list_test.go @@ -10,64 +10,75 @@ import ( "code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/structs" "github.com/stretchr/testify/assert" ) -func TestCountOrganizations(t *testing.T) { +func TestOrgList(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - expected, err := db.GetEngine(db.DefaultContext).Where("type=?", user_model.UserTypeOrganization).Count(&organization.Organization{}) + t.Run("CountOrganizations", testCountOrganizations) + t.Run("FindOrgs", testFindOrgs) + t.Run("GetUserOrgsList", testGetUserOrgsList) + t.Run("LoadOrgListTeams", testLoadOrgListTeams) + t.Run("DoerViewOtherVisibility", testDoerViewOtherVisibility) +} + +func testCountOrganizations(t *testing.T) { + expected, err := db.GetEngine(t.Context()).Where("type=?", user_model.UserTypeOrganization).Count(&organization.Organization{}) assert.NoError(t, err) - cnt, err := db.Count[organization.Organization](db.DefaultContext, organization.FindOrgOptions{IncludePrivate: true}) + cnt, err := db.Count[organization.Organization](t.Context(), organization.FindOrgOptions{IncludeVisibility: structs.VisibleTypePrivate}) assert.NoError(t, err) assert.Equal(t, expected, cnt) } -func TestFindOrgs(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - orgs, err := db.Find[organization.Organization](db.DefaultContext, organization.FindOrgOptions{ - UserID: 4, - IncludePrivate: true, +func testFindOrgs(t *testing.T) { + orgs, err := db.Find[organization.Organization](t.Context(), organization.FindOrgOptions{ + UserID: 4, + IncludeVisibility: structs.VisibleTypePrivate, }) assert.NoError(t, err) if assert.Len(t, orgs, 1) { assert.EqualValues(t, 3, orgs[0].ID) } - orgs, err = db.Find[organization.Organization](db.DefaultContext, organization.FindOrgOptions{ - UserID: 4, - IncludePrivate: false, + orgs, err = db.Find[organization.Organization](t.Context(), organization.FindOrgOptions{ + UserID: 4, }) assert.NoError(t, err) - assert.Len(t, orgs, 0) + assert.Empty(t, orgs) - total, err := db.Count[organization.Organization](db.DefaultContext, organization.FindOrgOptions{ - UserID: 4, - IncludePrivate: true, + total, err := db.Count[organization.Organization](t.Context(), organization.FindOrgOptions{ + UserID: 4, + IncludeVisibility: structs.VisibleTypePrivate, }) assert.NoError(t, err) assert.EqualValues(t, 1, total) } -func TestGetUserOrgsList(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - orgs, err := organization.GetUserOrgsList(db.DefaultContext, &user_model.User{ID: 4}) +func testGetUserOrgsList(t *testing.T) { + orgs, err := organization.GetUserOrgsList(t.Context(), &user_model.User{ID: 4}) assert.NoError(t, err) if assert.Len(t, orgs, 1) { assert.EqualValues(t, 3, orgs[0].ID) // repo_id: 3 is in the team, 32 is public, 5 is private with no team - assert.EqualValues(t, 2, orgs[0].NumRepos) + assert.Equal(t, 2, orgs[0].NumRepos) } } -func TestLoadOrgListTeams(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - orgs, err := organization.GetUserOrgsList(db.DefaultContext, &user_model.User{ID: 4}) +func testLoadOrgListTeams(t *testing.T) { + orgs, err := organization.GetUserOrgsList(t.Context(), &user_model.User{ID: 4}) assert.NoError(t, err) assert.Len(t, orgs, 1) - teamsMap, err := organization.OrgList(orgs).LoadTeams(db.DefaultContext) + teamsMap, err := organization.OrgList(orgs).LoadTeams(t.Context()) assert.NoError(t, err) assert.Len(t, teamsMap, 1) assert.Len(t, teamsMap[3], 5) } + +func testDoerViewOtherVisibility(t *testing.T) { + assert.Equal(t, structs.VisibleTypePublic, organization.DoerViewOtherVisibility(nil, nil)) + assert.Equal(t, structs.VisibleTypeLimited, organization.DoerViewOtherVisibility(&user_model.User{ID: 1}, &user_model.User{ID: 2})) + assert.Equal(t, structs.VisibleTypePrivate, organization.DoerViewOtherVisibility(&user_model.User{ID: 1}, &user_model.User{ID: 1})) + assert.Equal(t, structs.VisibleTypePrivate, organization.DoerViewOtherVisibility(&user_model.User{ID: 1, IsAdmin: true}, &user_model.User{ID: 2})) +} diff --git a/models/organization/org_repo.go b/models/organization/org_repo.go deleted file mode 100644 index f7e59928f42..00000000000 --- a/models/organization/org_repo.go +++ /dev/null @@ -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) -} diff --git a/models/organization/org_test.go b/models/organization/org_test.go index 7159f0fc465..e7c4d2f9f74 100644 --- a/models/organization/org_test.go +++ b/models/organization/org_test.go @@ -16,6 +16,7 @@ import ( "code.gitea.io/gitea/modules/structs" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestUser_IsOwnedBy(t *testing.T) { @@ -33,7 +34,7 @@ func TestUser_IsOwnedBy(t *testing.T) { {2, 3, false}, } { org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: testCase.OrgID}) - isOwner, err := org.IsOwnedBy(db.DefaultContext, testCase.UserID) + isOwner, err := org.IsOwnedBy(t.Context(), testCase.UserID) assert.NoError(t, err) assert.Equal(t, testCase.ExpectedOwner, isOwner) } @@ -54,7 +55,7 @@ func TestUser_IsOrgMember(t *testing.T) { {2, 3, false}, } { org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: testCase.OrgID}) - isMember, err := org.IsOrgMember(db.DefaultContext, testCase.UserID) + isMember, err := org.IsOrgMember(t.Context(), testCase.UserID) assert.NoError(t, err) assert.Equal(t, testCase.ExpectedMember, isMember) } @@ -63,35 +64,35 @@ func TestUser_IsOrgMember(t *testing.T) { func TestUser_GetTeam(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3}) - team, err := org.GetTeam(db.DefaultContext, "team1") + team, err := org.GetTeam(t.Context(), "team1") assert.NoError(t, err) assert.Equal(t, org.ID, team.OrgID) assert.Equal(t, "team1", team.LowerName) - _, err = org.GetTeam(db.DefaultContext, "does not exist") + _, err = org.GetTeam(t.Context(), "does not exist") assert.True(t, organization.IsErrTeamNotExist(err)) nonOrg := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 2}) - _, err = nonOrg.GetTeam(db.DefaultContext, "team") + _, err = nonOrg.GetTeam(t.Context(), "team") assert.True(t, organization.IsErrTeamNotExist(err)) } func TestUser_GetOwnerTeam(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3}) - team, err := org.GetOwnerTeam(db.DefaultContext) + team, err := org.GetOwnerTeam(t.Context()) assert.NoError(t, err) assert.Equal(t, org.ID, team.OrgID) nonOrg := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 2}) - _, err = nonOrg.GetOwnerTeam(db.DefaultContext) + _, err = nonOrg.GetOwnerTeam(t.Context()) assert.True(t, organization.IsErrTeamNotExist(err)) } func TestUser_GetTeams(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3}) - teams, err := org.LoadTeams(db.DefaultContext) + teams, err := org.LoadTeams(t.Context()) assert.NoError(t, err) if assert.Len(t, teams, 5) { assert.Equal(t, int64(1), teams[0].ID) @@ -105,7 +106,7 @@ func TestUser_GetTeams(t *testing.T) { func TestUser_GetMembers(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3}) - members, _, err := org.GetMembers(db.DefaultContext, &user_model.User{IsAdmin: true}) + members, _, err := org.GetMembers(t.Context(), &user_model.User{IsAdmin: true}) assert.NoError(t, err) if assert.Len(t, members, 3) { assert.Equal(t, int64(2), members[0].ID) @@ -117,24 +118,24 @@ func TestUser_GetMembers(t *testing.T) { func TestGetOrgByName(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - org, err := organization.GetOrgByName(db.DefaultContext, "org3") + org, err := organization.GetOrgByName(t.Context(), "org3") assert.NoError(t, err) assert.EqualValues(t, 3, org.ID) assert.Equal(t, "org3", org.Name) - _, err = organization.GetOrgByName(db.DefaultContext, "user2") // user2 is an individual + _, err = organization.GetOrgByName(t.Context(), "user2") // user2 is an individual assert.True(t, organization.IsErrOrgNotExist(err)) - _, err = organization.GetOrgByName(db.DefaultContext, "") // corner case + _, err = organization.GetOrgByName(t.Context(), "") // corner case assert.True(t, organization.IsErrOrgNotExist(err)) } func TestIsOrganizationOwner(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) test := func(orgID, userID int64, expected bool) { - isOwner, err := organization.IsOrganizationOwner(db.DefaultContext, orgID, userID) + isOwner, err := organization.IsOrganizationOwner(t.Context(), orgID, userID) assert.NoError(t, err) - assert.EqualValues(t, expected, isOwner) + assert.Equal(t, expected, isOwner) } test(3, 2, true) test(3, 3, false) @@ -146,9 +147,9 @@ func TestIsOrganizationOwner(t *testing.T) { func TestIsOrganizationMember(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) test := func(orgID, userID int64, expected bool) { - isMember, err := organization.IsOrganizationMember(db.DefaultContext, orgID, userID) + isMember, err := organization.IsOrganizationMember(t.Context(), orgID, userID) assert.NoError(t, err) - assert.EqualValues(t, expected, isMember) + assert.Equal(t, expected, isMember) } test(3, 2, true) test(3, 3, false) @@ -161,9 +162,9 @@ func TestIsOrganizationMember(t *testing.T) { func TestIsPublicMembership(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) test := func(orgID, userID int64, expected bool) { - isMember, err := organization.IsPublicMembership(db.DefaultContext, orgID, userID) + isMember, err := organization.IsPublicMembership(t.Context(), orgID, userID) assert.NoError(t, err) - assert.EqualValues(t, expected, isMember) + assert.Equal(t, expected, isMember) } test(3, 2, true) test(3, 3, false) @@ -180,9 +181,8 @@ func TestRestrictedUserOrgMembers(t *testing.T) { ID: 29, IsRestricted: true, }) - if !assert.True(t, restrictedUser.IsRestricted) { - return // ensure fixtures return restricted user - } + // ensure fixtures return restricted user + require.True(t, restrictedUser.IsRestricted) testCases := []struct { name string @@ -226,18 +226,18 @@ func TestRestrictedUserOrgMembers(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - count, err := organization.CountOrgMembers(db.DefaultContext, tc.opts) + count, err := organization.CountOrgMembers(t.Context(), tc.opts) assert.NoError(t, err) assert.EqualValues(t, len(tc.expectedUIDs), count) - members, err := organization.GetOrgUsersByOrgID(db.DefaultContext, tc.opts) + members, err := organization.GetOrgUsersByOrgID(t.Context(), tc.opts) assert.NoError(t, err) memberUIDs := make([]int64, 0, len(members)) for _, member := range members { memberUIDs = append(memberUIDs, member.UID) } slices.Sort(memberUIDs) - assert.EqualValues(t, tc.expectedUIDs, memberUIDs) + assert.Equal(t, tc.expectedUIDs, memberUIDs) }) } } @@ -250,12 +250,12 @@ func TestGetOrgUsersByOrgID(t *testing.T) { OrgID: 3, } assert.False(t, opts.PublicOnly()) - orgUsers, err := organization.GetOrgUsersByOrgID(db.DefaultContext, opts) + orgUsers, err := organization.GetOrgUsersByOrgID(t.Context(), opts) assert.NoError(t, err) sort.Slice(orgUsers, func(i, j int) bool { return orgUsers[i].ID < orgUsers[j].ID }) - assert.EqualValues(t, []*organization.OrgUser{{ + assert.Equal(t, []*organization.OrgUser{{ ID: 1, OrgID: 3, UID: 2, @@ -274,23 +274,23 @@ func TestGetOrgUsersByOrgID(t *testing.T) { opts = &organization.FindOrgMembersOpts{OrgID: 3} assert.True(t, opts.PublicOnly()) - orgUsers, err = organization.GetOrgUsersByOrgID(db.DefaultContext, opts) + orgUsers, err = organization.GetOrgUsersByOrgID(t.Context(), opts) assert.NoError(t, err) assert.Len(t, orgUsers, 2) - orgUsers, err = organization.GetOrgUsersByOrgID(db.DefaultContext, &organization.FindOrgMembersOpts{ + orgUsers, err = organization.GetOrgUsersByOrgID(t.Context(), &organization.FindOrgMembersOpts{ ListOptions: db.ListOptions{}, OrgID: unittest.NonexistentID, }) assert.NoError(t, err) - assert.Len(t, orgUsers, 0) + assert.Empty(t, orgUsers) } func TestChangeOrgUserStatus(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) testSuccess := func(orgID, userID int64, public bool) { - assert.NoError(t, organization.ChangeOrgUserStatus(db.DefaultContext, orgID, userID, public)) + assert.NoError(t, organization.ChangeOrgUserStatus(t.Context(), orgID, userID, public)) orgUser := unittest.AssertExistsAndLoadBean(t, &organization.OrgUser{OrgID: orgID, UID: userID}) assert.Equal(t, public, orgUser.IsPublic) } @@ -298,14 +298,14 @@ func TestChangeOrgUserStatus(t *testing.T) { testSuccess(3, 2, false) testSuccess(3, 2, false) testSuccess(3, 4, true) - assert.NoError(t, organization.ChangeOrgUserStatus(db.DefaultContext, unittest.NonexistentID, unittest.NonexistentID, true)) + assert.NoError(t, organization.ChangeOrgUserStatus(t.Context(), unittest.NonexistentID, unittest.NonexistentID, true)) } func TestUser_GetUserTeamIDs(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3}) testSuccess := func(userID int64, expected []int64) { - teamIDs, err := org.GetUserTeamIDs(db.DefaultContext, userID) + teamIDs, err := org.GetUserTeamIDs(t.Context(), userID) assert.NoError(t, err) assert.Equal(t, expected, teamIDs) } @@ -318,11 +318,11 @@ func TestAccessibleReposEnv_CountRepos(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3}) testSuccess := func(userID, expectedCount int64) { - env, err := organization.AccessibleReposEnv(db.DefaultContext, org, userID) + env, err := repo_model.AccessibleReposEnv(t.Context(), org, userID) assert.NoError(t, err) - count, err := env.CountRepos() + count, err := env.CountRepos(t.Context()) assert.NoError(t, err) - assert.EqualValues(t, expectedCount, count) + assert.Equal(t, expectedCount, count) } testSuccess(2, 3) testSuccess(4, 2) @@ -332,9 +332,9 @@ func TestAccessibleReposEnv_RepoIDs(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3}) testSuccess := func(userID int64, expectedRepoIDs []int64) { - env, err := organization.AccessibleReposEnv(db.DefaultContext, org, userID) + env, err := repo_model.AccessibleReposEnv(t.Context(), org, userID) assert.NoError(t, err) - repoIDs, err := env.RepoIDs(1, 100) + repoIDs, err := env.RepoIDs(t.Context()) assert.NoError(t, err) assert.Equal(t, expectedRepoIDs, repoIDs) } @@ -342,32 +342,13 @@ func TestAccessibleReposEnv_RepoIDs(t *testing.T) { testSuccess(4, []int64{3, 32}) } -func TestAccessibleReposEnv_Repos(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3}) - testSuccess := func(userID int64, expectedRepoIDs []int64) { - env, err := organization.AccessibleReposEnv(db.DefaultContext, org, userID) - assert.NoError(t, err) - repos, err := env.Repos(1, 100) - assert.NoError(t, err) - expectedRepos := make(repo_model.RepositoryList, len(expectedRepoIDs)) - for i, repoID := range expectedRepoIDs { - expectedRepos[i] = unittest.AssertExistsAndLoadBean(t, - &repo_model.Repository{ID: repoID}) - } - assert.Equal(t, expectedRepos, repos) - } - testSuccess(2, []int64{3, 5, 32}) - testSuccess(4, []int64{3, 32}) -} - func TestAccessibleReposEnv_MirrorRepos(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3}) testSuccess := func(userID int64, expectedRepoIDs []int64) { - env, err := organization.AccessibleReposEnv(db.DefaultContext, org, userID) + env, err := repo_model.AccessibleReposEnv(t.Context(), org, userID) assert.NoError(t, err) - repos, err := env.MirrorRepos() + repos, err := env.MirrorRepos(t.Context()) assert.NoError(t, err) expectedRepos := make(repo_model.RepositoryList, len(expectedRepoIDs)) for i, repoID := range expectedRepoIDs { @@ -392,12 +373,12 @@ func TestHasOrgVisibleTypePublic(t *testing.T) { } unittest.AssertNotExistsBean(t, &user_model.User{Name: org.Name, Type: user_model.UserTypeOrganization}) - assert.NoError(t, organization.CreateOrganization(db.DefaultContext, org, owner)) + assert.NoError(t, organization.CreateOrganization(t.Context(), org, owner)) org = unittest.AssertExistsAndLoadBean(t, &organization.Organization{Name: org.Name, Type: user_model.UserTypeOrganization}) - test1 := organization.HasOrgOrUserVisible(db.DefaultContext, org.AsUser(), owner) - test2 := organization.HasOrgOrUserVisible(db.DefaultContext, org.AsUser(), org3) - test3 := organization.HasOrgOrUserVisible(db.DefaultContext, org.AsUser(), nil) + test1 := organization.HasOrgOrUserVisible(t.Context(), org.AsUser(), owner) + test2 := organization.HasOrgOrUserVisible(t.Context(), org.AsUser(), org3) + test3 := organization.HasOrgOrUserVisible(t.Context(), org.AsUser(), nil) assert.True(t, test1) // owner of org assert.True(t, test2) // user not a part of org assert.True(t, test3) // logged out user @@ -415,12 +396,12 @@ func TestHasOrgVisibleTypeLimited(t *testing.T) { } unittest.AssertNotExistsBean(t, &user_model.User{Name: org.Name, Type: user_model.UserTypeOrganization}) - assert.NoError(t, organization.CreateOrganization(db.DefaultContext, org, owner)) + assert.NoError(t, organization.CreateOrganization(t.Context(), org, owner)) org = unittest.AssertExistsAndLoadBean(t, &organization.Organization{Name: org.Name, Type: user_model.UserTypeOrganization}) - test1 := organization.HasOrgOrUserVisible(db.DefaultContext, org.AsUser(), owner) - test2 := organization.HasOrgOrUserVisible(db.DefaultContext, org.AsUser(), org3) - test3 := organization.HasOrgOrUserVisible(db.DefaultContext, org.AsUser(), nil) + test1 := organization.HasOrgOrUserVisible(t.Context(), org.AsUser(), owner) + test2 := organization.HasOrgOrUserVisible(t.Context(), org.AsUser(), org3) + test3 := organization.HasOrgOrUserVisible(t.Context(), org.AsUser(), nil) assert.True(t, test1) // owner of org assert.True(t, test2) // user not a part of org assert.False(t, test3) // logged out user @@ -438,12 +419,12 @@ func TestHasOrgVisibleTypePrivate(t *testing.T) { } unittest.AssertNotExistsBean(t, &user_model.User{Name: org.Name, Type: user_model.UserTypeOrganization}) - assert.NoError(t, organization.CreateOrganization(db.DefaultContext, org, owner)) + assert.NoError(t, organization.CreateOrganization(t.Context(), org, owner)) org = unittest.AssertExistsAndLoadBean(t, &organization.Organization{Name: org.Name, Type: user_model.UserTypeOrganization}) - test1 := organization.HasOrgOrUserVisible(db.DefaultContext, org.AsUser(), owner) - test2 := organization.HasOrgOrUserVisible(db.DefaultContext, org.AsUser(), org3) - test3 := organization.HasOrgOrUserVisible(db.DefaultContext, org.AsUser(), nil) + test1 := organization.HasOrgOrUserVisible(t.Context(), org.AsUser(), owner) + test2 := organization.HasOrgOrUserVisible(t.Context(), org.AsUser(), org3) + test3 := organization.HasOrgOrUserVisible(t.Context(), org.AsUser(), nil) assert.True(t, test1) // owner of org assert.False(t, test2) // user not a part of org assert.False(t, test3) // logged out user @@ -452,7 +433,7 @@ func TestHasOrgVisibleTypePrivate(t *testing.T) { func TestGetUsersWhoCanCreateOrgRepo(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - users, err := organization.GetUsersWhoCanCreateOrgRepo(db.DefaultContext, 3) + users, err := organization.GetUsersWhoCanCreateOrgRepo(t.Context(), 3) assert.NoError(t, err) assert.Len(t, users, 2) var ids []int64 @@ -461,7 +442,7 @@ func TestGetUsersWhoCanCreateOrgRepo(t *testing.T) { } assert.ElementsMatch(t, ids, []int64{2, 28}) - users, err = organization.GetUsersWhoCanCreateOrgRepo(db.DefaultContext, 7) + users, err = organization.GetUsersWhoCanCreateOrgRepo(t.Context(), 7) assert.NoError(t, err) assert.Len(t, users, 1) assert.NotNil(t, users[5]) @@ -474,15 +455,15 @@ func TestUser_RemoveOrgRepo(t *testing.T) { // remove a repo that does belong to org unittest.AssertExistsAndLoadBean(t, &organization.TeamRepo{RepoID: repo.ID, OrgID: org.ID}) - assert.NoError(t, organization.RemoveOrgRepo(db.DefaultContext, org.ID, repo.ID)) + assert.NoError(t, organization.RemoveOrgRepo(t.Context(), org.ID, repo.ID)) unittest.AssertNotExistsBean(t, &organization.TeamRepo{RepoID: repo.ID, OrgID: org.ID}) unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repo.ID}) // repo should still exist // remove a repo that does not belong to org - assert.NoError(t, organization.RemoveOrgRepo(db.DefaultContext, org.ID, repo.ID)) + assert.NoError(t, organization.RemoveOrgRepo(t.Context(), org.ID, repo.ID)) unittest.AssertNotExistsBean(t, &organization.TeamRepo{RepoID: repo.ID, OrgID: org.ID}) - assert.NoError(t, organization.RemoveOrgRepo(db.DefaultContext, org.ID, unittest.NonexistentID)) + assert.NoError(t, organization.RemoveOrgRepo(t.Context(), org.ID, unittest.NonexistentID)) unittest.CheckConsistencyFor(t, &user_model.User{ID: org.ID}, @@ -501,7 +482,7 @@ func TestCreateOrganization(t *testing.T) { } unittest.AssertNotExistsBean(t, &user_model.User{Name: newOrgName, Type: user_model.UserTypeOrganization}) - assert.NoError(t, organization.CreateOrganization(db.DefaultContext, org, owner)) + assert.NoError(t, organization.CreateOrganization(t.Context(), org, owner)) org = unittest.AssertExistsAndLoadBean(t, &organization.Organization{Name: newOrgName, Type: user_model.UserTypeOrganization}) ownerTeam := unittest.AssertExistsAndLoadBean(t, @@ -521,7 +502,7 @@ func TestCreateOrganization2(t *testing.T) { } unittest.AssertNotExistsBean(t, &organization.Organization{Name: newOrgName, Type: user_model.UserTypeOrganization}) - err := organization.CreateOrganization(db.DefaultContext, org, owner) + err := organization.CreateOrganization(t.Context(), org, owner) assert.Error(t, err) assert.True(t, organization.IsErrUserNotAllowedCreateOrg(err)) unittest.AssertNotExistsBean(t, &organization.Organization{Name: newOrgName, Type: user_model.UserTypeOrganization}) @@ -535,7 +516,7 @@ func TestCreateOrganization3(t *testing.T) { owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) org := &organization.Organization{Name: "org3"} // should already exist unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: org.Name}) // sanity check - err := organization.CreateOrganization(db.DefaultContext, org, owner) + err := organization.CreateOrganization(t.Context(), org, owner) assert.Error(t, err) assert.True(t, user_model.IsErrUserAlreadyExist(err)) unittest.CheckConsistencyFor(t, &user_model.User{}, &organization.Team{}) @@ -546,7 +527,7 @@ func TestCreateOrganization4(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) - err := organization.CreateOrganization(db.DefaultContext, &organization.Organization{Name: "assets"}, owner) + err := organization.CreateOrganization(t.Context(), &organization.Organization{Name: "assets"}, owner) assert.Error(t, err) assert.True(t, db.IsErrNameReserved(err)) unittest.CheckConsistencyFor(t, &organization.Organization{}, &organization.Team{}) diff --git a/models/organization/org_user.go b/models/organization/org_user.go index 1d3b2fab44d..4d7527c15f7 100644 --- a/models/organization/org_user.go +++ b/models/organization/org_user.go @@ -36,6 +36,21 @@ func init() { 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. func GetOrganizationCount(ctx context.Context, u *user_model.User) (int64, error) { return db.GetEngine(ctx). @@ -63,7 +78,7 @@ func IsOrganizationAdmin(ctx context.Context, orgID, uid int64) (bool, error) { return false, err } for _, t := range teams { - if t.AccessMode >= perm.AccessModeAdmin { + if t.HasAdminAccess() { return true, nil } } diff --git a/models/organization/org_user_test.go b/models/organization/org_user_test.go index 55abb63203e..d21df4b474c 100644 --- a/models/organization/org_user_test.go +++ b/models/organization/org_user_test.go @@ -7,7 +7,6 @@ import ( "fmt" "testing" - "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" @@ -37,9 +36,9 @@ func TestUserIsPublicMember(t *testing.T) { } func testUserIsPublicMember(t *testing.T, uid, orgID int64, expected bool) { - user, err := user_model.GetUserByID(db.DefaultContext, uid) + user, err := user_model.GetUserByID(t.Context(), uid) assert.NoError(t, err) - is, err := organization.IsPublicMembership(db.DefaultContext, orgID, user.ID) + is, err := organization.IsPublicMembership(t.Context(), orgID, user.ID) assert.NoError(t, err) assert.Equal(t, expected, is) } @@ -65,9 +64,9 @@ func TestIsUserOrgOwner(t *testing.T) { } func testIsUserOrgOwner(t *testing.T, uid, orgID int64, expected bool) { - user, err := user_model.GetUserByID(db.DefaultContext, uid) + user, err := user_model.GetUserByID(t.Context(), uid) assert.NoError(t, err) - is, err := organization.IsOrganizationOwner(db.DefaultContext, orgID, user.ID) + is, err := organization.IsOrganizationOwner(t.Context(), orgID, user.ID) assert.NoError(t, err) assert.Equal(t, expected, is) } @@ -92,9 +91,9 @@ func TestUserListIsPublicMember(t *testing.T) { } func testUserListIsPublicMember(t *testing.T, orgID int64, expected map[int64]bool) { - org, err := organization.GetOrgByID(db.DefaultContext, orgID) + org, err := organization.GetOrgByID(t.Context(), orgID) assert.NoError(t, err) - _, membersIsPublic, err := org.GetMembers(db.DefaultContext, &user_model.User{IsAdmin: true}) + _, membersIsPublic, err := org.GetMembers(t.Context(), &user_model.User{IsAdmin: true}) assert.NoError(t, err) assert.Equal(t, expected, membersIsPublic) } @@ -119,11 +118,11 @@ func TestUserListIsUserOrgOwner(t *testing.T) { } func testUserListIsUserOrgOwner(t *testing.T, orgID int64, expected map[int64]bool) { - org, err := organization.GetOrgByID(db.DefaultContext, orgID) + org, err := organization.GetOrgByID(t.Context(), orgID) assert.NoError(t, err) - members, _, err := org.GetMembers(db.DefaultContext, &user_model.User{IsAdmin: true}) + members, _, err := org.GetMembers(t.Context(), &user_model.User{IsAdmin: true}) assert.NoError(t, err) - assert.Equal(t, expected, organization.IsUserOrgOwner(db.DefaultContext, members, orgID)) + assert.Equal(t, expected, organization.IsUserOrgOwner(t.Context(), members, orgID)) } func TestAddOrgUser(t *testing.T) { @@ -131,15 +130,15 @@ func TestAddOrgUser(t *testing.T) { testSuccess := func(orgID, userID int64, isPublic bool) { org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: orgID}) expectedNumMembers := org.NumMembers - if !unittest.BeanExists(t, &organization.OrgUser{OrgID: orgID, UID: userID}) { + if unittest.GetBean(t, &organization.OrgUser{OrgID: orgID, UID: userID}) == nil { expectedNumMembers++ } - assert.NoError(t, organization.AddOrgUser(db.DefaultContext, orgID, userID)) + assert.NoError(t, organization.AddOrgUser(t.Context(), orgID, userID)) ou := &organization.OrgUser{OrgID: orgID, UID: userID} unittest.AssertExistsAndLoadBean(t, ou) assert.Equal(t, isPublic, ou.IsPublic) org = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: orgID}) - assert.EqualValues(t, expectedNumMembers, org.NumMembers) + assert.Equal(t, expectedNumMembers, org.NumMembers) } setting.Service.DefaultOrgMemberVisible = false diff --git a/models/organization/org_worktime.go b/models/organization/org_worktime.go new file mode 100644 index 00000000000..3ef3e512adc --- /dev/null +++ b/models/organization/org_worktime.go @@ -0,0 +1,104 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package organization + +import ( + "context" + "sort" + + "code.gitea.io/gitea/models/db" + + "xorm.io/builder" +) + +type WorktimeSumByRepos struct { + RepoName string + SumTime int64 +} + +func GetWorktimeByRepos(ctx context.Context, org *Organization, unitFrom, unixTo int64) (results []WorktimeSumByRepos, err error) { + err = db.GetEngine(ctx). + Select("repository.name AS repo_name, SUM(tracked_time.time) AS sum_time"). + Table("tracked_time"). + Join("INNER", "issue", "tracked_time.issue_id = issue.id"). + Join("INNER", "repository", "issue.repo_id = repository.id"). + Where(builder.Eq{"repository.owner_id": org.ID}). + And(builder.Eq{"tracked_time.deleted": false}). + And(builder.Gte{"tracked_time.created_unix": unitFrom}). + And(builder.Lte{"tracked_time.created_unix": unixTo}). + GroupBy("repository.name"). + OrderBy("repository.name"). + Find(&results) + return results, err +} + +type WorktimeSumByMilestones struct { + RepoName string + MilestoneName string + MilestoneID int64 + MilestoneDeadline int64 + SumTime int64 + HideRepoName bool +} + +func GetWorktimeByMilestones(ctx context.Context, org *Organization, unitFrom, unixTo int64) (results []WorktimeSumByMilestones, err error) { + err = db.GetEngine(ctx). + Select("repository.name AS repo_name, milestone.name AS milestone_name, milestone.id AS milestone_id, milestone.deadline_unix as milestone_deadline, SUM(tracked_time.time) AS sum_time"). + Table("tracked_time"). + Join("INNER", "issue", "tracked_time.issue_id = issue.id"). + Join("INNER", "repository", "issue.repo_id = repository.id"). + Join("LEFT", "milestone", "issue.milestone_id = milestone.id"). + Where(builder.Eq{"repository.owner_id": org.ID}). + And(builder.Eq{"tracked_time.deleted": false}). + And(builder.Gte{"tracked_time.created_unix": unitFrom}). + And(builder.Lte{"tracked_time.created_unix": unixTo}). + GroupBy("repository.name, milestone.name, milestone.deadline_unix, milestone.id"). + OrderBy("repository.name, milestone.deadline_unix, milestone.id"). + Find(&results) + + // TODO: pgsql: NULL values are sorted last in default ascending order, so we need to sort them manually again. + sort.Slice(results, func(i, j int) bool { + if results[i].RepoName != results[j].RepoName { + return results[i].RepoName < results[j].RepoName + } + if results[i].MilestoneDeadline != results[j].MilestoneDeadline { + return results[i].MilestoneDeadline < results[j].MilestoneDeadline + } + return results[i].MilestoneID < results[j].MilestoneID + }) + + // Show only the first RepoName, for nicer output. + prevRepoName := "" + for i := 0; i < len(results); i++ { + res := &results[i] + res.MilestoneDeadline = 0 // clear the deadline because we do not really need it + if prevRepoName == res.RepoName { + res.HideRepoName = true + } + prevRepoName = res.RepoName + } + return results, err +} + +type WorktimeSumByMembers struct { + UserName string + SumTime int64 +} + +func GetWorktimeByMembers(ctx context.Context, org *Organization, unitFrom, unixTo int64) (results []WorktimeSumByMembers, err error) { + err = db.GetEngine(ctx). + Select("`user`.name AS user_name, SUM(tracked_time.time) AS sum_time"). + Table("tracked_time"). + Join("INNER", "issue", "tracked_time.issue_id = issue.id"). + Join("INNER", "repository", "issue.repo_id = repository.id"). + Join("INNER", "`user`", "tracked_time.user_id = `user`.id"). + Where(builder.Eq{"repository.owner_id": org.ID}). + And(builder.Eq{"tracked_time.deleted": false}). + And(builder.Gte{"tracked_time.created_unix": unitFrom}). + And(builder.Lte{"tracked_time.created_unix": unixTo}). + GroupBy("`user`.name"). + OrderBy("sum_time DESC"). + Find(&results) + return results, err +} diff --git a/models/organization/team.go b/models/organization/team.go index fb7f0c04939..7f3a9b38295 100644 --- a/models/organization/team.go +++ b/models/organization/team.go @@ -11,7 +11,6 @@ import ( "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/perm" - repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/log" @@ -78,9 +77,8 @@ type Team struct { LowerName string Name string Description string - AccessMode perm.AccessMode `xorm:"'authorize'"` - Repos []*repo_model.Repository `xorm:"-"` - Members []*user_model.User `xorm:"-"` + AccessMode perm.AccessMode `xorm:"'authorize'"` + Members []*user_model.User `xorm:"-"` NumRepos int NumMembers int Units []*TeamUnit `xorm:"-"` @@ -115,7 +113,7 @@ func (t *Team) LoadUnits(ctx context.Context) (err error) { // GetUnitNames returns the team units names func (t *Team) GetUnitNames() (res []string) { - if t.AccessMode >= perm.AccessModeAdmin { + if t.HasAdminAccess() { return unit.AllUnitKeyNames() } @@ -128,7 +126,7 @@ func (t *Team) GetUnitNames() (res []string) { // GetUnitsMap returns the team units permissions func (t *Team) GetUnitsMap() map[string]string { m := make(map[string]string) - if t.AccessMode >= perm.AccessModeAdmin { + if t.HasAdminAccess() { for _, u := range unit.Units { m[u.NameKey] = t.AccessMode.ToString() } @@ -155,15 +153,8 @@ func (t *Team) IsMember(ctx context.Context, userID int64) bool { 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 +func (t *Team) HasAdminAccess() bool { + return t.AccessMode >= perm.AccessModeAdmin } // LoadMembers returns paginated members in team of organization. @@ -251,22 +242,6 @@ func GetTeamByID(ctx context.Context, teamID int64) (*Team, error) { return t, nil } -// GetTeamNamesByID returns team's lower name from a list of team ids. -func GetTeamNamesByID(ctx context.Context, teamIDs []int64) ([]string, error) { - if len(teamIDs) == 0 { - return []string{}, nil - } - - var teamNames []string - err := db.GetEngine(ctx).Table("team"). - Select("lower_name"). - In("id", teamIDs). - Asc("name"). - Find(&teamNames) - - return teamNames, err -} - // IncrTeamRepoNum increases the number of repos for the given team by 1 func IncrTeamRepoNum(ctx context.Context, teamID int64) error { _, err := db.GetEngine(ctx).Incr("num_repos").ID(teamID).Update(new(Team)) diff --git a/models/organization/team_invite_test.go b/models/organization/team_invite_test.go index 45db8494e81..e9fabed1db5 100644 --- a/models/organization/team_invite_test.go +++ b/models/organization/team_invite_test.go @@ -6,7 +6,6 @@ package organization_test import ( "testing" - "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" @@ -23,26 +22,26 @@ func TestTeamInvite(t *testing.T) { user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) // user 2 already added to team 2, should result in error - _, err := organization.CreateTeamInvite(db.DefaultContext, user2, team, user2.Email) + _, err := organization.CreateTeamInvite(t.Context(), user2, team, user2.Email) assert.Error(t, err) }) t.Run("CreateAndRemove", func(t *testing.T) { user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) - invite, err := organization.CreateTeamInvite(db.DefaultContext, user1, team, "org3@example.com") + invite, err := organization.CreateTeamInvite(t.Context(), user1, team, "org3@example.com") assert.NotNil(t, invite) assert.NoError(t, err) // Shouldn't allow duplicate invite - _, err = organization.CreateTeamInvite(db.DefaultContext, user1, team, "org3@example.com") + _, err = organization.CreateTeamInvite(t.Context(), user1, team, "org3@example.com") assert.Error(t, err) // should remove invite - assert.NoError(t, organization.RemoveInviteByID(db.DefaultContext, invite.ID, invite.TeamID)) + assert.NoError(t, organization.RemoveInviteByID(t.Context(), invite.ID, invite.TeamID)) // invite should not exist - _, err = organization.GetInviteByToken(db.DefaultContext, invite.Token) + _, err = organization.GetInviteByToken(t.Context(), invite.Token) assert.Error(t, err) }) } diff --git a/models/organization/team_list.go b/models/organization/team_list.go index cc2a50236a4..0274f9c5ba4 100644 --- a/models/organization/team_list.go +++ b/models/organization/team_list.go @@ -9,7 +9,6 @@ import ( "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/perm" - repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" "xorm.io/builder" @@ -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 -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). Join("INNER", "team_repo", "team_repo.team_id = team.id"). - Where("team.org_id = ?", repo.OwnerID). - And("team_repo.repo_id=?", repo.ID). + Where("team.org_id = ?", orgID). + And("team_repo.repo_id=?", repoID). OrderBy("CASE WHEN name LIKE '" + OwnerTeamName + "' THEN '' ELSE name END"). Find(&teams) } @@ -131,3 +130,11 @@ func GetTeamsByOrgIDs(ctx context.Context, orgIDs []int64) (TeamList, error) { teams := make([]*Team, 0, 10) return teams, db.GetEngine(ctx).Where(builder.In("org_id", orgIDs)).Find(&teams) } + +func GetTeamsByIDs(ctx context.Context, teamIDs []int64) (map[int64]*Team, error) { + teams := make(map[int64]*Team, len(teamIDs)) + if len(teamIDs) == 0 { + return teams, nil + } + return teams, db.GetEngine(ctx).Where(builder.In("`id`", teamIDs)).Find(&teams) +} diff --git a/models/organization/team_list_test.go b/models/organization/team_list_test.go new file mode 100644 index 00000000000..df73f958986 --- /dev/null +++ b/models/organization/team_list_test.go @@ -0,0 +1,24 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package organization_test + +import ( + "testing" + + org_model "code.gitea.io/gitea/models/organization" + "code.gitea.io/gitea/models/unittest" + + "github.com/stretchr/testify/assert" +) + +func Test_GetTeamsByIDs(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + // 1 owner team, 2 normal team + teams, err := org_model.GetTeamsByIDs(t.Context(), []int64{1, 2}) + assert.NoError(t, err) + assert.Len(t, teams, 2) + assert.Equal(t, "Owners", teams[1].Name) + assert.Equal(t, "team1", teams[2].Name) +} diff --git a/models/organization/team_repo.go b/models/organization/team_repo.go index c90dfdeda04..b3e266dbc76 100644 --- a/models/organization/team_repo.go +++ b/models/organization/team_repo.go @@ -8,7 +8,6 @@ import ( "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/perm" - repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" "xorm.io/builder" @@ -32,29 +31,6 @@ func HasTeamRepo(ctx context.Context, orgID, teamID, repoID int64) bool { 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 func AddTeamRepo(ctx context.Context, orgID, teamID, repoID int64) error { _, err := db.GetEngine(ctx).Insert(&TeamRepo{ @@ -74,26 +50,27 @@ func RemoveTeamRepo(ctx context.Context, teamID, repoID int64) error { return err } -// GetTeamsWithAccessToRepo returns all teams in an organization that have given access level to the repository. -func GetTeamsWithAccessToRepo(ctx context.Context, orgID, repoID int64, mode perm.AccessMode) ([]*Team, error) { +// GetTeamsWithAccessToAnyRepoUnit returns all teams in an organization that have given access level to the repository special unit. +// This function is only used for finding some teams that can be used as branch protection allowlist or reviewers, it isn't really used for access control. +// FIXME: TEAM-UNIT-PERMISSION this logic is not complete, search the fixme keyword to see more details +func GetTeamsWithAccessToAnyRepoUnit(ctx context.Context, orgID, repoID int64, mode perm.AccessMode, unitType unit.Type, unitTypesMore ...unit.Type) ([]*Team, error) { teams := make([]*Team, 0, 5) - return teams, db.GetEngine(ctx).Where("team.authorize >= ?", mode). - Join("INNER", "team_repo", "team_repo.team_id = team.id"). - And("team_repo.org_id = ?", orgID). - And("team_repo.repo_id = ?", repoID). - OrderBy("name"). - Find(&teams) -} -// GetTeamsWithAccessToRepoUnit returns all teams in an organization that have given access level to the repository special unit. -func GetTeamsWithAccessToRepoUnit(ctx context.Context, orgID, repoID int64, mode perm.AccessMode, unitType unit.Type) ([]*Team, error) { - teams := make([]*Team, 0, 5) - return teams, db.GetEngine(ctx).Where("team_unit.access_mode >= ?", mode). + sub := builder.Select("team_id").From("team_unit"). + Where(builder.Expr("team_unit.team_id = team.id")). + And(builder.In("team_unit.type", append([]unit.Type{unitType}, unitTypesMore...))). + And(builder.Expr("team_unit.access_mode >= ?", mode)) + + err := db.GetEngine(ctx). Join("INNER", "team_repo", "team_repo.team_id = team.id"). - Join("INNER", "team_unit", "team_unit.team_id = team.id"). And("team_repo.org_id = ?", orgID). And("team_repo.repo_id = ?", repoID). - And("team_unit.type = ?", unitType). + And(builder.Or( + builder.Expr("team.authorize >= ?", mode), + builder.In("team.id", sub), + )). OrderBy("name"). Find(&teams) + + return teams, err } diff --git a/models/organization/team_repo_test.go b/models/organization/team_repo_test.go index c0d6750df90..af959f46585 100644 --- a/models/organization/team_repo_test.go +++ b/models/organization/team_repo_test.go @@ -6,7 +6,6 @@ package organization_test import ( "testing" - "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/perm" "code.gitea.io/gitea/models/repo" @@ -22,7 +21,7 @@ func TestGetTeamsWithAccessToRepoUnit(t *testing.T) { org41 := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 41}) repo61 := unittest.AssertExistsAndLoadBean(t, &repo.Repository{ID: 61}) - teams, err := organization.GetTeamsWithAccessToRepoUnit(db.DefaultContext, org41.ID, repo61.ID, perm.AccessModeRead, unit.TypePullRequests) + teams, err := organization.GetTeamsWithAccessToAnyRepoUnit(t.Context(), org41.ID, repo61.ID, perm.AccessModeRead, unit.TypePullRequests) assert.NoError(t, err) if assert.Len(t, teams, 2) { assert.EqualValues(t, 21, teams[0].ID) diff --git a/models/organization/team_test.go b/models/organization/team_test.go index 8c34e7a6124..fea5e8990ab 100644 --- a/models/organization/team_test.go +++ b/models/organization/team_test.go @@ -8,6 +8,7 @@ import ( "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/organization" + repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" "github.com/stretchr/testify/assert" @@ -27,14 +28,14 @@ func TestTeam_IsMember(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 1}) - assert.True(t, team.IsMember(db.DefaultContext, 2)) - assert.False(t, team.IsMember(db.DefaultContext, 4)) - assert.False(t, team.IsMember(db.DefaultContext, unittest.NonexistentID)) + assert.True(t, team.IsMember(t.Context(), 2)) + assert.False(t, team.IsMember(t.Context(), 4)) + assert.False(t, team.IsMember(t.Context(), unittest.NonexistentID)) team = unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 2}) - assert.True(t, team.IsMember(db.DefaultContext, 2)) - assert.True(t, team.IsMember(db.DefaultContext, 4)) - assert.False(t, team.IsMember(db.DefaultContext, unittest.NonexistentID)) + assert.True(t, team.IsMember(t.Context(), 2)) + assert.True(t, team.IsMember(t.Context(), 4)) + assert.False(t, team.IsMember(t.Context(), unittest.NonexistentID)) } func TestTeam_GetRepositories(t *testing.T) { @@ -42,9 +43,12 @@ func TestTeam_GetRepositories(t *testing.T) { test := func(teamID int64) { team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: teamID}) - assert.NoError(t, team.LoadRepositories(db.DefaultContext)) - assert.Len(t, team.Repos, team.NumRepos) - for _, repo := range team.Repos { + repos, err := repo_model.GetTeamRepositories(t.Context(), &repo_model.SearchTeamRepoOptions{ + TeamID: team.ID, + }) + assert.NoError(t, err) + assert.Len(t, repos, team.NumRepos) + for _, repo := range repos { unittest.AssertExistsAndLoadBean(t, &organization.TeamRepo{TeamID: teamID, RepoID: repo.ID}) } } @@ -57,7 +61,7 @@ func TestTeam_GetMembers(t *testing.T) { test := func(teamID int64) { team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: teamID}) - assert.NoError(t, team.LoadMembers(db.DefaultContext)) + assert.NoError(t, team.LoadMembers(t.Context())) assert.Len(t, team.Members, team.NumMembers) for _, member := range team.Members { unittest.AssertExistsAndLoadBean(t, &organization.TeamUser{UID: member.ID, TeamID: teamID}) @@ -71,17 +75,17 @@ func TestGetTeam(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) testSuccess := func(orgID int64, name string) { - team, err := organization.GetTeam(db.DefaultContext, orgID, name) + team, err := organization.GetTeam(t.Context(), orgID, name) assert.NoError(t, err) - assert.EqualValues(t, orgID, team.OrgID) + assert.Equal(t, orgID, team.OrgID) assert.Equal(t, name, team.Name) } testSuccess(3, "Owners") testSuccess(3, "team1") - _, err := organization.GetTeam(db.DefaultContext, 3, "nonexistent") + _, err := organization.GetTeam(t.Context(), 3, "nonexistent") assert.Error(t, err) - _, err = organization.GetTeam(db.DefaultContext, unittest.NonexistentID, "Owners") + _, err = organization.GetTeam(t.Context(), unittest.NonexistentID, "Owners") assert.Error(t, err) } @@ -89,23 +93,23 @@ func TestGetTeamByID(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) testSuccess := func(teamID int64) { - team, err := organization.GetTeamByID(db.DefaultContext, teamID) + team, err := organization.GetTeamByID(t.Context(), teamID) assert.NoError(t, err) - assert.EqualValues(t, teamID, team.ID) + assert.Equal(t, teamID, team.ID) } testSuccess(1) testSuccess(2) testSuccess(3) testSuccess(4) - _, err := organization.GetTeamByID(db.DefaultContext, unittest.NonexistentID) + _, err := organization.GetTeamByID(t.Context(), unittest.NonexistentID) assert.Error(t, err) } func TestIsTeamMember(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) test := func(orgID, teamID, userID int64, expected bool) { - isMember, err := organization.IsTeamMember(db.DefaultContext, orgID, teamID, userID) + isMember, err := organization.IsTeamMember(t.Context(), orgID, teamID, userID) assert.NoError(t, err) assert.Equal(t, expected, isMember) } @@ -126,7 +130,7 @@ func TestGetTeamMembers(t *testing.T) { test := func(teamID int64) { team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: teamID}) - members, err := organization.GetTeamMembers(db.DefaultContext, &organization.SearchMembersOptions{ + members, err := organization.GetTeamMembers(t.Context(), &organization.SearchMembersOptions{ TeamID: teamID, }) assert.NoError(t, err) @@ -142,7 +146,7 @@ func TestGetTeamMembers(t *testing.T) { func TestGetUserTeams(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) test := func(userID int64) { - teams, _, err := organization.SearchTeam(db.DefaultContext, &organization.SearchTeamOptions{UserID: userID}) + teams, _, err := organization.SearchTeam(t.Context(), &organization.SearchTeamOptions{UserID: userID}) assert.NoError(t, err) for _, team := range teams { unittest.AssertExistsAndLoadBean(t, &organization.TeamUser{TeamID: team.ID, UID: userID}) @@ -156,10 +160,10 @@ func TestGetUserTeams(t *testing.T) { func TestGetUserOrgTeams(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) test := func(orgID, userID int64) { - teams, err := organization.GetUserOrgTeams(db.DefaultContext, orgID, userID) + teams, err := organization.GetUserOrgTeams(t.Context(), orgID, userID) assert.NoError(t, err) for _, team := range teams { - assert.EqualValues(t, orgID, team.OrgID) + assert.Equal(t, orgID, team.OrgID) unittest.AssertExistsAndLoadBean(t, &organization.TeamUser{TeamID: team.ID, UID: userID}) } } @@ -173,7 +177,7 @@ func TestHasTeamRepo(t *testing.T) { test := func(teamID, repoID int64, expected bool) { team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: teamID}) - assert.Equal(t, expected, organization.HasTeamRepo(db.DefaultContext, team.OrgID, teamID, repoID)) + assert.Equal(t, expected, organization.HasTeamRepo(t.Context(), team.OrgID, teamID, repoID)) } test(1, 1, false) test(1, 3, true) @@ -188,7 +192,7 @@ func TestUsersInTeamsCount(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) test := func(teamIDs, userIDs []int64, expected int64) { - count, err := organization.UsersInTeamsCount(db.DefaultContext, teamIDs, userIDs) + count, err := organization.UsersInTeamsCount(t.Context(), teamIDs, userIDs) assert.NoError(t, err) assert.Equal(t, expected, count) } diff --git a/models/organization/team_unit.go b/models/organization/team_unit.go index 3087b70770c..c6ec6b39b2c 100644 --- a/models/organization/team_unit.go +++ b/models/organization/team_unit.go @@ -31,21 +31,16 @@ func getUnitsByTeamID(ctx context.Context, teamID int64) (units []*TeamUnit, err // UpdateTeamUnits updates a teams's units func UpdateTeamUnits(ctx context.Context, team *Team, units []TeamUnit) (err error) { - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return err - } - defer committer.Close() - - if _, err = db.GetEngine(ctx).Where("team_id = ?", team.ID).Delete(new(TeamUnit)); err != nil { - return err - } - - if len(units) > 0 { - if err = db.Insert(ctx, units); err != nil { + return db.WithTx(ctx, func(ctx context.Context) error { + if _, err = db.GetEngine(ctx).Where("team_id = ?", team.ID).Delete(new(TeamUnit)); err != nil { return err } - } - return committer.Commit() + if len(units) > 0 { + if err = db.Insert(ctx, units); err != nil { + return err + } + } + return nil + }) } diff --git a/models/packages/arch/search.go b/models/packages/arch/search.go new file mode 100644 index 00000000000..f35c037b23a --- /dev/null +++ b/models/packages/arch/search.go @@ -0,0 +1,38 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package arch + +import ( + "context" + + packages_model "code.gitea.io/gitea/models/packages" + arch_module "code.gitea.io/gitea/modules/packages/arch" +) + +// GetRepositories gets all available repositories +func GetRepositories(ctx context.Context, ownerID int64) ([]string, error) { + return packages_model.GetDistinctPropertyValues( + ctx, + packages_model.TypeArch, + ownerID, + packages_model.PropertyTypeFile, + arch_module.PropertyRepository, + nil, + ) +} + +// GetArchitectures gets all available architectures for the given repository +func GetArchitectures(ctx context.Context, ownerID int64, repository string) ([]string, error) { + return packages_model.GetDistinctPropertyValues( + ctx, + packages_model.TypeArch, + ownerID, + packages_model.PropertyTypeFile, + arch_module.PropertyArchitecture, + &packages_model.DistinctPropertyDependency{ + Name: arch_module.PropertyRepository, + Value: repository, + }, + ) +} diff --git a/models/packages/container/search.go b/models/packages/container/search.go index 5df35117ce5..9321d9eb410 100644 --- a/models/packages/container/search.go +++ b/models/packages/container/search.go @@ -25,6 +25,7 @@ type BlobSearchOptions struct { Digest string Tag string IsManifest bool + OnlyLead bool Repository string } @@ -43,7 +44,10 @@ func (opts *BlobSearchOptions) toConds() builder.Cond { cond = cond.And(builder.Eq{"package_version.lower_version": strings.ToLower(opts.Tag)}) } if opts.IsManifest { - cond = cond.And(builder.Eq{"package_file.lower_name": ManifestFilename}) + cond = cond.And(builder.Eq{"package_file.lower_name": container_module.ManifestFilename}) + } + if opts.OnlyLead { + cond = cond.And(builder.Eq{"package_file.is_lead": true}) } if opts.Digest != "" { var propsCond builder.Cond = builder.Eq{ @@ -73,11 +77,9 @@ func GetContainerBlob(ctx context.Context, opts *BlobSearchOptions) (*packages.P pfds, err := getContainerBlobsLimit(ctx, opts, 1) if err != nil { return nil, err - } - if len(pfds) != 1 { + } else if len(pfds) == 0 { return nil, ErrContainerBlobNotExist } - return pfds[0], nil } @@ -233,7 +235,7 @@ func SearchImageTags(ctx context.Context, opts *ImageTagsSearchOptions) ([]*pack func SearchExpiredUploadedBlobs(ctx context.Context, olderThan time.Duration) ([]*packages.PackageFile, error) { var cond builder.Cond = builder.Eq{ "package_version.is_internal": true, - "package_version.lower_version": UploadVersion, + "package_version.lower_version": container_module.UploadVersion, "package.type": packages.TypeContainer, } cond = cond.And(builder.Lt{"package_file.created_unix": time.Now().Add(-olderThan).Unix()}) diff --git a/models/packages/descriptor.go b/models/packages/descriptor.go index b8ef698d382..ea0e0d5e734 100644 --- a/models/packages/descriptor.go +++ b/models/packages/descriptor.go @@ -11,8 +11,10 @@ import ( repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/cache" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/packages/alpine" + "code.gitea.io/gitea/modules/packages/arch" "code.gitea.io/gitea/modules/packages/cargo" "code.gitea.io/gitea/modules/packages/chef" "code.gitea.io/gitea/modules/packages/composer" @@ -81,13 +83,13 @@ func (pd *PackageDescriptor) VersionWebLink() string { } // PackageHTMLURL returns the absolute package HTML URL -func (pd *PackageDescriptor) PackageHTMLURL() string { - return fmt.Sprintf("%s/-/packages/%s/%s", pd.Owner.HTMLURL(), string(pd.Package.Type), url.PathEscape(pd.Package.LowerName)) +func (pd *PackageDescriptor) PackageHTMLURL(ctx context.Context) string { + return fmt.Sprintf("%s/-/packages/%s/%s", pd.Owner.HTMLURL(ctx), string(pd.Package.Type), url.PathEscape(pd.Package.LowerName)) } // VersionHTMLURL returns the absolute package version HTML URL -func (pd *PackageDescriptor) VersionHTMLURL() string { - return fmt.Sprintf("%s/%s", pd.PackageHTMLURL(), url.PathEscape(pd.Version.LowerVersion)) +func (pd *PackageDescriptor) VersionHTMLURL(ctx context.Context) string { + return fmt.Sprintf("%s/%s", pd.PackageHTMLURL(ctx), url.PathEscape(pd.Version.LowerVersion)) } // CalculateBlobSize returns the total blobs size in bytes @@ -101,19 +103,26 @@ func (pd *PackageDescriptor) CalculateBlobSize() int64 { // GetPackageDescriptor gets the package description for a version func GetPackageDescriptor(ctx context.Context, pv *PackageVersion) (*PackageDescriptor, error) { - p, err := GetPackageByID(ctx, pv.PackageID) + return GetPackageDescriptorWithCache(ctx, pv, cache.NewEphemeralCache()) +} + +func GetPackageDescriptorWithCache(ctx context.Context, pv *PackageVersion, c *cache.EphemeralCache) (*PackageDescriptor, error) { + p, err := cache.GetWithEphemeralCache(ctx, c, "package", pv.PackageID, GetPackageByID) if err != nil { return nil, err } - o, err := user_model.GetUserByID(ctx, p.OwnerID) + o, err := cache.GetWithEphemeralCache(ctx, c, "user", p.OwnerID, user_model.GetUserByID) if err != nil { return nil, err } - repository, err := repo_model.GetRepositoryByID(ctx, p.RepoID) - if err != nil && !repo_model.IsErrRepoNotExist(err) { - return nil, err + var repository *repo_model.Repository + if p.RepoID > 0 { + repository, err = cache.GetWithEphemeralCache(ctx, c, "repo", p.RepoID, repo_model.GetRepositoryByID) + if err != nil && !repo_model.IsErrRepoNotExist(err) { + return nil, err + } } - creator, err := user_model.GetUserByID(ctx, pv.CreatorID) + creator, err := cache.GetWithEphemeralCache(ctx, c, "user", pv.CreatorID, user_model.GetUserByID) if err != nil { if errors.Is(err, util.ErrNotExist) { creator = user_model.NewGhostUser() @@ -141,15 +150,21 @@ func GetPackageDescriptor(ctx context.Context, pv *PackageVersion) (*PackageDesc return nil, err } - pfds, err := GetPackageFileDescriptors(ctx, pfs) - if err != nil { - return nil, err + pfds := make([]*PackageFileDescriptor, 0, len(pfs)) + for _, pf := range pfs { + pfd, err := getPackageFileDescriptor(ctx, pf, c) + if err != nil { + return nil, err + } + pfds = append(pfds, pfd) } var metadata any switch p.Type { case TypeAlpine: metadata = &alpine.VersionMetadata{} + case TypeArch: + metadata = &arch.VersionMetadata{} case TypeCargo: metadata = &cargo.Metadata{} case TypeChef: @@ -191,7 +206,7 @@ func GetPackageDescriptor(ctx context.Context, pv *PackageVersion) (*PackageDesc case TypeVagrant: metadata = &vagrant.Metadata{} default: - panic(fmt.Sprintf("unknown package type: %s", string(p.Type))) + panic("unknown package type: " + string(p.Type)) } if metadata != nil { if err := json.Unmarshal([]byte(pv.MetadataJSON), &metadata); err != nil { @@ -215,7 +230,11 @@ func GetPackageDescriptor(ctx context.Context, pv *PackageVersion) (*PackageDesc // GetPackageFileDescriptor gets a package file descriptor for a package file func GetPackageFileDescriptor(ctx context.Context, pf *PackageFile) (*PackageFileDescriptor, error) { - pb, err := GetBlobByID(ctx, pf.BlobID) + return getPackageFileDescriptor(ctx, pf, cache.NewEphemeralCache()) +} + +func getPackageFileDescriptor(ctx context.Context, pf *PackageFile, c *cache.EphemeralCache) (*PackageFileDescriptor, error) { + pb, err := cache.GetWithEphemeralCache(ctx, c, "package_file_blob", pf.BlobID, GetBlobByID) if err != nil { return nil, err } @@ -245,9 +264,13 @@ func GetPackageFileDescriptors(ctx context.Context, pfs []*PackageFile) ([]*Pack // GetPackageDescriptors gets the package descriptions for the versions func GetPackageDescriptors(ctx context.Context, pvs []*PackageVersion) ([]*PackageDescriptor, error) { + return getPackageDescriptors(ctx, pvs, cache.NewEphemeralCache()) +} + +func getPackageDescriptors(ctx context.Context, pvs []*PackageVersion, c *cache.EphemeralCache) ([]*PackageDescriptor, error) { pds := make([]*PackageDescriptor, 0, len(pvs)) for _, pv := range pvs { - pd, err := GetPackageDescriptor(ctx, pv) + pd, err := GetPackageDescriptorWithCache(ctx, pv, c) if err != nil { return nil, err } diff --git a/models/packages/nuget/search.go b/models/packages/nuget/search.go index 7a505ff08f3..a4b23f31d55 100644 --- a/models/packages/nuget/search.go +++ b/models/packages/nuget/search.go @@ -33,7 +33,7 @@ func SearchVersions(ctx context.Context, opts *packages_model.PackageSearchOptio Where(cond). OrderBy("package.name ASC") if opts.Paginator != nil { - skip, take := opts.GetSkipTake() + skip, take := opts.Paginator.GetSkipTake() inner = inner.Limit(take, skip) } diff --git a/models/packages/package.go b/models/packages/package.go index 65a25741509..38d1cdcf660 100644 --- a/models/packages/package.go +++ b/models/packages/package.go @@ -31,6 +31,7 @@ type Type string // List of supported packages const ( TypeAlpine Type = "alpine" + TypeArch Type = "arch" TypeCargo Type = "cargo" TypeChef Type = "chef" TypeComposer Type = "composer" @@ -55,6 +56,7 @@ const ( var TypeList = []Type{ TypeAlpine, + TypeArch, TypeCargo, TypeChef, TypeComposer, @@ -82,6 +84,8 @@ func (pt Type) Name() string { switch pt { case TypeAlpine: return "Alpine" + case TypeArch: + return "Arch" case TypeCargo: return "Cargo" case TypeChef: @@ -123,7 +127,7 @@ func (pt Type) Name() string { case TypeVagrant: return "Vagrant" } - panic(fmt.Sprintf("unknown package type: %s", string(pt))) + panic("unknown package type: " + string(pt)) } // SVGName gets the name of the package type svg image @@ -131,6 +135,8 @@ func (pt Type) SVGName() string { switch pt { case TypeAlpine: return "gitea-alpine" + case TypeArch: + return "gitea-arch" case TypeCargo: return "gitea-cargo" case TypeChef: @@ -172,7 +178,7 @@ func (pt Type) SVGName() string { case TypeVagrant: return "gitea-vagrant" } - panic(fmt.Sprintf("unknown package type: %s", string(pt))) + panic("unknown package type: " + string(pt)) } // Package represents a package @@ -222,6 +228,11 @@ func SetRepositoryLink(ctx context.Context, packageID, repoID int64) error { return err } +func UnlinkRepository(ctx context.Context, packageID int64) error { + _, err := db.GetEngine(ctx).ID(packageID).Cols("repo_id").Update(&Package{RepoID: 0}) + return err +} + // UnlinkRepositoryFromAllPackages unlinks every package from the repository func UnlinkRepositoryFromAllPackages(ctx context.Context, repoID int64) error { _, err := db.GetEngine(ctx).Where("repo_id = ?", repoID).Cols("repo_id").Update(&Package{}) @@ -242,6 +253,18 @@ func GetPackageByID(ctx context.Context, packageID int64) (*Package, error) { return p, nil } +// UpdatePackageNameByID updates the package's name, it is only for internal usage, for example: rename some legacy packages +func UpdatePackageNameByID(ctx context.Context, ownerID int64, packageType Type, packageID int64, name string) error { + var cond builder.Cond = builder.Eq{ + "package.id": packageID, + "package.owner_id": ownerID, + "package.type": packageType, + "package.is_internal": false, + } + _, err := db.GetEngine(ctx).Where(cond).Update(&Package{Name: name, LowerName: strings.ToLower(name)}) + return err +} + // GetPackageByName gets a package by name func GetPackageByName(ctx context.Context, ownerID int64, packageType Type, name string) (*Package, error) { var cond builder.Cond = builder.Eq{ @@ -295,6 +318,21 @@ func FindUnreferencedPackages(ctx context.Context) ([]*Package, error) { 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 func HasOwnerPackages(ctx context.Context, ownerID int64) (bool, error) { return db.GetEngine(ctx). diff --git a/models/packages/package_file.go b/models/packages/package_file.go index 1bb6b57a34e..bf877485d62 100644 --- a/models/packages/package_file.go +++ b/models/packages/package_file.go @@ -115,6 +115,11 @@ func DeleteFileByID(ctx context.Context, fileID int64) error { return err } +func UpdateFile(ctx context.Context, pf *PackageFile, cols []string) error { + _, err := db.GetEngine(ctx).ID(pf.ID).Cols(cols...).Update(pf) + return err +} + // PackageFileSearchOptions are options for SearchXXX methods type PackageFileSearchOptions struct { OwnerID int64 @@ -221,6 +226,11 @@ func SearchFiles(ctx context.Context, opts *PackageFileSearchOptions) ([]*Packag return pfs, count, err } +// HasFiles tests if there are files of packages matching the search options +func HasFiles(ctx context.Context, opts *PackageFileSearchOptions) (bool, error) { + return db.Exist[PackageFile](ctx, opts.toConds()) +} + // CalculateFileSize sums up all blob sizes matching the search options. // It does NOT respect the deduplication of blobs. func CalculateFileSize(ctx context.Context, opts *PackageFileSearchOptions) (int64, error) { diff --git a/models/packages/package_property.go b/models/packages/package_property.go index e0170016cfc..acc05d8d5ac 100644 --- a/models/packages/package_property.go +++ b/models/packages/package_property.go @@ -32,7 +32,7 @@ type PackageProperty struct { RefType PropertyType `xorm:"INDEX NOT NULL"` RefID int64 `xorm:"INDEX NOT NULL"` Name string `xorm:"INDEX NOT NULL"` - Value string `xorm:"TEXT NOT NULL"` + Value string `xorm:"LONGTEXT NOT NULL"` } // InsertProperty creates a property @@ -66,6 +66,20 @@ func UpdateProperty(ctx context.Context, pp *PackageProperty) error { return err } +func InsertOrUpdateProperty(ctx context.Context, refType PropertyType, refID int64, name, value string) error { + pp := PackageProperty{RefType: refType, RefID: refID, Name: name} + ok, err := db.GetEngine(ctx).Get(&pp) + if err != nil { + return err + } + if ok { + _, err = db.GetEngine(ctx).Where("ref_type=? AND ref_id=? AND name=?", refType, refID, name).Cols("value").Update(&PackageProperty{Value: value}) + return err + } + _, err = InsertProperty(ctx, refType, refID, name, value) + return err +} + // DeleteAllProperties deletes all properties of a ref func DeleteAllProperties(ctx context.Context, refType PropertyType, refID int64) error { _, err := db.GetEngine(ctx).Where("ref_type = ? AND ref_id = ?", refType, refID).Delete(&PackageProperty{}) @@ -78,8 +92,8 @@ func DeletePropertyByID(ctx context.Context, propertyID int64) error { return err } -// DeletePropertyByName deletes properties by name -func DeletePropertyByName(ctx context.Context, refType PropertyType, refID int64, name string) error { +// DeletePropertiesByName deletes properties by name +func DeletePropertiesByName(ctx context.Context, refType PropertyType, refID int64, name string) error { _, err := db.GetEngine(ctx).Where("ref_type = ? AND ref_id = ? AND name = ?", refType, refID, name).Delete(&PackageProperty{}) return err } diff --git a/models/packages/package_test.go b/models/packages/package_test.go index 7f03151e774..9c71f2980d1 100644 --- a/models/packages/package_test.go +++ b/models/packages/package_test.go @@ -6,7 +6,6 @@ package packages_test import ( "testing" - "code.gitea.io/gitea/models/db" packages_model "code.gitea.io/gitea/models/packages" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" @@ -27,7 +26,7 @@ func TestHasOwnerPackages(t *testing.T) { owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) - p, err := packages_model.TryInsertPackage(db.DefaultContext, &packages_model.Package{ + p, err := packages_model.TryInsertPackage(t.Context(), &packages_model.Package{ OwnerID: owner.ID, LowerName: "package", }) @@ -35,11 +34,11 @@ func TestHasOwnerPackages(t *testing.T) { assert.NoError(t, err) // A package without package versions gets automatically cleaned up and should return false - has, err := packages_model.HasOwnerPackages(db.DefaultContext, owner.ID) + has, err := packages_model.HasOwnerPackages(t.Context(), owner.ID) assert.False(t, has) assert.NoError(t, err) - pv, err := packages_model.GetOrInsertVersion(db.DefaultContext, &packages_model.PackageVersion{ + pv, err := packages_model.GetOrInsertVersion(t.Context(), &packages_model.PackageVersion{ PackageID: p.ID, LowerVersion: "internal", IsInternal: true, @@ -48,11 +47,11 @@ func TestHasOwnerPackages(t *testing.T) { assert.NoError(t, err) // A package with an internal package version gets automatically cleaned up and should return false - has, err = packages_model.HasOwnerPackages(db.DefaultContext, owner.ID) + has, err = packages_model.HasOwnerPackages(t.Context(), owner.ID) assert.False(t, has) assert.NoError(t, err) - pv, err = packages_model.GetOrInsertVersion(db.DefaultContext, &packages_model.PackageVersion{ + pv, err = packages_model.GetOrInsertVersion(t.Context(), &packages_model.PackageVersion{ PackageID: p.ID, LowerVersion: "normal", IsInternal: false, @@ -61,7 +60,7 @@ func TestHasOwnerPackages(t *testing.T) { assert.NoError(t, err) // A package with a normal package version should return true - has, err = packages_model.HasOwnerPackages(db.DefaultContext, owner.ID) + has, err = packages_model.HasOwnerPackages(t.Context(), owner.ID) assert.True(t, has) assert.NoError(t, err) } diff --git a/models/packages/package_version.go b/models/packages/package_version.go index 278e8e3a86b..0a478c03234 100644 --- a/models/packages/package_version.go +++ b/models/packages/package_version.go @@ -14,6 +14,7 @@ import ( "code.gitea.io/gitea/modules/util" "xorm.io/builder" + "xorm.io/xorm" ) // ErrDuplicatePackageVersion indicates a duplicated package version error @@ -36,6 +37,14 @@ type PackageVersion struct { DownloadCount int64 `xorm:"NOT NULL DEFAULT 0"` } +// IsPrerelease checks if the version is a prerelease version according to semantic versioning +func (pv *PackageVersion) IsPrerelease() bool { + if pv == nil || pv.Version == "" { + return false + } + return strings.Contains(pv.Version, "-") +} + // GetOrInsertVersion inserts a version. If the same version exist already ErrDuplicatePackageVersion is returned func GetOrInsertVersion(ctx context.Context, pv *PackageVersion) (*PackageVersion, error) { e := db.GetEngine(ctx) @@ -187,7 +196,7 @@ type PackageSearchOptions struct { HasFileWithName string // only results are found which are associated with a file with the specific name HasFiles optional.Option[bool] // only results are found which have associated files Sort VersionSort - db.Paginator + Paginator db.Paginator } func (opts *PackageSearchOptions) ToConds() builder.Cond { @@ -279,9 +288,19 @@ func (opts *PackageSearchOptions) configureOrderBy(e db.Engine) { default: e.Desc("package_version.created_unix") } + e.Desc("package_version.id") // Sort by id for stable order with duplicates in the other field +} - // Sort by id for stable order with duplicates in the other field - e.Asc("package_version.id") +func searchVersionsBySession(sess *xorm.Session, opts *PackageSearchOptions) ([]*PackageVersion, int64, error) { + opts.configureOrderBy(sess) + pvs := make([]*PackageVersion, 0, 10) + if opts.Paginator != nil { + sess = db.SetSessionPagination(sess, opts.Paginator) + count, err := sess.FindAndCount(&pvs) + return pvs, count, err + } + err := sess.Find(&pvs) + return pvs, int64(len(pvs)), err } // SearchVersions gets all versions of packages matching the search options @@ -291,16 +310,7 @@ func SearchVersions(ctx context.Context, opts *PackageSearchOptions) ([]*Package Table("package_version"). Join("INNER", "package", "package.id = package_version.package_id"). Where(opts.ToConds()) - - opts.configureOrderBy(sess) - - if opts.Paginator != nil { - sess = db.SetSessionPagination(sess, opts) - } - - pvs := make([]*PackageVersion, 0, 10) - count, err := sess.FindAndCount(&pvs) - return pvs, count, err + return searchVersionsBySession(sess, opts) } // SearchLatestVersions gets the latest version of every package matching the search options @@ -318,15 +328,7 @@ func SearchLatestVersions(ctx context.Context, opts *PackageSearchOptions) ([]*P Join("INNER", "package", "package.id = package_version.package_id"). Where(builder.In("package_version.id", in)) - opts.configureOrderBy(sess) - - if opts.Paginator != nil { - sess = db.SetSessionPagination(sess, opts) - } - - pvs := make([]*PackageVersion, 0, 10) - count, err := sess.FindAndCount(&pvs) - return pvs, count, err + return searchVersionsBySession(sess, opts) } // ExistVersion checks if a version matching the search options exist diff --git a/models/perm/access/access_test.go b/models/perm/access/access_test.go index 51d625707c5..f01993ab4e4 100644 --- a/models/perm/access/access_test.go +++ b/models/perm/access/access_test.go @@ -35,34 +35,34 @@ func TestAccessLevel(t *testing.T) { // org. owned private repo repo24 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 24}) - level, err := access_model.AccessLevel(db.DefaultContext, user2, repo1) + level, err := access_model.AccessLevel(t.Context(), user2, repo1) assert.NoError(t, err) assert.Equal(t, perm_model.AccessModeOwner, level) - level, err = access_model.AccessLevel(db.DefaultContext, user2, repo3) + level, err = access_model.AccessLevel(t.Context(), user2, repo3) assert.NoError(t, err) assert.Equal(t, perm_model.AccessModeOwner, level) - level, err = access_model.AccessLevel(db.DefaultContext, user5, repo1) + level, err = access_model.AccessLevel(t.Context(), user5, repo1) assert.NoError(t, err) assert.Equal(t, perm_model.AccessModeRead, level) - level, err = access_model.AccessLevel(db.DefaultContext, user5, repo3) + level, err = access_model.AccessLevel(t.Context(), user5, repo3) assert.NoError(t, err) assert.Equal(t, perm_model.AccessModeNone, level) // restricted user has no access to a public repo - level, err = access_model.AccessLevel(db.DefaultContext, user29, repo1) + level, err = access_model.AccessLevel(t.Context(), user29, repo1) assert.NoError(t, err) assert.Equal(t, perm_model.AccessModeNone, level) // ... unless he's a collaborator - level, err = access_model.AccessLevel(db.DefaultContext, user29, repo4) + level, err = access_model.AccessLevel(t.Context(), user29, repo4) assert.NoError(t, err) assert.Equal(t, perm_model.AccessModeWrite, level) // ... or a team member - level, err = access_model.AccessLevel(db.DefaultContext, user29, repo24) + level, err = access_model.AccessLevel(t.Context(), user29, repo24) assert.NoError(t, err) assert.Equal(t, perm_model.AccessModeRead, level) } @@ -79,17 +79,17 @@ func TestHasAccess(t *testing.T) { repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}) assert.True(t, repo2.IsPrivate) - has, err := access_model.HasAnyUnitAccess(db.DefaultContext, user1.ID, repo1) + has, err := access_model.HasAnyUnitAccess(t.Context(), user1.ID, repo1) assert.NoError(t, err) assert.True(t, has) - _, err = access_model.HasAnyUnitAccess(db.DefaultContext, user1.ID, repo2) + _, err = access_model.HasAnyUnitAccess(t.Context(), user1.ID, repo2) assert.NoError(t, err) - _, err = access_model.HasAnyUnitAccess(db.DefaultContext, user2.ID, repo1) + _, err = access_model.HasAnyUnitAccess(t.Context(), user2.ID, repo1) assert.NoError(t, err) - _, err = access_model.HasAnyUnitAccess(db.DefaultContext, user2.ID, repo2) + _, err = access_model.HasAnyUnitAccess(t.Context(), user2.ID, repo2) assert.NoError(t, err) } @@ -97,14 +97,14 @@ func TestRepository_RecalculateAccesses(t *testing.T) { // test with organization repo assert.NoError(t, unittest.PrepareTestDatabase()) repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}) - assert.NoError(t, repo1.LoadOwner(db.DefaultContext)) + assert.NoError(t, repo1.LoadOwner(t.Context())) - _, err := db.GetEngine(db.DefaultContext).Delete(&repo_model.Collaboration{UserID: 2, RepoID: 3}) + _, err := db.GetEngine(t.Context()).Delete(&repo_model.Collaboration{UserID: 2, RepoID: 3}) assert.NoError(t, err) - assert.NoError(t, access_model.RecalculateAccesses(db.DefaultContext, repo1)) + assert.NoError(t, access_model.RecalculateAccesses(t.Context(), repo1)) access := &access_model.Access{UserID: 2, RepoID: 3} - has, err := db.GetEngine(db.DefaultContext).Get(access) + has, err := db.GetEngine(t.Context()).Get(access) assert.NoError(t, err) assert.True(t, has) assert.Equal(t, perm_model.AccessModeOwner, access.Mode) @@ -114,13 +114,13 @@ func TestRepository_RecalculateAccesses2(t *testing.T) { // test with non-organization repo assert.NoError(t, unittest.PrepareTestDatabase()) repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4}) - assert.NoError(t, repo1.LoadOwner(db.DefaultContext)) + assert.NoError(t, repo1.LoadOwner(t.Context())) - _, err := db.GetEngine(db.DefaultContext).Delete(&repo_model.Collaboration{UserID: 4, RepoID: 4}) + _, err := db.GetEngine(t.Context()).Delete(&repo_model.Collaboration{UserID: 4, RepoID: 4}) assert.NoError(t, err) - assert.NoError(t, access_model.RecalculateAccesses(db.DefaultContext, repo1)) + assert.NoError(t, access_model.RecalculateAccesses(t.Context(), repo1)) - has, err := db.GetEngine(db.DefaultContext).Get(&access_model.Access{UserID: 4, RepoID: 4}) + has, err := db.GetEngine(t.Context()).Get(&access_model.Access{UserID: 4, RepoID: 4}) assert.NoError(t, err) assert.False(t, has) } diff --git a/models/perm/access/repo_permission.go b/models/perm/access/repo_permission.go index 0ed116a1324..7de43ecd07c 100644 --- a/models/perm/access/repo_permission.go +++ b/models/perm/access/repo_permission.go @@ -15,6 +15,7 @@ import ( "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" ) @@ -25,7 +26,8 @@ type Permission struct { units []*repo_model.RepoUnit unitsMode map[unit.Type]perm_model.AccessMode - everyoneAccessMode map[unit.Type]perm_model.AccessMode + everyoneAccessMode map[unit.Type]perm_model.AccessMode // the unit's minimal access mode for every signed-in user + anonymousAccessMode map[unit.Type]perm_model.AccessMode // the unit's minimal access mode for anonymous (non-signed-in) user } // IsOwner returns true if current user is the owner of repository. @@ -39,7 +41,8 @@ func (p *Permission) IsAdmin() bool { } // HasAnyUnitAccess returns true if the user might have at least one access mode to any unit of this repository. -// It doesn't count the "everyone access mode". +// It doesn't count the "public(anonymous/everyone) access mode". +// TODO: most calls to this function should be replaced with `HasAnyUnitAccessOrPublicAccess` func (p *Permission) HasAnyUnitAccess() bool { for _, v := range p.unitsMode { if v >= perm_model.AccessModeRead { @@ -49,13 +52,22 @@ func (p *Permission) HasAnyUnitAccess() bool { return p.AccessMode >= perm_model.AccessModeRead } -func (p *Permission) HasAnyUnitAccessOrEveryoneAccess() bool { +func (p *Permission) HasAnyUnitPublicAccess() bool { + for _, v := range p.anonymousAccessMode { + if v >= perm_model.AccessModeRead { + return true + } + } for _, v := range p.everyoneAccessMode { if v >= perm_model.AccessModeRead { return true } } - return p.HasAnyUnitAccess() + return false +} + +func (p *Permission) HasAnyUnitAccessOrPublicAccess() bool { + return p.HasAnyUnitPublicAccess() || p.HasAnyUnitAccess() } // HasUnits returns true if the permission contains attached units @@ -73,14 +85,16 @@ func (p *Permission) GetFirstUnitRepoID() int64 { } // UnitAccessMode returns current user access mode to the specify unit of the repository -// It also considers "everyone access mode" +// It also considers "public (anonymous/everyone) access mode" func (p *Permission) UnitAccessMode(unitType unit.Type) perm_model.AccessMode { // if the units map contains the access mode, use it, but admin/owner mode could override it if m, ok := p.unitsMode[unitType]; ok { return util.Iif(p.AccessMode >= perm_model.AccessModeAdmin, p.AccessMode, m) } // if the units map does not contain the access mode, return the default access mode if the unit exists - unitDefaultAccessMode := max(p.AccessMode, p.everyoneAccessMode[unitType]) + unitDefaultAccessMode := p.AccessMode + unitDefaultAccessMode = max(unitDefaultAccessMode, p.anonymousAccessMode[unitType]) + unitDefaultAccessMode = max(unitDefaultAccessMode, p.everyoneAccessMode[unitType]) hasUnit := slices.ContainsFunc(p.units, func(u *repo_model.RepoUnit) bool { return u.Type == unitType }) return util.Iif(hasUnit, unitDefaultAccessMode, perm_model.AccessModeNone) } @@ -152,7 +166,7 @@ func (p *Permission) ReadableUnitTypes() []unit.Type { } func (p *Permission) LogString() string { - format := "= perm_model.AccessModeRead && accessMode > (*modeMap)[unitType] { + if *modeMap == nil { + *modeMap = make(map[unit.Type]perm_model.AccessMode) + } + (*modeMap)[unitType] = accessMode + } +} + +func finalProcessRepoUnitPermission(user *user_model.User, perm *Permission) { + // apply public (anonymous) access permissions for _, u := range perm.units { - if u.EveryoneAccessMode >= perm_model.AccessModeRead && u.EveryoneAccessMode > perm.everyoneAccessMode[u.Type] { - if perm.everyoneAccessMode == nil { - perm.everyoneAccessMode = make(map[unit.Type]perm_model.AccessMode) + applyPublicAccessPermission(u.Type, u.AnonymousAccessMode, &perm.anonymousAccessMode) + } + + if user == nil || user.ID <= 0 { + // for anonymous access, it could be: + // AccessMode is None or Read, units has repo units, unitModes is nil + return + } + + // apply public (everyone) access permissions + for _, u := range perm.units { + applyPublicAccessPermission(u.Type, u.EveryoneAccessMode, &perm.everyoneAccessMode) + } + + if perm.unitsMode == nil { + // if unitsMode is not set, then it means that the default p.AccessMode applies to all units + return + } + + // remove no permission units + origPermUnits := perm.units + perm.units = make([]*repo_model.RepoUnit, 0, len(perm.units)) + for _, u := range origPermUnits { + shouldKeep := false + for t := range perm.unitsMode { + if shouldKeep = u.Type == t; shouldKeep { + break } - perm.everyoneAccessMode[u.Type] = u.EveryoneAccessMode + } + for t := range perm.anonymousAccessMode { + if shouldKeep = shouldKeep || u.Type == t; shouldKeep { + break + } + } + for t := range perm.everyoneAccessMode { + if shouldKeep = shouldKeep || u.Type == t; shouldKeep { + break + } + } + if shouldKeep { + perm.units = append(perm.units, u) } } } @@ -193,11 +257,9 @@ func applyEveryoneRepoPermission(user *user_model.User, perm *Permission) { func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, user *user_model.User) (perm Permission, err error) { defer func() { if err == nil { - applyEveryoneRepoPermission(user, &perm) - } - if log.IsTrace() { - log.Trace("Permission Loaded for user %-v in repo %-v, permissions: %-+v", user, repo, perm) + finalProcessRepoUnitPermission(user, &perm) } + log.Trace("Permission Loaded for user %-v in repo %-v, permissions: %-+v", user, repo, perm) }() if err = repo.LoadUnits(ctx); err != nil { @@ -206,7 +268,6 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use perm.units = repo.Units // anonymous user visit private repo. - // TODO: anonymous user visit public unit of private repo??? if user == nil && repo.IsPrivate { perm.AccessMode = perm_model.AccessModeNone return perm, nil @@ -225,7 +286,8 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use } // Prevent strangers from checking out public repo of private organization/users - // Allow user if they are collaborator of a repo within a private user or a private organization but not a member of the organization itself + // Allow user if they are a collaborator of a repo within a private user or a private organization but not a member of the organization itself + // TODO: rename it to "IsOwnerVisibleToDoer" if !organization.HasOrgOrUserVisible(ctx, repo.Owner, user) && !isCollaborator { perm.AccessMode = perm_model.AccessModeNone return perm, nil @@ -243,7 +305,7 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use return perm, nil } - // plain user + // plain user TODO: this check should be replaced, only need to check collaborator access mode perm.AccessMode, err = accessLevel(ctx, user, repo) if err != nil { return perm, err @@ -253,6 +315,19 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use return perm, nil } + // now: the owner is visible to doer, if the repo is public, then the min access mode is read + minAccessMode := util.Iif(!repo.IsPrivate && !user.IsRestricted, perm_model.AccessModeRead, perm_model.AccessModeNone) + perm.AccessMode = max(perm.AccessMode, minAccessMode) + + // get units mode from teams + teams, err := organization.GetUserRepoTeams(ctx, repo.OwnerID, user.ID, repo.ID) + if err != nil { + return perm, err + } + if len(teams) == 0 { + return perm, nil + } + perm.unitsMode = make(map[unit.Type]perm_model.AccessMode) // Collaborators on organization @@ -262,15 +337,9 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use } } - // get units mode from teams - teams, err := organization.GetUserRepoTeams(ctx, repo.OwnerID, user.ID, repo.ID) - if err != nil { - return perm, err - } - // if user in an owner team for _, team := range teams { - if team.AccessMode >= perm_model.AccessModeAdmin { + if team.HasAdminAccess() { perm.AccessMode = perm_model.AccessModeOwner perm.unitsMode = nil return perm, nil @@ -278,29 +347,12 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use } for _, u := range repo.Units { - var found bool for _, team := range teams { + unitAccessMode := minAccessMode if teamMode, exist := team.UnitAccessModeEx(ctx, u.Type); exist { - perm.unitsMode[u.Type] = max(perm.unitsMode[u.Type], teamMode) - found = true - } - } - - // for a public repo on an organization, a non-restricted user has read permission on non-team defined units. - if !found && !repo.IsPrivate && !user.IsRestricted { - if _, ok := perm.unitsMode[u.Type]; !ok { - perm.unitsMode[u.Type] = perm_model.AccessModeRead - } - } - } - - // remove no permission units - perm.units = make([]*repo_model.RepoUnit, 0, len(repo.Units)) - for t := range perm.unitsMode { - for _, u := range repo.Units { - if u.Type == t { - perm.units = append(perm.units, u) + unitAccessMode = max(perm.unitsMode[u.Type], unitAccessMode, teamMode) } + perm.unitsMode[u.Type] = unitAccessMode } } @@ -348,7 +400,7 @@ func IsUserRepoAdmin(ctx context.Context, repo *repo_model.Repository, user *use } for _, team := range teams { - if team.AccessMode >= perm_model.AccessModeAdmin { + if team.HasAdminAccess() { return true, nil } } @@ -357,13 +409,13 @@ func IsUserRepoAdmin(ctx context.Context, repo *repo_model.Repository, user *use // AccessLevel returns the Access a user has to a repository. Will return NoneAccess if the // user does not have access. -func AccessLevel(ctx context.Context, user *user_model.User, repo *repo_model.Repository) (perm_model.AccessMode, error) { //nolint +func AccessLevel(ctx context.Context, user *user_model.User, repo *repo_model.Repository) (perm_model.AccessMode, error) { //nolint:revive // export stutter return AccessLevelUnit(ctx, user, repo, unit.TypeCode) } // AccessLevelUnit returns the Access a user has to a repository's. Will return NoneAccess if the // user does not have access. -func AccessLevelUnit(ctx context.Context, user *user_model.User, repo *repo_model.Repository, unitType unit.Type) (perm_model.AccessMode, error) { //nolint +func AccessLevelUnit(ctx context.Context, user *user_model.User, repo *repo_model.Repository, unitType unit.Type) (perm_model.AccessMode, error) { //nolint:revive // export stutter perm, err := GetUserRepoPermission(ctx, repo, user) if err != nil { return perm_model.AccessModeNone, err @@ -471,3 +523,7 @@ func CheckRepoUnitUser(ctx context.Context, repo *repo_model.Repository, user *u return perm.CanRead(unitType) } + +func PermissionNoAccess() Permission { + return Permission{AccessMode: perm_model.AccessModeNone} +} diff --git a/models/perm/access/repo_permission_test.go b/models/perm/access/repo_permission_test.go index 50070c43684..c8675b1ded5 100644 --- a/models/perm/access/repo_permission_test.go +++ b/models/perm/access/repo_permission_test.go @@ -6,12 +6,16 @@ package access import ( "testing" + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/organization" perm_model "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/unittest" user_model "code.gitea.io/gitea/models/user" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestHasAnyUnitAccess(t *testing.T) { @@ -22,14 +26,21 @@ func TestHasAnyUnitAccess(t *testing.T) { units: []*repo_model.RepoUnit{{Type: unit.TypeWiki}}, } assert.False(t, perm.HasAnyUnitAccess()) - assert.False(t, perm.HasAnyUnitAccessOrEveryoneAccess()) + assert.False(t, perm.HasAnyUnitAccessOrPublicAccess()) perm = Permission{ units: []*repo_model.RepoUnit{{Type: unit.TypeWiki}}, everyoneAccessMode: map[unit.Type]perm_model.AccessMode{unit.TypeIssues: perm_model.AccessModeRead}, } assert.False(t, perm.HasAnyUnitAccess()) - assert.True(t, perm.HasAnyUnitAccessOrEveryoneAccess()) + assert.True(t, perm.HasAnyUnitAccessOrPublicAccess()) + + perm = Permission{ + units: []*repo_model.RepoUnit{{Type: unit.TypeWiki}}, + anonymousAccessMode: map[unit.Type]perm_model.AccessMode{unit.TypeIssues: perm_model.AccessModeRead}, + } + assert.False(t, perm.HasAnyUnitAccess()) + assert.True(t, perm.HasAnyUnitAccessOrPublicAccess()) perm = Permission{ AccessMode: perm_model.AccessModeRead, @@ -43,14 +54,32 @@ func TestHasAnyUnitAccess(t *testing.T) { assert.True(t, perm.HasAnyUnitAccess()) } -func TestApplyEveryoneRepoPermission(t *testing.T) { +func TestApplyPublicAccessRepoPermission(t *testing.T) { perm := Permission{ AccessMode: perm_model.AccessModeNone, units: []*repo_model.RepoUnit{ {Type: unit.TypeWiki, EveryoneAccessMode: perm_model.AccessModeRead}, }, } - applyEveryoneRepoPermission(nil, &perm) + finalProcessRepoUnitPermission(nil, &perm) + assert.False(t, perm.CanRead(unit.TypeWiki)) + + perm = Permission{ + AccessMode: perm_model.AccessModeNone, + units: []*repo_model.RepoUnit{ + {Type: unit.TypeWiki, AnonymousAccessMode: perm_model.AccessModeRead}, + }, + } + finalProcessRepoUnitPermission(nil, &perm) + assert.True(t, perm.CanRead(unit.TypeWiki)) + + perm = Permission{ + AccessMode: perm_model.AccessModeNone, + units: []*repo_model.RepoUnit{ + {Type: unit.TypeWiki, EveryoneAccessMode: perm_model.AccessModeRead}, + }, + } + finalProcessRepoUnitPermission(&user_model.User{ID: 0}, &perm) assert.False(t, perm.CanRead(unit.TypeWiki)) perm = Permission{ @@ -59,16 +88,7 @@ func TestApplyEveryoneRepoPermission(t *testing.T) { {Type: unit.TypeWiki, EveryoneAccessMode: perm_model.AccessModeRead}, }, } - applyEveryoneRepoPermission(&user_model.User{ID: 0}, &perm) - assert.False(t, perm.CanRead(unit.TypeWiki)) - - perm = Permission{ - AccessMode: perm_model.AccessModeNone, - units: []*repo_model.RepoUnit{ - {Type: unit.TypeWiki, EveryoneAccessMode: perm_model.AccessModeRead}, - }, - } - applyEveryoneRepoPermission(&user_model.User{ID: 1}, &perm) + finalProcessRepoUnitPermission(&user_model.User{ID: 1}, &perm) assert.True(t, perm.CanRead(unit.TypeWiki)) perm = Permission{ @@ -77,20 +97,22 @@ func TestApplyEveryoneRepoPermission(t *testing.T) { {Type: unit.TypeWiki, EveryoneAccessMode: perm_model.AccessModeRead}, }, } - applyEveryoneRepoPermission(&user_model.User{ID: 1}, &perm) + finalProcessRepoUnitPermission(&user_model.User{ID: 1}, &perm) // it should work the same as "EveryoneAccessMode: none" because the default AccessMode should be applied to units assert.True(t, perm.CanWrite(unit.TypeWiki)) perm = Permission{ units: []*repo_model.RepoUnit{ + {Type: unit.TypeCode}, // will be removed {Type: unit.TypeWiki, EveryoneAccessMode: perm_model.AccessModeRead}, }, unitsMode: map[unit.Type]perm_model.AccessMode{ unit.TypeWiki: perm_model.AccessModeWrite, }, } - applyEveryoneRepoPermission(&user_model.User{ID: 1}, &perm) + finalProcessRepoUnitPermission(&user_model.User{ID: 1}, &perm) assert.True(t, perm.CanWrite(unit.TypeWiki)) + assert.Len(t, perm.units, 1) } func TestUnitAccessMode(t *testing.T) { @@ -134,3 +156,45 @@ func TestUnitAccessMode(t *testing.T) { } assert.Equal(t, perm_model.AccessModeRead, perm.UnitAccessMode(unit.TypeWiki), "has unit, and map, use map") } + +func TestGetUserRepoPermission(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + ctx := t.Context() + repo32 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 32}) // org public repo + require.NoError(t, repo32.LoadOwner(ctx)) + require.True(t, repo32.Owner.IsOrganization()) + + require.NoError(t, db.TruncateBeans(ctx, &organization.Team{}, &organization.TeamUser{}, &organization.TeamRepo{}, &organization.TeamUnit{})) + org := repo32.Owner + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) + team := &organization.Team{OrgID: org.ID, LowerName: "test_team"} + require.NoError(t, db.Insert(ctx, team)) + + t.Run("DoerInTeamWithNoRepo", func(t *testing.T) { + require.NoError(t, db.Insert(ctx, &organization.TeamUser{OrgID: org.ID, TeamID: team.ID, UID: user.ID})) + perm, err := GetUserRepoPermission(ctx, repo32, user) + require.NoError(t, err) + assert.Equal(t, perm_model.AccessModeRead, perm.AccessMode) + assert.Nil(t, perm.unitsMode) // doer in the team, but has no access to the repo + }) + + require.NoError(t, db.Insert(ctx, &organization.TeamRepo{OrgID: org.ID, TeamID: team.ID, RepoID: repo32.ID})) + require.NoError(t, db.Insert(ctx, &organization.TeamUnit{OrgID: org.ID, TeamID: team.ID, Type: unit.TypeCode, AccessMode: perm_model.AccessModeNone})) + t.Run("DoerWithTeamUnitAccessNone", func(t *testing.T) { + perm, err := GetUserRepoPermission(ctx, repo32, user) + require.NoError(t, err) + assert.Equal(t, perm_model.AccessModeRead, perm.AccessMode) + assert.Equal(t, perm_model.AccessModeRead, perm.unitsMode[unit.TypeCode]) + assert.Equal(t, perm_model.AccessModeRead, perm.unitsMode[unit.TypeIssues]) + }) + + require.NoError(t, db.TruncateBeans(ctx, &organization.TeamUnit{})) + require.NoError(t, db.Insert(ctx, &organization.TeamUnit{OrgID: org.ID, TeamID: team.ID, Type: unit.TypeCode, AccessMode: perm_model.AccessModeWrite})) + t.Run("DoerWithTeamUnitAccessWrite", func(t *testing.T) { + perm, err := GetUserRepoPermission(ctx, repo32, user) + require.NoError(t, err) + assert.Equal(t, perm_model.AccessModeRead, perm.AccessMode) + assert.Equal(t, perm_model.AccessModeWrite, perm.unitsMode[unit.TypeCode]) + assert.Equal(t, perm_model.AccessModeRead, perm.unitsMode[unit.TypeIssues]) + }) +} diff --git a/models/perm/access_mode_test.go b/models/perm/access_mode_test.go index 982fceee5a5..c4c7d483fb2 100644 --- a/models/perm/access_mode_test.go +++ b/models/perm/access_mode_test.go @@ -15,7 +15,7 @@ func TestAccessMode(t *testing.T) { m := ParseAccessMode(name) assert.Equal(t, AccessMode(i), m) } - assert.Equal(t, AccessMode(4), AccessModeOwner) + assert.Equal(t, AccessModeOwner, AccessMode(4)) assert.Equal(t, "owner", AccessModeOwner.ToString()) assert.Equal(t, AccessModeNone, ParseAccessMode("owner")) assert.Equal(t, AccessModeNone, ParseAccessMode("invalid")) diff --git a/models/project/column.go b/models/project/column.go index 222f4485992..9b9d874997e 100644 --- a/models/project/column.go +++ b/models/project/column.go @@ -48,6 +48,8 @@ type Column struct { ProjectID int64 `xorm:"INDEX NOT NULL"` CreatorID int64 `xorm:"NOT NULL"` + NumIssues int64 `xorm:"-"` + CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` } @@ -57,20 +59,6 @@ func (Column) TableName() string { return "project_board" // TODO: the legacy table name should be project_column } -// NumIssues return counter of all issues assigned to the column -func (c *Column) NumIssues(ctx context.Context) int { - total, err := db.GetEngine(ctx).Table("project_issue"). - Where("project_id=?", c.ProjectID). - And("project_board_id=?", c.ID). - GroupBy("issue_id"). - Cols("issue_id"). - Count() - if err != nil { - return 0 - } - return int(total) -} - func (c *Column) GetIssues(ctx context.Context) ([]*ProjectIssue, error) { issues := make([]*ProjectIssue, 0, 5) if err := db.GetEngine(ctx).Where("project_id=?", c.ProjectID). @@ -159,7 +147,7 @@ func NewColumn(ctx context.Context, column *Column) error { return err } if res.ColumnCount >= maxProjectColumns { - return fmt.Errorf("NewBoard: maximum number of columns reached") + return errors.New("NewBoard: maximum number of columns reached") } column.Sorting = int8(util.Iif(res.ColumnCount > 0, res.MaxSorting+1, 0)) _, err := db.GetEngine(ctx).Insert(column) @@ -184,7 +172,7 @@ func deleteColumnByID(ctx context.Context, columnID int64) error { } if column.Default { - return fmt.Errorf("deleteColumnByID: cannot delete default column") + return errors.New("deleteColumnByID: cannot delete default column") } // move all issues to the default column @@ -192,7 +180,7 @@ func deleteColumnByID(ctx context.Context, columnID int64) error { if err != nil { return err } - defaultColumn, err := project.GetDefaultColumn(ctx) + defaultColumn, err := project.MustDefaultColumn(ctx) if err != nil { return err } @@ -257,8 +245,8 @@ func (p *Project) GetColumns(ctx context.Context) (ColumnList, error) { return columns, nil } -// GetDefaultColumn return default column and ensure only one exists -func (p *Project) GetDefaultColumn(ctx context.Context) (*Column, error) { +// getDefaultColumn return default column and ensure only one exists +func (p *Project) getDefaultColumn(ctx context.Context) (*Column, error) { var column Column has, err := db.GetEngine(ctx). Where("project_id=? AND `default` = ?", p.ID, true). @@ -270,6 +258,33 @@ func (p *Project) GetDefaultColumn(ctx context.Context) (*Column, error) { if has { return &column, nil } + return nil, ErrProjectColumnNotExist{ColumnID: 0} +} + +// MustDefaultColumn returns the default column for a project. +// If one exists, it is returned +// If none exists, the first column will be elevated to the default column of this project +func (p *Project) MustDefaultColumn(ctx context.Context) (*Column, error) { + c, err := p.getDefaultColumn(ctx) + if err != nil && !IsErrProjectColumnNotExist(err) { + return nil, err + } + if c != nil { + return c, nil + } + + var column Column + has, err := db.GetEngine(ctx).Where("project_id=?", p.ID).OrderBy("sorting, id").Get(&column) + if err != nil { + return nil, err + } + if has { + column.Default = true + if _, err := db.GetEngine(ctx).ID(column.ID).Cols("`default`").Update(&column); err != nil { + return nil, err + } + return &column, nil + } // create a default column if none is found column = Column{ @@ -321,6 +336,9 @@ func UpdateColumnSorting(ctx context.Context, cl ColumnList) error { func GetColumnsByIDs(ctx context.Context, projectID int64, columnsIDs []int64) (ColumnList, error) { columns := make([]*Column, 0, 5) + if len(columnsIDs) == 0 { + return columns, nil + } if err := db.GetEngine(ctx). Where("project_id =?", projectID). In("id", columnsIDs). diff --git a/models/project/column_test.go b/models/project/column_test.go index 911649fb726..948e012c62d 100644 --- a/models/project/column_test.go +++ b/models/project/column_test.go @@ -5,10 +5,8 @@ package project import ( "fmt" - "strings" "testing" - "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/unittest" "github.com/stretchr/testify/assert" @@ -17,29 +15,29 @@ import ( func TestGetDefaultColumn(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - projectWithoutDefault, err := GetProjectByID(db.DefaultContext, 5) + projectWithoutDefault, err := GetProjectByID(t.Context(), 5) assert.NoError(t, err) // check if default column was added - column, err := projectWithoutDefault.GetDefaultColumn(db.DefaultContext) + column, err := projectWithoutDefault.MustDefaultColumn(t.Context()) assert.NoError(t, err) assert.Equal(t, int64(5), column.ProjectID) - assert.Equal(t, "Uncategorized", column.Title) + assert.Equal(t, "Done", column.Title) - projectWithMultipleDefaults, err := GetProjectByID(db.DefaultContext, 6) + projectWithMultipleDefaults, err := GetProjectByID(t.Context(), 6) assert.NoError(t, err) // check if multiple defaults were removed - column, err = projectWithMultipleDefaults.GetDefaultColumn(db.DefaultContext) + column, err = projectWithMultipleDefaults.MustDefaultColumn(t.Context()) assert.NoError(t, err) assert.Equal(t, int64(6), column.ProjectID) - assert.Equal(t, int64(9), column.ID) + assert.Equal(t, int64(9), column.ID) // there are 2 default columns in the test data, use the latest one // set 8 as default column - assert.NoError(t, SetDefaultColumn(db.DefaultContext, column.ProjectID, 8)) + assert.NoError(t, SetDefaultColumn(t.Context(), column.ProjectID, 8)) // then 9 will become a non-default column - column, err = GetColumn(db.DefaultContext, 9) + column, err = GetColumn(t.Context(), 9) assert.NoError(t, err) assert.Equal(t, int64(6), column.ProjectID) assert.False(t, column.Default) @@ -50,25 +48,25 @@ func Test_moveIssuesToAnotherColumn(t *testing.T) { column1 := unittest.AssertExistsAndLoadBean(t, &Column{ID: 1, ProjectID: 1}) - issues, err := column1.GetIssues(db.DefaultContext) + issues, err := column1.GetIssues(t.Context()) assert.NoError(t, err) assert.Len(t, issues, 1) assert.EqualValues(t, 1, issues[0].ID) column2 := unittest.AssertExistsAndLoadBean(t, &Column{ID: 2, ProjectID: 1}) - issues, err = column2.GetIssues(db.DefaultContext) + issues, err = column2.GetIssues(t.Context()) assert.NoError(t, err) assert.Len(t, issues, 1) assert.EqualValues(t, 3, issues[0].ID) - err = column1.moveIssuesToAnotherColumn(db.DefaultContext, column2) + err = column1.moveIssuesToAnotherColumn(t.Context(), column2) assert.NoError(t, err) - issues, err = column1.GetIssues(db.DefaultContext) + issues, err = column1.GetIssues(t.Context()) assert.NoError(t, err) - assert.Len(t, issues, 0) + assert.Empty(t, issues) - issues, err = column2.GetIssues(db.DefaultContext) + issues, err = column2.GetIssues(t.Context()) assert.NoError(t, err) assert.Len(t, issues, 2) assert.EqualValues(t, 3, issues[0].ID) @@ -81,47 +79,47 @@ func Test_MoveColumnsOnProject(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) project1 := unittest.AssertExistsAndLoadBean(t, &Project{ID: 1}) - columns, err := project1.GetColumns(db.DefaultContext) + columns, err := project1.GetColumns(t.Context()) assert.NoError(t, err) assert.Len(t, columns, 3) assert.EqualValues(t, 0, columns[0].Sorting) // even if there is no default sorting, the code should also work assert.EqualValues(t, 0, columns[1].Sorting) assert.EqualValues(t, 0, columns[2].Sorting) - err = MoveColumnsOnProject(db.DefaultContext, project1, map[int64]int64{ + err = MoveColumnsOnProject(t.Context(), project1, map[int64]int64{ 0: columns[1].ID, 1: columns[2].ID, 2: columns[0].ID, }) assert.NoError(t, err) - columnsAfter, err := project1.GetColumns(db.DefaultContext) + columnsAfter, err := project1.GetColumns(t.Context()) assert.NoError(t, err) assert.Len(t, columnsAfter, 3) - assert.EqualValues(t, columns[1].ID, columnsAfter[0].ID) - assert.EqualValues(t, columns[2].ID, columnsAfter[1].ID) - assert.EqualValues(t, columns[0].ID, columnsAfter[2].ID) + assert.Equal(t, columns[1].ID, columnsAfter[0].ID) + assert.Equal(t, columns[2].ID, columnsAfter[1].ID) + assert.Equal(t, columns[0].ID, columnsAfter[2].ID) } func Test_NewColumn(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) project1 := unittest.AssertExistsAndLoadBean(t, &Project{ID: 1}) - columns, err := project1.GetColumns(db.DefaultContext) + columns, err := project1.GetColumns(t.Context()) assert.NoError(t, err) assert.Len(t, columns, 3) - for i := 0; i < maxProjectColumns-3; i++ { - err := NewColumn(db.DefaultContext, &Column{ + for i := range maxProjectColumns - 3 { + err := NewColumn(t.Context(), &Column{ Title: fmt.Sprintf("column-%d", i+4), ProjectID: project1.ID, }) assert.NoError(t, err) } - err = NewColumn(db.DefaultContext, &Column{ + err = NewColumn(t.Context(), &Column{ Title: "column-21", ProjectID: project1.ID, }) assert.Error(t, err) - assert.True(t, strings.Contains(err.Error(), "maximum number of columns reached")) + assert.Contains(t, err.Error(), "maximum number of columns reached") } diff --git a/models/project/issue.go b/models/project/issue.go index b4347a9c2b4..47d1537ec73 100644 --- a/models/project/issue.go +++ b/models/project/issue.go @@ -5,10 +5,9 @@ package project import ( "context" - "fmt" + "errors" "code.gitea.io/gitea/models/db" - "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/util" ) @@ -34,51 +33,9 @@ func deleteProjectIssuesByProjectID(ctx context.Context, projectID int64) error return err } -// NumIssues return counter of all issues assigned to a project -func (p *Project) NumIssues(ctx context.Context) int { - c, err := db.GetEngine(ctx).Table("project_issue"). - Where("project_id=?", p.ID). - GroupBy("issue_id"). - Cols("issue_id"). - Count() - if err != nil { - log.Error("NumIssues: %v", err) - return 0 - } - return int(c) -} - -// NumClosedIssues return counter of closed issues assigned to a project -func (p *Project) NumClosedIssues(ctx context.Context) int { - c, err := db.GetEngine(ctx).Table("project_issue"). - Join("INNER", "issue", "project_issue.issue_id=issue.id"). - Where("project_issue.project_id=? AND issue.is_closed=?", p.ID, true). - Cols("issue_id"). - Count() - if err != nil { - log.Error("NumClosedIssues: %v", err) - return 0 - } - return int(c) -} - -// NumOpenIssues return counter of open issues assigned to a project -func (p *Project) NumOpenIssues(ctx context.Context) int { - c, err := db.GetEngine(ctx).Table("project_issue"). - Join("INNER", "issue", "project_issue.issue_id=issue.id"). - Where("project_issue.project_id=? AND issue.is_closed=?", p.ID, false). - Cols("issue_id"). - Count() - if err != nil { - log.Error("NumOpenIssues: %v", err) - return 0 - } - return int(c) -} - func (c *Column) moveIssuesToAnotherColumn(ctx context.Context, newColumn *Column) error { if c.ProjectID != newColumn.ProjectID { - return fmt.Errorf("columns have to be in the same project") + return errors.New("columns have to be in the same project") } if c.ID == newColumn.ID { diff --git a/models/project/project.go b/models/project/project.go index 9779908b9d5..c003664fa3f 100644 --- a/models/project/project.go +++ b/models/project/project.go @@ -97,6 +97,9 @@ type Project struct { Type Type RenderedContent template.HTML `xorm:"-"` + NumOpenIssues int64 `xorm:"-"` + NumClosedIssues int64 `xorm:"-"` + NumIssues int64 `xorm:"-"` CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` @@ -126,6 +129,14 @@ func (p *Project) LoadRepo(ctx context.Context) (err error) { return err } +func ProjectLinkForOrg(org *user_model.User, projectID int64) string { //nolint:revive // export stutter + return fmt.Sprintf("%s/-/projects/%d", org.HomeLink(), projectID) +} + +func ProjectLinkForRepo(repo *repo_model.Repository, projectID int64) string { //nolint:revive // export stutter + return fmt.Sprintf("%s/projects/%d", repo.Link(), projectID) +} + // Link returns the project's relative URL. func (p *Project) Link(ctx context.Context) string { if p.OwnerID > 0 { @@ -134,7 +145,7 @@ func (p *Project) Link(ctx context.Context) string { log.Error("LoadOwner: %v", err) return "" } - return fmt.Sprintf("%s/-/projects/%d", p.Owner.HomeLink(), p.ID) + return ProjectLinkForOrg(p.Owner, p.ID) } if p.RepoID > 0 { err := p.LoadRepo(ctx) @@ -142,7 +153,7 @@ func (p *Project) Link(ctx context.Context) string { log.Error("LoadRepo: %v", err) return "" } - return fmt.Sprintf("%s/projects/%d", p.Repo.Link(), p.ID) + return ProjectLinkForRepo(p.Repo, p.ID) } return "" } @@ -236,6 +247,10 @@ func GetSearchOrderByBySortType(sortType string) db.SearchOrderBy { return db.SearchOrderByRecentUpdated case "leastupdate": return db.SearchOrderByLeastUpdated + case "alphabetically": + return "title ASC" + case "reversealphabetically": + return "title DESC" default: return db.SearchOrderByNewest } @@ -256,7 +271,7 @@ func NewProject(ctx context.Context, p *Project) error { return util.NewInvalidArgumentErrorf("project type is not valid") } - p.Title, _ = util.SplitStringAtByteN(p.Title, 255) + p.Title = util.EllipsisDisplayString(p.Title, 255) return db.WithTx(ctx, func(ctx context.Context) error { if err := db.Insert(ctx, p); err != nil { @@ -311,7 +326,7 @@ func UpdateProject(ctx context.Context, p *Project) error { p.CardType = CardTypeTextOnly } - p.Title, _ = util.SplitStringAtByteN(p.Title, 255) + p.Title = util.EllipsisDisplayString(p.Title, 255) _, err := db.GetEngine(ctx).ID(p.ID).Cols( "title", "description", @@ -344,41 +359,25 @@ func updateRepositoryProjectCount(ctx context.Context, repoID int64) error { // ChangeProjectStatusByRepoIDAndID toggles a project between opened and closed func ChangeProjectStatusByRepoIDAndID(ctx context.Context, repoID, projectID int64, isClosed bool) error { - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return err - } - defer committer.Close() + return db.WithTx(ctx, func(ctx context.Context) error { + p := new(Project) - p := new(Project) + has, err := db.GetEngine(ctx).ID(projectID).Where("repo_id = ?", repoID).Get(p) + if err != nil { + return err + } else if !has { + return ErrProjectNotExist{ID: projectID, RepoID: repoID} + } - has, err := db.GetEngine(ctx).ID(projectID).Where("repo_id = ?", repoID).Get(p) - if err != nil { - return err - } else if !has { - return ErrProjectNotExist{ID: projectID, RepoID: repoID} - } - - if err := changeProjectStatus(ctx, p, isClosed); err != nil { - return err - } - - return committer.Commit() + return changeProjectStatus(ctx, p, isClosed) + }) } // ChangeProjectStatus toggle a project between opened and closed func ChangeProjectStatus(ctx context.Context, p *Project, isClosed bool) error { - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return err - } - defer committer.Close() - - if err := changeProjectStatus(ctx, p, isClosed); err != nil { - return err - } - - return committer.Commit() + return db.WithTx(ctx, func(ctx context.Context) error { + return changeProjectStatus(ctx, p, isClosed) + }) } func changeProjectStatus(ctx context.Context, p *Project, isClosed bool) error { diff --git a/models/project/project_test.go b/models/project/project_test.go index dd421b46599..4b24615979b 100644 --- a/models/project/project_test.go +++ b/models/project/project_test.go @@ -34,13 +34,13 @@ func TestIsProjectTypeValid(t *testing.T) { func TestGetProjects(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - projects, err := db.Find[Project](db.DefaultContext, SearchOptions{RepoID: 1}) + projects, err := db.Find[Project](t.Context(), SearchOptions{RepoID: 1}) assert.NoError(t, err) // 1 value for this repo exists in the fixtures assert.Len(t, projects, 1) - projects, err = db.Find[Project](db.DefaultContext, SearchOptions{RepoID: 3}) + projects, err = db.Find[Project](t.Context(), SearchOptions{RepoID: 3}) assert.NoError(t, err) // 1 value for this repo exists in the fixtures @@ -60,24 +60,24 @@ func TestProject(t *testing.T) { CreatorID: 2, } - assert.NoError(t, NewProject(db.DefaultContext, project)) + assert.NoError(t, NewProject(t.Context(), project)) - _, err := GetProjectByID(db.DefaultContext, project.ID) + _, err := GetProjectByID(t.Context(), project.ID) assert.NoError(t, err) // Update project project.Title = "Updated title" - assert.NoError(t, UpdateProject(db.DefaultContext, project)) + assert.NoError(t, UpdateProject(t.Context(), project)) - projectFromDB, err := GetProjectByID(db.DefaultContext, project.ID) + projectFromDB, err := GetProjectByID(t.Context(), project.ID) assert.NoError(t, err) assert.Equal(t, project.Title, projectFromDB.Title) - assert.NoError(t, ChangeProjectStatus(db.DefaultContext, project, true)) + assert.NoError(t, ChangeProjectStatus(t.Context(), project, true)) // Retrieve from DB afresh to check if it is truly closed - projectFromDB, err = GetProjectByID(db.DefaultContext, project.ID) + projectFromDB, err = GetProjectByID(t.Context(), project.ID) assert.NoError(t, err) assert.True(t, projectFromDB.IsClosed) @@ -109,14 +109,14 @@ func TestProjectsSort(t *testing.T) { } for _, tt := range tests { - projects, count, err := db.FindAndCount[Project](db.DefaultContext, SearchOptions{ + projects, count, err := db.FindAndCount[Project](t.Context(), SearchOptions{ OrderBy: GetSearchOrderByBySortType(tt.sortType), }) assert.NoError(t, err) - assert.EqualValues(t, int64(6), count) + assert.Equal(t, int64(6), count) if assert.Len(t, projects, 6) { for i := range projects { - assert.EqualValues(t, tt.wants[i], projects[i].ID) + assert.Equal(t, tt.wants[i], projects[i].ID) } } } diff --git a/models/pull/automerge.go b/models/pull/automerge.go index f69fcb60d16..7f940a98492 100644 --- a/models/pull/automerge.go +++ b/models/pull/automerge.go @@ -5,23 +5,26 @@ package pull import ( "context" + "errors" "fmt" "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/modules/util" ) // AutoMerge represents a pull request scheduled for merging when checks succeed type AutoMerge struct { - ID int64 `xorm:"pk autoincr"` - PullID int64 `xorm:"UNIQUE"` - DoerID int64 `xorm:"INDEX NOT NULL"` - Doer *user_model.User `xorm:"-"` - MergeStyle repo_model.MergeStyle `xorm:"varchar(30)"` - Message string `xorm:"LONGTEXT"` - CreatedUnix timeutil.TimeStamp `xorm:"created"` + ID int64 `xorm:"pk autoincr"` + PullID int64 `xorm:"UNIQUE"` + DoerID int64 `xorm:"INDEX NOT NULL"` + Doer *user_model.User `xorm:"-"` + MergeStyle repo_model.MergeStyle `xorm:"varchar(30)"` + Message string `xorm:"LONGTEXT"` + DeleteBranchAfterMerge bool + CreatedUnix timeutil.TimeStamp `xorm:"created"` } // TableName return database table name for xorm @@ -49,7 +52,7 @@ func IsErrAlreadyScheduledToAutoMerge(err error) bool { } // ScheduleAutoMerge schedules a pull request to be merged when all checks succeed -func ScheduleAutoMerge(ctx context.Context, doer *user_model.User, pullID int64, style repo_model.MergeStyle, message string) error { +func ScheduleAutoMerge(ctx context.Context, doer *user_model.User, pullID int64, style repo_model.MergeStyle, message string, deleteBranchAfterMerge bool) error { // Check if we already have a merge scheduled for that pull request if exists, _, err := GetScheduledMergeByPullID(ctx, pullID); err != nil { return err @@ -58,10 +61,11 @@ func ScheduleAutoMerge(ctx context.Context, doer *user_model.User, pullID int64, } _, err := db.GetEngine(ctx).Insert(&AutoMerge{ - DoerID: doer.ID, - PullID: pullID, - MergeStyle: style, - Message: message, + DoerID: doer.ID, + PullID: pullID, + MergeStyle: style, + Message: message, + DeleteBranchAfterMerge: deleteBranchAfterMerge, }) return err } @@ -74,7 +78,10 @@ func GetScheduledMergeByPullID(ctx context.Context, pullID int64) (bool, *AutoMe return false, nil, err } - doer, err := user_model.GetUserByID(ctx, scheduledPRM.DoerID) + doer, err := user_model.GetPossibleUserByID(ctx, scheduledPRM.DoerID) + if errors.Is(err, util.ErrNotExist) { + doer, err = user_model.NewGhostUser(), nil + } if err != nil { return false, nil, err } diff --git a/models/pull/review_state.go b/models/pull/review_state.go index e46a22a49d6..137af00eab2 100644 --- a/models/pull/review_state.go +++ b/models/pull/review_state.go @@ -6,6 +6,7 @@ package pull import ( "context" "fmt" + "maps" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/modules/log" @@ -100,9 +101,7 @@ func mergeFiles(oldFiles, newFiles map[string]ViewedState) map[string]ViewedStat return oldFiles } - for file, viewed := range newFiles { - oldFiles[file] = viewed - } + maps.Copy(oldFiles, newFiles) return oldFiles } diff --git a/models/renderhelper/commit_checker.go b/models/renderhelper/commit_checker.go index 4815643e673..407e45fb543 100644 --- a/models/renderhelper/commit_checker.go +++ b/models/renderhelper/commit_checker.go @@ -47,7 +47,7 @@ func (c *commitChecker) IsCommitIDExisting(commitID string) bool { c.gitRepo, c.gitRepoCloser = r, closer } - exist = c.gitRepo.IsReferenceExist(commitID) // Don't use IsObjectExist since it doesn't support short hashs with gogit edition. + exist = c.gitRepo.IsReferenceExist(commitID) // Don't use IsObjectExist since it doesn't support short hashes with gogit edition. c.commitCache[commitID] = exist return exist } diff --git a/models/renderhelper/repo_comment.go b/models/renderhelper/repo_comment.go index 6bd5e91ad17..ae0fbf0abd4 100644 --- a/models/renderhelper/repo_comment.go +++ b/models/renderhelper/repo_comment.go @@ -28,14 +28,14 @@ func (r *RepoComment) IsCommitIDExisting(commitID string) bool { return r.commitChecker.IsCommitIDExisting(commitID) } -func (r *RepoComment) ResolveLink(link string, likeType markup.LinkType) (finalLink string) { - switch likeType { - case markup.LinkTypeApp: - finalLink = r.ctx.ResolveLinkApp(link) +func (r *RepoComment) ResolveLink(link, preferLinkType string) string { + linkType, link := markup.ParseRenderedLink(link, preferLinkType) + switch linkType { + case markup.LinkTypeRoot: + return r.ctx.ResolveLinkRoot(link) default: - finalLink = r.ctx.ResolveLinkRelative(r.repoLink, r.opts.CurrentRefPath, link) + return r.ctx.ResolveLinkRelative(r.repoLink, r.opts.CurrentRefPath, link) } - return finalLink } var _ markup.RenderHelper = (*RepoComment)(nil) @@ -44,30 +44,31 @@ type RepoCommentOptions struct { DeprecatedRepoName string // it is only a patch for the non-standard "markup" api DeprecatedOwnerName string // it is only a patch for the non-standard "markup" api CurrentRefPath string // eg: "branch/main" or "commit/11223344" + FootnoteContextID string // the extra context ID for footnotes, used to avoid conflicts with other footnotes in the same page } func NewRenderContextRepoComment(ctx context.Context, repo *repo_model.Repository, opts ...RepoCommentOptions) *markup.RenderContext { - helper := &RepoComment{ - repoLink: repo.Link(), - opts: util.OptionalArg(opts), - } + helper := &RepoComment{opts: util.OptionalArg(opts)} rctx := markup.NewRenderContext(ctx) helper.ctx = rctx + var metas map[string]string if repo != nil { helper.repoLink = repo.Link() helper.commitChecker = newCommitChecker(ctx, repo) - rctx = rctx.WithMetas(repo.ComposeMetas(ctx)) + metas = repo.ComposeCommentMetas(ctx) } else { - // this is almost dead code, only to pass the incorrect tests - helper.repoLink = fmt.Sprintf("%s/%s", helper.opts.DeprecatedOwnerName, helper.opts.DeprecatedRepoName) - rctx = rctx.WithMetas(map[string]string{ - "user": helper.opts.DeprecatedOwnerName, - "repo": helper.opts.DeprecatedRepoName, - - "markdownLineBreakStyle": "comment", - "markupAllowShortIssuePattern": "true", - }) + // repo can be nil when rendering a commit message in user's dashboard feedback whose repository has been deleted + metas = map[string]string{} + if helper.opts.DeprecatedOwnerName != "" { + // this is almost dead code, only to pass the incorrect tests + helper.repoLink = fmt.Sprintf("%s/%s", helper.opts.DeprecatedOwnerName, helper.opts.DeprecatedRepoName) + metas["user"] = helper.opts.DeprecatedOwnerName + metas["repo"] = helper.opts.DeprecatedRepoName + } + metas["markdownNewLineHardBreak"] = "true" + metas["markupAllowShortIssuePattern"] = "true" } - rctx = rctx.WithHelper(helper) + metas["footnoteContextId"] = helper.opts.FootnoteContextID + rctx = rctx.WithMetas(metas).WithHelper(helper) return rctx } diff --git a/models/renderhelper/repo_comment_test.go b/models/renderhelper/repo_comment_test.go index 01e20b9e026..3b13bff73c7 100644 --- a/models/renderhelper/repo_comment_test.go +++ b/models/renderhelper/repo_comment_test.go @@ -4,7 +4,6 @@ package renderhelper import ( - "context" "testing" repo_model "code.gitea.io/gitea/models/repo" @@ -21,7 +20,7 @@ func TestRepoComment(t *testing.T) { repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) t.Run("AutoLink", func(t *testing.T) { - rctx := NewRenderContextRepoComment(context.Background(), repo1).WithMarkupType(markdown.MarkupName) + rctx := NewRenderContextRepoComment(t.Context(), repo1).WithMarkupType(markdown.MarkupName) rendered, err := markup.RenderString(rctx, ` 65f1bf27bc3bf70f64657658635e66094edbcb4d #1 @@ -36,7 +35,7 @@ func TestRepoComment(t *testing.T) { }) t.Run("AbsoluteAndRelative", func(t *testing.T) { - rctx := NewRenderContextRepoComment(context.Background(), repo1).WithMarkupType(markdown.MarkupName) + rctx := NewRenderContextRepoComment(t.Context(), repo1).WithMarkupType(markdown.MarkupName) // It is Gitea's old behavior, the relative path is resolved to the repo path // It is different from GitHub, GitHub resolves relative links to current page's path @@ -56,7 +55,7 @@ func TestRepoComment(t *testing.T) { }) t.Run("WithCurrentRefPath", func(t *testing.T) { - rctx := NewRenderContextRepoComment(context.Background(), repo1, RepoCommentOptions{CurrentRefPath: "/commit/1234"}). + rctx := NewRenderContextRepoComment(t.Context(), repo1, RepoCommentOptions{CurrentRefPath: "/commit/1234"}). WithMarkupType(markdown.MarkupName) // the ref path is only used to render commit message: a commit message is rendered at the commit page with its commit ID path @@ -73,4 +72,11 @@ func TestRepoComment(t *testing.T) { ./image

`, rendered) }) + + t.Run("NoRepo", func(t *testing.T) { + rctx := NewRenderContextRepoComment(t.Context(), nil).WithMarkupType(markdown.MarkupName) + rendered, err := markup.RenderString(rctx, "any") + assert.NoError(t, err) + assert.Equal(t, "

any

\n", rendered) + }) } diff --git a/models/renderhelper/repo_file.go b/models/renderhelper/repo_file.go index 794828c6174..e0375ed2809 100644 --- a/models/renderhelper/repo_file.go +++ b/models/renderhelper/repo_file.go @@ -29,17 +29,17 @@ func (r *RepoFile) IsCommitIDExisting(commitID string) bool { return r.commitChecker.IsCommitIDExisting(commitID) } -func (r *RepoFile) ResolveLink(link string, likeType markup.LinkType) string { - finalLink := link - switch likeType { - case markup.LinkTypeApp: - finalLink = r.ctx.ResolveLinkApp(link) - case markup.LinkTypeDefault: - finalLink = r.ctx.ResolveLinkRelative(path.Join(r.repoLink, "src", r.opts.CurrentRefPath), r.opts.CurrentTreePath, link) +func (r *RepoFile) ResolveLink(link, preferLinkType string) (finalLink string) { + linkType, link := markup.ParseRenderedLink(link, preferLinkType) + switch linkType { + case markup.LinkTypeRoot: + finalLink = r.ctx.ResolveLinkRoot(link) case markup.LinkTypeRaw: finalLink = r.ctx.ResolveLinkRelative(path.Join(r.repoLink, "raw", r.opts.CurrentRefPath), r.opts.CurrentTreePath, link) case markup.LinkTypeMedia: finalLink = r.ctx.ResolveLinkRelative(path.Join(r.repoLink, "media", r.opts.CurrentRefPath), r.opts.CurrentTreePath, link) + default: + finalLink = r.ctx.ResolveLinkRelative(path.Join(r.repoLink, "src", r.opts.CurrentRefPath), r.opts.CurrentTreePath, link) } return finalLink } @@ -61,15 +61,13 @@ func NewRenderContextRepoFile(ctx context.Context, repo *repo_model.Repository, if repo != nil { helper.repoLink = repo.Link() helper.commitChecker = newCommitChecker(ctx, repo) - rctx = rctx.WithMetas(repo.ComposeDocumentMetas(ctx)) + rctx = rctx.WithMetas(repo.ComposeRepoFileMetas(ctx)) } else { // this is almost dead code, only to pass the incorrect tests helper.repoLink = fmt.Sprintf("%s/%s", helper.opts.DeprecatedOwnerName, helper.opts.DeprecatedRepoName) rctx = rctx.WithMetas(map[string]string{ "user": helper.opts.DeprecatedOwnerName, "repo": helper.opts.DeprecatedRepoName, - - "markdownLineBreakStyle": "document", }) } rctx = rctx.WithHelper(helper) diff --git a/models/renderhelper/repo_file_test.go b/models/renderhelper/repo_file_test.go index 959648b660b..3b48efba3af 100644 --- a/models/renderhelper/repo_file_test.go +++ b/models/renderhelper/repo_file_test.go @@ -4,7 +4,6 @@ package renderhelper import ( - "context" "testing" repo_model "code.gitea.io/gitea/models/repo" @@ -22,7 +21,7 @@ func TestRepoFile(t *testing.T) { repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) t.Run("AutoLink", func(t *testing.T) { - rctx := NewRenderContextRepoFile(context.Background(), repo1).WithMarkupType(markdown.MarkupName) + rctx := NewRenderContextRepoFile(t.Context(), repo1).WithMarkupType(markdown.MarkupName) rendered, err := markup.RenderString(rctx, ` 65f1bf27bc3bf70f64657658635e66094edbcb4d #1 @@ -37,7 +36,7 @@ func TestRepoFile(t *testing.T) { }) t.Run("AbsoluteAndRelative", func(t *testing.T) { - rctx := NewRenderContextRepoFile(context.Background(), repo1, RepoFileOptions{CurrentRefPath: "branch/main"}). + rctx := NewRenderContextRepoFile(t.Context(), repo1, RepoFileOptions{CurrentRefPath: "branch/main"}). WithMarkupType(markdown.MarkupName) rendered, err := markup.RenderString(rctx, ` [/test](/test) @@ -49,13 +48,13 @@ func TestRepoFile(t *testing.T) { assert.Equal(t, `

/test ./test -/image -./image

+/image +./image

`, rendered) }) t.Run("WithCurrentRefPath", func(t *testing.T) { - rctx := NewRenderContextRepoFile(context.Background(), repo1, RepoFileOptions{CurrentRefPath: "/commit/1234"}). + rctx := NewRenderContextRepoFile(t.Context(), repo1, RepoFileOptions{CurrentRefPath: "/commit/1234"}). WithMarkupType(markdown.MarkupName) rendered, err := markup.RenderString(rctx, ` [/test](/test) @@ -63,12 +62,12 @@ func TestRepoFile(t *testing.T) { `) assert.NoError(t, err) assert.Equal(t, `

/test -/image

+/image

`, rendered) }) t.Run("WithCurrentRefPathByTag", func(t *testing.T) { - rctx := NewRenderContextRepoFile(context.Background(), repo1, RepoFileOptions{ + rctx := NewRenderContextRepoFile(t.Context(), repo1, RepoFileOptions{ CurrentRefPath: "/commit/1234", CurrentTreePath: "my-dir", }). @@ -78,7 +77,7 @@ func TestRepoFile(t *testing.T) {