diff --git a/.eslintrc.yaml b/.eslintrc.yaml new file mode 100644 index 0000000000000..0dd9a6687d8bb --- /dev/null +++ b/.eslintrc.yaml @@ -0,0 +1,967 @@ +root: true +reportUnusedDisableDirectives: true + +ignorePatterns: + - /web_src/js/vendor + - /web_src/fomantic + - /public/assets/js + +parser: "@typescript-eslint/parser" + +parserOptions: + sourceType: module + ecmaVersion: latest + project: true + extraFileExtensions: [".vue"] + parser: "@typescript-eslint/parser" # for vue plugin - https://eslint.vuejs.org/user-guide/#how-to-use-a-custom-parser + +settings: + import-x/extensions: [".js", ".ts"] + import-x/parsers: + "@typescript-eslint/parser": [".js", ".ts"] + import-x/resolver: + typescript: true + +plugins: + - "@eslint-community/eslint-plugin-eslint-comments" + - "@stylistic/eslint-plugin-js" + - "@typescript-eslint/eslint-plugin" + - eslint-plugin-array-func + - eslint-plugin-github + - eslint-plugin-import-x + - eslint-plugin-no-jquery + - eslint-plugin-no-use-extend-native + - eslint-plugin-regexp + - eslint-plugin-sonarjs + - eslint-plugin-unicorn + - eslint-plugin-vitest + - eslint-plugin-vitest-globals + - eslint-plugin-wc + +env: + es2024: true + node: true + +overrides: + - files: ["web_src/**/*"] + globals: + __webpack_public_path__: true + process: false # https://github.com/webpack/webpack/issues/15833 + - files: ["web_src/**/*", "docs/**/*"] + env: + browser: true + node: false + - files: ["web_src/**/*worker.*"] + env: + worker: true + rules: + no-restricted-globals: [2, addEventListener, blur, close, closed, confirm, defaultStatus, defaultstatus, error, event, external, find, focus, frameElement, frames, history, innerHeight, innerWidth, isFinite, isNaN, length, locationbar, menubar, moveBy, moveTo, name, onblur, onerror, onfocus, onload, onresize, onunload, open, opener, opera, outerHeight, outerWidth, pageXOffset, pageYOffset, parent, print, removeEventListener, resizeBy, resizeTo, screen, screenLeft, screenTop, screenX, screenY, scroll, scrollbars, scrollBy, scrollTo, scrollX, scrollY, status, statusbar, stop, toolbar, top] + - files: ["*.config.*"] + rules: + import-x/no-unused-modules: [0] + - files: ["**/*.d.ts"] + rules: + import-x/no-unused-modules: [0] + "@typescript-eslint/consistent-type-definitions": [0] + "@typescript-eslint/consistent-type-imports": [0] + - files: ["web_src/js/types.ts"] + rules: + import-x/no-unused-modules: [0] + - files: ["**/*.test.*", "web_src/js/test/setup.ts"] + env: + vitest-globals/env: true + rules: + vitest/consistent-test-filename: [0] + vitest/consistent-test-it: [0] + vitest/expect-expect: [0] + vitest/max-expects: [0] + vitest/max-nested-describe: [0] + vitest/no-alias-methods: [0] + vitest/no-commented-out-tests: [0] + vitest/no-conditional-expect: [0] + vitest/no-conditional-in-test: [0] + vitest/no-conditional-tests: [0] + vitest/no-disabled-tests: [0] + vitest/no-done-callback: [0] + vitest/no-duplicate-hooks: [0] + vitest/no-focused-tests: [0] + vitest/no-hooks: [0] + vitest/no-identical-title: [2] + vitest/no-interpolation-in-snapshots: [0] + vitest/no-large-snapshots: [0] + vitest/no-mocks-import: [0] + vitest/no-restricted-matchers: [0] + vitest/no-restricted-vi-methods: [0] + vitest/no-standalone-expect: [0] + vitest/no-test-prefixes: [0] + vitest/no-test-return-statement: [0] + vitest/prefer-called-with: [0] + vitest/prefer-comparison-matcher: [0] + vitest/prefer-each: [0] + vitest/prefer-equality-matcher: [0] + vitest/prefer-expect-resolves: [0] + vitest/prefer-hooks-in-order: [0] + vitest/prefer-hooks-on-top: [2] + vitest/prefer-lowercase-title: [0] + vitest/prefer-mock-promise-shorthand: [0] + vitest/prefer-snapshot-hint: [0] + vitest/prefer-spy-on: [0] + vitest/prefer-strict-equal: [0] + vitest/prefer-to-be: [0] + vitest/prefer-to-be-falsy: [0] + vitest/prefer-to-be-object: [0] + vitest/prefer-to-be-truthy: [0] + vitest/prefer-to-contain: [0] + vitest/prefer-to-have-length: [0] + vitest/prefer-todo: [0] + vitest/require-hook: [0] + vitest/require-to-throw-message: [0] + vitest/require-top-level-describe: [0] + vitest/valid-describe-callback: [2] + vitest/valid-expect: [2] + vitest/valid-title: [2] + - files: ["web_src/js/modules/fetch.ts", "web_src/js/standalone/**/*"] + rules: + no-restricted-syntax: [2, WithStatement, ForInStatement, LabeledStatement, SequenceExpression] + - files: ["**/*.vue"] + plugins: + - eslint-plugin-vue + - eslint-plugin-vue-scoped-css + extends: + - plugin:vue/vue3-recommended + - plugin:vue-scoped-css/vue3-recommended + rules: + vue/attributes-order: [0] + vue/html-closing-bracket-spacing: [2, {startTag: never, endTag: never, selfClosingTag: never}] + vue/max-attributes-per-line: [0] + vue/singleline-html-element-content-newline: [0] + - files: ["tests/e2e/**"] + plugins: + - eslint-plugin-playwright + extends: plugin:playwright/recommended + +rules: + "@eslint-community/eslint-comments/disable-enable-pair": [2] + "@eslint-community/eslint-comments/no-aggregating-enable": [2] + "@eslint-community/eslint-comments/no-duplicate-disable": [2] + "@eslint-community/eslint-comments/no-restricted-disable": [0] + "@eslint-community/eslint-comments/no-unlimited-disable": [2] + "@eslint-community/eslint-comments/no-unused-disable": [2] + "@eslint-community/eslint-comments/no-unused-enable": [2] + "@eslint-community/eslint-comments/no-use": [0] + "@eslint-community/eslint-comments/require-description": [0] + "@stylistic/js/array-bracket-newline": [0] + "@stylistic/js/array-bracket-spacing": [2, never] + "@stylistic/js/array-element-newline": [0] + "@stylistic/js/arrow-parens": [2, always] + "@stylistic/js/arrow-spacing": [2, {before: true, after: true}] + "@stylistic/js/block-spacing": [0] + "@stylistic/js/brace-style": [2, 1tbs, {allowSingleLine: true}] + "@stylistic/js/comma-dangle": [2, always-multiline] + "@stylistic/js/comma-spacing": [2, {before: false, after: true}] + "@stylistic/js/comma-style": [2, last] + "@stylistic/js/computed-property-spacing": [2, never] + "@stylistic/js/dot-location": [2, property] + "@stylistic/js/eol-last": [2] + "@stylistic/js/function-call-argument-newline": [0] + "@stylistic/js/function-call-spacing": [2, never] + "@stylistic/js/function-paren-newline": [0] + "@stylistic/js/generator-star-spacing": [0] + "@stylistic/js/implicit-arrow-linebreak": [0] + "@stylistic/js/indent": [2, 2, {ignoreComments: true, SwitchCase: 1}] + "@stylistic/js/key-spacing": [2] + "@stylistic/js/keyword-spacing": [2] + "@stylistic/js/line-comment-position": [0] + "@stylistic/js/linebreak-style": [2, unix] + "@stylistic/js/lines-around-comment": [0] + "@stylistic/js/lines-between-class-members": [0] + "@stylistic/js/max-len": [0] + "@stylistic/js/max-statements-per-line": [0] + "@stylistic/js/multiline-comment-style": [0] + "@stylistic/js/multiline-ternary": [0] + "@stylistic/js/new-parens": [2] + "@stylistic/js/newline-per-chained-call": [0] + "@stylistic/js/no-confusing-arrow": [0] + "@stylistic/js/no-extra-parens": [0] + "@stylistic/js/no-extra-semi": [2] + "@stylistic/js/no-floating-decimal": [0] + "@stylistic/js/no-mixed-operators": [0] + "@stylistic/js/no-mixed-spaces-and-tabs": [2] + "@stylistic/js/no-multi-spaces": [2, {ignoreEOLComments: true, exceptions: {Property: true}}] + "@stylistic/js/no-multiple-empty-lines": [2, {max: 1, maxEOF: 0, maxBOF: 0}] + "@stylistic/js/no-tabs": [2] + "@stylistic/js/no-trailing-spaces": [2] + "@stylistic/js/no-whitespace-before-property": [2] + "@stylistic/js/nonblock-statement-body-position": [2] + "@stylistic/js/object-curly-newline": [0] + "@stylistic/js/object-curly-spacing": [2, never] + "@stylistic/js/object-property-newline": [0] + "@stylistic/js/one-var-declaration-per-line": [0] + "@stylistic/js/operator-linebreak": [2, after] + "@stylistic/js/padded-blocks": [2, never] + "@stylistic/js/padding-line-between-statements": [0] + "@stylistic/js/quote-props": [0] + "@stylistic/js/quotes": [2, single, {avoidEscape: true, allowTemplateLiterals: true}] + "@stylistic/js/rest-spread-spacing": [2, never] + "@stylistic/js/semi": [2, always, {omitLastInOneLineBlock: true}] + "@stylistic/js/semi-spacing": [2, {before: false, after: true}] + "@stylistic/js/semi-style": [2, last] + "@stylistic/js/space-before-blocks": [2, always] + "@stylistic/js/space-before-function-paren": [2, {anonymous: ignore, named: never, asyncArrow: always}] + "@stylistic/js/space-in-parens": [2, never] + "@stylistic/js/space-infix-ops": [2] + "@stylistic/js/space-unary-ops": [2] + "@stylistic/js/spaced-comment": [2, always] + "@stylistic/js/switch-colon-spacing": [2] + "@stylistic/js/template-curly-spacing": [2, never] + "@stylistic/js/template-tag-spacing": [2, never] + "@stylistic/js/wrap-iife": [2, inside] + "@stylistic/js/wrap-regex": [0] + "@stylistic/js/yield-star-spacing": [2, after] + "@typescript-eslint/adjacent-overload-signatures": [0] + "@typescript-eslint/array-type": [0] + "@typescript-eslint/await-thenable": [2] + "@typescript-eslint/ban-ts-comment": [2, {'ts-expect-error': false, 'ts-ignore': true, 'ts-nocheck': false, 'ts-check': false}] + "@typescript-eslint/ban-tslint-comment": [0] + "@typescript-eslint/class-literal-property-style": [0] + "@typescript-eslint/class-methods-use-this": [0] + "@typescript-eslint/consistent-generic-constructors": [0] + "@typescript-eslint/consistent-indexed-object-style": [0] + "@typescript-eslint/consistent-return": [0] + "@typescript-eslint/consistent-type-assertions": [2, {assertionStyle: as, objectLiteralTypeAssertions: allow}] + "@typescript-eslint/consistent-type-definitions": [2, type] + "@typescript-eslint/consistent-type-exports": [2, {fixMixedExportsWithInlineTypeSpecifier: false}] + "@typescript-eslint/consistent-type-imports": [2, {prefer: type-imports, fixStyle: separate-type-imports, disallowTypeAnnotations: true}] + "@typescript-eslint/default-param-last": [0] + "@typescript-eslint/dot-notation": [0] + "@typescript-eslint/explicit-function-return-type": [0] + "@typescript-eslint/explicit-member-accessibility": [0] + "@typescript-eslint/explicit-module-boundary-types": [0] + "@typescript-eslint/init-declarations": [0] + "@typescript-eslint/max-params": [0] + "@typescript-eslint/member-ordering": [0] + "@typescript-eslint/method-signature-style": [0] + "@typescript-eslint/naming-convention": [0] + "@typescript-eslint/no-array-constructor": [2] + "@typescript-eslint/no-array-delete": [2] + "@typescript-eslint/no-base-to-string": [0] + "@typescript-eslint/no-confusing-non-null-assertion": [2] + "@typescript-eslint/no-confusing-void-expression": [0] + "@typescript-eslint/no-deprecated": [2] + "@typescript-eslint/no-dupe-class-members": [0] + "@typescript-eslint/no-duplicate-enum-values": [2] + "@typescript-eslint/no-duplicate-type-constituents": [2, {ignoreUnions: true}] + "@typescript-eslint/no-dynamic-delete": [0] + "@typescript-eslint/no-empty-function": [0] + "@typescript-eslint/no-empty-interface": [0] + "@typescript-eslint/no-empty-object-type": [2] + "@typescript-eslint/no-explicit-any": [0] + "@typescript-eslint/no-extra-non-null-assertion": [2] + "@typescript-eslint/no-extraneous-class": [0] + "@typescript-eslint/no-floating-promises": [0] + "@typescript-eslint/no-for-in-array": [2] + "@typescript-eslint/no-implied-eval": [2] + "@typescript-eslint/no-import-type-side-effects": [0] # dupe with consistent-type-imports + "@typescript-eslint/no-inferrable-types": [0] + "@typescript-eslint/no-invalid-this": [0] + "@typescript-eslint/no-invalid-void-type": [0] + "@typescript-eslint/no-loop-func": [0] + "@typescript-eslint/no-loss-of-precision": [0] + "@typescript-eslint/no-magic-numbers": [0] + "@typescript-eslint/no-meaningless-void-operator": [0] + "@typescript-eslint/no-misused-new": [2] + "@typescript-eslint/no-misused-promises": [2, {checksVoidReturn: {attributes: false, arguments: false}}] + "@typescript-eslint/no-mixed-enums": [0] + "@typescript-eslint/no-namespace": [2] + "@typescript-eslint/no-non-null-asserted-nullish-coalescing": [0] + "@typescript-eslint/no-non-null-asserted-optional-chain": [2] + "@typescript-eslint/no-non-null-assertion": [0] + "@typescript-eslint/no-redeclare": [0] + "@typescript-eslint/no-redundant-type-constituents": [2] + "@typescript-eslint/no-require-imports": [2] + "@typescript-eslint/no-restricted-imports": [0] + "@typescript-eslint/no-restricted-types": [0] + "@typescript-eslint/no-shadow": [0] + "@typescript-eslint/no-this-alias": [0] # handled by unicorn/no-this-assignment + "@typescript-eslint/no-unnecessary-boolean-literal-compare": [0] + "@typescript-eslint/no-unnecessary-condition": [0] + "@typescript-eslint/no-unnecessary-qualifier": [0] + "@typescript-eslint/no-unnecessary-template-expression": [0] + "@typescript-eslint/no-unnecessary-type-arguments": [0] + "@typescript-eslint/no-unnecessary-type-assertion": [2] + "@typescript-eslint/no-unnecessary-type-constraint": [2] + "@typescript-eslint/no-unsafe-argument": [0] + "@typescript-eslint/no-unsafe-assignment": [0] + "@typescript-eslint/no-unsafe-call": [0] + "@typescript-eslint/no-unsafe-declaration-merging": [2] + "@typescript-eslint/no-unsafe-enum-comparison": [2] + "@typescript-eslint/no-unsafe-function-type": [2] + "@typescript-eslint/no-unsafe-member-access": [0] + "@typescript-eslint/no-unsafe-return": [0] + "@typescript-eslint/no-unsafe-unary-minus": [2] + "@typescript-eslint/no-unused-expressions": [0] + "@typescript-eslint/no-unused-vars": [2, {vars: all, args: all, caughtErrors: all, ignoreRestSiblings: false, argsIgnorePattern: ^_, varsIgnorePattern: ^_, caughtErrorsIgnorePattern: ^_, destructuredArrayIgnorePattern: ^_}] + "@typescript-eslint/no-use-before-define": [0] + "@typescript-eslint/no-useless-constructor": [0] + "@typescript-eslint/no-useless-empty-export": [0] + "@typescript-eslint/no-wrapper-object-types": [2] + "@typescript-eslint/non-nullable-type-assertion-style": [0] + "@typescript-eslint/only-throw-error": [2] + "@typescript-eslint/parameter-properties": [0] + "@typescript-eslint/prefer-as-const": [2] + "@typescript-eslint/prefer-destructuring": [0] + "@typescript-eslint/prefer-enum-initializers": [0] + "@typescript-eslint/prefer-find": [2] + "@typescript-eslint/prefer-for-of": [2] + "@typescript-eslint/prefer-function-type": [2] + "@typescript-eslint/prefer-includes": [2] + "@typescript-eslint/prefer-literal-enum-member": [0] + "@typescript-eslint/prefer-namespace-keyword": [0] + "@typescript-eslint/prefer-nullish-coalescing": [0] + "@typescript-eslint/prefer-optional-chain": [2, {requireNullish: true}] + "@typescript-eslint/prefer-promise-reject-errors": [0] + "@typescript-eslint/prefer-readonly": [0] + "@typescript-eslint/prefer-readonly-parameter-types": [0] + "@typescript-eslint/prefer-reduce-type-parameter": [0] + "@typescript-eslint/prefer-regexp-exec": [0] + "@typescript-eslint/prefer-return-this-type": [0] + "@typescript-eslint/prefer-string-starts-ends-with": [2, {allowSingleElementEquality: always}] + "@typescript-eslint/promise-function-async": [0] + "@typescript-eslint/require-array-sort-compare": [0] + "@typescript-eslint/require-await": [0] + "@typescript-eslint/restrict-plus-operands": [2] + "@typescript-eslint/restrict-template-expressions": [0] + "@typescript-eslint/return-await": [0] + "@typescript-eslint/strict-boolean-expressions": [0] + "@typescript-eslint/switch-exhaustiveness-check": [0] + "@typescript-eslint/triple-slash-reference": [2] + "@typescript-eslint/typedef": [0] + "@typescript-eslint/unbound-method": [0] # too many false-positives + "@typescript-eslint/unified-signatures": [2] + accessor-pairs: [2] + array-callback-return: [2, {checkForEach: true}] + array-func/avoid-reverse: [2] + array-func/from-map: [2] + array-func/no-unnecessary-this-arg: [2] + array-func/prefer-array-from: [2] + array-func/prefer-flat-map: [0] # handled by unicorn/prefer-array-flat-map + array-func/prefer-flat: [0] # handled by unicorn/prefer-array-flat + arrow-body-style: [0] + block-scoped-var: [2] + camelcase: [0] + capitalized-comments: [0] + class-methods-use-this: [0] + complexity: [0] + consistent-return: [0] + consistent-this: [0] + constructor-super: [2] + curly: [0] + default-case-last: [2] + default-case: [0] + default-param-last: [0] + dot-notation: [0] + eqeqeq: [2] + for-direction: [2] + func-name-matching: [2] + func-names: [0] + func-style: [0] + getter-return: [2] + github/a11y-aria-label-is-well-formatted: [0] + github/a11y-no-title-attribute: [0] + github/a11y-no-visually-hidden-interactive-element: [0] + github/a11y-role-supports-aria-props: [0] + github/a11y-svg-has-accessible-name: [0] + github/array-foreach: [0] + github/async-currenttarget: [2] + github/async-preventdefault: [2] + github/authenticity-token: [0] + github/get-attribute: [0] + github/js-class-name: [0] + github/no-blur: [0] + github/no-d-none: [0] + github/no-dataset: [2] + github/no-dynamic-script-tag: [2] + github/no-implicit-buggy-globals: [2] + github/no-inner-html: [0] + github/no-innerText: [2] + github/no-then: [2] + github/no-useless-passive: [2] + github/prefer-observers: [2] + github/require-passive-events: [2] + github/unescaped-html-literal: [0] + grouped-accessor-pairs: [2] + guard-for-in: [0] + id-blacklist: [0] + id-length: [0] + id-match: [0] + import-x/consistent-type-specifier-style: [0] + import-x/default: [0] + import-x/dynamic-import-chunkname: [0] + import-x/export: [2] + import-x/exports-last: [0] + import-x/extensions: [2, always, {ignorePackages: true}] + import-x/first: [2] + import-x/group-exports: [0] + import-x/max-dependencies: [0] + import-x/named: [2] + import-x/namespace: [0] + import-x/newline-after-import: [0] + import-x/no-absolute-path: [0] + import-x/no-amd: [2] + import-x/no-anonymous-default-export: [0] + import-x/no-commonjs: [2] + import-x/no-cycle: [2, {ignoreExternal: true, maxDepth: 1}] + import-x/no-default-export: [0] + import-x/no-deprecated: [0] + import-x/no-dynamic-require: [0] + import-x/no-empty-named-blocks: [2] + import-x/no-extraneous-dependencies: [2] + import-x/no-import-module-exports: [0] + import-x/no-internal-modules: [0] + import-x/no-mutable-exports: [0] + import-x/no-named-as-default-member: [0] + import-x/no-named-as-default: [0] + import-x/no-named-default: [0] + import-x/no-named-export: [0] + import-x/no-namespace: [0] + import-x/no-nodejs-modules: [0] + import-x/no-relative-packages: [0] + import-x/no-relative-parent-imports: [0] + import-x/no-restricted-paths: [0] + import-x/no-self-import: [2] + import-x/no-unassigned-import: [0] + import-x/no-unresolved: [2, {commonjs: true, ignore: ["\\?.+$"]}] + import-x/no-unused-modules: [2, {unusedExports: true}] + import-x/no-useless-path-segments: [2, {commonjs: true}] + import-x/no-webpack-loader-syntax: [2] + import-x/order: [0] + import-x/prefer-default-export: [0] + import-x/unambiguous: [0] + init-declarations: [0] + line-comment-position: [0] + logical-assignment-operators: [0] + max-classes-per-file: [0] + max-depth: [0] + max-lines-per-function: [0] + max-lines: [0] + max-nested-callbacks: [0] + max-params: [0] + max-statements: [0] + multiline-comment-style: [2, separate-lines] + new-cap: [0] + no-alert: [0] + no-array-constructor: [0] # handled by @typescript-eslint/no-array-constructor + no-async-promise-executor: [0] + no-await-in-loop: [0] + no-bitwise: [0] + no-buffer-constructor: [0] + no-caller: [2] + no-case-declarations: [2] + no-class-assign: [2] + no-compare-neg-zero: [2] + no-cond-assign: [2, except-parens] + no-console: [1, {allow: [debug, info, warn, error]}] + no-const-assign: [2] + no-constant-binary-expression: [2] + no-constant-condition: [0] + no-constructor-return: [2] + no-continue: [0] + no-control-regex: [0] + no-debugger: [1] + no-delete-var: [2] + no-div-regex: [0] + no-dupe-args: [2] + no-dupe-class-members: [2] + no-dupe-else-if: [2] + no-dupe-keys: [2] + no-duplicate-case: [2] + no-duplicate-imports: [0] + no-else-return: [2] + no-empty-character-class: [2] + no-empty-function: [0] + no-empty-pattern: [2] + no-empty-static-block: [2] + no-empty: [2, {allowEmptyCatch: true}] + no-eq-null: [2] + no-eval: [2] + no-ex-assign: [2] + no-extend-native: [2] + no-extra-bind: [2] + no-extra-boolean-cast: [2] + no-extra-label: [0] + no-fallthrough: [2] + no-func-assign: [2] + no-global-assign: [2] + no-implicit-coercion: [2] + no-implicit-globals: [0] + no-implied-eval: [0] # handled by @typescript-eslint/no-implied-eval + no-import-assign: [2] + no-inline-comments: [0] + no-inner-declarations: [2] + no-invalid-regexp: [2] + no-invalid-this: [0] + no-irregular-whitespace: [2] + no-iterator: [2] + no-jquery/no-ajax-events: [2] + no-jquery/no-ajax: [2] + no-jquery/no-and-self: [2] + no-jquery/no-animate-toggle: [2] + no-jquery/no-animate: [2] + no-jquery/no-append-html: [2] + no-jquery/no-attr: [2] + no-jquery/no-bind: [2] + no-jquery/no-box-model: [2] + no-jquery/no-browser: [2] + no-jquery/no-camel-case: [2] + no-jquery/no-class-state: [2] + no-jquery/no-class: [0] + no-jquery/no-clone: [2] + no-jquery/no-closest: [0] + no-jquery/no-constructor-attributes: [2] + no-jquery/no-contains: [2] + no-jquery/no-context-prop: [2] + no-jquery/no-css: [2] + no-jquery/no-data: [0] + no-jquery/no-deferred: [2] + no-jquery/no-delegate: [2] + no-jquery/no-done-fail: [2] + no-jquery/no-each-collection: [0] + no-jquery/no-each-util: [0] + no-jquery/no-each: [0] + no-jquery/no-error-shorthand: [2] + no-jquery/no-error: [2] + no-jquery/no-escape-selector: [2] + no-jquery/no-event-shorthand: [2] + no-jquery/no-extend: [2] + no-jquery/no-fade: [2] + no-jquery/no-filter: [0] + no-jquery/no-find-collection: [0] + no-jquery/no-find-util: [2] + no-jquery/no-find: [0] + no-jquery/no-fx-interval: [2] + no-jquery/no-fx: [2] + no-jquery/no-global-eval: [2] + no-jquery/no-global-selector: [0] + no-jquery/no-grep: [2] + no-jquery/no-has: [2] + no-jquery/no-hold-ready: [2] + no-jquery/no-html: [0] + no-jquery/no-in-array: [2] + no-jquery/no-is-array: [2] + no-jquery/no-is-empty-object: [2] + no-jquery/no-is-function: [2] + no-jquery/no-is-numeric: [2] + no-jquery/no-is-plain-object: [2] + no-jquery/no-is-window: [2] + no-jquery/no-is: [2] + no-jquery/no-jquery-constructor: [0] + no-jquery/no-live: [2] + no-jquery/no-load-shorthand: [2] + no-jquery/no-load: [2] + no-jquery/no-map-collection: [0] + no-jquery/no-map-util: [2] + no-jquery/no-map: [2] + no-jquery/no-merge: [2] + no-jquery/no-node-name: [2] + no-jquery/no-noop: [2] + no-jquery/no-now: [2] + no-jquery/no-on-ready: [2] + no-jquery/no-other-methods: [0] + no-jquery/no-other-utils: [2] + no-jquery/no-param: [2] + no-jquery/no-parent: [0] + no-jquery/no-parents: [2] + no-jquery/no-parse-html-literal: [2] + no-jquery/no-parse-html: [2] + no-jquery/no-parse-json: [2] + no-jquery/no-parse-xml: [2] + no-jquery/no-prop: [2] + no-jquery/no-proxy: [2] + no-jquery/no-ready-shorthand: [2] + no-jquery/no-ready: [2] + no-jquery/no-selector-prop: [2] + no-jquery/no-serialize: [2] + no-jquery/no-size: [2] + no-jquery/no-sizzle: [2] + no-jquery/no-slide: [2] + no-jquery/no-sub: [2] + no-jquery/no-support: [2] + no-jquery/no-text: [2] + no-jquery/no-trigger: [0] + no-jquery/no-trim: [2] + no-jquery/no-type: [2] + no-jquery/no-unique: [2] + no-jquery/no-unload-shorthand: [2] + no-jquery/no-val: [0] + no-jquery/no-visibility: [2] + no-jquery/no-when: [2] + no-jquery/no-wrap: [2] + no-jquery/variable-pattern: [2] + no-label-var: [2] + no-labels: [0] # handled by no-restricted-syntax + no-lone-blocks: [2] + no-lonely-if: [0] + no-loop-func: [0] + no-loss-of-precision: [2] + no-magic-numbers: [0] + no-misleading-character-class: [2] + no-multi-assign: [0] + no-multi-str: [2] + no-negated-condition: [0] + no-nested-ternary: [0] + no-new-func: [2] + no-new-native-nonconstructor: [2] + no-new-object: [2] + no-new-symbol: [2] + no-new-wrappers: [2] + no-new: [0] + no-nonoctal-decimal-escape: [2] + no-obj-calls: [2] + no-octal-escape: [2] + no-octal: [2] + no-param-reassign: [0] + no-plusplus: [0] + no-promise-executor-return: [0] + no-proto: [2] + no-prototype-builtins: [2] + no-redeclare: [0] # must be disabled for typescript overloads + no-regex-spaces: [2] + no-restricted-exports: [0] + no-restricted-globals: [2, addEventListener, blur, close, closed, confirm, defaultStatus, defaultstatus, error, event, external, find, focus, frameElement, frames, history, innerHeight, innerWidth, isFinite, isNaN, length, location, locationbar, menubar, moveBy, moveTo, name, onblur, onerror, onfocus, onload, onresize, onunload, open, opener, opera, outerHeight, outerWidth, pageXOffset, pageYOffset, parent, print, removeEventListener, resizeBy, resizeTo, screen, screenLeft, screenTop, screenX, screenY, scroll, scrollbars, scrollBy, scrollTo, scrollX, scrollY, self, status, statusbar, stop, toolbar, top, __dirname, __filename] + no-restricted-imports: [0] + no-restricted-syntax: [2, WithStatement, ForInStatement, LabeledStatement, SequenceExpression, {selector: "CallExpression[callee.name='fetch']", message: "use modules/fetch.ts instead"}] + no-return-assign: [0] + no-script-url: [2] + no-self-assign: [2, {props: true}] + no-self-compare: [2] + no-sequences: [2] + no-setter-return: [2] + no-shadow-restricted-names: [2] + no-shadow: [0] + no-sparse-arrays: [2] + no-template-curly-in-string: [2] + no-ternary: [0] + no-this-before-super: [2] + no-throw-literal: [2] + no-undef-init: [2] + no-undef: [2, {typeof: true}] # TODO: disable this rule after tsc passes + no-undefined: [0] + no-underscore-dangle: [0] + no-unexpected-multiline: [2] + no-unmodified-loop-condition: [2] + no-unneeded-ternary: [2] + no-unreachable-loop: [2] + no-unreachable: [2] + no-unsafe-finally: [2] + no-unsafe-negation: [2] + no-unused-expressions: [2] + no-unused-labels: [2] + no-unused-private-class-members: [2] + no-unused-vars: [0] # handled by @typescript-eslint/no-unused-vars + no-use-before-define: [2, {functions: false, classes: true, variables: true, allowNamedExports: true}] + no-use-extend-native/no-use-extend-native: [2] + no-useless-backreference: [2] + no-useless-call: [2] + no-useless-catch: [2] + no-useless-computed-key: [2] + no-useless-concat: [2] + no-useless-constructor: [2] + no-useless-escape: [2] + no-useless-rename: [2] + no-useless-return: [2] + no-var: [2] + no-void: [2] + no-warning-comments: [0] + no-with: [0] # handled by no-restricted-syntax + object-shorthand: [2, always] + one-var-declaration-per-line: [0] + one-var: [0] + operator-assignment: [2, always] + operator-linebreak: [2, after] + prefer-arrow-callback: [2, {allowNamedFunctions: true, allowUnboundThis: true}] + prefer-const: [2, {destructuring: all, ignoreReadBeforeAssign: true}] + prefer-destructuring: [0] + prefer-exponentiation-operator: [2] + prefer-named-capture-group: [0] + prefer-numeric-literals: [2] + prefer-object-has-own: [2] + prefer-object-spread: [2] + prefer-promise-reject-errors: [2, {allowEmptyReject: false}] + prefer-regex-literals: [2] + prefer-rest-params: [2] + prefer-spread: [2] + prefer-template: [2] + radix: [2, as-needed] + regexp/confusing-quantifier: [2] + regexp/control-character-escape: [2] + regexp/hexadecimal-escape: [0] + regexp/letter-case: [0] + regexp/match-any: [2] + regexp/negation: [2] + regexp/no-contradiction-with-assertion: [0] + regexp/no-control-character: [0] + regexp/no-dupe-characters-character-class: [2] + regexp/no-dupe-disjunctions: [2] + regexp/no-empty-alternative: [2] + regexp/no-empty-capturing-group: [2] + regexp/no-empty-character-class: [0] + regexp/no-empty-group: [2] + regexp/no-empty-lookarounds-assertion: [2] + regexp/no-empty-string-literal: [2] + regexp/no-escape-backspace: [2] + regexp/no-extra-lookaround-assertions: [0] + regexp/no-invalid-regexp: [2] + regexp/no-invisible-character: [2] + regexp/no-lazy-ends: [2] + regexp/no-legacy-features: [2] + regexp/no-misleading-capturing-group: [0] + regexp/no-misleading-unicode-character: [0] + regexp/no-missing-g-flag: [2] + regexp/no-non-standard-flag: [2] + regexp/no-obscure-range: [2] + regexp/no-octal: [2] + regexp/no-optional-assertion: [2] + regexp/no-potentially-useless-backreference: [2] + regexp/no-standalone-backslash: [2] + regexp/no-super-linear-backtracking: [0] + regexp/no-super-linear-move: [0] + regexp/no-trivially-nested-assertion: [2] + regexp/no-trivially-nested-quantifier: [2] + regexp/no-unused-capturing-group: [0] + regexp/no-useless-assertions: [2] + regexp/no-useless-backreference: [2] + regexp/no-useless-character-class: [2] + regexp/no-useless-dollar-replacements: [2] + regexp/no-useless-escape: [2] + regexp/no-useless-flag: [2] + regexp/no-useless-lazy: [2] + regexp/no-useless-non-capturing-group: [2] + regexp/no-useless-quantifier: [2] + regexp/no-useless-range: [2] + regexp/no-useless-set-operand: [2] + regexp/no-useless-string-literal: [2] + regexp/no-useless-two-nums-quantifier: [2] + regexp/no-zero-quantifier: [2] + regexp/optimal-lookaround-quantifier: [2] + regexp/optimal-quantifier-concatenation: [0] + regexp/prefer-character-class: [0] + regexp/prefer-d: [0] + regexp/prefer-escape-replacement-dollar-char: [0] + regexp/prefer-lookaround: [0] + regexp/prefer-named-backreference: [0] + regexp/prefer-named-capture-group: [0] + regexp/prefer-named-replacement: [0] + regexp/prefer-plus-quantifier: [2] + regexp/prefer-predefined-assertion: [2] + regexp/prefer-quantifier: [0] + regexp/prefer-question-quantifier: [2] + regexp/prefer-range: [2] + regexp/prefer-regexp-exec: [2] + regexp/prefer-regexp-test: [2] + regexp/prefer-result-array-groups: [0] + regexp/prefer-set-operation: [2] + regexp/prefer-star-quantifier: [2] + regexp/prefer-unicode-codepoint-escapes: [2] + regexp/prefer-w: [0] + regexp/require-unicode-regexp: [0] + regexp/simplify-set-operations: [2] + regexp/sort-alternatives: [0] + regexp/sort-character-class-elements: [0] + regexp/sort-flags: [0] + regexp/strict: [2] + regexp/unicode-escape: [0] + regexp/use-ignore-case: [0] + require-atomic-updates: [0] + require-await: [0] # handled by @typescript-eslint/require-await + require-unicode-regexp: [0] + require-yield: [2] + sonarjs/cognitive-complexity: [0] + sonarjs/elseif-without-else: [0] + sonarjs/max-switch-cases: [0] + sonarjs/no-all-duplicated-branches: [2] + sonarjs/no-collapsible-if: [0] + sonarjs/no-collection-size-mischeck: [2] + sonarjs/no-duplicate-string: [0] + sonarjs/no-duplicated-branches: [0] + sonarjs/no-element-overwrite: [2] + sonarjs/no-empty-collection: [2] + sonarjs/no-extra-arguments: [2] + sonarjs/no-gratuitous-expressions: [2] + sonarjs/no-identical-conditions: [2] + sonarjs/no-identical-expressions: [2] + sonarjs/no-identical-functions: [2, 5] + sonarjs/no-ignored-return: [2] + sonarjs/no-inverted-boolean-check: [2] + sonarjs/no-nested-switch: [0] + sonarjs/no-nested-template-literals: [0] + sonarjs/no-one-iteration-loop: [2] + sonarjs/no-redundant-boolean: [2] + sonarjs/no-redundant-jump: [2] + sonarjs/no-same-line-conditional: [2] + sonarjs/no-small-switch: [0] + sonarjs/no-unused-collection: [2] + sonarjs/no-use-of-empty-return-value: [2] + sonarjs/no-useless-catch: [2] + sonarjs/non-existent-operator: [2] + sonarjs/prefer-immediate-return: [0] + sonarjs/prefer-object-literal: [0] + sonarjs/prefer-single-boolean-return: [0] + sonarjs/prefer-while: [2] + sort-imports: [0] + sort-keys: [0] + sort-vars: [0] + strict: [0] + symbol-description: [2] + unicode-bom: [2, never] + unicorn/better-regex: [0] + unicorn/catch-error-name: [0] + unicorn/consistent-destructuring: [2] + unicorn/consistent-empty-array-spread: [2] + unicorn/consistent-existence-index-check: [0] + unicorn/consistent-function-scoping: [0] + unicorn/custom-error-definition: [0] + unicorn/empty-brace-spaces: [2] + unicorn/error-message: [0] + unicorn/escape-case: [0] + unicorn/expiring-todo-comments: [0] + unicorn/explicit-length-check: [0] + unicorn/filename-case: [0] + unicorn/import-index: [0] + unicorn/import-style: [0] + unicorn/new-for-builtins: [2] + unicorn/no-abusive-eslint-disable: [0] + unicorn/no-anonymous-default-export: [0] + unicorn/no-array-callback-reference: [0] + unicorn/no-array-for-each: [2] + unicorn/no-array-method-this-argument: [2] + unicorn/no-array-push-push: [2] + unicorn/no-array-reduce: [2] + unicorn/no-await-expression-member: [0] + unicorn/no-await-in-promise-methods: [2] + unicorn/no-console-spaces: [0] + unicorn/no-document-cookie: [2] + unicorn/no-empty-file: [2] + unicorn/no-for-loop: [0] + unicorn/no-hex-escape: [0] + unicorn/no-instanceof-array: [0] + unicorn/no-invalid-fetch-options: [2] + unicorn/no-invalid-remove-event-listener: [2] + unicorn/no-keyword-prefix: [0] + unicorn/no-length-as-slice-end: [2] + unicorn/no-lonely-if: [2] + unicorn/no-magic-array-flat-depth: [0] + unicorn/no-negated-condition: [0] + unicorn/no-negation-in-equality-check: [2] + unicorn/no-nested-ternary: [0] + unicorn/no-new-array: [0] + unicorn/no-new-buffer: [0] + unicorn/no-null: [0] + unicorn/no-object-as-default-parameter: [0] + unicorn/no-process-exit: [0] + unicorn/no-single-promise-in-promise-methods: [2] + unicorn/no-static-only-class: [2] + unicorn/no-thenable: [2] + unicorn/no-this-assignment: [2] + unicorn/no-typeof-undefined: [2] + unicorn/no-unnecessary-await: [2] + unicorn/no-unnecessary-polyfills: [2] + unicorn/no-unreadable-array-destructuring: [0] + unicorn/no-unreadable-iife: [2] + unicorn/no-unused-properties: [2] + unicorn/no-useless-fallback-in-spread: [2] + unicorn/no-useless-length-check: [2] + unicorn/no-useless-promise-resolve-reject: [2] + unicorn/no-useless-spread: [2] + unicorn/no-useless-switch-case: [2] + unicorn/no-useless-undefined: [0] + unicorn/no-zero-fractions: [2] + unicorn/number-literal-case: [0] + unicorn/numeric-separators-style: [0] + unicorn/prefer-add-event-listener: [2] + unicorn/prefer-array-find: [2] + unicorn/prefer-array-flat-map: [2] + unicorn/prefer-array-flat: [2] + unicorn/prefer-array-index-of: [2] + unicorn/prefer-array-some: [2] + unicorn/prefer-at: [0] + unicorn/prefer-blob-reading-methods: [2] + unicorn/prefer-code-point: [0] + unicorn/prefer-date-now: [2] + unicorn/prefer-default-parameters: [0] + unicorn/prefer-dom-node-append: [2] + unicorn/prefer-dom-node-dataset: [0] + unicorn/prefer-dom-node-remove: [2] + unicorn/prefer-dom-node-text-content: [2] + unicorn/prefer-event-target: [2] + unicorn/prefer-export-from: [0] + unicorn/prefer-global-this: [0] + unicorn/prefer-includes: [2] + unicorn/prefer-json-parse-buffer: [0] + unicorn/prefer-keyboard-event-key: [2] + unicorn/prefer-logical-operator-over-ternary: [2] + unicorn/prefer-math-min-max: [2] + unicorn/prefer-math-trunc: [2] + unicorn/prefer-modern-dom-apis: [0] + unicorn/prefer-modern-math-apis: [2] + unicorn/prefer-module: [2] + unicorn/prefer-native-coercion-functions: [2] + unicorn/prefer-negative-index: [2] + unicorn/prefer-node-protocol: [2] + unicorn/prefer-number-properties: [0] + unicorn/prefer-object-from-entries: [2] + unicorn/prefer-object-has-own: [0] + unicorn/prefer-optional-catch-binding: [2] + unicorn/prefer-prototype-methods: [0] + unicorn/prefer-query-selector: [2] + unicorn/prefer-reflect-apply: [0] + unicorn/prefer-regexp-test: [2] + unicorn/prefer-set-has: [0] + unicorn/prefer-set-size: [2] + unicorn/prefer-spread: [0] + unicorn/prefer-string-raw: [0] + unicorn/prefer-string-replace-all: [0] + unicorn/prefer-string-slice: [0] + unicorn/prefer-string-starts-ends-with: [2] + unicorn/prefer-string-trim-start-end: [2] + unicorn/prefer-structured-clone: [2] + unicorn/prefer-switch: [0] + unicorn/prefer-ternary: [0] + unicorn/prefer-text-content: [2] + unicorn/prefer-top-level-await: [0] + unicorn/prefer-type-error: [0] + unicorn/prevent-abbreviations: [0] + unicorn/relative-url-style: [2] + unicorn/require-array-join-separator: [2] + unicorn/require-number-to-fixed-digits-argument: [2] + unicorn/require-post-message-target-origin: [0] + unicorn/string-content: [0] + unicorn/switch-case-braces: [0] + unicorn/template-indent: [2] + unicorn/text-encoding-identifier-case: [0] + unicorn/throw-new-error: [2] + use-isnan: [2] + valid-typeof: [2, {requireStringLiterals: true}] + vars-on-top: [0] + wc/attach-shadow-constructor: [2] + wc/define-tag-after-class-definition: [0] + wc/expose-class-on-global: [0] + wc/file-name-matches-element: [2] + wc/guard-define-call: [0] + wc/guard-super-call: [2] + wc/max-elements-per-file: [0] + wc/no-child-traversal-in-attributechangedcallback: [2] + wc/no-child-traversal-in-connectedcallback: [2] + wc/no-closed-shadow-root: [2] + wc/no-constructor-attributes: [2] + wc/no-constructor-params: [2] + wc/no-constructor: [2] + wc/no-customized-built-in-elements: [2] + wc/no-exports-with-element: [0] + wc/no-invalid-element-name: [2] + wc/no-invalid-extends: [2] + wc/no-method-prefixed-with-on: [2] + wc/no-self-class: [2] + wc/no-typos: [2] + wc/require-listener-teardown: [2] + wc/tag-name-matches-class: [2] + yoda: [2, never] diff --git a/.gitea/issue_template.md b/.gitea/issue_template.md new file mode 100644 index 0000000000000..cf173a67cae53 --- /dev/null +++ b/.gitea/issue_template.md @@ -0,0 +1,42 @@ + + + + +- Gitea version (or commit ref): +- Git version: +- Operating system: + + + +- Database (use `[x]`): + - [ ] PostgreSQL + - [ ] MySQL + - [ ] MSSQL + - [ ] SQLite +- Can you reproduce the bug at https://demo.gitea.com: + - [ ] Yes (provide example URL) + - [ ] No +- Log gist: + + + + +## Description + + +... + + +## Screenshots + + diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000000000..1447a6ea322bf --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +open_collective: gitea diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 7207998ab093f..4e48bff5289d7 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -2,8 +2,9 @@ Please check the following: 1. Make sure you are targeting the `main` branch, pull requests on release branches are only allowed for backports. 2. Make sure you have read contributing guidelines: https://github.com/davidgraymi/bindersnap/blob/main/CONTRIBUTING.md . -3. Describe what your pull request does and which issue you're targeting (if any). -4. It is recommended to enable "Allow edits by maintainers", so maintainers can help more easily. -5. Your input here will be included in the commit message when this PR has been merged. If you don't want some content to be included, please separate them with a line like `---`. -6. Delete all these tips before posting. +3. For documentations contribution, please go to https://gitea.com/gitea/docs +4. Describe what your pull request does and which issue you're targeting (if any). +5. It is recommended to enable "Allow edits by maintainers", so maintainers can help more easily. +6. Your input here will be included in the commit message when this PR has been merged. If you don't want some content to be included, please separate them with a line like `---`. +7. Delete all these tips before posting. diff --git a/.github/workflows/pull-db-tests.yml b/.github/workflows/pull-db-tests.yml index 0b23de0a66c01..255552e3aaab1 100644 --- a/.github/workflows/pull-db-tests.yml +++ b/.github/workflows/pull-db-tests.yml @@ -202,12 +202,11 @@ 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 + # NOTE: mssql-2017 docker image will panic when run on hosts that have Ubuntu newer than 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/release-nightly.yml b/.github/workflows/release-nightly.yml index 2264c9e822d1c..2558a16a71692 100644 --- a/.github/workflows/release-nightly.yml +++ b/.github/workflows/release-nightly.yml @@ -59,6 +59,8 @@ jobs: aws s3 sync dist/release s3://${{ secrets.AWS_S3_BUCKET }}/gitea/${{ steps.clean_name.outputs.branch }} --no-progress nightly-docker-rootful: 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 @@ -85,17 +87,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: 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 @@ -122,6 +134,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 +149,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 a406602dc0a74..37b3ff57d2fc2 100644 --- a/.github/workflows/release-tag-rc.yml +++ b/.github/workflows/release-tag-rc.yml @@ -69,6 +69,8 @@ jobs: GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} docker-rootful: 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: 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 f67b76f40873b..4250623da0ffb 100644 --- a/.github/workflows/release-tag-version.yml +++ b/.github/workflows/release-tag-version.yml @@ -14,6 +14,8 @@ concurrency: jobs: binary: 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 @@ -71,6 +73,8 @@ jobs: GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} docker-rootful: 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,26 +85,34 @@ 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 }} @@ -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/CHANGELOG.md b/CHANGELOG.md index ab8c2ac2234c5..635b6534c428e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,583 @@ 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.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-03 + +* 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 + +## [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.4](https://github.com/go-gitea/gitea/releases/tag/v1.22.4) - 2024-11-14 * SECURITY diff --git a/Makefile b/Makefile index 32ea823e1e773..fca5350899865 100644 --- a/Makefile +++ b/Makefile @@ -109,7 +109,7 @@ 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/...) @@ -508,7 +508,7 @@ unit-test-coverage: 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) + $(MAKE) --no-print-directory $(GO_LICENSE_FILE) vendor: go.mod go.sum $(GO) mod vendor @@ -806,22 +806,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) diff --git a/README.md b/README.md index a9176249c917f..79d0e3b861df0 100644 --- a/README.md +++ b/README.md @@ -101,8 +101,8 @@ for the full license text. 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) | +|![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/assets/go-licenses.json b/assets/go-licenses.json index 42368c1f17ae4..8c808da8de157 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", @@ -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", @@ -524,16 +504,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", @@ -574,11 +544,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", @@ -655,8 +620,8 @@ "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/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" }, { @@ -784,11 +749,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", @@ -919,6 +879,11 @@ "path": "github.com/mitchellh/mapstructure/LICENSE", "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2013 Mitchell Hashimoto\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": "github.com/mmcloughlin/avo", + "path": "github.com/mmcloughlin/avo/LICENSE", + "licenseText": "BSD 3-Clause License\n\nCopyright (c) 2018, Michael McLoughlin\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/modern-go/concurrent", "path": "github.com/modern-go/concurrent/LICENSE", @@ -974,11 +939,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", @@ -987,7 +947,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", @@ -1069,21 +1029,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", @@ -1194,16 +1144,6 @@ "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", @@ -1264,6 +1204,11 @@ "path": "golang.org/x/time/rate/LICENSE", "licenseText": "Copyright 2009 The Go Authors.\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 LLC 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": "golang.org/x/tools", + "path": "golang.org/x/tools/LICENSE", + "licenseText": "Copyright 2009 The Go Authors.\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 LLC 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": "google.golang.org/genproto/googleapis/rpc/status", "path": "google.golang.org/genproto/googleapis/rpc/status/LICENSE", diff --git a/cmd/admin_user_create.go b/cmd/admin_user_create.go index 106d14b25a75f..37853cd31210d 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" @@ -61,6 +62,16 @@ var microcmdUserCreate = &cli.Command{ 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", @@ -69,6 +80,10 @@ var microcmdUserCreate = &cli.Command{ } func runCreateUser(c *cli.Context) 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 err := argsSet(c, "email"); err != nil { return err } @@ -158,23 +173,39 @@ func runCreateUser(c *cli.Context) error { IsRestricted: restricted, } - if err := user_model.CreateUser(ctx, u, &user_model.Meta{}, overwriteDefault); err != nil { - return fmt.Errorf("CreateUser: %w", err) + 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") } - if c.Bool("access-token") { - t := &auth_model.AccessToken{ - Name: "gitea-admin", - UID: u.ID, - } + // 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) + } + // 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 83754e97b10a4..9f109b888e235 100644 --- a/cmd/admin_user_create_test.go +++ b/cmd/admin_user_create_test.go @@ -8,37 +8,97 @@ 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(db.DefaultContext, &user_model.User{})) + require.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.EmailAddress{})) + require.NoError(t, db.TruncateBeans(db.DefaultContext, &auth_model.AccessToken{})) } + t.Run("MustChangePassword", func(t *testing.T) { + type check struct{ IsAdmin, MustChangePassword bool } + createCheck := func(name, args string) check { + 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 check{u.IsAdmin, u.MustChangePassword} + } + reset() + assert.Equal(t, check{IsAdmin: false, MustChangePassword: false}, createCheck("u", ""), "first non-admin user doesn't need to change password") - 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} + 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, args string) error { + return app.Run(strings.Fields(fmt.Sprintf("./gitea admin user create --username %s --email %s@gitea.local %s", name, name, 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") - - 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")) + + 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") + }) } diff --git a/cmd/admin_user_generate_access_token.go b/cmd/admin_user_generate_access_token.go index 6c2c10494ee5f..f6db7a74bd1ec 100644 --- a/cmd/admin_user_generate_access_token.go +++ b/cmd/admin_user_generate_access_token.go @@ -34,8 +34,8 @@ 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, @@ -43,7 +43,7 @@ var microcmdUserGenerateAccessToken = &cli.Command{ func runGenerateAccessToken(c *cli.Context) 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() @@ -77,6 +77,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/main.go b/cmd/main.go index d406a2ced7f84..0aebc05e32663 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -119,10 +119,10 @@ type AppVersion struct { func NewMainApp(appVer AppVersion) *cli.App { app := cli.NewApp() - app.Name = "LedgerHub" - app.HelpName = "ledgerhub" - app.Usage = "A painless, fully-managed document service" - app.Description = `LedgerHub 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.Name = "Bindersnap" + app.HelpName = "bsnap" + app.Usage = "A painless, fully managed document service" + app.Description = `Bindersnap 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 diff --git a/cmd/migrate.go b/cmd/migrate.go index 4e4dd45af3a15..459805a76d732 100644 --- a/cmd/migrate.go +++ b/cmd/migrate.go @@ -18,7 +18,7 @@ import ( 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, } diff --git a/cmd/web.go b/cmd/web.go index ef8a7426c14d9..f8217758e559e 100644 --- a/cmd/web.go +++ b/cmd/web.go @@ -12,6 +12,7 @@ import ( "path/filepath" "strconv" "strings" + "time" _ "net/http/pprof" // Used for debugging if enabled and a web server is running @@ -115,6 +116,16 @@ 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 { diff --git a/cmd/web_acme.go b/cmd/web_acme.go index 90e4a02764b67..172dde913b6c6 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 diff --git a/contrib/backport/backport.go b/contrib/backport/backport.go index eb194374452c5..645030a6fc28a 100644 --- a/contrib/backport/backport.go +++ b/contrib/backport/backport.go @@ -17,7 +17,7 @@ import ( "strings" "syscall" - "github.com/google/go-github/v61/github" + "github.com/google/go-github/v71/github" "github.com/urfave/cli/v2" "gopkg.in/yaml.v3" ) diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 315221aae9ae4..0e39dde445b16 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -774,6 +774,9 @@ LEVEL = Info ;ALLOW_ONLY_EXTERNAL_REGISTRATION = false ;; ;; User must sign in to view anything. +;; After 1.23.7, 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,10 +787,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 login methods. +;; 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. @@ -1339,9 +1345,6 @@ 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] @@ -1485,6 +1488,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 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/custom/templates/base/head_navbar.tmpl b/custom/templates/base/head_navbar.tmpl index fe3205f76cb43..fe0eb8687ea80 100644 --- a/custom/templates/base/head_navbar.tmpl +++ b/custom/templates/base/head_navbar.tmpl @@ -52,18 +52,18 @@ {{ctx.Locale.Tr "milestones"}} {{end}} {{end}} - + {{ctx.Locale.Tr "explore"}} {{else if .IsLandingPageOrganizations}} - + {{ctx.Locale.Tr "explore"}} {{else}} - + {{ctx.Locale.Tr "explore"}} {{end}} {{template "custom/extra_links" .}} {{if not .IsSigned}} - + {{ctx.Locale.Tr "help"}} {{end}} diff --git a/docker-compose.yml b/docker-compose.yml index dee550eb3c6ee..37fff10b4a935 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -43,11 +43,11 @@ services: # # until curl -s -X POST --cacert config/certs/ca/ca.crt -u "elastic:${ELASTIC_PASSWORD}" -H "Content-Type: application/json" https://es01:9200/_security/user/kibana_system/_password -d "{\"password\":\"${KIBANA_PASSWORD}\"}" | grep -q "^{}"; do sleep 10; done; # echo "All done!"; # ' - # healthcheck: - # test: ["CMD-SHELL", "[ -f config/certs/es01/es01.crt ]"] - # interval: 1s - # timeout: 5s - # retries: 120 + # healthcheck: + # test: ["CMD-SHELL", "[ -f config/certs/es01/es01.crt ]"] + # interval: 1s + # timeout: 5s + # retries: 120 es01: # depends_on: @@ -135,8 +135,8 @@ services: depends_on: es01: condition: service_healthy - build: - context: . + build: + context: . dockerfile: Dockerfile restart: always networks: @@ -152,8 +152,8 @@ services: - "222:22" volumes: -# certs: -# driver: local + # certs: + # driver: local esdata01: driver: local # esdata02: diff --git a/docker/manifest.rootless.tmpl b/docker/manifest.rootless.tmpl index 1ebf5b73c8477..3fa94ab0ec3e4 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 08ccf61b57854..c68ca46dd8695 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 dbb3bafd35245..48e7d4b2117a6 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 d9dbb3ebe0b3d..08587fc4f4dcf 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/go.mod b/go.mod index f9f8806064683..bc5ce83342c3b 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,6 @@ module code.gitea.io/gitea -go 1.23.4 - -toolchain go1.24.0 +go 1.23.8 // 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: @@ -26,11 +24,10 @@ require ( 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/go-ntlmssp v0.0.0-20221128193559-754e69321358 - github.com/ProtonMail/go-crypto v1.0.0 + github.com/ProtonMail/go-crypto v1.1.6 github.com/PuerkitoBio/goquery v1.10.0 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/alecthomas/chroma/v2 v2.15.0 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/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb @@ -58,19 +55,18 @@ require ( 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-git/go-billy/v5 v5.6.1 + github.com/go-git/go-git/v5 v5.13.1 github.com/go-ldap/ldap/v3 v3.4.8 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/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/uuid v1.6.0 @@ -84,7 +80,6 @@ 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/lib/pq v1.10.9 @@ -106,13 +101,13 @@ require ( github.com/pquerna/otp v1.4.0 github.com/prometheus/client_golang v1.20.5 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 @@ -124,14 +119,14 @@ require ( github.com/yuin/goldmark v1.7.8 github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc github.com/yuin/goldmark-meta v1.1.0 - golang.org/x/crypto v0.33.0 + golang.org/x/crypto v0.36.0 golang.org/x/image v0.21.0 - golang.org/x/net v0.35.0 - golang.org/x/oauth2 v0.23.0 - golang.org/x/sync v0.11.0 - golang.org/x/sys v0.30.0 - golang.org/x/text v0.22.0 - golang.org/x/tools v0.26.0 + golang.org/x/net v0.38.0 + golang.org/x/oauth2 v0.27.0 + golang.org/x/sync v0.12.0 + golang.org/x/sys v0.31.0 + golang.org/x/text v0.23.0 + golang.org/x/tools v0.29.0 google.golang.org/grpc v1.67.1 google.golang.org/protobuf v1.35.1 gopkg.in/ini.v1 v1.67.0 @@ -148,8 +143,6 @@ require ( 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 @@ -194,7 +187,7 @@ require ( github.com/couchbase/gomemcached v0.3.2 // 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.3.6 // 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 @@ -207,8 +200,6 @@ require ( 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-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 @@ -225,11 +216,11 @@ require ( 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/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/google/btree v1.1.3 // indirect @@ -265,6 +256,7 @@ require ( 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/mmcloughlin/avo v0.6.0 // 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 @@ -274,10 +266,9 @@ require ( github.com/oklog/ulid v1.3.1 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/onsi/ginkgo v1.16.5 // indirect - github.com/paulmach/orb v0.11.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/pjbgf/sha1cd v0.3.1 // 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 @@ -289,7 +280,6 @@ require ( 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 @@ -314,13 +304,11 @@ require ( 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.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/mod v0.22.0 // indirect golang.org/x/time v0.7.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect @@ -339,6 +327,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 ec6ee9b74f5eb..12977bc97dd68 100644 --- a/go.sum +++ b/go.sum @@ -14,12 +14,12 @@ 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/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= @@ -59,10 +59,6 @@ github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzS 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/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/Julusian/godocdown v0.0.0-20170816220326-6d19f8ff2df8/go.mod h1:INZr5t32rG59/5xeltqoCJoNY7e5x/3xoY9WSWVWg74= @@ -75,8 +71,8 @@ github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSC 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/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw= +github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= github.com/PuerkitoBio/goquery v1.10.0 h1:6fiXdLuUvYs2OJSvNRqlNPoBm6YABE226xrbavY5Wv4= github.com/PuerkitoBio/goquery v1.10.0/go.mod h1:TjZZl68Q3eGHNBA8CWaxAN7rOU1EbDz3CWuolcO5Yu4= github.com/RoaringBitmap/roaring v0.4.23/go.mod h1:D0gp8kJQgE1A4LQ5wFLggQEyvDi06Mq5mKs52e1TwOo= @@ -85,11 +81,11 @@ github.com/RoaringBitmap/roaring v1.9.4 h1:yhEIoH4YezLYT04s1nHehNO64EKFTop/wBhxv github.com/RoaringBitmap/roaring v1.9.4/go.mod h1:6AXUsoIEzDTFFQCe1RbGA6uFONMhvejWj5rqITANK90= 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.15.0 h1:LxXTQHFoYrstG2nnV9y2X5O94sOBzf0CIUpSTbpxvMc= +github.com/alecthomas/chroma/v2 v2.15.0/go.mod h1:gUhVLrPDXPtp/f+L1jo9xepo9gL4eLwRuGAunSZMkio= 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= @@ -109,8 +105,6 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPd 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= @@ -194,7 +188,6 @@ 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/caddyserver/zerossl v0.1.3 h1:onS+pxp3M8HnHpN5MMbOMyNjmTheJyWRaZYwn+YTAyA= @@ -211,7 +204,6 @@ 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/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= @@ -229,8 +221,8 @@ 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.3.6 h1:4d9N5ykBnSp5Xn2JkhocYDkOpURL/18CYMpo6xB9uWM= +github.com/cyphar/filepath-securejoin v0.3.6/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= @@ -239,8 +231,6 @@ github.com/davidgraymi/html-diff v0.1.0 h1:e0p/mtyeqcM1T58XWyzNbbPkDT1sjIKw4unVm github.com/davidgraymi/html-diff v0.1.0/go.mod h1:nd7dFtbKGBp+6RRr+1OsHNTN91pe5JN7jpkgE8Lf/xc= 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= @@ -264,8 +254,8 @@ github.com/dvyukov/go-fuzz v0.0.0-20210429054444-fca39067bc72/go.mod h1:11Gm+ccJ 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/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.2.3 h1:xwIyKHbaP5yfT6O9KIeYJR5549MXRQkoQMRXGztz8YQ= +github.com/elazarl/goproxy v1.2.3/go.mod h1:YfEbZtqP4AetfO6d40vWchF3znWX7C7Vd6ZMfdL8z64= 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= @@ -319,20 +309,16 @@ github.com/go-enry/go-enry/v2 v2.9.1 h1:G9iDteJ/Mc0F4Di5NeQknf83R2OkRbwY9cAYmcqV github.com/go-enry/go-enry/v2 v2.9.1/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.1 h1:u+dcrgaguSSkbjzHwelEjc0Yj300NUevrrPphk/SoRA= +github.com/go-git/go-billy/v5 v5.6.1/go.mod h1:0AsLr1z2+Uksi4NlElmMblP5rPcDZNRCD8ujZCRR2BE= 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.13.1 h1:DAQ9APonnlvSWpvolXWIuV6Q6zXy2wHbN4cVlNR5Q+M= +github.com/go-git/go-git/v5 v5.13.1/go.mod h1:qryJB4cSBoq3FRoBRf5A77joojuBcmPJ0qu3XXXVixc= 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= @@ -374,8 +360,6 @@ github.com/go-swagger/go-swagger v0.31.0/go.mod h1:WSigRRWEig8zV6t6Sm8Y+EmUjlzA/ 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= @@ -387,23 +371,23 @@ github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6Wezm 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/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= @@ -412,7 +396,6 @@ 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= @@ -429,10 +412,11 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw 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= @@ -499,22 +483,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= @@ -535,10 +503,6 @@ github.com/jessevdk/go-flags v1.6.1 h1:Cvu5U8UGrLay1rZfv/zP7iLpSHGUZ/Ou68T0iX1bB 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= @@ -550,13 +514,8 @@ 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/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= @@ -628,12 +587,13 @@ github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyua 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/mmcloughlin/avo v0.6.0 h1:QH6FU8SKoTLaVs80GA8TJuLNkUYl4VokHKlPhVDg4YY= +github.com/mmcloughlin/avo v0.6.0/go.mod h1:8CoAGaCSYXtCPR+8y18Y9aB/kxb8JSS6FRI7mSkvD+8= 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= @@ -674,9 +634,6 @@ github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3I github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= -github.com/paulmach/orb v0.11.1 h1:3koVegMC4X/WeiXYz9iswopaTwMem53NzTJuTF20JzU= -github.com/paulmach/orb v0.11.1/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU= -github.com/paulmach/protoscan v0.2.1/go.mod h1:SpcSwydNLrxUGSDvXvO0P7g7AuhJ7lcKfDlhJCDw2gY= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= @@ -684,8 +641,8 @@ github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG 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/pjbgf/sha1cd v0.3.1 h1:Dh2GYdpJnO84lIw0LJwTFXjcNbasP/bklicSznyAaPI= +github.com/pjbgf/sha1cd v0.3.1/go.mod h1:Y8t7jSB/dEI/lQE04A1HVKteqjj9bX5O4+Cex0TCu8s= 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= @@ -707,8 +664,8 @@ github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoG 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= @@ -739,8 +696,6 @@ github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6Ng 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= @@ -787,21 +742,19 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf 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.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= @@ -827,9 +780,6 @@ github.com/xanzy/go-gitlab v0.112.0 h1:6Z0cqEooCvBMfBIHw+CgO4AKGRV8na/9781xOb0+D 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= @@ -846,9 +796,7 @@ github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZ 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= @@ -867,13 +815,8 @@ github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l 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.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= @@ -890,16 +833,14 @@ 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.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= -golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= -golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= 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= @@ -913,8 +854,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 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.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= -golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= +golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= 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= @@ -926,34 +867,31 @@ 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.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= -golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= -golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= -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.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= +golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= +golang.org/x/oauth2 v0.27.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-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.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.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= -golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= +golang.org/x/sync v0.12.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= @@ -978,8 +916,6 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc 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= @@ -989,14 +925,12 @@ 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.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= -golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.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= @@ -1004,38 +938,34 @@ 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.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= -golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= -golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= +golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= +golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= 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.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= -golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= -golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= 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/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.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.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.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE= +golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588= 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= @@ -1050,8 +980,6 @@ google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQ 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/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/main_timezones.go b/main_timezones.go new file mode 100644 index 0000000000000..e1233007c62f2 --- /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 0bc66ba24e846..706eb2e43ad61 100644 --- a/models/actions/artifact.go +++ b/models/actions/artifact.go @@ -114,6 +114,12 @@ type FindArtifactsOptions struct { Status int } +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 { @@ -132,7 +138,7 @@ func (opts FindArtifactsOptions) ToConds() builder.Cond { 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 diff --git a/models/actions/run.go b/models/actions/run.go index a224a910ab59a..a268f760db66c 100644 --- a/models/actions/run.go +++ b/models/actions/run.go @@ -194,7 +194,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, @@ -204,14 +204,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. @@ -219,7 +221,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. @@ -238,27 +240,29 @@ 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, fmt.Errorf("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 @@ -275,7 +279,7 @@ func InsertRun(ctx context.Context, run *ActionRun, jobs []*jobparser.SingleWork return err } run.Index = index - run.Title = util.EllipsisDisplayString(run.Title, 255) + run.Title, _ = util.SplitStringAtByteN(run.Title, 255) if err := db.Insert(ctx, run); err != nil { return err @@ -308,7 +312,7 @@ func InsertRun(ctx context.Context, run *ActionRun, jobs []*jobparser.SingleWork } else { hasWaiting = true } - job.Name = util.EllipsisDisplayString(job.Name, 255) + job.Name, _ = util.SplitStringAtByteN(job.Name, 255) runJobs = append(runJobs, &ActionRunJob{ RunID: run.ID, RepoID: run.RepoID, @@ -402,7 +406,7 @@ func UpdateRun(ctx context.Context, run *ActionRun, cols ...string) error { if len(cols) > 0 { sess.Cols(cols...) } - run.Title = util.EllipsisDisplayString(run.Title, 255) + run.Title, _ = util.SplitStringAtByteN(run.Title, 255) affected, err := sess.Update(run) if err != nil { return err diff --git a/models/actions/runner.go b/models/actions/runner.go index 0d5464a5bef98..c4e5f9d1219f5 100644 --- a/models/actions/runner.go +++ b/models/actions/runner.go @@ -167,6 +167,7 @@ func init() { type FindRunnerOptions struct { db.ListOptions + IDs []int64 RepoID int64 OwnerID int64 // it will be ignored if RepoID is set Sort string @@ -178,6 +179,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 +261,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.EllipsisDisplayString(r.Name, 255) + r.Name, _ = util.SplitStringAtByteN(r.Name, 255) var err error if len(cols) == 0 { _, err = e.ID(r.ID).AllCols().Update(r) @@ -279,7 +288,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.EllipsisDisplayString(t.Name, 255) + t.Name, _ = util.SplitStringAtByteN(t.Name, 255) return db.Insert(ctx, t) } diff --git a/models/actions/runner_token.go b/models/actions/runner_token.go index bbd2af73b650b..1eab5efcce70e 100644 --- a/models/actions/runner_token.go +++ b/models/actions/runner_token.go @@ -10,6 +10,7 @@ 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/base" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" ) @@ -51,7 +52,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 "%s...": %w`, util.TruncateRunes(token, 3), util.ErrNotExist) + return nil, fmt.Errorf(`runner token "%s...": %w`, base.TruncateString(token, 3), util.ErrNotExist) } return &runnerToken, nil } diff --git a/models/actions/schedule.go b/models/actions/schedule.go index e2cc32eedca54..cb381dfd435bd 100644 --- a/models/actions/schedule.go +++ b/models/actions/schedule.go @@ -68,7 +68,7 @@ func CreateScheduleTask(ctx context.Context, rows []*ActionSchedule) error { // Loop through each schedule row for _, row := range rows { - row.Title = util.EllipsisDisplayString(row.Title, 255) + row.Title, _ = util.SplitStringAtByteN(row.Title, 255) // Create new schedule row if err = db.Insert(ctx, row); err != nil { return err @@ -120,21 +120,22 @@ func DeleteScheduleTaskByRepo(ctx context.Context, id int64) error { return committer.Commit() } -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/task.go b/models/actions/task.go index 9f13ff94c9e4a..af74faf937e5f 100644 --- a/models/actions/task.go +++ b/models/actions/task.go @@ -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.EllipsisDisplayString(v.String(), 255) + name, _ := util.SplitStringAtByteN(v.String(), 255) steps[i] = &ActionTaskStep{ Name: name, TaskID: task.ID, diff --git a/models/actions/variable.go b/models/actions/variable.go index d0f917d923280..163bb12c9360c 100644 --- a/models/actions/variable.go +++ b/models/actions/variable.go @@ -58,6 +58,7 @@ func InsertVariable(ctx context.Context, ownerID, repoID int64, name, data strin type FindVariablesOpts struct { db.ListOptions + IDs []int64 RepoID int64 OwnerID int64 // it will be ignored if RepoID is set Name string @@ -65,6 +66,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 +95,12 @@ 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) { + variable.Name = strings.ToUpper(variable.Name) + count, err := db.GetEngine(ctx). + ID(variable.ID). + Cols(cols...). + Update(variable) return count != 0, err } diff --git a/models/activities/action.go b/models/activities/action.go index 8304210188bed..7432e073b929d 100644 --- a/models/activities/action.go +++ b/models/activities/action.go @@ -20,12 +20,12 @@ 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/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 +72,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: @@ -226,7 +226,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 util.EllipsisDisplayString(a.GetActUserName(ctx), 20) + return base.EllipsisString(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. @@ -260,7 +260,7 @@ 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 util.EllipsisDisplayString(a.GetRepoUserName(ctx), 20) + return base.EllipsisString(a.GetRepoUserName(ctx), 20) } // GetRepoName returns the name of the action repository. @@ -275,7 +275,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 util.EllipsisDisplayString(a.GetRepoName(ctx), 33) + return base.EllipsisString(a.GetRepoName(ctx), 33) } // GetRepoPath returns the virtual path to the action repository. @@ -454,6 +454,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,7 +529,7 @@ func ActivityQueryCondition(ctx context.Context, opts GetFeedsOptions) (builder. } if opts.RequestedTeam != nil { - env := repo_model.AccessibleTeamReposEnv(ctx, organization.OrgFromUser(opts.RequestedUser), opts.RequestedTeam) + env := organization.OrgFromUser(opts.RequestedUser).AccessibleTeamReposEnv(ctx, opts.RequestedTeam) teamRepoIDs, err := env.RepoIDs(1, opts.RequestedUser.NumRepos) if err != nil { return nil, fmt.Errorf("GetTeamRepositories: %w", err) @@ -534,17 +552,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 } diff --git a/models/activities/action_list.go b/models/activities/action_list.go index 5f9acb8f2aa46..f7ea48f03e7ab 100644 --- a/models/activities/action_list.go +++ b/models/activities/action_list.go @@ -208,9 +208,31 @@ func GetFeeds(ctx context.Context, opts GetFeedsOptions) (ActionList, int64, err return nil, 0, fmt.Errorf("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) diff --git a/models/activities/user_heatmap_test.go b/models/activities/user_heatmap_test.go index 380045d3c5d54..a039fd3613252 100644 --- a/models/activities/user_heatmap_test.go +++ b/models/activities/user_heatmap_test.go @@ -64,9 +64,11 @@ func TestGetUserHeatmapDataByUser(t *testing.T) { for _, tc := range testCases { user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: tc.userID}) - var doer *user_model.User - if tc.doerID != 0 { - doer = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: tc.doerID}) + doer := &user_model.User{ID: tc.doerID} + _, err := unittest.LoadBeanIfExists(doer) + assert.NoError(t, err) + if tc.doerID == 0 { + doer = nil } // get the action for comparison diff --git a/models/admin/task.go b/models/admin/task.go index 10f8e6d57046d..0541a8ec78ebd 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 2e65d76612982..03bc82302f100 100644 --- a/models/asymkey/error.go +++ b/models/asymkey/error.go @@ -217,7 +217,6 @@ func (err ErrGPGKeyAccessDenied) Unwrap() error { // ErrKeyAccessDenied represents a "KeyAccessDenied" kind of error. type ErrKeyAccessDenied struct { UserID int64 - RepoID int64 KeyID int64 Note string } @@ -229,8 +228,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, repo_id: %d, key_id: %d, note: %s]", - err.UserID, err.RepoID, err.KeyID, err.Note) + 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) } func (err ErrKeyAccessDenied) Unwrap() error { diff --git a/models/asymkey/gpg_key.go b/models/asymkey/gpg_key.go index 5236b2d4500e4..e921340730bd6 100644 --- a/models/asymkey/gpg_key.go +++ b/models/asymkey/gpg_key.go @@ -13,8 +13,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" ) @@ -141,7 +141,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 +160,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)) diff --git a/models/asymkey/gpg_key_add.go b/models/asymkey/gpg_key_add.go index 11124b1366511..6c0f6e01a7cb5 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" ) // __________________ ________ ____ __. @@ -83,12 +83,12 @@ 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) diff --git a/models/asymkey/gpg_key_commit_verification.go b/models/asymkey/gpg_key_commit_verification.go index 26fad3bb3f211..9219a509df0e0 100644 --- a/models/asymkey/gpg_key_commit_verification.go +++ b/models/asymkey/gpg_key_commit_verification.go @@ -16,7 +16,7 @@ import ( "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" ) // __________________ ________ ____ __. diff --git a/models/asymkey/gpg_key_common.go b/models/asymkey/gpg_key_common.go index 28cb8f4e766ee..92c34a2569ece 100644 --- a/models/asymkey/gpg_key_common.go +++ b/models/asymkey/gpg_key_common.go @@ -13,9 +13,9 @@ import ( "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" ) // __________________ ________ ____ __. @@ -80,7 +80,7 @@ func base64DecPubKey(content string) (*packet.PublicKey, error) { 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 +88,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 diff --git a/models/asymkey/gpg_key_test.go b/models/asymkey/gpg_key_test.go index d3fbb01d82b1d..de463dfbe2777 100644 --- a/models/asymkey/gpg_key_test.go +++ b/models/asymkey/gpg_key_test.go @@ -13,7 +13,8 @@ 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" ) @@ -403,3 +404,25 @@ func TestTryGetKeyIDFromSignature(t *testing.T) { 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(db.DefaultContext, &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) + assert.NoError(t, err) + k, err := parseGPGKey(db.DefaultContext, 1, e, true) + assert.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(db.DefaultContext, 1, e, true) + assert.NoError(t, err) + assert.NotEmpty(t, k.KeyID) + assert.Empty(t, k.Emails) // the key is revoked, matches no email +} diff --git a/models/auth/access_token_scope.go b/models/auth/access_token_scope.go index 897ff3fc9ee3d..aa1cc5b9deba4 100644 --- a/models/auth/access_token_scope.go +++ b/models/auth/access_token_scope.go @@ -283,6 +283,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/source_test.go b/models/auth/source_test.go index 84aede0a6b055..36e76d5e281dd 100644 --- a/models/auth/source_test.go +++ b/models/auth/source_test.go @@ -13,8 +13,6 @@ import ( "code.gitea.io/gitea/modules/json" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "xorm.io/xorm" "xorm.io/xorm/schemas" ) @@ -56,8 +54,7 @@ func TestDumpAuthSource(t *testing.T) { sb := new(strings.Builder) - // TODO: this test is quite hacky, it should use a low-level "select" (without model processors) but not a database dump - engine := db.GetEngine(db.DefaultContext).(*xorm.Engine) - require.NoError(t, engine.DumpTables([]*schemas.Table{authSourceSchema}, sb)) + db.DumpTables([]*schemas.Table{authSourceSchema}, sb) + assert.Contains(t, sb.String(), `"Provider":"ConvertibleSourceName"`) } diff --git a/models/auth/webauthn_test.go b/models/auth/webauthn_test.go index 654427e9743aa..f1cf398adf558 100644 --- a/models/auth/webauthn_test.go +++ b/models/auth/webauthn_test.go @@ -44,7 +44,7 @@ func TestWebAuthnCredential_UpdateSignCount(t *testing.T) { cred := unittest.AssertExistsAndLoadBean(t, &auth_model.WebAuthnCredential{ID: 1}) cred.SignCount = 1 assert.NoError(t, cred.UpdateSignCount(db.DefaultContext)) - unittest.AssertExistsAndLoadBean(t, &auth_model.WebAuthnCredential{ID: 1, SignCount: 1}) + unittest.AssertExistsIf(t, true, &auth_model.WebAuthnCredential{ID: 1, SignCount: 1}) } func TestWebAuthnCredential_UpdateLargeCounter(t *testing.T) { @@ -52,7 +52,7 @@ func TestWebAuthnCredential_UpdateLargeCounter(t *testing.T) { cred := unittest.AssertExistsAndLoadBean(t, &auth_model.WebAuthnCredential{ID: 1}) cred.SignCount = 0xffffffff assert.NoError(t, cred.UpdateSignCount(db.DefaultContext)) - unittest.AssertExistsAndLoadBean(t, &auth_model.WebAuthnCredential{ID: 1, SignCount: 0xffffffff}) + unittest.AssertExistsIf(t, true, &auth_model.WebAuthnCredential{ID: 1, SignCount: 0xffffffff}) } func TestCreateCredential(t *testing.T) { @@ -63,5 +63,5 @@ func TestCreateCredential(t *testing.T) { assert.Equal(t, "WebAuthn Created Credential", res.Name) assert.Equal(t, []byte("Test"), res.CredentialID) - unittest.AssertExistsAndLoadBean(t, &auth_model.WebAuthnCredential{Name: "WebAuthn Created Credential", UserID: 1}) + unittest.AssertExistsIf(t, true, &auth_model.WebAuthnCredential{Name: "WebAuthn Created Credential", UserID: 1}) } diff --git a/models/db/collation.go b/models/db/collation.go index 79ade873803f2..a7db9f54423b9 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(xormEngine) + return CheckCollations(x) } func alterDatabaseCollation(x *xorm.Engine, collation string) error { diff --git a/models/db/context.go b/models/db/context.go index 51627712b1167..171e26b933f7c 100644 --- a/models/db/context.go +++ b/models/db/context.go @@ -94,7 +94,7 @@ func GetEngine(ctx context.Context) Engine { if e := getExistingEngine(ctx); e != nil { return e } - return xormEngine.Context(ctx) + return x.Context(ctx) } // getExistingEngine gets an existing db Engine/Statement from this context or returns nil @@ -155,7 +155,7 @@ func TxContext(parentCtx context.Context) (*Context, Committer, error) { return newContext(parentCtx, sess), &halfCommitter{committer: sess}, nil } - sess := xormEngine.NewSession() + sess := x.NewSession() if err := sess.Begin(); err != nil { _ = sess.Close() return nil, nil, err @@ -179,7 +179,7 @@ func WithTx(parentCtx context.Context, f func(ctx context.Context) error) error } func txWithNoCheck(parentCtx context.Context, f func(ctx context.Context) error) error { - sess := xormEngine.NewSession() + sess := x.NewSession() defer sess.Close() if err := sess.Begin(); err != nil { return err @@ -322,7 +322,7 @@ func CountByBean(ctx context.Context, bean any) (int64, error) { // TableName returns the table name according a bean object func TableName(bean any) string { - return xormEngine.TableName(bean) + return x.TableName(bean) } // InTransaction returns true if the engine is in a transaction otherwise return false diff --git a/models/db/convert.go b/models/db/convert.go index 80b0f7b04b4b4..8c124471aba25 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 xormEngine.Dialect().URI().DBType != schemas.MYSQL { + if x.Dialect().URI().DBType != schemas.MYSQL { return nil } - r, err := CheckCollations(xormEngine) + r, err := CheckCollations(x) if err != nil { return err } - _, err = xormEngine.Exec(fmt.Sprintf("ALTER DATABASE `%s` CHARACTER SET utf8mb4 COLLATE %s", setting.Database.Name, r.ExpectedCollation)) + _, err = x.Exec(fmt.Sprintf("ALTER DATABASE `%s` CHARACTER SET utf8mb4 COLLATE %s", setting.Database.Name, r.ExpectedCollation)) if err != nil { return err } - tables, err := xormEngine.DBMetas() + tables, err := x.DBMetas() if err != nil { return err } for _, table := range tables { - if _, err := xormEngine.Exec(fmt.Sprintf("ALTER TABLE `%s` ROW_FORMAT=dynamic", table.Name)); err != nil { + if _, err := x.Exec(fmt.Sprintf("ALTER TABLE `%s` ROW_FORMAT=dynamic", table.Name)); err != nil { return err } - if _, err := xormEngine.Exec(fmt.Sprintf("ALTER TABLE `%s` CONVERT TO CHARACTER SET utf8mb4 COLLATE %s", table.Name, r.ExpectedCollation)); err != nil { + if _, err := x.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 xormEngine.Dialect().URI().DBType != schemas.MSSQL { + if x.Dialect().URI().DBType != schemas.MSSQL { return nil } - sess := xormEngine.NewSession() + sess := x.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 91015f7038467..b17188945a458 100755 --- a/models/db/engine.go +++ b/models/db/engine.go @@ -8,10 +8,17 @@ 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 @@ -20,9 +27,9 @@ import ( ) var ( - xormEngine *xorm.Engine - registeredModels []any - registeredInitFuncs []func() error + x *xorm.Engine + tables []any + initFuncs []func() error ) // Engine represents a xorm engine or session. @@ -63,38 +70,167 @@ type Engine interface { // TableInfo returns table's information via an object func TableInfo(v any) (*schemas.Table, error) { - return xormEngine.TableInfo(v) + return x.TableInfo(v) +} + +// 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 initFuncs provided, it will be invoked after data model sync +// RegisterModel registers model, if initfunc provided, it will be invoked after data model sync func RegisterModel(bean any, initFunc ...func() error) { - registeredModels = append(registeredModels, bean) - if len(registeredInitFuncs) > 0 && initFunc[0] != nil { - registeredInitFuncs = append(registeredInitFuncs, initFunc[0]) + tables = append(tables, bean) + if len(initFuncs) > 0 && initFunc[0] != nil { + initFuncs = append(initFuncs, 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 := xormEngine.StoreEngine("InnoDB").SyncWithOptions(xorm.SyncOptions{ + _, err := x.StoreEngine("InnoDB").SyncWithOptions(xorm.SyncOptions{ WarnIfDatabaseColumnMissed: true, - }, registeredModels...) + }, tables...) 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, registeredModels...) + beans = append(beans, tables...) return beans, nil } // Need to map provided names to beans... beanMap := make(map[string]any) - for _, bean := range registeredModels { + for _, bean := range tables { beanMap[strings.ToLower(reflect.Indirect(reflect.ValueOf(bean)).Type().Name())] = bean - beanMap[strings.ToLower(xormEngine.TableName(bean))] = bean - beanMap[strings.ToLower(xormEngine.TableName(bean, true))] = bean + beanMap[strings.ToLower(x.TableName(bean))] = bean + beanMap[strings.ToLower(x.TableName(bean, true))] = bean } gotBean := make(map[any]bool) @@ -111,9 +247,36 @@ 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 := xormEngine.TableInfo(bean) + t, err := x.TableInfo(bean) if err != nil { return 50 } @@ -122,18 +285,18 @@ func MaxBatchInsertSize(bean any) int { // IsTableNotEmpty returns true if table has at least one record func IsTableNotEmpty(beanOrTableName any) (bool, error) { - return xormEngine.Table(beanOrTableName).Exist() + return x.Table(beanOrTableName).Exist() } // DeleteAllRecords will delete all the records of this table func DeleteAllRecords(tableName string) error { - _, err := xormEngine.Exec(fmt.Sprintf("DELETE FROM %s", tableName)) + _, err := x.Exec(fmt.Sprintf("DELETE FROM %s", tableName)) return err } // GetMaxID will return max id of the table func GetMaxID(beanOrTableName any) (maxID int64, err error) { - _, err = xormEngine.Select("MAX(id)").Table(beanOrTableName).Get(&maxID) + _, err = x.Select("MAX(id)").Table(beanOrTableName).Get(&maxID) return maxID, err } @@ -145,3 +308,24 @@ func SetLogSQL(ctx context.Context, on bool) { sess.Engine().ShowSQL(on) } } + +type SlowQueryHook struct { + Threshold time.Duration + Logger log.Logger +} + +var _ contexts.Hook = &SlowQueryHook{} + +func (SlowQueryHook) BeforeProcess(c *contexts.ContextHook) (context.Context, error) { + return c.Ctx, nil +} + +func (h *SlowQueryHook) AfterProcess(c *contexts.ContextHook) error { + if c.ExecuteTime >= h.Threshold { + // 8 is the amount of skips passed to runtime.Caller, so that in the log the correct function + // is being displayed (the function that ultimately wants to execute the query in the code) + // instead of the function of the slow query hook being called. + h.Logger.Log(8, log.WARN, "[Slow SQL Query] %s %v - %v", c.SQL, c.Args, c.ExecuteTime) + } + return nil +} diff --git a/models/db/engine_dump.go b/models/db/engine_dump.go deleted file mode 100644 index 63f2d4e093cbf..0000000000000 --- a/models/db/engine_dump.go +++ /dev/null @@ -1,33 +0,0 @@ -// 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 deleted file mode 100644 index b4c543c3dd824..0000000000000 --- a/models/db/engine_hook.go +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright 2024 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package db - -import ( - "context" - "time" - - "code.gitea.io/gitea/modules/log" - - "xorm.io/xorm/contexts" -) - -type SlowQueryHook struct { - Threshold time.Duration - Logger log.Logger -} - -var _ contexts.Hook = (*SlowQueryHook)(nil) - -func (*SlowQueryHook) BeforeProcess(c *contexts.ContextHook) (context.Context, error) { - return c.Ctx, nil -} - -func (h *SlowQueryHook) AfterProcess(c *contexts.ContextHook) error { - if c.ExecuteTime >= h.Threshold { - // 8 is the amount of skips passed to runtime.Caller, so that in the log the correct function - // is being displayed (the function that ultimately wants to execute the query in the code) - // instead of the function of the slow query hook being called. - h.Logger.Log(8, log.WARN, "[Slow SQL Query] %s %v - %v", c.SQL, c.Args, c.ExecuteTime) - } - return nil -} diff --git a/models/db/engine_init.go b/models/db/engine_init.go deleted file mode 100644 index da85018957b8b..0000000000000 --- a/models/db/engine_init.go +++ /dev/null @@ -1,140 +0,0 @@ -// Copyright 2024 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package db - -import ( - "context" - "fmt" - "strings" - - "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/setting" - - "xorm.io/xorm" - "xorm.io/xorm/names" -) - -func init() { - gonicNames := []string{"SSL", "UID"} - for _, name := range gonicNames { - names.LintGonicMapper[name] = true - } -} - -// newXORMEngine returns a new XORM engine from the configuration -func newXORMEngine() (*xorm.Engine, error) { - connStr, err := setting.DBConnStr() - if err != nil { - return nil, err - } - - var engine *xorm.Engine - - if setting.Database.Type.IsPostgreSQL() && len(setting.Database.Schema) > 0 { - // OK whilst we sort out our schema issues - create a schema aware postgres - registerPostgresSchemaDriver() - engine, err = xorm.NewEngine("postgresschema", connStr) - } else { - engine, err = xorm.NewEngine(setting.Database.Type.String(), connStr) - } - - if err != nil { - return nil, err - } - if setting.Database.Type == "mysql" { - engine.Dialect().SetParams(map[string]string{"rowFormat": "DYNAMIC"}) - } else if setting.Database.Type == "mssql" { - engine.Dialect().SetParams(map[string]string{"DEFAULT_VARCHAR": "nvarchar"}) - } - engine.SetSchema(setting.Database.Schema) - return engine, nil -} - -// InitEngine initializes the xorm.Engine and sets it as db.DefaultContext -func InitEngine(ctx context.Context) error { - xe, err := newXORMEngine() - if err != nil { - if strings.Contains(err.Error(), "SQLite3 support") { - return fmt.Errorf(`sqlite3 requires: -tags sqlite,sqlite_unlock_notify%s%w`, "\n", err) - } - return fmt.Errorf("failed to connect to database: %w", err) - } - - xe.SetMapper(names.GonicMapper{}) - // WARNING: for serv command, MUST remove the output to os.stdout, - // so use log file to instead print to stdout. - xe.SetLogger(NewXORMLogger(setting.Database.LogSQL)) - xe.ShowSQL(setting.Database.LogSQL) - xe.SetMaxOpenConns(setting.Database.MaxOpenConns) - xe.SetMaxIdleConns(setting.Database.MaxIdleConns) - xe.SetConnMaxLifetime(setting.Database.ConnMaxLifetime) - xe.SetDefaultContext(ctx) - - if setting.Database.SlowQueryThreshold > 0 { - xe.AddHook(&SlowQueryHook{ - Threshold: setting.Database.SlowQueryThreshold, - Logger: log.GetLogger("xorm"), - }) - } - - SetDefaultEngine(ctx, xe) - return nil -} - -// SetDefaultEngine sets the default engine for db -func SetDefaultEngine(ctx context.Context, eng *xorm.Engine) { - xormEngine = eng - DefaultContext = &Context{Context: ctx, engine: xormEngine} -} - -// UnsetDefaultEngine closes and unsets the default engine -// We hope the SetDefaultEngine and UnsetDefaultEngine can be paired, but it's impossible now, -// there are many calls to InitEngine -> SetDefaultEngine directly to overwrite the `xormEngine` and DefaultContext without close -// Global database engine related functions are all racy and there is no graceful close right now. -func UnsetDefaultEngine() { - if xormEngine != nil { - _ = xormEngine.Close() - xormEngine = nil - } - DefaultContext = nil -} - -// InitEngineWithMigration initializes a new xorm.Engine and sets it as the db.DefaultContext -// This function must never call .Sync() if the provided migration function fails. -// When called from the "doctor" command, the migration function is a version check -// that prevents the doctor from fixing anything in the database if the migration level -// is different from the expected value. -func InitEngineWithMigration(ctx context.Context, migrateFunc func(*xorm.Engine) error) (err error) { - if err = InitEngine(ctx); err != nil { - return err - } - - if err = xormEngine.Ping(); err != nil { - return err - } - - preprocessDatabaseCollation(xormEngine) - - // We have to run migrateFunc here in case the user is re-running installation on a previously created DB. - // If we do not then table schemas will be changed and there will be conflicts when the migrations run properly. - // - // Installation should only be being re-run if users want to recover an old database. - // However, we should think carefully about should we support re-install on an installed instance, - // as there may be other problems due to secret reinitialization. - if err = migrateFunc(xormEngine); err != nil { - return fmt.Errorf("migrate: %w", err) - } - - if err = SyncAllTables(); err != nil { - return fmt.Errorf("sync database struct error: %w", err) - } - - for _, initFunc := range registeredInitFuncs { - if err := initFunc(); err != nil { - return fmt.Errorf("initFunc failed: %w", err) - } - } - - return nil -} diff --git a/models/db/name.go b/models/db/name.go index 55c9dffb6ab28..51be33a8bcb73 100644 --- a/models/db/name.go +++ b/models/db/name.go @@ -16,7 +16,7 @@ var ( // ErrNameEmpty name is empty error ErrNameEmpty = util.SilentWrap{Message: "name is empty", Err: util.ErrInvalidArgument} - // AlphaDashDotPattern characters prohibited in a username (anything except A-Za-z0-9_.-) + // AlphaDashDotPattern characters prohibited in a user name (anything except A-Za-z0-9_.-) AlphaDashDotPattern = regexp.MustCompile(`[^\w-\.]`) ) diff --git a/models/db/sequence.go b/models/db/sequence.go index 9adc5113acc52..f49ad935de099 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 := xormEngine.NewSession() + sess := x.NewSession() defer sess.Close() var sequences []string - schema := xormEngine.Dialect().URI().Schema + schema := x.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 := xormEngine.NewSession() + sess := x.NewSession() defer sess.Close() if err := sess.Begin(); err != nil { return err diff --git a/models/error.go b/models/error.go new file mode 100644 index 0000000000000..75c53245de5ae --- /dev/null +++ b/models/error.go @@ -0,0 +1,552 @@ +// 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/fixtures/action_run_job.yml b/models/fixtures/action_run_job.yml index 9b6f5b9a887b4..8837e6ec2d80d 100644 --- a/models/fixtures/action_run_job.yml +++ b/models/fixtures/action_run_job.yml @@ -64,7 +64,7 @@ name: job2 attempt: 1 job_id: job2 - needs: [job1] + needs: '["job1"]' task_id: 51 status: 5 started: 1683636528 diff --git a/models/fixtures/protected_tag.yml b/models/fixtures/protected_tag.yml index dbec52c0c23c9..1944e7bd84ca4 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/git/branch.go b/models/git/branch.go index e683ce47e657e..9ac6c45578f7c 100644 --- a/models/git/branch.go +++ b/models/git/branch.go @@ -167,9 +167,24 @@ 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 } +// 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)) @@ -440,6 +455,8 @@ type FindRecentlyPushedNewBranchesOptions struct { } type RecentlyPushedNewBranch struct { + BranchRepo *repo_model.Repository + BranchName string BranchDisplayName string BranchLink string BranchCompareURL string @@ -540,7 +557,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/issues/comment.go b/models/issues/comment.go index e4537aa872e9f..729fc1b9e54ec 100644 --- a/models/issues/comment.go +++ b/models/issues/comment.go @@ -197,6 +197,20 @@ func (t CommentType) HasMailReplySupport() bool { return false } +func (t CommentType) CountedAsConversation() bool { + for _, ct := range ConversationCountedCommentType() { + if t == ct { + return true + } + } + return false +} + +// 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 @@ -893,7 +907,7 @@ 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 @@ -1182,8 +1196,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 } } @@ -1300,6 +1314,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 { @@ -1332,8 +1361,7 @@ func InsertIssueComments(ctx context.Context, comments []*Comment) error { } 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 { + if err := UpdateIssueNumComments(ctx, issueID); err != nil { return err } } diff --git a/models/issues/comment_code.go b/models/issues/comment_code.go index 67a77ceb13f36..b562aab5005f6 100644 --- a/models/issues/comment_code.go +++ b/models/issues/comment_code.go @@ -86,8 +86,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 diff --git a/models/issues/comment_test.go b/models/issues/comment_test.go index d81f33f953d0e..cb31a21f92105 100644 --- a/models/issues/comment_test.go +++ b/models/issues/comment_test.go @@ -97,3 +97,12 @@ func TestMigrate_InsertIssueComments(t *testing.T) { 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(db.DefaultContext, issue2.ID)) + issue2 = unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2}) + assert.EqualValues(t, 1, issue2.NumComments) +} diff --git a/models/issues/dependency_test.go b/models/issues/dependency_test.go index 67418039ded5e..6eed483cc9be7 100644 --- a/models/issues/dependency_test.go +++ b/models/issues/dependency_test.go @@ -49,13 +49,9 @@ func TestCreateIssueDependency(t *testing.T) { assert.False(t, left) // Close #2 and check again - _, err = issues_model.CloseIssue(db.DefaultContext, issue2, user1) + _, err = issues_model.ChangeIssueStatus(db.DefaultContext, issue2, user1, true) assert.NoError(t, err) - issue2Closed, err := issues_model.GetIssueByID(db.DefaultContext, 2) - assert.NoError(t, err) - assert.True(t, issue2Closed.IsClosed) - left, err = issues_model.IssueNoDependenciesLeft(db.DefaultContext, issue1) assert.NoError(t, err) assert.True(t, left) @@ -63,11 +59,4 @@ func TestCreateIssueDependency(t *testing.T) { // Test removing the dependency err = issues_model.RemoveIssueDependency(db.DefaultContext, user1, issue1, issue2, issues_model.DependencyTypeBlockedBy) assert.NoError(t, err) - - _, err = issues_model.ReopenIssue(db.DefaultContext, issue2, user1) - assert.NoError(t, err) - - issue2Reopened, err := issues_model.GetIssueByID(db.DefaultContext, 2) - assert.NoError(t, err) - assert.False(t, issue2Reopened.IsClosed) } diff --git a/models/issues/issue.go b/models/issues/issue.go index fe347c271560e..f4b575d80470d 100644 --- a/models/issues/issue.go +++ b/models/issues/issue.go @@ -17,6 +17,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" @@ -531,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) diff --git a/models/issues/issue_project.go b/models/issues/issue_project.go index c4515fd898595..01852447834c7 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, 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 ip.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 f1cd125d495c4..694b918755dda 100644 --- a/models/issues/issue_search.go +++ b/models/issues/issue_search.go @@ -49,9 +49,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. @@ -273,8 +273,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())) } } @@ -321,20 +325,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 ), ) } diff --git a/models/issues/issue_update.go b/models/issues/issue_update.go index 2a6f3603bc5d4..5b929c9115b32 100644 --- a/models/issues/issue_update.go +++ b/models/issues/issue_update.go @@ -119,8 +119,8 @@ func doChangeIssueStatus(ctx context.Context, issue *Issue, doer *user_model.Use }) } -// CloseIssue changes issue status to closed. -func CloseIssue(ctx context.Context, issue *Issue, doer *user_model.User) (*Comment, error) { +// ChangeIssueStatus changes issue status to open or closed. +func ChangeIssueStatus(ctx context.Context, issue *Issue, doer *user_model.User, isClosed bool) (*Comment, error) { if err := issue.LoadRepo(ctx); err != nil { return nil, err } @@ -128,45 +128,7 @@ func CloseIssue(ctx context.Context, issue *Issue, doer *user_model.User) (*Comm return nil, err } - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return nil, err - } - defer committer.Close() - - comment, err := changeIssueStatus(ctx, issue, doer, true, false) - if err != nil { - return nil, err - } - if err := committer.Commit(); err != nil { - return nil, err - } - return comment, nil -} - -// 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 - } - - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return nil, err - } - defer committer.Close() - - comment, err := changeIssueStatus(ctx, issue, doer, false, false) - if err != nil { - return nil, err - } - if err := committer.Commit(); err != nil { - return nil, err - } - return comment, nil + return changeIssueStatus(ctx, issue, doer, isClosed, false) } // ChangeIssueTitle changes the title of this issue, as the given user. @@ -177,7 +139,7 @@ func ChangeIssueTitle(ctx context.Context, issue *Issue, doer *user_model.User, } defer committer.Close() - issue.Title = util.EllipsisDisplayString(issue.Title, 255) + issue.Title, _ = util.SplitStringAtByteN(issue.Title, 255) if err = UpdateIssueCols(ctx, issue, "name"); err != nil { return fmt.Errorf("updateIssueCols: %w", err) } @@ -440,7 +402,7 @@ func NewIssue(ctx context.Context, repo *repo_model.Repository, issue *Issue, la } issue.Index = idx - issue.Title = util.EllipsisDisplayString(issue.Title, 255) + issue.Title, _ = util.SplitStringAtByteN(issue.Title, 255) if err = NewIssueWithIndex(ctx, issue.Poster, NewIssueOptions{ Repo: repo, diff --git a/models/issues/issue_user_test.go b/models/issues/issue_user_test.go index 7c21aa15eef6a..ce47adb53a0b5 100644 --- a/models/issues/issue_user_test.go +++ b/models/issues/issue_user_test.go @@ -12,7 +12,6 @@ import ( "code.gitea.io/gitea/models/unittest" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) func Test_NewIssueUsers(t *testing.T) { @@ -28,8 +27,9 @@ func Test_NewIssueUsers(t *testing.T) { } // artificially insert new issue - require.NoError(t, db.Insert(db.DefaultContext, newIssue)) - require.NoError(t, issues_model.NewIssueUsers(db.DefaultContext, repo, newIssue)) + unittest.AssertSuccessfulInsert(t, newIssue) + + assert.NoError(t, issues_model.NewIssueUsers(db.DefaultContext, repo, newIssue)) // issue_user table should now have entries for new issue unittest.AssertExistsAndLoadBean(t, &issues_model.IssueUser{IssueID: newIssue.ID, UID: newIssue.PosterID}) diff --git a/models/issues/issue_xref_test.go b/models/issues/issue_xref_test.go index 7f257330b769e..f1b1bb2a6b1b3 100644 --- a/models/issues/issue_xref_test.go +++ b/models/issues/issue_xref_test.go @@ -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.CloseIssue(db.DefaultContext, i3, d) + _, err := issues_model.ChangeIssueStatus(db.DefaultContext, i3, d, true) assert.NoError(t, err) pr := testCreatePR(t, 1, 2, "titlepr", fmt.Sprintf("closes #%d", i1.Index)) diff --git a/models/issues/label_test.go b/models/issues/label_test.go index 185fa11bbcf14..1d4b6f4684c4c 100644 --- a/models/issues/label_test.go +++ b/models/issues/label_test.go @@ -387,7 +387,7 @@ func TestDeleteIssueLabel(t *testing.T) { expectedNumIssues := label.NumIssues expectedNumClosedIssues := label.NumClosedIssues - if unittest.GetBean(t, &issues_model.IssueLabel{IssueID: issueID, LabelID: labelID}) != nil { + if unittest.BeanExists(t, &issues_model.IssueLabel{IssueID: issueID, LabelID: labelID}) { expectedNumIssues-- if issue.IsClosed { expectedNumClosedIssues-- diff --git a/models/issues/pull.go b/models/issues/pull.go index efb24e89848d1..319ead5dbd32c 100644 --- a/models/issues/pull.go +++ b/models/issues/pull.go @@ -301,7 +301,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 +320,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 } @@ -572,7 +572,7 @@ func NewPullRequest(ctx context.Context, repo *repo_model.Repository, issue *Iss } issue.Index = idx - issue.Title = util.EllipsisDisplayString(issue.Title, 255) + issue.Title, _ = util.SplitStringAtByteN(issue.Title, 255) if err = NewIssueWithIndex(ctx, issue.Poster, NewIssueOptions{ Repo: repo, diff --git a/models/issues/review.go b/models/issues/review.go index 8b345e5fd8002..d8dc539c3b384 100644 --- a/models/issues/review.go +++ b/models/issues/review.go @@ -639,6 +639,10 @@ func InsertReviews(ctx context.Context, reviews []*Review) error { return err } } + + if err := UpdateIssueNumComments(ctx, review.IssueID); err != nil { + return err + } } return committer.Commit() @@ -659,7 +663,7 @@ func AddReviewRequest(ctx context.Context, issue *Issue, reviewer, doer *user_mo } if review != nil { - // skip it when reviewer hase been request to review + // skip it when reviewer has 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. } diff --git a/models/issues/review_list.go b/models/issues/review_list.go index bc7d7ec0f0168..928f24fb2dcd7 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" @@ -153,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) { +// GetReviewsByIssueID gets the latest review of each reviewer for a pull request +// 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) - // 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 + // 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 } - return reviews, nil -} - -// GetReviewsByIssueID gets the latest review of each reviewer for a pull request -func GetReviewsByIssueID(ctx context.Context, issueID int64) (ReviewList, error) { - reviews := make([]*Review, 0, 10) - - sess := db.GetEngine(ctx) + // 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) + } + } - // 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 + 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 + }) - 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 + 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 + }) - if len(teamReviewRequests) > 0 { - reviews = append(reviews, teamReviewRequests...) + teamReviewRequests := make([]*Review, 0, 5) + 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 + }) - return reviews, nil + return append(individualReviews, teamReviewRequests...), originalReviews, nil } diff --git a/models/issues/review_test.go b/models/issues/review_test.go index 50330e3ff2c60..2588b8ba41b05 100644 --- a/models/issues/review_test.go +++ b/models/issues/review_test.go @@ -162,8 +162,9 @@ func TestGetReviewersByIssueID(t *testing.T) { }, ) - allReviews, err := issues_model.GetReviewsByIssueID(db.DefaultContext, issue.ID) + allReviews, migratedReviews, err := issues_model.GetReviewsByIssueID(db.DefaultContext, issue.ID) assert.NoError(t, err) + assert.Empty(t, migratedReviews) for _, review := range allReviews { assert.NoError(t, review.LoadReviewer(db.DefaultContext)) } diff --git a/models/issues/stopwatch.go b/models/issues/stopwatch.go index 629af95b5774f..baffe04ace811 100644 --- a/models/issues/stopwatch.go +++ b/models/issues/stopwatch.go @@ -48,7 +48,7 @@ func (s Stopwatch) Seconds() int64 { // Duration returns a human-readable duration string based on local server time func (s Stopwatch) Duration() string { - return util.SecToTime(s.Seconds()) + return util.SecToHours(s.Seconds()) } func getStopwatch(ctx context.Context, userID, issueID int64) (sw *Stopwatch, exists bool, err error) { @@ -201,7 +201,7 @@ func FinishIssueStopwatch(ctx context.Context, user *user_model.User, issue *Iss Doer: user, Issue: issue, Repo: issue.Repo, - Content: util.SecToTime(timediff), + Content: util.SecToHours(timediff), Type: CommentTypeStopTracking, TimeID: tt.ID, }); err != nil { diff --git a/models/migrations/base/tests.go b/models/migrations/base/tests.go index 2eb85cd8a7442..c2134f702a51a 100644 --- a/models/migrations/base/tests.go +++ b/models/migrations/base/tests.go @@ -13,9 +13,9 @@ 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/test" "code.gitea.io/gitea/modules/testlogger" "github.com/stretchr/testify/require" @@ -92,7 +92,10 @@ func PrepareTestEnv(t *testing.T, skip int, syncModels ...any) (*xorm.Engine, fu func MainTest(m *testing.M) { testlogger.Init() - giteaRoot := test.SetupGiteaRoot() + giteaRoot := base.SetupGiteaRoot() + if giteaRoot == "" { + testlogger.Fatalf("Environment variable $GITEA_ROOT not set\n") + } giteaBinary := "gitea" if runtime.GOOS == "windows" { giteaBinary += ".exe" diff --git a/models/migrations/v1_23/v299.go b/models/migrations/v1_23/v299.go index f6db960c3b41b..e5fde3749b6ce 100644 --- a/models/migrations/v1_23/v299.go +++ b/models/migrations/v1_23/v299.go @@ -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 f1f1cccdbfb5e..51de43da5e6da 100644 --- a/models/migrations/v1_23/v300.go +++ b/models/migrations/v1_23/v300.go @@ -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 b7797f6c6b5eb..99c8e3d8eac2f 100644 --- a/models/migrations/v1_23/v301.go +++ b/models/migrations/v1_23/v301.go @@ -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/v303.go b/models/migrations/v1_23/v303.go index adfe917d3f241..1e3638893021b 100644 --- a/models/migrations/v1_23/v303.go +++ b/models/migrations/v1_23/v303.go @@ -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/v306.go b/models/migrations/v1_23/v306.go index 276b438e95bcf..a1e698fe31f03 100644 --- a/models/migrations/v1_23/v306.go +++ b/models/migrations/v1_23/v306.go @@ -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/v310.go b/models/migrations/v1_23/v310.go index 394417f5a01b0..c856a708f9175 100644 --- a/models/migrations/v1_23/v310.go +++ b/models/migrations/v1_23/v310.go @@ -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 index 0fc1ac8c0e85c..21293d83be046 100644 --- a/models/migrations/v1_23/v311.go +++ b/models/migrations/v1_23/v311.go @@ -11,6 +11,9 @@ func AddTimeEstimateColumnToIssueTable(x *xorm.Engine) error { type Issue struct { TimeEstimate int64 `xorm:"NOT NULL DEFAULT 0"` } - - return x.Sync(new(Issue)) + _, err := x.SyncWithOptions(xorm.SyncOptions{ + IgnoreConstrains: true, + IgnoreIndices: true, + }, new(Issue)) + return err } diff --git a/models/organization/org.go b/models/organization/org.go index 3e55a36758963..725a99356e974 100644 --- a/models/organization/org.go +++ b/models/organization/org.go @@ -9,8 +9,11 @@ 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" @@ -404,6 +407,33 @@ 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 @@ -574,9 +604,7 @@ func RemoveOrgRepo(ctx context.Context, orgID, repoID int64) error { return err } -// 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) { +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). @@ -588,8 +616,7 @@ func (org *Organization) GetUserTeams(ctx context.Context, userID int64, cols .. Find(&teams) } -// 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) { +func (org *Organization) getUserTeamIDs(ctx context.Context, userID int64) ([]int64, error) { teamIDs := make([]int64, 0, org.NumTeams) return teamIDs, db.GetEngine(ctx). Table("team"). @@ -613,3 +640,175 @@ func getUserTeamIDsQueryBuilder(orgID, userID int64) *builder.Builder { 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_repo.go b/models/organization/org_repo.go new file mode 100644 index 0000000000000..f7e59928f42ee --- /dev/null +++ b/models/organization/org_repo.go @@ -0,0 +1,17 @@ +// 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 2c5b4090df301..5e99e88689ed9 100644 --- a/models/organization/org_test.go +++ b/models/organization/org_test.go @@ -318,7 +318,7 @@ 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 := repo_model.AccessibleReposEnv(db.DefaultContext, org, userID) + env, err := organization.AccessibleReposEnv(db.DefaultContext, org, userID) assert.NoError(t, err) count, err := env.CountRepos() assert.NoError(t, err) @@ -332,7 +332,7 @@ 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 := repo_model.AccessibleReposEnv(db.DefaultContext, org, userID) + env, err := organization.AccessibleReposEnv(db.DefaultContext, org, userID) assert.NoError(t, err) repoIDs, err := env.RepoIDs(1, 100) assert.NoError(t, err) @@ -346,7 +346,7 @@ 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 := repo_model.AccessibleReposEnv(db.DefaultContext, org, userID) + env, err := organization.AccessibleReposEnv(db.DefaultContext, org, userID) assert.NoError(t, err) repos, err := env.Repos(1, 100) assert.NoError(t, err) @@ -365,7 +365,7 @@ 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 := repo_model.AccessibleReposEnv(db.DefaultContext, org, userID) + env, err := organization.AccessibleReposEnv(db.DefaultContext, org, userID) assert.NoError(t, err) repos, err := env.MirrorRepos() assert.NoError(t, err) diff --git a/models/organization/org_user.go b/models/organization/org_user.go index 08d936d922f05..1d3b2fab44dd3 100644 --- a/models/organization/org_user.go +++ b/models/organization/org_user.go @@ -36,21 +36,6 @@ 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). diff --git a/models/organization/org_user_test.go b/models/organization/org_user_test.go index c5110b2a3484e..55abb63203e86 100644 --- a/models/organization/org_user_test.go +++ b/models/organization/org_user_test.go @@ -131,7 +131,7 @@ 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.GetBean(t, &organization.OrgUser{OrgID: orgID, UID: userID}) == nil { + if !unittest.BeanExists(t, &organization.OrgUser{OrgID: orgID, UID: userID}) { expectedNumMembers++ } assert.NoError(t, organization.AddOrgUser(db.DefaultContext, orgID, userID)) diff --git a/models/organization/team.go b/models/organization/team.go index 96666da39a778..fb7f0c04939e6 100644 --- a/models/organization/team.go +++ b/models/organization/team.go @@ -11,6 +11,7 @@ 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" @@ -77,8 +78,9 @@ type Team struct { LowerName string Name string Description string - AccessMode perm.AccessMode `xorm:"'authorize'"` - Members []*user_model.User `xorm:"-"` + AccessMode perm.AccessMode `xorm:"'authorize'"` + Repos []*repo_model.Repository `xorm:"-"` + Members []*user_model.User `xorm:"-"` NumRepos int NumMembers int Units []*TeamUnit `xorm:"-"` @@ -153,6 +155,17 @@ 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 +} + // LoadMembers returns paginated members in team of organization. func (t *Team) LoadMembers(ctx context.Context) (err error) { t.Members, err = GetTeamMembers(ctx, &SearchMembersOptions{ diff --git a/models/organization/team_list.go b/models/organization/team_list.go index 6f2a922e954d9..4ceb405e31004 100644 --- a/models/organization/team_list.go +++ b/models/organization/team_list.go @@ -9,6 +9,7 @@ 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" @@ -97,11 +98,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, orgID, repoID int64) (teams TeamList, err error) { +func GetRepoTeams(ctx context.Context, repo *repo_model.Repository) (teams TeamList, err error) { return teams, db.GetEngine(ctx). Join("INNER", "team_repo", "team_repo.team_id = team.id"). - Where("team.org_id = ?", orgID). - And("team_repo.repo_id=?", repoID). + Where("team.org_id = ?", repo.OwnerID). + And("team_repo.repo_id=?", repo.ID). OrderBy("CASE WHEN name LIKE '" + OwnerTeamName + "' THEN '' ELSE name END"). Find(&teams) } diff --git a/models/organization/team_repo.go b/models/organization/team_repo.go index 53edd203a8a7c..c90dfdeda04dc 100644 --- a/models/organization/team_repo.go +++ b/models/organization/team_repo.go @@ -8,7 +8,10 @@ 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" ) // TeamRepo represents an team-repository relation. @@ -29,6 +32,29 @@ 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{ diff --git a/models/organization/team_test.go b/models/organization/team_test.go index deaabbfa2c00f..8c34e7a612479 100644 --- a/models/organization/team_test.go +++ b/models/organization/team_test.go @@ -8,7 +8,6 @@ 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" @@ -43,12 +42,9 @@ func TestTeam_GetRepositories(t *testing.T) { test := func(teamID int64) { team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: teamID}) - repos, err := repo_model.GetTeamRepositories(db.DefaultContext, &repo_model.SearchTeamRepoOptions{ - TeamID: team.ID, - }) - assert.NoError(t, err) - assert.Len(t, repos, team.NumRepos) - for _, repo := range repos { + assert.NoError(t, team.LoadRepositories(db.DefaultContext)) + assert.Len(t, team.Repos, team.NumRepos) + for _, repo := range team.Repos { unittest.AssertExistsAndLoadBean(t, &organization.TeamRepo{TeamID: teamID, RepoID: repo.ID}) } } diff --git a/models/packages/package.go b/models/packages/package.go index c12f345f0e272..b7464f914063a 100644 --- a/models/packages/package.go +++ b/models/packages/package.go @@ -248,6 +248,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{ @@ -301,21 +313,6 @@ 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_version.go b/models/packages/package_version.go index 278e8e3a86b0e..bb7fd895f81da 100644 --- a/models/packages/package_version.go +++ b/models/packages/package_version.go @@ -279,9 +279,7 @@ func (opts *PackageSearchOptions) configureOrderBy(e db.Engine) { default: e.Desc("package_version.created_unix") } - - // Sort by id for stable order with duplicates in the other field - e.Asc("package_version.id") + e.Desc("package_version.id") // Sort by id for stable order with duplicates in the other field } // SearchVersions gets all versions of packages matching the search options diff --git a/models/project/column.go b/models/project/column.go index 222f44859928e..5f581b58804cb 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). @@ -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{ diff --git a/models/project/column_test.go b/models/project/column_test.go index 566667e45d133..66db23a3e429c 100644 --- a/models/project/column_test.go +++ b/models/project/column_test.go @@ -20,19 +20,19 @@ func TestGetDefaultColumn(t *testing.T) { assert.NoError(t, err) // check if default column was added - column, err := projectWithoutDefault.GetDefaultColumn(db.DefaultContext) + column, err := projectWithoutDefault.MustDefaultColumn(db.DefaultContext) 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) assert.NoError(t, err) // check if multiple defaults were removed - column, err = projectWithMultipleDefaults.GetDefaultColumn(db.DefaultContext) + column, err = projectWithMultipleDefaults.MustDefaultColumn(db.DefaultContext) 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)) diff --git a/models/project/issue.go b/models/project/issue.go index b4347a9c2b4c6..98eed2a2137b5 100644 --- a/models/project/issue.go +++ b/models/project/issue.go @@ -8,7 +8,6 @@ import ( "fmt" "code.gitea.io/gitea/models/db" - "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/util" ) @@ -34,48 +33,6 @@ 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") diff --git a/models/project/project.go b/models/project/project.go index 87e679e1b73fe..78cba8b574c13 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 + return fmt.Sprintf("%s/-/projects/%d", org.HomeLink(), projectID) +} + +func ProjectLinkForRepo(repo *repo_model.Repository, projectID int64) string { //nolint + 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 "" } @@ -256,7 +267,7 @@ func NewProject(ctx context.Context, p *Project) error { return util.NewInvalidArgumentErrorf("project type is not valid") } - p.Title = util.EllipsisDisplayString(p.Title, 255) + p.Title, _ = util.SplitStringAtByteN(p.Title, 255) return db.WithTx(ctx, func(ctx context.Context) error { if err := db.Insert(ctx, p); err != nil { @@ -311,7 +322,7 @@ func UpdateProject(ctx context.Context, p *Project) error { p.CardType = CardTypeTextOnly } - p.Title = util.EllipsisDisplayString(p.Title, 255) + p.Title, _ = util.SplitStringAtByteN(p.Title, 255) _, err := db.GetEngine(ctx).ID(p.ID).Cols( "title", "description", diff --git a/models/repo.go b/models/repo.go index 3e9c52fdd9c06..598f8df6a43ee 100644 --- a/models/repo.go +++ b/models/repo.go @@ -6,16 +6,21 @@ package models import ( "context" + "fmt" "strconv" _ "image/jpeg" // Needed for jpeg support + asymkey_model "code.gitea.io/gitea/models/asymkey" "code.gitea.io/gitea/models/db" issues_model "code.gitea.io/gitea/models/issues" + access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/log" + + "xorm.io/builder" ) // Init initialize model @@ -24,7 +29,7 @@ func Init(ctx context.Context) error { } type repoChecker struct { - querySQL func(ctx context.Context) ([]map[string][]byte, error) + querySQL func(ctx context.Context) ([]int64, error) correctSQL func(ctx context.Context, id int64) error desc string } @@ -35,8 +40,7 @@ func repoStatsCheck(ctx context.Context, checker *repoChecker) { log.Error("Select %s: %v", checker.desc, err) return } - for _, result := range results { - id, _ := strconv.ParseInt(string(result["id"]), 10, 64) + for _, id := range results { select { case <-ctx.Done(): log.Warn("CheckRepoStats: Cancelled before checking %s for with id=%d", checker.desc, id) @@ -51,21 +55,23 @@ func repoStatsCheck(ctx context.Context, checker *repoChecker) { } } -func StatsCorrectSQL(ctx context.Context, sql string, id int64) error { - _, err := db.GetEngine(ctx).Exec(sql, id, id) +func StatsCorrectSQL(ctx context.Context, sql any, ids ...any) error { + args := []any{sql} + args = append(args, ids...) + _, err := db.GetEngine(ctx).Exec(args...) return err } func repoStatsCorrectNumWatches(ctx context.Context, id int64) error { - return StatsCorrectSQL(ctx, "UPDATE `repository` SET num_watches=(SELECT COUNT(*) FROM `watch` WHERE repo_id=? AND mode<>2) WHERE id=?", id) + return StatsCorrectSQL(ctx, "UPDATE `repository` SET num_watches=(SELECT COUNT(*) FROM `watch` WHERE repo_id=? AND mode<>2) WHERE id=?", id, id) } func repoStatsCorrectNumStars(ctx context.Context, id int64) error { - return StatsCorrectSQL(ctx, "UPDATE `repository` SET num_stars=(SELECT COUNT(*) FROM `star` WHERE repo_id=?) WHERE id=?", id) + return StatsCorrectSQL(ctx, "UPDATE `repository` SET num_stars=(SELECT COUNT(*) FROM `star` WHERE repo_id=?) WHERE id=?", id, id) } func labelStatsCorrectNumIssues(ctx context.Context, id int64) error { - return StatsCorrectSQL(ctx, "UPDATE `label` SET num_issues=(SELECT COUNT(*) FROM `issue_label` WHERE label_id=?) WHERE id=?", id) + return StatsCorrectSQL(ctx, "UPDATE `label` SET num_issues=(SELECT COUNT(*) FROM `issue_label` WHERE label_id=?) WHERE id=?", id, id) } func labelStatsCorrectNumIssuesRepo(ctx context.Context, id int64) error { @@ -102,11 +108,11 @@ func milestoneStatsCorrectNumIssuesRepo(ctx context.Context, id int64) error { } func userStatsCorrectNumRepos(ctx context.Context, id int64) error { - return StatsCorrectSQL(ctx, "UPDATE `user` SET num_repos=(SELECT COUNT(*) FROM `repository` WHERE owner_id=?) WHERE id=?", id) + return StatsCorrectSQL(ctx, "UPDATE `user` SET num_repos=(SELECT COUNT(*) FROM `repository` WHERE owner_id=?) WHERE id=?", id, id) } func repoStatsCorrectIssueNumComments(ctx context.Context, id int64) error { - return StatsCorrectSQL(ctx, "UPDATE `issue` SET num_comments=(SELECT COUNT(*) FROM `comment` WHERE issue_id=? AND type=0) WHERE id=?", id) + return StatsCorrectSQL(ctx, issues_model.UpdateIssueNumCommentsBuilder(id)) } func repoStatsCorrectNumIssues(ctx context.Context, id int64) error { @@ -125,9 +131,12 @@ func repoStatsCorrectNumClosedPulls(ctx context.Context, id int64) error { return repo_model.UpdateRepoIssueNumbers(ctx, id, true, true) } -func statsQuery(args ...any) func(context.Context) ([]map[string][]byte, error) { - return func(ctx context.Context) ([]map[string][]byte, error) { - return db.GetEngine(ctx).Query(args...) +// statsQuery returns a function that queries the database for a list of IDs +// sql could be a string or a *builder.Builder +func statsQuery(sql any, args ...any) func(context.Context) ([]int64, error) { + return func(ctx context.Context) ([]int64, error) { + var ids []int64 + return ids, db.GetEngine(ctx).SQL(sql, args...).Find(&ids) } } @@ -198,7 +207,16 @@ func CheckRepoStats(ctx context.Context) error { }, // Issue.NumComments { - statsQuery("SELECT `issue`.id FROM `issue` WHERE `issue`.num_comments!=(SELECT COUNT(*) FROM `comment` WHERE issue_id=`issue`.id AND type=0)"), + statsQuery(builder.Select("`issue`.id").From("`issue`").Where( + builder.Neq{ + "`issue`.num_comments": builder.Select("COUNT(*)").From("`comment`").Where( + builder.Expr("issue_id = `issue`.id").And( + builder.In("type", issues_model.ConversationCountedCommentType()), + ), + ), + }, + ), + ), repoStatsCorrectIssueNumComments, "issue count 'num_comments'", }, @@ -312,3 +330,48 @@ func DoctorUserStarNum(ctx context.Context) (err error) { return err } + +// DeleteDeployKey delete deploy keys +func DeleteDeployKey(ctx context.Context, doer *user_model.User, id int64) error { + key, err := asymkey_model.GetDeployKeyByID(ctx, id) + if err != nil { + if asymkey_model.IsErrDeployKeyNotExist(err) { + return nil + } + return fmt.Errorf("GetDeployKeyByID: %w", err) + } + + // Check if user has access to delete this key. + if !doer.IsAdmin { + repo, err := repo_model.GetRepositoryByID(ctx, key.RepoID) + if err != nil { + return fmt.Errorf("GetRepositoryByID: %w", err) + } + has, err := access_model.IsUserRepoAdmin(ctx, repo, doer) + if err != nil { + return fmt.Errorf("GetUserRepoPermission: %w", err) + } else if !has { + return asymkey_model.ErrKeyAccessDenied{ + UserID: doer.ID, + KeyID: key.ID, + Note: "deploy", + } + } + } + + if _, err := db.DeleteByID[asymkey_model.DeployKey](ctx, key.ID); err != nil { + return fmt.Errorf("delete deploy key [%d]: %w", key.ID, err) + } + + // Check if this is the last reference to same key content. + has, err := asymkey_model.IsDeployKeyExistByKeyID(ctx, key.KeyID) + if err != nil { + return err + } else if !has { + if _, err = db.DeleteByID[asymkey_model.PublicKey](ctx, key.KeyID); err != nil { + return err + } + } + + return nil +} diff --git a/models/repo/license.go b/models/repo/license.go index 366b4598cc4e0..9bcf0f7bc97f5 100644 --- a/models/repo/license.go +++ b/models/repo/license.go @@ -54,6 +54,7 @@ func UpdateRepoLicenses(ctx context.Context, repo *Repository, commitID string, for _, o := range oldLicenses { // Update already existing license if o.License == license { + o.CommitID = commitID if _, err := db.GetEngine(ctx).ID(o.ID).Cols("`commit_id`").Update(o); err != nil { return err } diff --git a/models/repo/org_repo.go b/models/repo/org_repo.go deleted file mode 100644 index 5f0af2d475abd..0000000000000 --- a/models/repo/org_repo.go +++ /dev/null @@ -1,206 +0,0 @@ -// Copyright 2022 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package repo - -import ( - "context" - "fmt" - "strings" - - "code.gitea.io/gitea/models/db" - org_model "code.gitea.io/gitea/models/organization" - user_model "code.gitea.io/gitea/models/user" - - "xorm.io/builder" -) - -// GetOrgRepositories get repos belonging to the given organization -func GetOrgRepositories(ctx context.Context, orgID int64) (RepositoryList, error) { - var orgRepos []*Repository - return orgRepos, db.GetEngine(ctx).Where("owner_id = ?", orgID).Find(&orgRepos) -} - -type SearchTeamRepoOptions struct { - db.ListOptions - TeamID int64 -} - -// GetRepositories returns paginated repositories in team of organization. -func GetTeamRepositories(ctx context.Context, opts *SearchTeamRepoOptions) (RepositoryList, error) { - sess := db.GetEngine(ctx) - if opts.TeamID > 0 { - sess = sess.In("id", - builder.Select("repo_id"). - From("team_repo"). - Where(builder.Eq{"team_id": opts.TeamID}), - ) - } - if opts.PageSize > 0 { - sess.Limit(opts.PageSize, (opts.Page-1)*opts.PageSize) - } - var repos []*Repository - return repos, sess.OrderBy("repository.name"). - Find(&repos) -} - -// AccessibleReposEnvironment operations involving the repositories that are -// accessible to a particular user -type AccessibleReposEnvironment interface { - CountRepos() (int64, error) - RepoIDs(page, pageSize int) ([]int64, error) - Repos(page, pageSize int) (RepositoryList, error) - MirrorRepos() (RepositoryList, error) - AddKeyword(keyword string) - SetSort(db.SearchOrderBy) -} - -type accessibleReposEnv struct { - org *org_model.Organization - user *user_model.User - team *org_model.Team - teamIDs []int64 - ctx context.Context - keyword string - orderBy db.SearchOrderBy -} - -// AccessibleReposEnv builds an AccessibleReposEnvironment for the repositories in `org` -// that are accessible to the specified user. -func AccessibleReposEnv(ctx context.Context, org *org_model.Organization, userID int64) (AccessibleReposEnvironment, error) { - var user *user_model.User - - if userID > 0 { - u, err := user_model.GetUserByID(ctx, userID) - if err != nil { - return nil, err - } - user = u - } - - teamIDs, err := org.GetUserTeamIDs(ctx, userID) - if err != nil { - return nil, err - } - return &accessibleReposEnv{ - org: org, - user: user, - teamIDs: teamIDs, - ctx: ctx, - orderBy: db.SearchOrderByRecentUpdated, - }, nil -} - -// AccessibleTeamReposEnv an AccessibleReposEnvironment for the repositories in `org` -// that are accessible to the specified team. -func AccessibleTeamReposEnv(ctx context.Context, org *org_model.Organization, team *org_model.Team) AccessibleReposEnvironment { - return &accessibleReposEnv{ - org: org, - team: team, - ctx: ctx, - orderBy: db.SearchOrderByRecentUpdated, - } -} - -func (env *accessibleReposEnv) cond() builder.Cond { - cond := builder.NewCond() - if env.team != nil { - cond = cond.And(builder.Eq{"team_repo.team_id": env.team.ID}) - } else { - if env.user == nil || !env.user.IsRestricted { - cond = cond.Or(builder.Eq{ - "`repository`.owner_id": env.org.ID, - "`repository`.is_private": false, - }) - } - if len(env.teamIDs) > 0 { - cond = cond.Or(builder.In("team_repo.team_id", env.teamIDs)) - } - } - if env.keyword != "" { - cond = cond.And(builder.Like{"`repository`.lower_name", strings.ToLower(env.keyword)}) - } - return cond -} - -func (env *accessibleReposEnv) CountRepos() (int64, error) { - repoCount, err := db.GetEngine(env.ctx). - Join("INNER", "team_repo", "`team_repo`.repo_id=`repository`.id"). - Where(env.cond()). - Distinct("`repository`.id"). - Count(&Repository{}) - if err != nil { - return 0, fmt.Errorf("count user repositories in organization: %w", err) - } - return repoCount, nil -} - -func (env *accessibleReposEnv) RepoIDs(page, pageSize int) ([]int64, error) { - if page <= 0 { - page = 1 - } - - repoIDs := make([]int64, 0, pageSize) - return repoIDs, db.GetEngine(env.ctx). - Table("repository"). - Join("INNER", "team_repo", "`team_repo`.repo_id=`repository`.id"). - Where(env.cond()). - GroupBy("`repository`.id,`repository`."+strings.Fields(string(env.orderBy))[0]). - OrderBy(string(env.orderBy)). - Limit(pageSize, (page-1)*pageSize). - Cols("`repository`.id"). - Find(&repoIDs) -} - -func (env *accessibleReposEnv) Repos(page, pageSize int) (RepositoryList, error) { - repoIDs, err := env.RepoIDs(page, pageSize) - if err != nil { - return nil, fmt.Errorf("GetUserRepositoryIDs: %w", err) - } - - repos := make([]*Repository, 0, len(repoIDs)) - if len(repoIDs) == 0 { - return repos, nil - } - - return repos, db.GetEngine(env.ctx). - In("`repository`.id", repoIDs). - OrderBy(string(env.orderBy)). - Find(&repos) -} - -func (env *accessibleReposEnv) MirrorRepoIDs() ([]int64, error) { - repoIDs := make([]int64, 0, 10) - return repoIDs, db.GetEngine(env.ctx). - Table("repository"). - Join("INNER", "team_repo", "`team_repo`.repo_id=`repository`.id AND `repository`.is_mirror=?", true). - Where(env.cond()). - GroupBy("`repository`.id, `repository`.updated_unix"). - OrderBy(string(env.orderBy)). - Cols("`repository`.id"). - Find(&repoIDs) -} - -func (env *accessibleReposEnv) MirrorRepos() (RepositoryList, error) { - repoIDs, err := env.MirrorRepoIDs() - if err != nil { - return nil, fmt.Errorf("MirrorRepoIDs: %w", err) - } - - repos := make([]*Repository, 0, len(repoIDs)) - if len(repoIDs) == 0 { - return repos, nil - } - - return repos, db.GetEngine(env.ctx). - In("`repository`.id", repoIDs). - Find(&repos) -} - -func (env *accessibleReposEnv) AddKeyword(keyword string) { - env.keyword = keyword -} - -func (env *accessibleReposEnv) SetSort(orderBy db.SearchOrderBy) { - env.orderBy = orderBy -} diff --git a/models/repo/release.go b/models/repo/release.go index 1c2e4a48e399d..ba7a3b3159aa4 100644 --- a/models/repo/release.go +++ b/models/repo/release.go @@ -156,7 +156,7 @@ func IsReleaseExist(ctx context.Context, repoID int64, tagName string) (bool, er // UpdateRelease updates all columns of a release func UpdateRelease(ctx context.Context, rel *Release) error { - rel.Title = util.EllipsisDisplayString(rel.Title, 255) + rel.Title, _ = util.SplitStringAtByteN(rel.Title, 255) _, err := db.GetEngine(ctx).ID(rel.ID).AllCols().Update(rel) return err } diff --git a/models/repo/repo.go b/models/repo/repo.go index 2d9b9de88d499..c5060a419f458 100644 --- a/models/repo/repo.go +++ b/models/repo/repo.go @@ -37,7 +37,7 @@ type ErrUserDoesNotHaveAccessToRepo struct { RepoName string } -// IsErrUserDoesNotHaveAccessToRepo checks if an error is a ErrUserDoesNotHaveAccessToRepo. +// IsErrUserDoesNotHaveAccessToRepo checks if an error is a ErrRepoFileAlreadyExists. func IsErrUserDoesNotHaveAccessToRepo(err error) bool { _, ok := err.(ErrUserDoesNotHaveAccessToRepo) return ok @@ -276,6 +276,8 @@ func (repo *Repository) IsBroken() bool { } // MarkAsBrokenEmpty marks the repo as broken and empty +// FIXME: the status "broken" and "is_empty" were abused, +// The code always set them together, no way to distinguish whether a repo is really "empty" or "broken" func (repo *Repository) MarkAsBrokenEmpty() { repo.Status = RepositoryBroken repo.IsEmpty = true @@ -866,21 +868,6 @@ func (repo *Repository) TemplateRepo(ctx context.Context) *Repository { return repo } -// ErrUserOwnRepos represents a "UserOwnRepos" kind of error. -type ErrUserOwnRepos struct { - UID int64 -} - -// IsErrUserOwnRepos checks if an error is a ErrUserOwnRepos. -func IsErrUserOwnRepos(err error) bool { - _, ok := err.(ErrUserOwnRepos) - return ok -} - -func (err ErrUserOwnRepos) Error() string { - return fmt.Sprintf("user still has ownership of repositories [uid: %d]", err.UID) -} - type CountRepositoryOptions struct { OwnerID int64 Private optional.Option[bool] diff --git a/models/repo/update.go b/models/repo/update.go index e7ca2240282f2..fce357a1acf35 100644 --- a/models/repo/update.go +++ b/models/repo/update.go @@ -46,6 +46,12 @@ func UpdateRepositoryCols(ctx context.Context, repo *Repository, cols ...string) return err } +// UpdateRepositoryColsNoAutoTime updates repository's columns and but applies time change automatically +func UpdateRepositoryColsNoAutoTime(ctx context.Context, repo *Repository, cols ...string) error { + _, err := db.GetEngine(ctx).ID(repo.ID).Cols(cols...).NoAutoTime().Update(repo) + return err +} + // ErrReachLimitOfRepo represents a "ReachLimitOfRepo" kind of error. type ErrReachLimitOfRepo struct { Limit int diff --git a/models/repo_test.go b/models/repo_test.go index 2a8a4a743ed27..bcf62237f08d7 100644 --- a/models/repo_test.go +++ b/models/repo_test.go @@ -7,6 +7,7 @@ import ( "testing" "code.gitea.io/gitea/models/db" + issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/unittest" "github.com/stretchr/testify/assert" @@ -22,3 +23,16 @@ func TestDoctorUserStarNum(t *testing.T) { assert.NoError(t, DoctorUserStarNum(db.DefaultContext)) } + +func Test_repoStatsCorrectIssueNumComments(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + issue2 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2}) + assert.NotNil(t, issue2) + assert.EqualValues(t, 0, issue2.NumComments) // the fixture data is wrong, but we don't fix it here + + assert.NoError(t, repoStatsCorrectIssueNumComments(db.DefaultContext, 2)) + // reload the issue + issue2 = unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2}) + assert.EqualValues(t, 1, issue2.NumComments) +} diff --git a/models/repo/transfer.go b/models/repo_transfer.go similarity index 73% rename from models/repo/transfer.go rename to models/repo_transfer.go index 43e15b33bcfc1..37f591f65dcc2 100644 --- a/models/repo/transfer.go +++ b/models/repo_transfer.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package repo +package models import ( "context" @@ -10,58 +10,16 @@ import ( "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/organization" + repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/timeutil" - "code.gitea.io/gitea/modules/util" "xorm.io/builder" ) -// ErrNoPendingRepoTransfer is an error type for repositories without a pending -// transfer request -type ErrNoPendingRepoTransfer struct { - RepoID int64 -} - -func (err ErrNoPendingRepoTransfer) Error() string { - return fmt.Sprintf("repository doesn't have a pending transfer [repo_id: %d]", err.RepoID) -} - -// IsErrNoPendingTransfer is an error type when a repository has no pending -// transfers -func IsErrNoPendingTransfer(err error) bool { - _, ok := err.(ErrNoPendingRepoTransfer) - return ok -} - -func (err ErrNoPendingRepoTransfer) Unwrap() error { - return util.ErrNotExist -} - -// ErrRepoTransferInProgress represents the state of a repository that has an -// ongoing transfer -type ErrRepoTransferInProgress struct { - Uname string - Name string -} - -// IsErrRepoTransferInProgress checks if an error is a ErrRepoTransferInProgress. -func IsErrRepoTransferInProgress(err error) bool { - _, ok := err.(ErrRepoTransferInProgress) - return ok -} - -func (err ErrRepoTransferInProgress) Error() string { - return fmt.Sprintf("repository is already being transferred [uname: %s, name: %s]", err.Uname, err.Name) -} - -func (err ErrRepoTransferInProgress) Unwrap() error { - return util.ErrAlreadyExist -} - // RepoTransfer is used to manage repository transfers -type RepoTransfer struct { //nolint +type RepoTransfer struct { ID int64 `xorm:"pk autoincr"` DoerID int64 Doer *user_model.User `xorm:"-"` @@ -168,7 +126,7 @@ func GetPendingRepositoryTransfers(ctx context.Context, opts *PendingRepositoryT // GetPendingRepositoryTransfer fetches the most recent and ongoing transfer // process for the repository -func GetPendingRepositoryTransfer(ctx context.Context, repo *Repository) (*RepoTransfer, error) { +func GetPendingRepositoryTransfer(ctx context.Context, repo *repo_model.Repository) (*RepoTransfer, error) { transfers, err := GetPendingRepositoryTransfers(ctx, &PendingRepositoryTransferOptions{RepoID: repo.ID}) if err != nil { return nil, err @@ -187,11 +145,11 @@ func DeleteRepositoryTransfer(ctx context.Context, repoID int64) error { } // TestRepositoryReadyForTransfer make sure repo is ready to transfer -func TestRepositoryReadyForTransfer(status RepositoryStatus) error { +func TestRepositoryReadyForTransfer(status repo_model.RepositoryStatus) error { switch status { - case RepositoryBeingMigrated: + case repo_model.RepositoryBeingMigrated: return errors.New("repo is not ready, currently migrating") - case RepositoryPendingTransfer: + case repo_model.RepositoryPendingTransfer: return ErrRepoTransferInProgress{} } return nil @@ -201,7 +159,7 @@ func TestRepositoryReadyForTransfer(status RepositoryStatus) error { // it marks the repository transfer as "pending" func CreatePendingRepositoryTransfer(ctx context.Context, doer, newOwner *user_model.User, repoID int64, teams []*organization.Team) error { return db.WithTx(ctx, func(ctx context.Context) error { - repo, err := GetRepositoryByID(ctx, repoID) + repo, err := repo_model.GetRepositoryByID(ctx, repoID) if err != nil { return err } @@ -211,16 +169,16 @@ func CreatePendingRepositoryTransfer(ctx context.Context, doer, newOwner *user_m return err } - repo.Status = RepositoryPendingTransfer - if err := UpdateRepositoryCols(ctx, repo, "status"); err != nil { + repo.Status = repo_model.RepositoryPendingTransfer + if err := repo_model.UpdateRepositoryCols(ctx, repo, "status"); err != nil { return err } // Check if new owner has repository with same name. - if has, err := IsRepositoryModelExist(ctx, newOwner, repo.Name); err != nil { + if has, err := repo_model.IsRepositoryModelExist(ctx, newOwner, repo.Name); err != nil { return fmt.Errorf("IsRepositoryExist: %w", err) } else if has { - return ErrRepoAlreadyExist{ + return repo_model.ErrRepoAlreadyExist{ Uname: newOwner.LowerName, Name: repo.Name, } diff --git a/models/unittest/fixtures.go b/models/unittest/fixtures.go index 517584fdc2d1c..8acd72d465fc0 100644 --- a/models/unittest/fixtures.go +++ b/models/unittest/fixtures.go @@ -1,6 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT +//nolint:forbidigo package unittest import ( @@ -11,12 +12,15 @@ import ( "code.gitea.io/gitea/modules/auth/password/hash" "code.gitea.io/gitea/modules/setting" - "github.com/go-testfixtures/testfixtures/v3" "xorm.io/xorm" "xorm.io/xorm/schemas" ) -var fixturesLoader *testfixtures.Loader +type FixturesLoader interface { + Load() error +} + +var fixturesLoader FixturesLoader // GetXORMEngine gets the XORM engine func GetXORMEngine(engine ...*xorm.Engine) (x *xorm.Engine) { @@ -29,44 +33,16 @@ func GetXORMEngine(engine ...*xorm.Engine) (x *xorm.Engine) { // InitFixtures initialize test fixtures for a test database func InitFixtures(opts FixturesOptions, engine ...*xorm.Engine) (err error) { e := GetXORMEngine(engine...) - var fixtureOptionFiles func(*testfixtures.Loader) error - if opts.Dir != "" { - fixtureOptionFiles = testfixtures.Directory(opts.Dir) - } else { - fixtureOptionFiles = testfixtures.Files(opts.Files...) - } - var dialect string - switch e.Dialect().URI().DBType { - case schemas.POSTGRES: - dialect = "postgres" - case schemas.MYSQL: - dialect = "mysql" - case schemas.MSSQL: - dialect = "mssql" - case schemas.SQLITE: - dialect = "sqlite3" - default: - return fmt.Errorf("unsupported RDBMS for integration tests: %q", e.Dialect().URI().DBType) - } - loaderOptions := []func(loader *testfixtures.Loader) error{ - testfixtures.Database(e.DB().DB), - testfixtures.Dialect(dialect), - testfixtures.DangerousSkipTestDatabaseCheck(), - fixtureOptionFiles, - } - - if e.Dialect().URI().DBType == schemas.POSTGRES { - loaderOptions = append(loaderOptions, testfixtures.SkipResetSequences()) - } - - fixturesLoader, err = testfixtures.New(loaderOptions...) + fixturesLoader, err = NewFixturesLoader(e, opts) if err != nil { return err } // register the dummy hash algorithm function used in the test fixtures _ = hash.Register("dummy", hash.NewDummyHasher) + setting.PasswordHashAlgo, _ = hash.SetDefaultPasswordHashAlgorithm("dummy") + return err } @@ -82,7 +58,7 @@ func LoadFixtures(engine ...*xorm.Engine) error { time.Sleep(200 * time.Millisecond) } if err != nil { - return fmt.Errorf("LoadFixtures failed after retries: %w", err) + fmt.Printf("LoadFixtures failed after retries: %v\n", err) } // Now if we're running postgres we need to tell it to update the sequences if e.Dialect().URI().DBType == schemas.POSTGRES { @@ -103,18 +79,21 @@ func LoadFixtures(engine ...*xorm.Engine) error { AND T.relname = PGT.tablename ORDER BY S.relname;`) if err != nil { - return fmt.Errorf("failed to generate sequence update: %w", err) + fmt.Printf("Failed to generate sequence update: %v\n", err) + return err } for _, r := range results { for _, value := range r { _, err = e.Exec(value) if err != nil { - return fmt.Errorf("failed to update sequence: %s, error: %w", value, err) + fmt.Printf("Failed to update sequence: %s Error: %v\n", value, err) + return err } } } } _ = hash.Register("dummy", hash.NewDummyHasher) setting.PasswordHashAlgo, _ = hash.SetDefaultPasswordHashAlgorithm("dummy") - return nil + + return err } diff --git a/models/unittest/fixtures_loader.go b/models/unittest/fixtures_loader.go new file mode 100644 index 0000000000000..14686caf63de4 --- /dev/null +++ b/models/unittest/fixtures_loader.go @@ -0,0 +1,201 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package unittest + +import ( + "database/sql" + "encoding/hex" + "fmt" + "os" + "path/filepath" + "slices" + "strings" + + "gopkg.in/yaml.v3" + "xorm.io/xorm" + "xorm.io/xorm/schemas" +) + +type fixtureItem struct { + tableName string + tableNameQuoted string + sqlInserts []string + sqlInsertArgs [][]any + + mssqlHasIdentityColumn bool +} + +type fixturesLoaderInternal struct { + db *sql.DB + dbType schemas.DBType + files []string + fixtures map[string]*fixtureItem + quoteObject func(string) string + paramPlaceholder func(idx int) string +} + +func (f *fixturesLoaderInternal) mssqlTableHasIdentityColumn(db *sql.DB, tableName string) (bool, error) { + row := db.QueryRow(`SELECT COUNT(*) FROM sys.identity_columns WHERE OBJECT_ID = OBJECT_ID(?)`, tableName) + var count int + if err := row.Scan(&count); err != nil { + return false, err + } + return count > 0, nil +} + +func (f *fixturesLoaderInternal) preprocessFixtureRow(row []map[string]any) (err error) { + for _, m := range row { + for k, v := range m { + if s, ok := v.(string); ok { + if strings.HasPrefix(s, "0x") { + if m[k], err = hex.DecodeString(s[2:]); err != nil { + return err + } + } + } + } + } + return nil +} + +func (f *fixturesLoaderInternal) prepareFixtureItem(file string) (_ *fixtureItem, err error) { + fixture := &fixtureItem{} + fixture.tableName, _, _ = strings.Cut(filepath.Base(file), ".") + fixture.tableNameQuoted = f.quoteObject(fixture.tableName) + + if f.dbType == schemas.MSSQL { + fixture.mssqlHasIdentityColumn, err = f.mssqlTableHasIdentityColumn(f.db, fixture.tableName) + if err != nil { + return nil, err + } + } + + data, err := os.ReadFile(file) + if err != nil { + return nil, fmt.Errorf("failed to read file %q: %w", file, err) + } + + var rows []map[string]any + if err = yaml.Unmarshal(data, &rows); err != nil { + return nil, fmt.Errorf("failed to unmarshal yaml data from %q: %w", file, err) + } + if err = f.preprocessFixtureRow(rows); err != nil { + return nil, fmt.Errorf("failed to preprocess fixture rows from %q: %w", file, err) + } + + var sqlBuf []byte + var sqlArguments []any + for _, row := range rows { + sqlBuf = append(sqlBuf, fmt.Sprintf("INSERT INTO %s (", fixture.tableNameQuoted)...) + for k, v := range row { + sqlBuf = append(sqlBuf, f.quoteObject(k)...) + sqlBuf = append(sqlBuf, ","...) + sqlArguments = append(sqlArguments, v) + } + sqlBuf = sqlBuf[:len(sqlBuf)-1] + sqlBuf = append(sqlBuf, ") VALUES ("...) + paramIdx := 1 + for range row { + sqlBuf = append(sqlBuf, f.paramPlaceholder(paramIdx)...) + sqlBuf = append(sqlBuf, ',') + paramIdx++ + } + sqlBuf[len(sqlBuf)-1] = ')' + fixture.sqlInserts = append(fixture.sqlInserts, string(sqlBuf)) + fixture.sqlInsertArgs = append(fixture.sqlInsertArgs, slices.Clone(sqlArguments)) + sqlBuf = sqlBuf[:0] + sqlArguments = sqlArguments[:0] + } + return fixture, nil +} + +func (f *fixturesLoaderInternal) loadFixtures(tx *sql.Tx, file string) (err error) { + fixture := f.fixtures[file] + if fixture == nil { + if fixture, err = f.prepareFixtureItem(file); err != nil { + return err + } + f.fixtures[file] = fixture + } + + _, err = tx.Exec(fmt.Sprintf("DELETE FROM %s", fixture.tableNameQuoted)) // sqlite3 doesn't support truncate + if err != nil { + return err + } + + if fixture.mssqlHasIdentityColumn { + _, err = tx.Exec(fmt.Sprintf("SET IDENTITY_INSERT %s ON", fixture.tableNameQuoted)) + if err != nil { + return err + } + defer func() { _, err = tx.Exec(fmt.Sprintf("SET IDENTITY_INSERT %s OFF", fixture.tableNameQuoted)) }() + } + for i := range fixture.sqlInserts { + _, err = tx.Exec(fixture.sqlInserts[i], fixture.sqlInsertArgs[i]...) + } + if err != nil { + return err + } + return nil +} + +func (f *fixturesLoaderInternal) Load() error { + tx, err := f.db.Begin() + if err != nil { + return err + } + defer func() { _ = tx.Rollback() }() + + for _, file := range f.files { + if err := f.loadFixtures(tx, file); err != nil { + return fmt.Errorf("failed to load fixtures from %s: %w", file, err) + } + } + return tx.Commit() +} + +func FixturesFileFullPaths(dir string, files []string) ([]string, error) { + if files != nil && len(files) == 0 { + return nil, nil // load nothing + } + files = slices.Clone(files) + if len(files) == 0 { + entries, err := os.ReadDir(dir) + if err != nil { + return nil, err + } + for _, e := range entries { + files = append(files, e.Name()) + } + } + for i, file := range files { + if !filepath.IsAbs(file) { + files[i] = filepath.Join(dir, file) + } + } + return files, nil +} + +func NewFixturesLoader(x *xorm.Engine, opts FixturesOptions) (FixturesLoader, error) { + files, err := FixturesFileFullPaths(opts.Dir, opts.Files) + if err != nil { + return nil, fmt.Errorf("failed to get fixtures files: %w", err) + } + f := &fixturesLoaderInternal{db: x.DB().DB, dbType: x.Dialect().URI().DBType, files: files, fixtures: map[string]*fixtureItem{}} + switch f.dbType { + case schemas.SQLITE: + f.quoteObject = func(s string) string { return fmt.Sprintf(`"%s"`, s) } + f.paramPlaceholder = func(idx int) string { return "?" } + case schemas.POSTGRES: + f.quoteObject = func(s string) string { return fmt.Sprintf(`"%s"`, s) } + f.paramPlaceholder = func(idx int) string { return fmt.Sprintf(`$%d`, idx) } + case schemas.MYSQL: + f.quoteObject = func(s string) string { return fmt.Sprintf("`%s`", s) } + f.paramPlaceholder = func(idx int) string { return "?" } + case schemas.MSSQL: + f.quoteObject = func(s string) string { return fmt.Sprintf("[%s]", s) } + f.paramPlaceholder = func(idx int) string { return "?" } + } + return f, nil +} diff --git a/models/unittest/reflection.go b/models/unittest/reflection.go index bc96a05973319..141fc66b9981a 100644 --- a/models/unittest/reflection.go +++ b/models/unittest/reflection.go @@ -4,7 +4,7 @@ package unittest import ( - "fmt" + "log" "reflect" ) @@ -14,7 +14,7 @@ func fieldByName(v reflect.Value, field string) reflect.Value { } f := v.FieldByName(field) if !f.IsValid() { - panic(fmt.Errorf("can not read %s for %v", field, v)) + log.Panicf("can not read %s for %v", field, v) } return f } diff --git a/models/unittest/testdb.go b/models/unittest/testdb.go index 3fb53541dff37..5a1c27dbeae88 100644 --- a/models/unittest/testdb.go +++ b/models/unittest/testdb.go @@ -14,13 +14,13 @@ import ( "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/system" "code.gitea.io/gitea/modules/auth/password/hash" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/cache" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting/config" "code.gitea.io/gitea/modules/storage" - "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/modules/util" "github.com/stretchr/testify/assert" @@ -34,6 +34,11 @@ var ( fixturesDir string ) +// FixturesDir returns the fixture directory +func FixturesDir() string { + return fixturesDir +} + func fatalTestError(fmtStr string, args ...any) { _, _ = fmt.Fprintf(os.Stderr, fmtStr, args...) os.Exit(1) @@ -201,7 +206,7 @@ func CreateTestEngine(opts FixturesOptions) error { x, err := xorm.NewEngine("sqlite3", "file::memory:?cache=shared&_txlock=immediate") if err != nil { if strings.Contains(err.Error(), "unknown driver") { - return fmt.Errorf(`sqlite3 requires: -tags sqlite,sqlite_unlock_notify%s%w`, "\n", err) + return fmt.Errorf(`sqlite3 requires: import _ "github.com/mattn/go-sqlite3" or -tags sqlite,sqlite_unlock_notify%s%w`, "\n", err) } return err } @@ -230,5 +235,5 @@ func PrepareTestEnv(t testing.TB) { assert.NoError(t, PrepareTestDatabase()) metaPath := filepath.Join(giteaRoot, "tests", "gitea-repositories-meta") assert.NoError(t, SyncDirs(metaPath, setting.RepoRootPath)) - test.SetupGiteaRoot() // Makes sure GITEA_ROOT is set + base.SetupGiteaRoot() // Makes sure GITEA_ROOT is set } diff --git a/models/unittest/unit_tests.go b/models/unittest/unit_tests.go index 1c5595aef8545..4ac858e04ea18 100644 --- a/models/unittest/unit_tests.go +++ b/models/unittest/unit_tests.go @@ -4,17 +4,13 @@ package unittest import ( - "fmt" "math" - "os" - "strings" "code.gitea.io/gitea/models/db" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "xorm.io/builder" - "xorm.io/xorm" ) // Code in this file is mainly used by unittest.CheckConsistencyFor, which is not in the unit test for various reasons. @@ -55,23 +51,22 @@ func whereOrderConditions(e db.Engine, conditions []any) db.Engine { return e.OrderBy(orderBy) } -func getBeanIfExists(bean any, conditions ...any) (bool, error) { +// LoadBeanIfExists loads beans from fixture database if exist +func LoadBeanIfExists(bean any, conditions ...any) (bool, error) { e := db.GetEngine(db.DefaultContext) return whereOrderConditions(e, conditions).Get(bean) } -func GetBean[T any](t require.TestingT, bean T, conditions ...any) (ret T) { - exists, err := getBeanIfExists(bean, conditions...) - require.NoError(t, err) - if exists { - return bean - } - return ret +// BeanExists for testing, check if a bean exists +func BeanExists(t assert.TestingT, bean any, conditions ...any) bool { + exists, err := LoadBeanIfExists(bean, conditions...) + assert.NoError(t, err) + return exists } // AssertExistsAndLoadBean assert that a bean exists and load it from the test database func AssertExistsAndLoadBean[T any](t require.TestingT, bean T, conditions ...any) T { - exists, err := getBeanIfExists(bean, conditions...) + exists, err := LoadBeanIfExists(bean, conditions...) require.NoError(t, err) require.True(t, exists, "Expected to find %+v (of type %T, with conditions %+v), but did not", @@ -117,11 +112,25 @@ func GetCount(t assert.TestingT, bean any, conditions ...any) int { // AssertNotExistsBean assert that a bean does not exist in the test database func AssertNotExistsBean(t assert.TestingT, bean any, conditions ...any) { - exists, err := getBeanIfExists(bean, conditions...) + exists, err := LoadBeanIfExists(bean, conditions...) assert.NoError(t, err) assert.False(t, exists) } +// AssertExistsIf asserts that a bean exists or does not exist, depending on +// what is expected. +func AssertExistsIf(t assert.TestingT, expected bool, bean any, conditions ...any) { + exists, err := LoadBeanIfExists(bean, conditions...) + assert.NoError(t, err) + assert.Equal(t, expected, exists) +} + +// AssertSuccessfulInsert assert that beans is successfully inserted +func AssertSuccessfulInsert(t assert.TestingT, beans ...any) { + err := db.Insert(db.DefaultContext, beans...) + assert.NoError(t, err) +} + // AssertCount assert the count of a bean func AssertCount(t assert.TestingT, bean, expected any) bool { return assert.EqualValues(t, expected, GetCount(t, bean)) @@ -146,39 +155,3 @@ func AssertCountByCond(t assert.TestingT, tableName string, cond builder.Cond, e return assert.EqualValues(t, expected, GetCountByCond(t, tableName, cond), "Failed consistency test, the counted bean (of table %s) was %+v", tableName, cond) } - -// DumpQueryResult dumps the result of a query for debugging purpose -func DumpQueryResult(t require.TestingT, sqlOrBean any, sqlArgs ...any) { - x := db.GetEngine(db.DefaultContext).(*xorm.Engine) - goDB := x.DB().DB - sql, ok := sqlOrBean.(string) - if !ok { - sql = fmt.Sprintf("SELECT * FROM %s", db.TableName(sqlOrBean)) - } else if !strings.Contains(sql, " ") { - sql = fmt.Sprintf("SELECT * FROM %s", sql) - } - rows, err := goDB.Query(sql, sqlArgs...) - require.NoError(t, err) - defer rows.Close() - columns, err := rows.Columns() - require.NoError(t, err) - - _, _ = fmt.Fprintf(os.Stdout, "====== DumpQueryResult: %s ======\n", sql) - idx := 0 - for rows.Next() { - row := make([]any, len(columns)) - rowPointers := make([]any, len(columns)) - for i := range row { - rowPointers[i] = &row[i] - } - require.NoError(t, rows.Scan(rowPointers...)) - _, _ = fmt.Fprintf(os.Stdout, "- # row[%d]\n", idx) - for i, col := range columns { - _, _ = fmt.Fprintf(os.Stdout, " %s: %v\n", col, row[i]) - } - idx++ - } - if idx == 0 { - _, _ = fmt.Fprintf(os.Stdout, "(no result, columns: %s)\n", strings.Join(columns, ", ")) - } -} diff --git a/models/user/avatar.go b/models/user/avatar.go index 5453c78fc61db..3d9fc4452f8ab 100644 --- a/models/user/avatar.go +++ b/models/user/avatar.go @@ -38,27 +38,32 @@ func GenerateRandomAvatar(ctx context.Context, u *User) error { u.Avatar = avatars.HashEmail(seed) - // Don't share the images so that we can delete them easily - if err := storage.SaveFrom(storage.Avatars, u.CustomAvatarRelativePath(), func(w io.Writer) error { - if err := png.Encode(w, img); err != nil { - log.Error("Encode: %v", err) + _, err = storage.Avatars.Stat(u.CustomAvatarRelativePath()) + if err != nil { + // If unable to Stat the avatar file (usually it means non-existing), then try to save a new one + // Don't share the images so that we can delete them easily + if err := storage.SaveFrom(storage.Avatars, u.CustomAvatarRelativePath(), func(w io.Writer) error { + if err := png.Encode(w, img); err != nil { + log.Error("Encode: %v", err) + } + return nil + }); err != nil { + return fmt.Errorf("failed to save avatar %s: %w", u.CustomAvatarRelativePath(), err) } - return err - }); err != nil { - return fmt.Errorf("Failed to create dir %s: %w", u.CustomAvatarRelativePath(), err) } if _, err := db.GetEngine(ctx).ID(u.ID).Cols("avatar").Update(u); err != nil { return err } - log.Info("New random avatar created: %d", u.ID) return nil } // AvatarLinkWithSize returns a link to the user's avatar with size. size <= 0 means default size func (u *User) AvatarLinkWithSize(ctx context.Context, size int) string { - if u.IsGhost() { + // ghost user was deleted, Gitea actions is a bot user, 0 means the user should be a virtual user + // which comes from git configure information + if u.IsGhost() || u.IsGiteaActions() || u.ID <= 0 { return avatars.DefaultAvatarLink() } diff --git a/models/user/avatar_test.go b/models/user/avatar_test.go index 1078875ee1611..a1cc01316f324 100644 --- a/models/user/avatar_test.go +++ b/models/user/avatar_test.go @@ -4,13 +4,19 @@ package user import ( + "context" + "io" + "strings" "testing" "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/storage" "code.gitea.io/gitea/modules/test" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestUserAvatarLink(t *testing.T) { @@ -26,3 +32,37 @@ func TestUserAvatarLink(t *testing.T) { link = u.AvatarLink(db.DefaultContext) assert.Equal(t, "https://localhost/sub-path/avatars/avatar.png", link) } + +func TestUserAvatarGenerate(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + var err error + tmpDir := t.TempDir() + storage.Avatars, err = storage.NewLocalStorage(context.Background(), &setting.Storage{Path: tmpDir}) + require.NoError(t, err) + + u := unittest.AssertExistsAndLoadBean(t, &User{ID: 2}) + + // there was no avatar, generate a new one + assert.Empty(t, u.Avatar) + err = GenerateRandomAvatar(db.DefaultContext, u) + require.NoError(t, err) + assert.NotEmpty(t, u.Avatar) + + // make sure the generated one exists + oldAvatarPath := u.CustomAvatarRelativePath() + _, err = storage.Avatars.Stat(u.CustomAvatarRelativePath()) + require.NoError(t, err) + // and try to change its content + _, err = storage.Avatars.Save(u.CustomAvatarRelativePath(), strings.NewReader("abcd"), 4) + require.NoError(t, err) + + // try to generate again + err = GenerateRandomAvatar(db.DefaultContext, u) + require.NoError(t, err) + assert.Equal(t, oldAvatarPath, u.CustomAvatarRelativePath()) + f, err := storage.Avatars.Open(u.CustomAvatarRelativePath()) + require.NoError(t, err) + defer f.Close() + content, _ := io.ReadAll(f) + assert.Equal(t, "abcd", string(content)) +} diff --git a/models/user/user.go b/models/user/user.go index 19879fbcc7915..97852e916f7de 100644 --- a/models/user/user.go +++ b/models/user/user.go @@ -191,9 +191,9 @@ func (u *User) BeforeUpdate() { } u.LowerName = strings.ToLower(u.Name) - u.Location = util.TruncateRunes(u.Location, 255) - u.Website = util.TruncateRunes(u.Website, 255) - u.Description = util.TruncateRunes(u.Description, 255) + u.Location = base.TruncateString(u.Location, 255) + u.Website = base.TruncateString(u.Website, 255) + u.Description = base.TruncateString(u.Description, 255) } // AfterLoad is invoked from XORM after filling all the fields of this object. @@ -491,9 +491,9 @@ func (u *User) GitName() string { // ShortName ellipses username to length func (u *User) ShortName(length int) string { if setting.UI.DefaultShowFullName && len(u.FullName) > 0 { - return util.EllipsisDisplayString(u.FullName, length) + return base.EllipsisString(u.FullName, length) } - return util.EllipsisDisplayString(u.Name, length) + return base.EllipsisString(u.Name, length) } // IsMailable checks if a user is eligible @@ -778,21 +778,6 @@ func createUser(ctx context.Context, u *User, meta *Meta, createdByAdmin bool, o return committer.Commit() } -// ErrDeleteLastAdminUser represents a "DeleteLastAdminUser" kind of error. -type ErrDeleteLastAdminUser struct { - UID int64 -} - -// IsErrDeleteLastAdminUser checks if an error is a ErrDeleteLastAdminUser. -func IsErrDeleteLastAdminUser(err error) bool { - _, ok := err.(ErrDeleteLastAdminUser) - return ok -} - -func (err ErrDeleteLastAdminUser) Error() string { - return fmt.Sprintf("can not delete the last admin user [uid: %d]", err.UID) -} - // IsLastAdminUser check whether user is the last admin func IsLastAdminUser(ctx context.Context, user *User) bool { if user.IsAdmin && CountUsers(ctx, &CountUserFilter{IsAdmin: optional.Some(true)}) <= 1 { diff --git a/models/user/user_system.go b/models/user/user_system.go index 612cdb2caef26..7ac48f5ea5583 100644 --- a/models/user/user_system.go +++ b/models/user/user_system.go @@ -24,6 +24,10 @@ func NewGhostUser() *User { } } +func IsGhostUserName(name string) bool { + return strings.EqualFold(name, GhostUserName) +} + // IsGhost check if user is fake user for a deleted account func (u *User) IsGhost() bool { if u == nil { @@ -48,6 +52,10 @@ const ( ActionsEmail = "teabot@gitea.io" ) +func IsGiteaActionsUserName(name string) bool { + return strings.EqualFold(name, ActionsUserName) +} + // NewActionsUser creates and returns a fake user for running the actions. func NewActionsUser() *User { return &User{ @@ -65,6 +73,16 @@ func NewActionsUser() *User { } } -func (u *User) IsActions() bool { +func (u *User) IsGiteaActions() bool { return u != nil && u.ID == ActionsUserID } + +func GetSystemUserByName(name string) *User { + if IsGhostUserName(name) { + return NewGhostUser() + } + if IsGiteaActionsUserName(name) { + return NewActionsUser() + } + return nil +} diff --git a/models/user/user_system_test.go b/models/user/user_system_test.go new file mode 100644 index 0000000000000..97768b509be3d --- /dev/null +++ b/models/user/user_system_test.go @@ -0,0 +1,32 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package user + +import ( + "testing" + + "code.gitea.io/gitea/models/db" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestSystemUser(t *testing.T) { + u, err := GetPossibleUserByID(db.DefaultContext, -1) + require.NoError(t, err) + assert.Equal(t, "Ghost", u.Name) + assert.Equal(t, "ghost", u.LowerName) + assert.True(t, u.IsGhost()) + assert.True(t, IsGhostUserName("gHost")) + + u, err = GetPossibleUserByID(db.DefaultContext, -2) + require.NoError(t, err) + assert.Equal(t, "gitea-actions", u.Name) + assert.Equal(t, "gitea-actions", u.LowerName) + assert.True(t, u.IsGiteaActions()) + assert.True(t, IsGiteaActionsUserName("Gitea-actionS")) + + _, err = GetPossibleUserByID(db.DefaultContext, -3) + require.Error(t, err) +} diff --git a/models/webhook/webhook.go b/models/webhook/webhook.go index 894357e36a12d..a17582c0c9aa1 100644 --- a/models/webhook/webhook.go +++ b/models/webhook/webhook.go @@ -299,6 +299,11 @@ func (w *Webhook) HasPackageEvent() bool { (w.ChooseEvents && w.HookEvents.Package) } +func (w *Webhook) HasStatusEvent() bool { + return w.SendEverything || + (w.ChooseEvents && w.HookEvents.Status) +} + // HasPullRequestReviewRequestEvent returns true if hook enabled pull request review request event. func (w *Webhook) HasPullRequestReviewRequestEvent() bool { return w.SendEverything || @@ -337,6 +342,7 @@ func (w *Webhook) EventCheckers() []struct { {w.HasReleaseEvent, webhook_module.HookEventRelease}, {w.HasPackageEvent, webhook_module.HookEventPackage}, {w.HasPullRequestReviewRequestEvent, webhook_module.HookEventPullRequestReviewRequest}, + {w.HasStatusEvent, webhook_module.HookEventStatus}, } } diff --git a/models/webhook/webhook_test.go b/models/webhook/webhook_test.go index c6c3f40d46eff..5e135369e6c53 100644 --- a/models/webhook/webhook_test.go +++ b/models/webhook/webhook_test.go @@ -74,7 +74,7 @@ func TestWebhook_EventsArray(t *testing.T) { "pull_request", "pull_request_assign", "pull_request_label", "pull_request_milestone", "pull_request_comment", "pull_request_review_approved", "pull_request_review_rejected", "pull_request_review_comment", "pull_request_sync", "wiki", "repository", "release", - "package", "pull_request_review_request", + "package", "pull_request_review_request", "status", }, (&Webhook{ HookEvent: &webhook_module.HookEvent{SendEverything: true}, diff --git a/modules/actions/workflows.go b/modules/actions/workflows.go index 0d2b0dd9194d9..a538b6e290bec 100644 --- a/modules/actions/workflows.go +++ b/modules/actions/workflows.go @@ -463,7 +463,7 @@ func matchPullRequestEvent(gitRepo *git.Repository, commit *git.Commit, prPayloa matchTimes++ } case "paths": - filesChanged, err := headCommit.GetFilesChangedSinceCommit(prPayload.PullRequest.Base.Ref) + filesChanged, err := headCommit.GetFilesChangedSinceCommit(prPayload.PullRequest.MergeBase) if err != nil { log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", headCommit.ID.String(), err) } else { @@ -476,7 +476,7 @@ func matchPullRequestEvent(gitRepo *git.Repository, commit *git.Commit, prPayloa } } case "paths-ignore": - filesChanged, err := headCommit.GetFilesChangedSinceCommit(prPayload.PullRequest.Base.Ref) + filesChanged, err := headCommit.GetFilesChangedSinceCommit(prPayload.PullRequest.MergeBase) if err != nil { log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", headCommit.ID.String(), err) } else { diff --git a/modules/auth/pam/pam_test.go b/modules/auth/pam/pam_test.go index 7265b5d0c1507..d4ab058ec7878 100644 --- a/modules/auth/pam/pam_test.go +++ b/modules/auth/pam/pam_test.go @@ -15,5 +15,5 @@ func TestPamAuth(t *testing.T) { result, err := Auth("gitea", "user1", "false-pwd") assert.Error(t, err) assert.EqualError(t, err, "Authentication failure") - assert.Len(t, result) + assert.Empty(t, result) } diff --git a/modules/base/base.go b/modules/base/base.go new file mode 100644 index 0000000000000..dddce202daf77 --- /dev/null +++ b/modules/base/base.go @@ -0,0 +1,9 @@ +// Copyright 2014 The Gogs Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package base + +type ( + // TplName template relative path type + TplName string +) diff --git a/modules/base/tool.go b/modules/base/tool.go index 1d16186bc5d01..928c80700b828 100644 --- a/modules/base/tool.go +++ b/modules/base/tool.go @@ -13,14 +13,17 @@ import ( "errors" "fmt" "hash" + "os" + "path/filepath" + "runtime" "strconv" "strings" "time" + "unicode/utf8" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/util" "github.com/dustin/go-humanize" ) @@ -35,7 +38,7 @@ func EncodeSha256(str string) string { // ShortSha is basically just truncating. // It is DEPRECATED and will be removed in the future. func ShortSha(sha1 string) string { - return util.TruncateRunes(sha1, 10) + return TruncateString(sha1, 10) } // BasicAuthDecode decode basic auth string @@ -116,6 +119,27 @@ func FileSize(s int64) string { return humanize.IBytes(uint64(s)) } +// EllipsisString returns a truncated short string, +// it appends '...' in the end of the length of string is too large. +func EllipsisString(str string, length int) string { + if length <= 3 { + return "..." + } + if utf8.RuneCountInString(str) <= length { + return str + } + return string([]rune(str)[:length-3]) + "..." +} + +// TruncateString returns a truncated string with given limit, +// it returns input string if length is not reached limit. +func TruncateString(str string, limit int) string { + if utf8.RuneCountInString(str) < limit { + return str + } + return string([]rune(str)[:limit]) +} + // StringsToInt64s converts a slice of string to a slice of int64. func StringsToInt64s(strs []string) ([]int64, error) { if strs == nil { @@ -165,3 +189,49 @@ func EntryIcon(entry *git.TreeEntry) string { return "file" } + +// SetupGiteaRoot Sets GITEA_ROOT if it is not already set and returns the value +func SetupGiteaRoot() string { + giteaRoot := os.Getenv("GITEA_ROOT") + if giteaRoot == "" { + _, filename, _, _ := runtime.Caller(0) + giteaRoot = strings.TrimSuffix(filename, "modules/base/tool.go") + wd, err := os.Getwd() + if err != nil { + rel, err := filepath.Rel(giteaRoot, wd) + if err != nil && strings.HasPrefix(filepath.ToSlash(rel), "../") { + giteaRoot = wd + } + } + if _, err := os.Stat(filepath.Join(giteaRoot, "gitea")); os.IsNotExist(err) { + giteaRoot = "" + } else if err := os.Setenv("GITEA_ROOT", giteaRoot); err != nil { + giteaRoot = "" + } + } + return giteaRoot +} + +// FormatNumberSI format a number +func FormatNumberSI(data any) string { + var num int64 + if num1, ok := data.(int64); ok { + num = num1 + } else if num1, ok := data.(int); ok { + num = int64(num1) + } else { + return "" + } + + if num < 1000 { + return fmt.Sprintf("%d", num) + } else if num < 1000000 { + num2 := float32(num) / float32(1000.0) + return fmt.Sprintf("%.1fk", num2) + } else if num < 1000000000 { + num2 := float32(num) / float32(1000000.0) + return fmt.Sprintf("%.1fM", num2) + } + num2 := float32(num) / float32(1000000000.0) + return fmt.Sprintf("%.1fG", num2) +} diff --git a/modules/base/tool_test.go b/modules/base/tool_test.go index c821a55c19c0e..f63679048e318 100644 --- a/modules/base/tool_test.go +++ b/modules/base/tool_test.go @@ -113,6 +113,36 @@ func TestFileSize(t *testing.T) { assert.Equal(t, "2.0 EiB", FileSize(size)) } +func TestEllipsisString(t *testing.T) { + assert.Equal(t, "...", EllipsisString("foobar", 0)) + assert.Equal(t, "...", EllipsisString("foobar", 1)) + assert.Equal(t, "...", EllipsisString("foobar", 2)) + assert.Equal(t, "...", EllipsisString("foobar", 3)) + assert.Equal(t, "f...", EllipsisString("foobar", 4)) + assert.Equal(t, "fo...", EllipsisString("foobar", 5)) + assert.Equal(t, "foobar", EllipsisString("foobar", 6)) + assert.Equal(t, "foobar", EllipsisString("foobar", 10)) + assert.Equal(t, "测...", EllipsisString("测试文本一二三四", 4)) + assert.Equal(t, "测试...", EllipsisString("测试文本一二三四", 5)) + assert.Equal(t, "测试文...", EllipsisString("测试文本一二三四", 6)) + assert.Equal(t, "测试文本一二三四", EllipsisString("测试文本一二三四", 10)) +} + +func TestTruncateString(t *testing.T) { + assert.Equal(t, "", TruncateString("foobar", 0)) + assert.Equal(t, "f", TruncateString("foobar", 1)) + assert.Equal(t, "fo", TruncateString("foobar", 2)) + assert.Equal(t, "foo", TruncateString("foobar", 3)) + assert.Equal(t, "foob", TruncateString("foobar", 4)) + assert.Equal(t, "fooba", TruncateString("foobar", 5)) + assert.Equal(t, "foobar", TruncateString("foobar", 6)) + assert.Equal(t, "foobar", TruncateString("foobar", 7)) + assert.Equal(t, "测试文本", TruncateString("测试文本一二三四", 4)) + assert.Equal(t, "测试文本一", TruncateString("测试文本一二三四", 5)) + assert.Equal(t, "测试文本一二", TruncateString("测试文本一二三四", 6)) + assert.Equal(t, "测试文本一二三", TruncateString("测试文本一二三四", 7)) +} + func TestStringsToInt64s(t *testing.T) { testSuccess := func(input []string, expected []int64) { result, err := StringsToInt64s(input) @@ -139,3 +169,18 @@ func TestInt64sToStrings(t *testing.T) { } // TODO: Test EntryIcon + +func TestSetupGiteaRoot(t *testing.T) { + t.Setenv("GITEA_ROOT", "test") + assert.Equal(t, "test", SetupGiteaRoot()) + t.Setenv("GITEA_ROOT", "") + assert.NotEqual(t, "test", SetupGiteaRoot()) +} + +func TestFormatNumberSI(t *testing.T) { + assert.Equal(t, "125", FormatNumberSI(int(125))) + assert.Equal(t, "1.3k", FormatNumberSI(int64(1317))) + assert.Equal(t, "21.3M", FormatNumberSI(21317675)) + assert.Equal(t, "45.7G", FormatNumberSI(45721317675)) + assert.Equal(t, "", FormatNumberSI("test")) +} diff --git a/modules/cache/cache.go b/modules/cache/cache.go index b5400b0bd6a14..f7828e3cae2df 100644 --- a/modules/cache/cache.go +++ b/modules/cache/cache.go @@ -37,10 +37,15 @@ func Init() error { } const ( - testCacheKey = "DefaultCache.TestKey" - SlowCacheThreshold = 100 * time.Microsecond + testCacheKey = "DefaultCache.TestKey" + // SlowCacheThreshold marks cache tests as slow + // set to 30ms per discussion: https://github.com/go-gitea/gitea/issues/33190 + // TODO: Replace with metrics histogram + SlowCacheThreshold = 30 * time.Millisecond ) +// Test performs delete, put and get operations on a predefined key +// returns func Test() (time.Duration, error) { if defaultCache == nil { return 0, fmt.Errorf("default cache not initialized") diff --git a/modules/cache/cache_test.go b/modules/cache/cache_test.go index d0352947a831a..5408020306b38 100644 --- a/modules/cache/cache_test.go +++ b/modules/cache/cache_test.go @@ -43,7 +43,8 @@ func TestTest(t *testing.T) { elapsed, err := Test() assert.NoError(t, err) // mem cache should take from 300ns up to 1ms on modern hardware ... - assert.Less(t, elapsed, time.Millisecond) + assert.Positive(t, elapsed) + assert.Less(t, elapsed, SlowCacheThreshold) } func TestGetCache(t *testing.T) { diff --git a/modules/git/batch_reader.go b/modules/git/batch_reader.go index 532dbad9894b5..4e9f854ad9dc6 100644 --- a/modules/git/batch_reader.go +++ b/modules/git/batch_reader.go @@ -7,8 +7,10 @@ import ( "bufio" "bytes" "context" + "fmt" "io" "math" + "runtime" "strconv" "strings" @@ -30,6 +32,7 @@ type WriteCloserError interface { func ensureValidGitRepository(ctx context.Context, repoPath string) error { stderr := strings.Builder{} err := NewCommand(ctx, "rev-parse"). + SetDescription(fmt.Sprintf("%s rev-parse [repo_path: %s]", GitExecutable, repoPath)). Run(&RunOpts{ Dir: repoPath, Stderr: &stderr, @@ -59,9 +62,13 @@ func catFileBatchCheck(ctx context.Context, repoPath string) (WriteCloserError, cancel() }() + _, filename, line, _ := runtime.Caller(2) + filename = strings.TrimPrefix(filename, callerPrefix) + go func() { stderr := strings.Builder{} err := NewCommand(ctx, "cat-file", "--batch-check"). + SetDescription(fmt.Sprintf("%s cat-file --batch-check [repo_path: %s] (%s:%d)", GitExecutable, repoPath, filename, line)). Run(&RunOpts{ Dir: repoPath, Stdin: batchStdinReader, @@ -107,9 +114,13 @@ func catFileBatch(ctx context.Context, repoPath string) (WriteCloserError, *bufi cancel() }() + _, filename, line, _ := runtime.Caller(2) + filename = strings.TrimPrefix(filename, callerPrefix) + go func() { stderr := strings.Builder{} err := NewCommand(ctx, "cat-file", "--batch"). + SetDescription(fmt.Sprintf("%s cat-file --batch [repo_path: %s] (%s:%d)", GitExecutable, repoPath, filename, line)). Run(&RunOpts{ Dir: repoPath, Stdin: batchStdinReader, @@ -242,7 +253,7 @@ func BinToHex(objectFormat ObjectFormat, sha, out []byte) []byte { return out } -// ParseTreeLine reads an entry from a tree in a cat-file --batch stream +// ParseCatFileTreeLine reads an entry from a tree in a cat-file --batch stream // This carefully avoids allocations - except where fnameBuf is too small. // It is recommended therefore to pass in an fnameBuf large enough to avoid almost all allocations // @@ -250,7 +261,7 @@ func BinToHex(objectFormat ObjectFormat, sha, out []byte) []byte { // SP NUL // // We don't attempt to convert the raw HASH to save a lot of time -func ParseTreeLine(objectFormat ObjectFormat, rd *bufio.Reader, modeBuf, fnameBuf, shaBuf []byte) (mode, fname, sha []byte, n int, err error) { +func ParseCatFileTreeLine(objectFormat ObjectFormat, rd *bufio.Reader, modeBuf, fnameBuf, shaBuf []byte) (mode, fname, sha []byte, n int, err error) { var readBytes []byte // Read the Mode & fname @@ -260,7 +271,7 @@ func ParseTreeLine(objectFormat ObjectFormat, rd *bufio.Reader, modeBuf, fnameBu } idx := bytes.IndexByte(readBytes, ' ') if idx < 0 { - log.Debug("missing space in readBytes ParseTreeLine: %s", readBytes) + log.Debug("missing space in readBytes ParseCatFileTreeLine: %s", readBytes) return mode, fname, sha, n, &ErrNotExist{} } @@ -309,6 +320,13 @@ func ParseTreeLine(objectFormat ObjectFormat, rd *bufio.Reader, modeBuf, fnameBu return mode, fname, sha, n, err } +var callerPrefix string + +func init() { + _, filename, _, _ := runtime.Caller(0) + callerPrefix = strings.TrimSuffix(filename, "modules/git/batch_reader.go") +} + func DiscardFull(rd *bufio.Reader, discard int64) error { if discard > math.MaxInt32 { n, err := rd.Discard(math.MaxInt32) diff --git a/modules/git/blame.go b/modules/git/blame.go index cad720edf43a1..a9b2706f21739 100644 --- a/modules/git/blame.go +++ b/modules/git/blame.go @@ -7,6 +7,7 @@ import ( "bufio" "bytes" "context" + "fmt" "io" "os" @@ -141,7 +142,9 @@ func CreateBlameReader(ctx context.Context, objectFormat ObjectFormat, repoPath // There is no equivalent on Windows. May be implemented if Gitea uses an external git backend. cmd.AddOptionValues("--ignore-revs-file", *ignoreRevsFile) } - cmd.AddDynamicArguments(commit.ID.String()).AddDashesAndList(file) + cmd.AddDynamicArguments(commit.ID.String()). + AddDashesAndList(file). + SetDescription(fmt.Sprintf("GetBlame [repo_path: %s]", repoPath)) reader, stdout, err := os.Pipe() if err != nil { if ignoreRevsFile != nil { diff --git a/modules/git/command.go b/modules/git/command.go index b231c3beea012..dc75adfacefca 100644 --- a/modules/git/command.go +++ b/modules/git/command.go @@ -12,7 +12,6 @@ import ( "io" "os" "os/exec" - "path/filepath" "runtime" "strings" "time" @@ -44,24 +43,19 @@ type Command struct { prog string args []string parentContext context.Context + desc string globalArgsLength int brokenArgs []string + cmd *exec.Cmd // for debug purpose only } -func logArgSanitize(arg string) string { - if strings.Contains(arg, "://") && strings.Contains(arg, "@") { - return util.SanitizeCredentialURLs(arg) - } else if filepath.IsAbs(arg) { - base := filepath.Base(arg) - dir := filepath.Dir(arg) - return filepath.Join(filepath.Base(dir), base) - } - return arg +func (c *Command) String() string { + return c.toString(false) } -func (c *Command) LogString() string { +func (c *Command) toString(sanitizing bool) string { // WARNING: this function is for debugging purposes only. It's much better than old code (which only joins args with space), - // It's impossible to make a simple and 100% correct implementation of argument quoting for different platforms here. + // It's impossible to make a simple and 100% correct implementation of argument quoting for different platforms. debugQuote := func(s string) string { if strings.ContainsAny(s, " `'\"\t\r\n") { return fmt.Sprintf("%q", s) @@ -70,11 +64,12 @@ func (c *Command) LogString() string { } a := make([]string, 0, len(c.args)+1) a = append(a, debugQuote(c.prog)) - if c.globalArgsLength > 0 { - a = append(a, "...global...") - } - for i := c.globalArgsLength; i < len(c.args); i++ { - a = append(a, debugQuote(logArgSanitize(c.args[i]))) + for _, arg := range c.args { + if sanitizing && (strings.Contains(arg, "://") && strings.Contains(arg, "@")) { + a = append(a, debugQuote(util.SanitizeCredentialURLs(arg))) + } else { + a = append(a, debugQuote(arg)) + } } return strings.Join(a, " ") } @@ -118,6 +113,12 @@ func (c *Command) SetParentContext(ctx context.Context) *Command { return c } +// SetDescription sets the description for this command which be returned on c.String() +func (c *Command) SetDescription(desc string) *Command { + c.desc = desc + return c +} + // isSafeArgumentValue checks if the argument is safe to be used as a value (not an option) func isSafeArgumentValue(s string) bool { return s == "" || s[0] != '-' @@ -271,12 +272,8 @@ var ErrBrokenCommand = errors.New("git command is broken") // Run runs the command with the RunOpts func (c *Command) Run(opts *RunOpts) error { - return c.run(1, opts) -} - -func (c *Command) run(skip int, opts *RunOpts) error { if len(c.brokenArgs) != 0 { - log.Error("git command is broken: %s, broken args: %s", c.LogString(), strings.Join(c.brokenArgs, " ")) + log.Error("git command is broken: %s, broken args: %s", c.String(), strings.Join(c.brokenArgs, " ")) return ErrBrokenCommand } if opts == nil { @@ -289,14 +286,20 @@ func (c *Command) run(skip int, opts *RunOpts) error { timeout = defaultCommandExecutionTimeout } - var desc string - callerInfo := util.CallerFuncName(1 /* util */ + 1 /* this */ + skip /* parent */) - if pos := strings.LastIndex(callerInfo, "/"); pos >= 0 { - callerInfo = callerInfo[pos+1:] + if len(opts.Dir) == 0 { + log.Debug("git.Command.Run: %s", c) + } else { + log.Debug("git.Command.RunDir(%s): %s", opts.Dir, c) + } + + desc := c.desc + if desc == "" { + if opts.Dir == "" { + desc = fmt.Sprintf("git: %s", c.toString(true)) + } else { + desc = fmt.Sprintf("git(dir:%s): %s", opts.Dir, c.toString(true)) + } } - // these logs are for debugging purposes only, so no guarantee of correctness or stability - desc = fmt.Sprintf("git.Run(by:%s, repo:%s): %s", callerInfo, logArgSanitize(opts.Dir), c.LogString()) - log.Debug("git.Command: %s", desc) var ctx context.Context var cancel context.CancelFunc @@ -312,6 +315,7 @@ func (c *Command) run(skip int, opts *RunOpts) error { startTime := time.Now() cmd := exec.CommandContext(ctx, c.prog, c.args...) + c.cmd = cmd // for debug purpose only if opts.Env == nil { cmd.Env = os.Environ() } else { @@ -346,9 +350,10 @@ func (c *Command) run(skip int, opts *RunOpts) error { // We need to check if the context is canceled by the program on Windows. // This is because Windows does not have signal checking when terminating the process. // It always returns exit code 1, unlike Linux, which has many exit codes for signals. + // `err.Error()` returns "exit status 1" when using the `git check-attr` command after the context is canceled. if runtime.GOOS == "windows" && err != nil && - err.Error() == "" && + (err.Error() == "" || err.Error() == "exit status 1") && cmd.ProcessState.ExitCode() == 1 && ctx.Err() == context.Canceled { return ctx.Err() @@ -399,7 +404,7 @@ func IsErrorExitCode(err error, code int) bool { // RunStdString runs the command with options and returns stdout/stderr as string. and store stderr to returned error (err combined with stderr). func (c *Command) RunStdString(opts *RunOpts) (stdout, stderr string, runErr RunStdError) { - stdoutBytes, stderrBytes, err := c.runStdBytes(opts) + stdoutBytes, stderrBytes, err := c.RunStdBytes(opts) stdout = util.UnsafeBytesToString(stdoutBytes) stderr = util.UnsafeBytesToString(stderrBytes) if err != nil { @@ -411,10 +416,6 @@ func (c *Command) RunStdString(opts *RunOpts) (stdout, stderr string, runErr Run // RunStdBytes runs the command with options and returns stdout/stderr as bytes. and store stderr to returned error (err combined with stderr). func (c *Command) RunStdBytes(opts *RunOpts) (stdout, stderr []byte, runErr RunStdError) { - return c.runStdBytes(opts) -} - -func (c *Command) runStdBytes(opts *RunOpts) (stdout, stderr []byte, runErr RunStdError) { if opts == nil { opts = &RunOpts{} } @@ -437,7 +438,7 @@ func (c *Command) runStdBytes(opts *RunOpts) (stdout, stderr []byte, runErr RunS PipelineFunc: opts.PipelineFunc, } - err := c.run(2, newOpts) + err := c.Run(newOpts) stderr = stderrBuf.Bytes() if err != nil { return nil, stderr, &runStdError{err: err, stderr: util.UnsafeBytesToString(stderr)} diff --git a/modules/git/command_test.go b/modules/git/command_test.go index 0823afd7f76a9..9a6228c9ad05f 100644 --- a/modules/git/command_test.go +++ b/modules/git/command_test.go @@ -55,8 +55,8 @@ func TestGitArgument(t *testing.T) { func TestCommandString(t *testing.T) { cmd := NewCommandContextNoGlobals(context.Background(), "a", "-m msg", "it's a test", `say "hello"`) - assert.EqualValues(t, cmd.prog+` a "-m msg" "it's a test" "say \"hello\""`, cmd.LogString()) + assert.EqualValues(t, cmd.prog+` a "-m msg" "it's a test" "say \"hello\""`, cmd.String()) - cmd = NewCommandContextNoGlobals(context.Background(), "url: https://a:b@c/", "/root/dir-a/dir-b") - assert.EqualValues(t, cmd.prog+` "url: https://sanitized-credential@c/" dir-a/dir-b`, cmd.LogString()) + cmd = NewCommandContextNoGlobals(context.Background(), "url: https://a:b@c/") + assert.EqualValues(t, cmd.prog+` "url: https://sanitized-credential@c/"`, cmd.toString(true)) } diff --git a/modules/git/commit_test.go b/modules/git/commit_test.go index 6ac65564dc179..ee20e95c4517f 100644 --- a/modules/git/commit_test.go +++ b/modules/git/commit_test.go @@ -360,5 +360,5 @@ func Test_GetCommitBranchStart(t *testing.T) { startCommitID, err := repo.GetCommitBranchStart(os.Environ(), "branch1", commit.ID.String()) assert.NoError(t, err) assert.NotEmpty(t, startCommitID) - assert.EqualValues(t, "9c9aef8dd84e02bc7ec12641deb4c930a7c30185", startCommitID) + assert.EqualValues(t, "95bb4d39648ee7e325106df01a621c530863a653", startCommitID) } diff --git a/modules/git/parse.go b/modules/git/parse.go new file mode 100644 index 0000000000000..eb26632cc0e5c --- /dev/null +++ b/modules/git/parse.go @@ -0,0 +1,78 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package git + +import ( + "bytes" + "fmt" + "strconv" + "strings" + + "code.gitea.io/gitea/modules/optional" +) + +var sepSpace = []byte{' '} + +type LsTreeEntry struct { + ID ObjectID + EntryMode EntryMode + Name string + Size optional.Option[int64] +} + +func parseLsTreeLine(line []byte) (*LsTreeEntry, error) { + // expect line to be of the form: + // \t + // \t + + var err error + posTab := bytes.IndexByte(line, '\t') + if posTab == -1 { + return nil, fmt.Errorf("invalid ls-tree output (no tab): %q", line) + } + + entry := new(LsTreeEntry) + + entryAttrs := line[:posTab] + entryName := line[posTab+1:] + + entryMode, entryAttrs, _ := bytes.Cut(entryAttrs, sepSpace) + _ /* entryType */, entryAttrs, _ = bytes.Cut(entryAttrs, sepSpace) // the type is not used, the mode is enough to determine the type + entryObjectID, entryAttrs, _ := bytes.Cut(entryAttrs, sepSpace) + if len(entryAttrs) > 0 { + entrySize := entryAttrs // the last field is the space-padded-size + size, _ := strconv.ParseInt(strings.TrimSpace(string(entrySize)), 10, 64) + entry.Size = optional.Some(size) + } + + switch string(entryMode) { + case "100644": + entry.EntryMode = EntryModeBlob + case "100755": + entry.EntryMode = EntryModeExec + case "120000": + entry.EntryMode = EntryModeSymlink + case "160000": + entry.EntryMode = EntryModeCommit + case "040000", "040755": // git uses 040000 for tree object, but some users may get 040755 for unknown reasons + entry.EntryMode = EntryModeTree + default: + return nil, fmt.Errorf("unknown type: %v", string(entryMode)) + } + + entry.ID, err = NewIDFromString(string(entryObjectID)) + if err != nil { + return nil, fmt.Errorf("invalid ls-tree output (invalid object id): %q, err: %w", line, err) + } + + if len(entryName) > 0 && entryName[0] == '"' { + entry.Name, err = strconv.Unquote(string(entryName)) + if err != nil { + return nil, fmt.Errorf("invalid ls-tree output (invalid name): %q, err: %w", line, err) + } + } else { + entry.Name = string(entryName) + } + return entry, nil +} diff --git a/modules/git/parse_nogogit.go b/modules/git/parse_nogogit.go index 546b38be37964..676bb3c76c09f 100644 --- a/modules/git/parse_nogogit.go +++ b/modules/git/parse_nogogit.go @@ -10,8 +10,6 @@ import ( "bytes" "fmt" "io" - "strconv" - "strings" "code.gitea.io/gitea/modules/log" ) @@ -21,71 +19,30 @@ func ParseTreeEntries(data []byte) ([]*TreeEntry, error) { return parseTreeEntries(data, nil) } -var sepSpace = []byte{' '} - +// parseTreeEntries FIXME this function's design is not right, it should make the caller read all data into memory func parseTreeEntries(data []byte, ptree *Tree) ([]*TreeEntry, error) { - var err error entries := make([]*TreeEntry, 0, bytes.Count(data, []byte{'\n'})+1) for pos := 0; pos < len(data); { - // expect line to be of the form: - // \t - // \t posEnd := bytes.IndexByte(data[pos:], '\n') if posEnd == -1 { posEnd = len(data) } else { posEnd += pos } - line := data[pos:posEnd] - posTab := bytes.IndexByte(line, '\t') - if posTab == -1 { - return nil, fmt.Errorf("invalid ls-tree output (no tab): %q", line) - } - - entry := new(TreeEntry) - entry.ptree = ptree - - entryAttrs := line[:posTab] - entryName := line[posTab+1:] - - entryMode, entryAttrs, _ := bytes.Cut(entryAttrs, sepSpace) - _ /* entryType */, entryAttrs, _ = bytes.Cut(entryAttrs, sepSpace) // the type is not used, the mode is enough to determine the type - entryObjectID, entryAttrs, _ := bytes.Cut(entryAttrs, sepSpace) - if len(entryAttrs) > 0 { - entrySize := entryAttrs // the last field is the space-padded-size - entry.size, _ = strconv.ParseInt(strings.TrimSpace(string(entrySize)), 10, 64) - entry.sized = true - } - switch string(entryMode) { - case "100644": - entry.entryMode = EntryModeBlob - case "100755": - entry.entryMode = EntryModeExec - case "120000": - entry.entryMode = EntryModeSymlink - case "160000": - entry.entryMode = EntryModeCommit - case "040000", "040755": // git uses 040000 for tree object, but some users may get 040755 for unknown reasons - entry.entryMode = EntryModeTree - default: - return nil, fmt.Errorf("unknown type: %v", string(entryMode)) - } - - entry.ID, err = NewIDFromString(string(entryObjectID)) + line := data[pos:posEnd] + lsTreeLine, err := parseLsTreeLine(line) if err != nil { - return nil, fmt.Errorf("invalid ls-tree output (invalid object id): %q, err: %w", line, err) + return nil, err } - - if len(entryName) > 0 && entryName[0] == '"' { - entry.name, err = strconv.Unquote(string(entryName)) - if err != nil { - return nil, fmt.Errorf("invalid ls-tree output (invalid name): %q, err: %w", line, err) - } - } else { - entry.name = string(entryName) + entry := &TreeEntry{ + ptree: ptree, + ID: lsTreeLine.ID, + entryMode: lsTreeLine.EntryMode, + name: lsTreeLine.Name, + size: lsTreeLine.Size.Value(), + sized: lsTreeLine.Size.Has(), } - pos = posEnd + 1 entries = append(entries, entry) } @@ -100,7 +57,7 @@ func catBatchParseTreeEntries(objectFormat ObjectFormat, ptree *Tree, rd *bufio. loop: for sz > 0 { - mode, fname, sha, count, err := ParseTreeLine(objectFormat, rd, modeBuf, fnameBuf, shaBuf) + mode, fname, sha, count, err := ParseCatFileTreeLine(objectFormat, rd, modeBuf, fnameBuf, shaBuf) if err != nil { if err == io.EOF { break loop diff --git a/modules/git/pipeline/lfs_nogogit.go b/modules/git/pipeline/lfs_nogogit.go index b22805c1327e9..92e35c5a1028a 100644 --- a/modules/git/pipeline/lfs_nogogit.go +++ b/modules/git/pipeline/lfs_nogogit.go @@ -114,7 +114,7 @@ func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, err case "tree": var n int64 for n < size { - mode, fname, binObjectID, count, err := git.ParseTreeLine(objectID.Type(), batchReader, modeBuf, fnameBuf, workingShaBuf) + mode, fname, binObjectID, count, err := git.ParseCatFileTreeLine(objectID.Type(), batchReader, modeBuf, fnameBuf, workingShaBuf) if err != nil { return nil, err } diff --git a/modules/git/ref_test.go b/modules/git/ref_test.go index 58f679b7d6ee8..1fd33b5163ead 100644 --- a/modules/git/ref_test.go +++ b/modules/git/ref_test.go @@ -20,6 +20,8 @@ func TestRefName(t *testing.T) { // Test pull names assert.Equal(t, "1", RefName("refs/pull/1/head").PullName()) + assert.True(t, RefName("refs/pull/1/head").IsPull()) + assert.True(t, RefName("refs/pull/1/merge").IsPull()) assert.Equal(t, "my/pull", RefName("refs/pull/my/pull/head").PullName()) // Test for branch names diff --git a/modules/git/remote.go b/modules/git/remote.go index de8d74eded75f..a872b3b82e8bf 100644 --- a/modules/git/remote.go +++ b/modules/git/remote.go @@ -5,12 +5,9 @@ package git import ( "context" - "fmt" - "net/url" "strings" giturl "code.gitea.io/gitea/modules/git/url" - "code.gitea.io/gitea/modules/util" ) // GetRemoteAddress returns remote url of git repository in the repoPath with special remote name @@ -42,60 +39,11 @@ func GetRemoteURL(ctx context.Context, repoPath, remoteName string) (*giturl.Git return giturl.Parse(addr) } -// ErrInvalidCloneAddr represents a "InvalidCloneAddr" kind of error. -type ErrInvalidCloneAddr struct { - Host string - IsURLError bool - IsInvalidPath bool - IsProtocolInvalid bool - IsPermissionDenied bool - LocalPath bool -} - -// IsErrInvalidCloneAddr checks if an error is a ErrInvalidCloneAddr. -func IsErrInvalidCloneAddr(err error) bool { - _, ok := err.(*ErrInvalidCloneAddr) - return ok -} - -func (err *ErrInvalidCloneAddr) Error() string { - if err.IsInvalidPath { - return fmt.Sprintf("migration/cloning from '%s' is not allowed: the provided path is invalid", err.Host) - } - if err.IsProtocolInvalid { - return fmt.Sprintf("migration/cloning from '%s' is not allowed: the provided url protocol is not allowed", err.Host) - } - if err.IsPermissionDenied { - return fmt.Sprintf("migration/cloning from '%s' is not allowed.", err.Host) - } - if err.IsURLError { - return fmt.Sprintf("migration/cloning from '%s' is not allowed: the provided url is invalid", err.Host) - } - - return fmt.Sprintf("migration/cloning from '%s' is not allowed", err.Host) -} - -func (err *ErrInvalidCloneAddr) Unwrap() error { - return util.ErrInvalidArgument -} - -// ParseRemoteAddr checks if given remote address is valid, -// and returns composed URL with needed username and password. -func ParseRemoteAddr(remoteAddr, authUsername, authPassword string) (string, error) { - remoteAddr = strings.TrimSpace(remoteAddr) - // Remote address can be HTTP/HTTPS/Git URL or local path. - if strings.HasPrefix(remoteAddr, "http://") || - strings.HasPrefix(remoteAddr, "https://") || - strings.HasPrefix(remoteAddr, "git://") { - u, err := url.Parse(remoteAddr) - if err != nil { - return "", &ErrInvalidCloneAddr{IsURLError: true, Host: remoteAddr} - } - if len(authUsername)+len(authPassword) > 0 { - u.User = url.UserPassword(authUsername, authPassword) - } - remoteAddr = u.String() - } - - return remoteAddr, nil +// IsRemoteNotExistError checks the prefix of the error message to see whether a remote does not exist. +func IsRemoteNotExistError(err error) bool { + // see: https://github.com/go-gitea/gitea/issues/32889#issuecomment-2571848216 + // Should not add space in the end, sometimes git will add a `:` + prefix1 := "exit status 128 - fatal: No such remote" // git < 2.30 + prefix2 := "exit status 2 - error: No such remote" // git >= 2.30 + return strings.HasPrefix(err.Error(), prefix1) || strings.HasPrefix(err.Error(), prefix2) } diff --git a/modules/git/repo.go b/modules/git/repo.go index 0993a4ac37d08..fc6e6e7accbb2 100644 --- a/modules/git/repo.go +++ b/modules/git/repo.go @@ -18,6 +18,7 @@ import ( "time" "code.gitea.io/gitea/modules/proxy" + "code.gitea.io/gitea/modules/util" ) // GPGSettings represents the default GPG settings for this repository @@ -159,6 +160,12 @@ func CloneWithArgs(ctx context.Context, args TrustedCmdArgs, from, to string, op } cmd.AddDashesAndList(from, to) + if strings.Contains(from, "://") && strings.Contains(from, "@") { + cmd.SetDescription(fmt.Sprintf("clone branch %s from %s to %s (shared: %t, mirror: %t, depth: %d)", opts.Branch, util.SanitizeCredentialURLs(from), to, opts.Shared, opts.Mirror, opts.Depth)) + } else { + cmd.SetDescription(fmt.Sprintf("clone branch %s from %s to %s (shared: %t, mirror: %t, depth: %d)", opts.Branch, from, to, opts.Shared, opts.Mirror, opts.Depth)) + } + if opts.Timeout <= 0 { opts.Timeout = -1 } @@ -206,6 +213,12 @@ func Push(ctx context.Context, repoPath string, opts PushOptions) error { } cmd.AddDashesAndList(remoteBranchArgs...) + if strings.Contains(opts.Remote, "://") && strings.Contains(opts.Remote, "@") { + cmd.SetDescription(fmt.Sprintf("push branch %s to %s (force: %t, mirror: %t)", opts.Branch, util.SanitizeCredentialURLs(opts.Remote), opts.Force, opts.Mirror)) + } else { + cmd.SetDescription(fmt.Sprintf("push branch %s to %s (force: %t, mirror: %t)", opts.Branch, opts.Remote, opts.Force, opts.Mirror)) + } + stdout, stderr, err := cmd.RunStdString(&RunOpts{Env: opts.Env, Timeout: opts.Timeout, Dir: repoPath}) if err != nil { if strings.Contains(stderr, "non-fast-forward") { diff --git a/modules/git/repo_attribute.go b/modules/git/repo_attribute.go index 90eb783fe8761..e7c6c1a47fa08 100644 --- a/modules/git/repo_attribute.go +++ b/modules/git/repo_attribute.go @@ -9,6 +9,8 @@ import ( "fmt" "io" "os" + "path/filepath" + "time" "code.gitea.io/gitea/modules/log" ) @@ -102,7 +104,7 @@ type CheckAttributeReader struct { stdinReader io.ReadCloser stdinWriter *os.File - stdOut attributeWriter + stdOut *nulSeparatedAttributeWriter cmd *Command env []string ctx context.Context @@ -152,7 +154,6 @@ func (c *CheckAttributeReader) Init(ctx context.Context) error { return nil } -// Run run cmd func (c *CheckAttributeReader) Run() error { defer func() { _ = c.stdinReader.Close() @@ -176,7 +177,7 @@ func (c *CheckAttributeReader) Run() error { func (c *CheckAttributeReader) CheckPath(path string) (rs map[string]string, err error) { defer func() { if err != nil && err != c.ctx.Err() { - log.Error("Unexpected error when checking path %s in %s. Error: %v", path, c.Repo.Path, err) + log.Error("Unexpected error when checking path %s in %s, error: %v", path, filepath.Base(c.Repo.Path), err) } }() @@ -191,9 +192,31 @@ func (c *CheckAttributeReader) CheckPath(path string) (rs map[string]string, err return nil, err } + reportTimeout := func() error { + stdOutClosed := false + select { + case <-c.stdOut.closed: + stdOutClosed = true + default: + } + debugMsg := fmt.Sprintf("check path %q in repo %q", path, filepath.Base(c.Repo.Path)) + debugMsg += fmt.Sprintf(", stdOut: tmp=%q, pos=%d, closed=%v", string(c.stdOut.tmp), c.stdOut.pos, stdOutClosed) + if c.cmd.cmd != nil { + debugMsg += fmt.Sprintf(", process state: %q", c.cmd.cmd.ProcessState.String()) + } + _ = c.Close() + return fmt.Errorf("CheckPath timeout: %s", debugMsg) + } + rs = make(map[string]string) for range c.Attributes { select { + case <-time.After(5 * time.Second): + // There is a strange "hang" problem in gitdiff.GetDiff -> CheckPath + // So add a timeout here to mitigate the problem, and output more logs for debug purpose + // In real world, if CheckPath runs long than seconds, it blocks the end user's operation, + // and at the moment the CheckPath result is not so important, so we can just ignore it. + return nil, reportTimeout() case attr, ok := <-c.stdOut.ReadAttribute(): if !ok { return nil, c.ctx.Err() @@ -206,18 +229,12 @@ func (c *CheckAttributeReader) CheckPath(path string) (rs map[string]string, err return rs, nil } -// Close close pip after use func (c *CheckAttributeReader) Close() error { c.cancel() err := c.stdinWriter.Close() return err } -type attributeWriter interface { - io.WriteCloser - ReadAttribute() <-chan attributeTriple -} - type attributeTriple struct { Filename string Attribute string @@ -263,7 +280,7 @@ func (wr *nulSeparatedAttributeWriter) Write(p []byte) (n int, err error) { } } wr.tmp = append(wr.tmp, p...) - return len(p), nil + return l, nil } func (wr *nulSeparatedAttributeWriter) ReadAttribute() <-chan attributeTriple { @@ -281,7 +298,7 @@ func (wr *nulSeparatedAttributeWriter) Close() error { return nil } -// Create a check attribute reader for the current repository and provided commit ID +// CheckAttributeReader creates a check attribute reader for the current repository and provided commit ID func (repo *Repository) CheckAttributeReader(commitID string) (*CheckAttributeReader, context.CancelFunc) { indexFilename, worktree, deleteTemporaryFile, err := repo.ReadTreeToTemporaryIndex(commitID) if err != nil { @@ -303,21 +320,21 @@ func (repo *Repository) CheckAttributeReader(commitID string) (*CheckAttributeRe } ctx, cancel := context.WithCancel(repo.Ctx) if err := checker.Init(ctx); err != nil { - log.Error("Unable to open checker for %s. Error: %v", commitID, err) + log.Error("Unable to open attribute checker for commit %s, error: %v", commitID, err) } else { go func() { err := checker.Run() - if err != nil && err != ctx.Err() { - log.Error("Unable to open checker for %s. Error: %v", commitID, err) + if err != nil && !IsErrCanceledOrKilled(err) { + log.Error("Attribute checker for commit %s exits with error: %v", commitID, err) } cancel() }() } - deferable := func() { + deferrable := func() { _ = checker.Close() cancel() deleteTemporaryFile() } - return checker, deferable + return checker, deferrable } diff --git a/modules/git/repo_attribute_test.go b/modules/git/repo_attribute_test.go index 0fcd94b4c794e..2e1abe17a9a03 100644 --- a/modules/git/repo_attribute_test.go +++ b/modules/git/repo_attribute_test.go @@ -4,10 +4,16 @@ package git import ( + "context" + mathRand "math/rand/v2" + "path/filepath" + "slices" + "sync" "testing" "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func Test_nulSeparatedAttributeWriter_ReadAttribute(t *testing.T) { @@ -95,3 +101,57 @@ func Test_nulSeparatedAttributeWriter_ReadAttribute(t *testing.T) { Value: "unspecified", }, attr) } + +func TestAttributeReader(t *testing.T) { + t.Skip() // for debug purpose only, do not run in CI + + ctx := context.Background() + + timeout := 1 * time.Second + repoPath := filepath.Join(testReposDir, "language_stats_repo") + commitRef := "HEAD" + + oneRound := func(t *testing.T, roundIdx int) { + ctx, cancel := context.WithTimeout(ctx, timeout) + _ = cancel + gitRepo, err := OpenRepository(ctx, repoPath) + require.NoError(t, err) + defer gitRepo.Close() + + commit, err := gitRepo.GetCommit(commitRef) + require.NoError(t, err) + + files, err := gitRepo.LsFiles() + require.NoError(t, err) + + randomFiles := slices.Clone(files) + randomFiles = append(randomFiles, "any-file-1", "any-file-2") + + t.Logf("Round %v with %d files", roundIdx, len(randomFiles)) + + attrReader, deferrable := gitRepo.CheckAttributeReader(commit.ID.String()) + defer deferrable() + + wg := sync.WaitGroup{} + wg.Add(1) + + go func() { + for { + file := randomFiles[mathRand.IntN(len(randomFiles))] + _, err := attrReader.CheckPath(file) + if err != nil { + for i := 0; i < 10; i++ { + _, _ = attrReader.CheckPath(file) + } + break + } + } + wg.Done() + }() + wg.Wait() + } + + for i := 0; i < 100; i++ { + oneRound(t, i) + } +} diff --git a/modules/git/repo_commit.go b/modules/git/repo_commit.go index 647894bb21346..02d8e163e4f25 100644 --- a/modules/git/repo_commit.go +++ b/modules/git/repo_commit.go @@ -519,6 +519,7 @@ func (repo *Repository) AddLastCommitCache(cacheKey, fullName, sha string) error return nil } +// GetCommitBranchStart returns the commit where the branch diverged func (repo *Repository) GetCommitBranchStart(env []string, branch, endCommitID string) (string, error) { cmd := NewCommand(repo.Ctx, "log", prettyLogFormat) cmd.AddDynamicArguments(endCommitID) @@ -533,7 +534,8 @@ func (repo *Repository) GetCommitBranchStart(env []string, branch, endCommitID s parts := bytes.Split(bytes.TrimSpace(stdout), []byte{'\n'}) - var startCommitID string + // check the commits one by one until we find a commit contained by another branch + // and we think this commit is the divergence point for _, commitID := range parts { branches, err := repo.getBranches(env, string(commitID), 2) if err != nil { @@ -541,11 +543,9 @@ func (repo *Repository) GetCommitBranchStart(env []string, branch, endCommitID s } for _, b := range branches { if b != branch { - return startCommitID, nil + return string(commitID), nil } } - - startCommitID = string(commitID) } return "", nil diff --git a/modules/git/repo_compare.go b/modules/git/repo_compare.go index 877a7ff3b86b0..16fcdcf4c8f96 100644 --- a/modules/git/repo_compare.go +++ b/modules/git/repo_compare.go @@ -233,34 +233,72 @@ func parseDiffStat(stdout string) (numFiles, totalAdditions, totalDeletions int, return numFiles, totalAdditions, totalDeletions, err } +// GetDiffOrPatch generates either diff or formatted patch data between given revisions +func (repo *Repository) GetDiffOrPatch(base, head string, w io.Writer, patch, binary bool) error { + if patch { + return repo.GetPatch(base, head, w) + } + if binary { + return repo.GetDiffBinary(base, head, w) + } + return repo.GetDiff(base, head, w) +} + // GetDiff generates and returns patch data between given revisions, optimized for human readability -func (repo *Repository) GetDiff(compareArg string, w io.Writer) error { +func (repo *Repository) GetDiff(base, head string, w io.Writer) error { stderr := new(bytes.Buffer) - return NewCommand(repo.Ctx, "diff", "-p").AddDynamicArguments(compareArg). + err := NewCommand(repo.Ctx, "diff", "-p").AddDynamicArguments(base + "..." + head). Run(&RunOpts{ Dir: repo.Path, Stdout: w, Stderr: stderr, }) + if err != nil && bytes.Contains(stderr.Bytes(), []byte("no merge base")) { + return NewCommand(repo.Ctx, "diff", "-p").AddDynamicArguments(base, head). + Run(&RunOpts{ + Dir: repo.Path, + Stdout: w, + }) + } + return err } // GetDiffBinary generates and returns patch data between given revisions, including binary diffs. -func (repo *Repository) GetDiffBinary(compareArg string, w io.Writer) error { - return NewCommand(repo.Ctx, "diff", "-p", "--binary", "--histogram").AddDynamicArguments(compareArg).Run(&RunOpts{ - Dir: repo.Path, - Stdout: w, - }) +func (repo *Repository) GetDiffBinary(base, head string, w io.Writer) error { + stderr := new(bytes.Buffer) + err := NewCommand(repo.Ctx, "diff", "-p", "--binary", "--histogram").AddDynamicArguments(base + "..." + head). + Run(&RunOpts{ + Dir: repo.Path, + Stdout: w, + Stderr: stderr, + }) + if err != nil && bytes.Contains(stderr.Bytes(), []byte("no merge base")) { + return NewCommand(repo.Ctx, "diff", "-p", "--binary", "--histogram").AddDynamicArguments(base, head). + Run(&RunOpts{ + Dir: repo.Path, + Stdout: w, + }) + } + return err } // GetPatch generates and returns format-patch data between given revisions, able to be used with `git apply` -func (repo *Repository) GetPatch(compareArg string, w io.Writer) error { +func (repo *Repository) GetPatch(base, head string, w io.Writer) error { stderr := new(bytes.Buffer) - return NewCommand(repo.Ctx, "format-patch", "--binary", "--stdout").AddDynamicArguments(compareArg). + err := NewCommand(repo.Ctx, "format-patch", "--binary", "--stdout").AddDynamicArguments(base + "..." + head). Run(&RunOpts{ Dir: repo.Path, Stdout: w, Stderr: stderr, }) + if err != nil && bytes.Contains(stderr.Bytes(), []byte("no merge base")) { + return NewCommand(repo.Ctx, "format-patch", "--binary", "--stdout").AddDynamicArguments(base, head). + Run(&RunOpts{ + Dir: repo.Path, + Stdout: w, + }) + } + return err } // GetFilesChangedBetween returns a list of all files that have been changed between the given commits @@ -291,6 +329,21 @@ func (repo *Repository) GetFilesChangedBetween(base, head string) ([]string, err return split, err } +// GetDiffFromMergeBase generates and return patch data from merge base to head +func (repo *Repository) GetDiffFromMergeBase(base, head string, w io.Writer) error { + stderr := new(bytes.Buffer) + err := NewCommand(repo.Ctx, "diff", "-p", "--binary").AddDynamicArguments(base + "..." + head). + Run(&RunOpts{ + Dir: repo.Path, + Stdout: w, + Stderr: stderr, + }) + if err != nil && bytes.Contains(stderr.Bytes(), []byte("no merge base")) { + return repo.GetDiffBinary(base, head, w) + } + return err +} + // ReadPatchCommit will check if a diff patch exists and return stats func (repo *Repository) ReadPatchCommit(prID int64) (commitSHA string, err error) { // Migrated repositories download patches to "pulls" location diff --git a/modules/git/repo_compare_test.go b/modules/git/repo_compare_test.go index 25ee4c5198568..454ed6b9f8533 100644 --- a/modules/git/repo_compare_test.go +++ b/modules/git/repo_compare_test.go @@ -28,7 +28,7 @@ func TestGetFormatPatch(t *testing.T) { defer repo.Close() rd := &bytes.Buffer{} - err = repo.GetPatch("8d92fc95^...8d92fc95", rd) + err = repo.GetPatch("8d92fc95^", "8d92fc95", rd) if err != nil { assert.NoError(t, err) return diff --git a/modules/git/submodule.go b/modules/git/submodule.go new file mode 100644 index 0000000000000..017b644052b93 --- /dev/null +++ b/modules/git/submodule.go @@ -0,0 +1,66 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package git + +import ( + "bufio" + "context" + "fmt" + "os" + + "code.gitea.io/gitea/modules/log" +) + +type TemplateSubmoduleCommit struct { + Path string + Commit string +} + +// GetTemplateSubmoduleCommits returns a list of submodules paths and their commits from a repository +// This function is only for generating new repos based on existing template, the template couldn't be too large. +func GetTemplateSubmoduleCommits(ctx context.Context, repoPath string) (submoduleCommits []TemplateSubmoduleCommit, _ error) { + stdoutReader, stdoutWriter, err := os.Pipe() + if err != nil { + return nil, err + } + opts := &RunOpts{ + Dir: repoPath, + Stdout: stdoutWriter, + PipelineFunc: func(ctx context.Context, cancel context.CancelFunc) error { + _ = stdoutWriter.Close() + defer stdoutReader.Close() + + scanner := bufio.NewScanner(stdoutReader) + for scanner.Scan() { + entry, err := parseLsTreeLine(scanner.Bytes()) + if err != nil { + cancel() + return err + } + if entry.EntryMode == EntryModeCommit { + submoduleCommits = append(submoduleCommits, TemplateSubmoduleCommit{Path: entry.Name, Commit: entry.ID.String()}) + } + } + return scanner.Err() + }, + } + err = NewCommand(ctx, "ls-tree", "-r", "--", "HEAD").Run(opts) + if err != nil { + return nil, fmt.Errorf("GetTemplateSubmoduleCommits: error running git ls-tree: %v", err) + } + return submoduleCommits, nil +} + +// AddTemplateSubmoduleIndexes Adds the given submodules to the git index. +// It is only for generating new repos based on existing template, requires the .gitmodules file to be already present in the work dir. +func AddTemplateSubmoduleIndexes(ctx context.Context, repoPath string, submodules []TemplateSubmoduleCommit) error { + for _, submodule := range submodules { + cmd := NewCommand(ctx, "update-index", "--add", "--cacheinfo", "160000").AddDynamicArguments(submodule.Commit, submodule.Path) + if stdout, _, err := cmd.RunStdString(&RunOpts{Dir: repoPath}); err != nil { + log.Error("Unable to add %s as submodule to repo %s: stdout %s\nError: %v", submodule.Path, repoPath, stdout, err) + return err + } + } + return nil +} diff --git a/modules/git/submodule_test.go b/modules/git/submodule_test.go new file mode 100644 index 0000000000000..d53946a27d40f --- /dev/null +++ b/modules/git/submodule_test.go @@ -0,0 +1,48 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package git + +import ( + "context" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGetTemplateSubmoduleCommits(t *testing.T) { + testRepoPath := filepath.Join(testReposDir, "repo4_submodules") + submodules, err := GetTemplateSubmoduleCommits(DefaultContext, testRepoPath) + require.NoError(t, err) + + assert.Len(t, submodules, 2) + + assert.EqualValues(t, "<°)))><", submodules[0].Path) + assert.EqualValues(t, "d2932de67963f23d43e1c7ecf20173e92ee6c43c", submodules[0].Commit) + + assert.EqualValues(t, "libtest", submodules[1].Path) + assert.EqualValues(t, "1234567890123456789012345678901234567890", submodules[1].Commit) +} + +func TestAddTemplateSubmoduleIndexes(t *testing.T) { + ctx := context.Background() + tmpDir := t.TempDir() + var err error + _, _, err = NewCommand(ctx, "init").RunStdString(&RunOpts{Dir: tmpDir}) + require.NoError(t, err) + _ = os.Mkdir(filepath.Join(tmpDir, "new-dir"), 0o755) + err = AddTemplateSubmoduleIndexes(ctx, tmpDir, []TemplateSubmoduleCommit{{Path: "new-dir", Commit: "1234567890123456789012345678901234567890"}}) + require.NoError(t, err) + _, _, err = NewCommand(ctx, "add", "--all").RunStdString(&RunOpts{Dir: tmpDir}) + require.NoError(t, err) + _, _, err = NewCommand(ctx, "-c", "user.name=a", "-c", "user.email=b", "commit", "-m=test").RunStdString(&RunOpts{Dir: tmpDir}) + require.NoError(t, err) + submodules, err := GetTemplateSubmoduleCommits(DefaultContext, tmpDir) + require.NoError(t, err) + assert.Len(t, submodules, 1) + assert.EqualValues(t, "new-dir", submodules[0].Path) + assert.EqualValues(t, "1234567890123456789012345678901234567890", submodules[0].Commit) +} diff --git a/modules/git/tests/repos/repo4_submodules/HEAD b/modules/git/tests/repos/repo4_submodules/HEAD new file mode 100644 index 0000000000000..cb089cd89a7d7 --- /dev/null +++ b/modules/git/tests/repos/repo4_submodules/HEAD @@ -0,0 +1 @@ +ref: refs/heads/master diff --git a/modules/git/tests/repos/repo4_submodules/config b/modules/git/tests/repos/repo4_submodules/config new file mode 100644 index 0000000000000..07d359d07cf1e --- /dev/null +++ b/modules/git/tests/repos/repo4_submodules/config @@ -0,0 +1,4 @@ +[core] + repositoryformatversion = 0 + filemode = true + bare = true diff --git a/modules/git/tests/repos/repo4_submodules/objects/97/c3d30df0e6492348292600920a6482feaebb74 b/modules/git/tests/repos/repo4_submodules/objects/97/c3d30df0e6492348292600920a6482feaebb74 new file mode 100644 index 0000000000000..7596090b49fc8 Binary files /dev/null and b/modules/git/tests/repos/repo4_submodules/objects/97/c3d30df0e6492348292600920a6482feaebb74 differ diff --git a/modules/git/tests/repos/repo4_submodules/objects/c7/e064ed49b44523cba8a5dfbc37d2ce1bb41d34 b/modules/git/tests/repos/repo4_submodules/objects/c7/e064ed49b44523cba8a5dfbc37d2ce1bb41d34 new file mode 100644 index 0000000000000..e3a13c156dce4 Binary files /dev/null and b/modules/git/tests/repos/repo4_submodules/objects/c7/e064ed49b44523cba8a5dfbc37d2ce1bb41d34 differ diff --git a/modules/git/tests/repos/repo4_submodules/objects/e1/e59caba97193d48862d6809912043871f37437 b/modules/git/tests/repos/repo4_submodules/objects/e1/e59caba97193d48862d6809912043871f37437 new file mode 100644 index 0000000000000..a8d6e5c17c8f2 --- /dev/null +++ b/modules/git/tests/repos/repo4_submodules/objects/e1/e59caba97193d48862d6809912043871f37437 @@ -0,0 +1,2 @@ +x[ +Â0EýÎ*æ_é$MÑ5tifBk IÅ•¹7æk~ÞÃ9ܘ—åÜ ü¦ð.jÖÈ ÅOÚ äÉ"zÂ`ß#IirF…µÍ¹ÀØ$%¹Âçò|4)°¯?t¼É=”Ë:K¦ï­#[$D¿¯û¿^˜…¡®Ó’y½HU/f?G \ No newline at end of file diff --git a/modules/git/tests/repos/repo4_submodules/refs/heads/master b/modules/git/tests/repos/repo4_submodules/refs/heads/master new file mode 100644 index 0000000000000..102bc34da8ca0 --- /dev/null +++ b/modules/git/tests/repos/repo4_submodules/refs/heads/master @@ -0,0 +1 @@ +e1e59caba97193d48862d6809912043871f37437 diff --git a/modules/git/tree.go b/modules/git/tree.go index 1da4a9fa5debc..5a644f6c87aa1 100644 --- a/modules/git/tree.go +++ b/modules/git/tree.go @@ -17,7 +17,7 @@ func NewTree(repo *Repository, id ObjectID) *Tree { } } -// SubTree get a sub tree by the sub dir path +// SubTree get a subtree by the sub dir path func (t *Tree) SubTree(rpath string) (*Tree, error) { if len(rpath) == 0 { return t, nil @@ -62,3 +62,14 @@ func (repo *Repository) LsTree(ref string, filenames ...string) ([]string, error return filelist, err } + +// GetTreePathLatestCommit returns the latest commit of a tree path +func (repo *Repository) GetTreePathLatestCommit(refName, treePath string) (*Commit, error) { + stdout, _, err := NewCommand(repo.Ctx, "rev-list", "-1"). + AddDynamicArguments(refName).AddDashesAndList(treePath). + RunStdString(&RunOpts{Dir: repo.Path}) + if err != nil { + return nil, err + } + return repo.GetCommit(strings.TrimSpace(stdout)) +} diff --git a/modules/git/tree_blob_nogogit.go b/modules/git/tree_blob_nogogit.go index 92d3d107a7425..b7bcf40edd2a9 100644 --- a/modules/git/tree_blob_nogogit.go +++ b/modules/git/tree_blob_nogogit.go @@ -17,7 +17,6 @@ func (t *Tree) GetTreeEntryByPath(relpath string) (*TreeEntry, error) { ptree: t, ID: t.ID, name: "", - fullName: "", entryMode: EntryModeTree, }, nil } diff --git a/modules/git/tree_entry_nogogit.go b/modules/git/tree_entry_nogogit.go index 1c3bcd197a01d..81fb638d56fbe 100644 --- a/modules/git/tree_entry_nogogit.go +++ b/modules/git/tree_entry_nogogit.go @@ -9,23 +9,17 @@ import "code.gitea.io/gitea/modules/log" // TreeEntry the leaf in the git tree type TreeEntry struct { - ID ObjectID - + ID ObjectID ptree *Tree entryMode EntryMode name string - - size int64 - sized bool - fullName string + size int64 + sized bool } // Name returns the name of the entry func (te *TreeEntry) Name() string { - if te.fullName != "" { - return te.fullName - } return te.name } diff --git a/modules/git/tree_test.go b/modules/git/tree_test.go index 6d2b5c84d5069..5fee64b038755 100644 --- a/modules/git/tree_test.go +++ b/modules/git/tree_test.go @@ -25,3 +25,18 @@ func TestSubTree_Issue29101(t *testing.T) { assert.True(t, IsErrNotExist(err)) } } + +func Test_GetTreePathLatestCommit(t *testing.T) { + repo, err := openRepositoryWithDefaultContext(filepath.Join(testReposDir, "repo6_blame")) + assert.NoError(t, err) + defer repo.Close() + + commitID, err := repo.GetBranchCommitID("master") + assert.NoError(t, err) + assert.EqualValues(t, "544d8f7a3b15927cddf2299b4b562d6ebd71b6a7", commitID) + + commit, err := repo.GetTreePathLatestCommit("master", "blame.txt") + assert.NoError(t, err) + assert.NotNil(t, commit) + assert.EqualValues(t, "45fb6cbc12f970b04eacd5cd4165edd11c8d7376", commit.ID.String()) +} diff --git a/modules/gitgraph/graph.go b/modules/gitgraph/graph.go index 7e12be030fb35..2628a1a55af7e 100644 --- a/modules/gitgraph/graph.go +++ b/modules/gitgraph/graph.go @@ -29,7 +29,7 @@ func GetCommitGraph(r *git.Repository, page, maxAllowedColors int, hidePRRefs bo } if len(branches) == 0 { - graphCmd.AddArguments("--all") + graphCmd.AddArguments("--tags", "--branches") } graphCmd.AddArguments("-C", "-M", "--date=iso-strict"). diff --git a/modules/gitrepo/gitrepo.go b/modules/gitrepo/gitrepo.go index 831b9d7bb7858..14d809aedbe4a 100644 --- a/modules/gitrepo/gitrepo.go +++ b/modules/gitrepo/gitrepo.go @@ -10,7 +10,6 @@ import ( "strings" "code.gitea.io/gitea/modules/git" - "code.gitea.io/gitea/modules/reqctx" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" ) @@ -39,32 +38,63 @@ func OpenWikiRepository(ctx context.Context, repo Repository) (*git.Repository, // contextKey is a value for use with context.WithValue. type contextKey struct { - repoPath string + name string +} + +// RepositoryContextKey is a context key. It is used with context.Value() to get the current Repository for the context +var RepositoryContextKey = &contextKey{"repository"} + +// RepositoryFromContext attempts to get the repository from the context +func repositoryFromContext(ctx context.Context, repo Repository) *git.Repository { + value := ctx.Value(RepositoryContextKey) + if value == nil { + return nil + } + + if gitRepo, ok := value.(*git.Repository); ok && gitRepo != nil { + if gitRepo.Path == repoPath(repo) { + return gitRepo + } + } + + return nil } // RepositoryFromContextOrOpen attempts to get the repository from the context or just opens it func RepositoryFromContextOrOpen(ctx context.Context, repo Repository) (*git.Repository, io.Closer, error) { - ds := reqctx.GetRequestDataStore(ctx) - if ds != nil { - gitRepo, err := RepositoryFromRequestContextOrOpen(ctx, ds, repo) - return gitRepo, util.NopCloser{}, err + gitRepo := repositoryFromContext(ctx, repo) + if gitRepo != nil { + return gitRepo, util.NopCloser{}, nil } + gitRepo, err := OpenRepository(ctx, repo) return gitRepo, gitRepo, err } -// RepositoryFromRequestContextOrOpen opens the repository at the given relative path in the provided request context -// The repo will be automatically closed when the request context is done -func RepositoryFromRequestContextOrOpen(ctx context.Context, ds reqctx.RequestDataStore, repo Repository) (*git.Repository, error) { - ck := contextKey{repoPath: repoPath(repo)} - if gitRepo, ok := ctx.Value(ck).(*git.Repository); ok { - return gitRepo, nil +// repositoryFromContextPath attempts to get the repository from the context +func repositoryFromContextPath(ctx context.Context, path string) *git.Repository { + value := ctx.Value(RepositoryContextKey) + if value == nil { + return nil } - gitRepo, err := git.OpenRepository(ctx, ck.repoPath) - if err != nil { - return nil, err + + if repo, ok := value.(*git.Repository); ok && repo != nil { + if repo.Path == path { + return repo + } } - ds.AddCloser(gitRepo) - ds.SetContextValue(ck, gitRepo) - return gitRepo, nil + + return nil +} + +// RepositoryFromContextOrOpenPath attempts to get the repository from the context or just opens it +// Deprecated: Use RepositoryFromContextOrOpen instead +func RepositoryFromContextOrOpenPath(ctx context.Context, path string) (*git.Repository, io.Closer, error) { + gitRepo := repositoryFromContextPath(ctx, path) + if gitRepo != nil { + return gitRepo, util.NopCloser{}, nil + } + + gitRepo, err := git.OpenRepository(ctx, path) + return gitRepo, gitRepo, err } diff --git a/modules/gitrepo/walk_gogit.go b/modules/gitrepo/walk_gogit.go index 709897ba0cfe7..6370faf08e7df 100644 --- a/modules/gitrepo/walk_gogit.go +++ b/modules/gitrepo/walk_gogit.go @@ -14,11 +14,15 @@ import ( // WalkReferences walks all the references from the repository // refname is empty, ObjectTag or ObjectBranch. All other values should be treated as equivalent to empty. func WalkReferences(ctx context.Context, repo Repository, walkfn func(sha1, refname string) error) (int, error) { - gitRepo, closer, err := RepositoryFromContextOrOpen(ctx, repo) - if err != nil { - return 0, err + gitRepo := repositoryFromContext(ctx, repo) + if gitRepo == nil { + var err error + gitRepo, err = OpenRepository(ctx, repo) + if err != nil { + return 0, err + } + defer gitRepo.Close() } - defer closer.Close() i := 0 iter, err := gitRepo.GoGitRepo().References() diff --git a/modules/graceful/manager.go b/modules/graceful/manager.go index 433e8c4c27c77..991b2f2b7af1e 100644 --- a/modules/graceful/manager.go +++ b/modules/graceful/manager.go @@ -9,7 +9,6 @@ import ( "sync" "time" - "code.gitea.io/gitea/modules/gtprof" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/process" "code.gitea.io/gitea/modules/setting" @@ -137,7 +136,7 @@ func (g *Manager) doShutdown() { } g.lock.Lock() g.shutdownCtxCancel() - atShutdownCtx := pprof.WithLabels(g.hammerCtx, pprof.Labels(gtprof.LabelGracefulLifecycle, "post-shutdown")) + atShutdownCtx := pprof.WithLabels(g.hammerCtx, pprof.Labels("graceful-lifecycle", "post-shutdown")) pprof.SetGoroutineLabels(atShutdownCtx) for _, fn := range g.toRunAtShutdown { go fn() @@ -168,7 +167,7 @@ func (g *Manager) doHammerTime(d time.Duration) { default: log.Warn("Setting Hammer condition") g.hammerCtxCancel() - atHammerCtx := pprof.WithLabels(g.terminateCtx, pprof.Labels(gtprof.LabelGracefulLifecycle, "post-hammer")) + atHammerCtx := pprof.WithLabels(g.terminateCtx, pprof.Labels("graceful-lifecycle", "post-hammer")) pprof.SetGoroutineLabels(atHammerCtx) } g.lock.Unlock() @@ -184,7 +183,7 @@ func (g *Manager) doTerminate() { default: log.Warn("Terminating") g.terminateCtxCancel() - atTerminateCtx := pprof.WithLabels(g.managerCtx, pprof.Labels(gtprof.LabelGracefulLifecycle, "post-terminate")) + atTerminateCtx := pprof.WithLabels(g.managerCtx, pprof.Labels("graceful-lifecycle", "post-terminate")) pprof.SetGoroutineLabels(atTerminateCtx) for _, fn := range g.toRunAtTerminate { diff --git a/modules/graceful/manager_common.go b/modules/graceful/manager_common.go index 7cfbdfbeb0ead..f6dbcc748dc80 100644 --- a/modules/graceful/manager_common.go +++ b/modules/graceful/manager_common.go @@ -8,8 +8,6 @@ import ( "runtime/pprof" "sync" "time" - - "code.gitea.io/gitea/modules/gtprof" ) // FIXME: it seems that there is a bug when using systemd Type=notify: the "Install Page" (INSTALL_LOCK=false) doesn't notify properly. @@ -67,10 +65,10 @@ func (g *Manager) prepare(ctx context.Context) { g.hammerCtx, g.hammerCtxCancel = context.WithCancel(ctx) g.managerCtx, g.managerCtxCancel = context.WithCancel(ctx) - g.terminateCtx = pprof.WithLabels(g.terminateCtx, pprof.Labels(gtprof.LabelGracefulLifecycle, "with-terminate")) - g.shutdownCtx = pprof.WithLabels(g.shutdownCtx, pprof.Labels(gtprof.LabelGracefulLifecycle, "with-shutdown")) - g.hammerCtx = pprof.WithLabels(g.hammerCtx, pprof.Labels(gtprof.LabelGracefulLifecycle, "with-hammer")) - g.managerCtx = pprof.WithLabels(g.managerCtx, pprof.Labels(gtprof.LabelGracefulLifecycle, "with-manager")) + g.terminateCtx = pprof.WithLabels(g.terminateCtx, pprof.Labels("graceful-lifecycle", "with-terminate")) + g.shutdownCtx = pprof.WithLabels(g.shutdownCtx, pprof.Labels("graceful-lifecycle", "with-shutdown")) + g.hammerCtx = pprof.WithLabels(g.hammerCtx, pprof.Labels("graceful-lifecycle", "with-hammer")) + g.managerCtx = pprof.WithLabels(g.managerCtx, pprof.Labels("graceful-lifecycle", "with-manager")) if !g.setStateTransition(stateInit, stateRunning) { panic("invalid graceful manager state: transition from init to running failed") diff --git a/modules/httplib/request.go b/modules/httplib/request.go index 880d7ad3cbe36..5e409228961c9 100644 --- a/modules/httplib/request.go +++ b/modules/httplib/request.go @@ -8,6 +8,7 @@ import ( "bytes" "context" "crypto/tls" + "errors" "fmt" "io" "net" @@ -99,10 +100,13 @@ func (r *Request) Param(key, value string) *Request { return r } -// Body adds request raw body. -// it supports string and []byte. +// Body adds request raw body. It supports string, []byte and io.Reader as body. func (r *Request) Body(data any) *Request { + if r == nil { + return nil + } switch t := data.(type) { + case nil: // do nothing case string: bf := bytes.NewBufferString(t) r.req.Body = io.NopCloser(bf) @@ -111,6 +115,12 @@ func (r *Request) Body(data any) *Request { bf := bytes.NewBuffer(t) r.req.Body = io.NopCloser(bf) r.req.ContentLength = int64(len(t)) + case io.ReadCloser: + r.req.Body = t + case io.Reader: + r.req.Body = io.NopCloser(t) + default: + panic(fmt.Sprintf("unsupported request body type %T", t)) } return r } @@ -141,7 +151,7 @@ func (r *Request) getResponse() (*http.Response, error) { } } else if r.req.Method == "POST" && r.req.Body == nil && len(paramBody) > 0 { r.Header("Content-Type", "application/x-www-form-urlencoded") - r.Body(paramBody) + r.Body(paramBody) // string } var err error @@ -185,7 +195,11 @@ func (r *Request) getResponse() (*http.Response, error) { } // Response executes request client gets response manually. +// Caller MUST close the response body if no error occurs func (r *Request) Response() (*http.Response, error) { + if r == nil { + return nil, errors.New("invalid request") + } return r.getResponse() } diff --git a/modules/indexer/code/bleve/bleve.go b/modules/indexer/code/bleve/bleve.go index 772317fa594ba..395c7a0d314db 100644 --- a/modules/indexer/code/bleve/bleve.go +++ b/modules/indexer/code/bleve/bleve.go @@ -28,7 +28,6 @@ import ( "github.com/blevesearch/bleve/v2" analyzer_custom "github.com/blevesearch/bleve/v2/analysis/analyzer/custom" analyzer_keyword "github.com/blevesearch/bleve/v2/analysis/analyzer/keyword" - "github.com/blevesearch/bleve/v2/analysis/token/camelcase" "github.com/blevesearch/bleve/v2/analysis/token/lowercase" "github.com/blevesearch/bleve/v2/analysis/token/unicodenorm" "github.com/blevesearch/bleve/v2/analysis/tokenizer/letter" @@ -70,7 +69,7 @@ const ( filenameIndexerAnalyzer = "filenameIndexerAnalyzer" filenameIndexerTokenizer = "filenameIndexerTokenizer" repoIndexerDocType = "repoIndexerDocType" - repoIndexerLatestVersion = 8 + repoIndexerLatestVersion = 9 ) // generateBleveIndexMapping generates a bleve index mapping for the repo indexer @@ -107,7 +106,7 @@ func generateBleveIndexMapping() (mapping.IndexMapping, error) { "type": analyzer_custom.Name, "char_filters": []string{}, "tokenizer": letter.Name, - "token_filters": []string{unicodeNormalizeName, camelcase.Name, lowercase.Name}, + "token_filters": []string{unicodeNormalizeName, lowercase.Name}, }); err != nil { return nil, err } @@ -266,7 +265,7 @@ func (b *Indexer) Search(ctx context.Context, opts *internal.SearchOptions) (int pathQuery.FieldVal = "Filename" pathQuery.SetBoost(10) - contentQuery := bleve.NewMatchQuery(opts.Keyword) + contentQuery := bleve.NewMatchPhraseQuery(opts.Keyword) contentQuery.FieldVal = "Content" if opts.IsKeywordFuzzy { diff --git a/modules/indexer/code/indexer.go b/modules/indexer/code/indexer.go index c1ab26569c6f4..728b37fab6ecd 100644 --- a/modules/indexer/code/indexer.go +++ b/modules/indexer/code/indexer.go @@ -123,13 +123,12 @@ func Init() { for _, indexerData := range items { log.Trace("IndexerData Process Repo: %d", indexerData.RepoID) if err := index(ctx, indexer, indexerData.RepoID); err != nil { - unhandled = append(unhandled, indexerData) if !setting.IsInTesting { log.Error("Codes indexer handler: index error for repo %v: %v", indexerData.RepoID, err) } } } - return unhandled + return nil // do not re-queue the failed items, otherwise some broken repo will block the queue } indexerQueue = queue.CreateUniqueQueue(ctx, "code_indexer", handler) diff --git a/modules/indexer/code/indexer_test.go b/modules/indexer/code/indexer_test.go index d04088531ac28..48afdd1a71197 100644 --- a/modules/indexer/code/indexer_test.go +++ b/modules/indexer/code/indexer_test.go @@ -15,6 +15,8 @@ import ( "code.gitea.io/gitea/modules/indexer/code/bleve" "code.gitea.io/gitea/modules/indexer/code/elasticsearch" "code.gitea.io/gitea/modules/indexer/code/internal" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" _ "code.gitea.io/gitea/models" _ "code.gitea.io/gitea/models/actions" @@ -163,35 +165,6 @@ func testIndexer(name string, t *testing.T, indexer internal.Indexer) { }, }, }, - // Search for matches on the contents of files within the repo '62'. - // This scenario yields two results (both are based on contents, the first one is an exact match where as the second is a 'fuzzy' one) - { - RepoIDs: []int64{62}, - Keyword: "This is not cheese", - Langs: 1, - Results: []codeSearchResult{ - { - Filename: "potato/ham.md", - Content: "This is not cheese", - }, - { - Filename: "ham.md", - Content: "This is also not cheese", - }, - }, - }, - // Search for matches on the contents of files regardless of case. - { - RepoIDs: nil, - Keyword: "dESCRIPTION", - Langs: 1, - Results: []codeSearchResult{ - { - Filename: "README.md", - Content: "# repo1\n\nDescription for repo1", - }, - }, - }, // Search for an exact match on the filename within the repo '62' (case insenstive). // This scenario yields a single result (the file avocado.md on the repo '62') { @@ -231,6 +204,47 @@ func testIndexer(name string, t *testing.T, indexer internal.Indexer) { }, } + if name == "elastic_search" { + // Additional scenarios for elastic_search only + additional := []struct { + RepoIDs []int64 + Keyword string + Langs int + Results []codeSearchResult + }{ + // Search for matches on the contents of files within the repo '62'. + // This scenario yields two results (both are based on contents, the first one is an exact match where as the second is a 'fuzzy' one) + { + RepoIDs: []int64{62}, + Keyword: "This is not cheese", + Langs: 1, + Results: []codeSearchResult{ + { + Filename: "potato/ham.md", + Content: "This is not cheese", + }, + { + Filename: "ham.md", + Content: "This is also not cheese", + }, + }, + }, + // Search for matches on the contents of files regardless of case. + { + RepoIDs: nil, + Keyword: "dESCRIPTION", + Langs: 1, + Results: []codeSearchResult{ + { + Filename: "README.md", + Content: "# repo1\n\nDescription for repo1", + }, + }, + }, + } + keywords = append(keywords, additional...) + } + for _, kw := range keywords { t.Run(kw.Keyword, func(t *testing.T) { total, res, langs, err := indexer.Search(context.TODO(), &internal.SearchOptions{ @@ -279,7 +293,7 @@ func testIndexer(name string, t *testing.T, indexer internal.Indexer) { func TestBleveIndexAndSearch(t *testing.T) { unittest.PrepareTestEnv(t) - + defer test.MockVariableValue(&setting.Indexer.TypeBleveMaxFuzzniess, 2)() dir := t.TempDir() idx := bleve.NewIndexer(dir) diff --git a/modules/indexer/internal/bleve/util.go b/modules/indexer/internal/bleve/util.go index a0c3dc4ad459e..b6daa9e14b46a 100644 --- a/modules/indexer/internal/bleve/util.go +++ b/modules/indexer/internal/bleve/util.go @@ -9,6 +9,7 @@ import ( "unicode" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" "github.com/blevesearch/bleve/v2" @@ -54,9 +55,9 @@ func openIndexer(path string, latestVersion int) (bleve.Index, int, error) { return index, 0, nil } -// This method test the GuessFuzzinessByKeyword method. The fuzziness is based on the levenshtein distance and determines how many chars -// may be different on two string and they still be considered equivalent. -// Given a phrasse, its shortest word determines its fuzziness. If a phrase uses CJK (eg: `갃갃갃` `啊啊啊`), the fuzziness is zero. +// GuessFuzzinessByKeyword guesses fuzziness based on the levenshtein distance and determines how many chars +// may be different on two string, and they still be considered equivalent. +// Given a phrase, its shortest word determines its fuzziness. If a phrase uses CJK (eg: `갃갃갃` `啊啊啊`), the fuzziness is zero. func GuessFuzzinessByKeyword(s string) int { tokenizer := unicode_tokenizer.NewUnicodeTokenizer() tokens := tokenizer.Tokenize([]byte(s)) @@ -85,5 +86,5 @@ func guessFuzzinessByKeyword(s string) int { return 0 } } - return min(maxFuzziness, len(s)/4) + return min(min(setting.Indexer.TypeBleveMaxFuzzniess, maxFuzziness), len(s)/4) } diff --git a/modules/indexer/internal/bleve/util_test.go b/modules/indexer/internal/bleve/util_test.go index 8f7844464e739..1a7e4db0f4234 100644 --- a/modules/indexer/internal/bleve/util_test.go +++ b/modules/indexer/internal/bleve/util_test.go @@ -7,10 +7,15 @@ import ( "fmt" "testing" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" + "github.com/stretchr/testify/assert" ) func TestBleveGuessFuzzinessByKeyword(t *testing.T) { + defer test.MockVariableValue(&setting.Indexer.TypeBleveMaxFuzzniess, 2)() + scenarios := []struct { Input string Fuzziness int // See util.go for the definition of fuzziness in this particular context @@ -46,7 +51,7 @@ func TestBleveGuessFuzzinessByKeyword(t *testing.T) { } for _, scenario := range scenarios { - t.Run(fmt.Sprintf("ensure fuzziness of '%s' is '%d'", scenario.Input, scenario.Fuzziness), func(t *testing.T) { + t.Run(fmt.Sprintf("Fuziniess:%s=%d", scenario.Input, scenario.Fuzziness), func(t *testing.T) { assert.Equal(t, scenario.Fuzziness, GuessFuzzinessByKeyword(scenario.Input)) }) } diff --git a/modules/indexer/issues/db/options.go b/modules/indexer/issues/db/options.go index 42834f6e8863b..87ce398a202d0 100644 --- a/modules/indexer/issues/db/options.go +++ b/modules/indexer/issues/db/options.go @@ -73,9 +73,9 @@ func ToDBOptions(ctx context.Context, options *internal.SearchOptions) (*issue_m UpdatedBeforeUnix: options.UpdatedBeforeUnix.Value(), PriorityRepoID: 0, IsArchived: options.IsArchived, - Org: nil, + Owner: nil, Team: nil, - User: nil, + Doer: nil, } if len(options.MilestoneIDs) == 1 && options.MilestoneIDs[0] == 0 { diff --git a/modules/indexer/issues/util.go b/modules/indexer/issues/util.go index deb19adc49dd9..19d835a1d80aa 100644 --- a/modules/indexer/issues/util.go +++ b/modules/indexer/issues/util.go @@ -92,6 +92,11 @@ func getIssueIndexerData(ctx context.Context, issueID int64) (*internal.IndexerD projectID = issue.Project.ID } + projectColumnID, err := issue.ProjectColumnID(ctx) + if err != nil { + return nil, false, err + } + return &internal.IndexerData{ ID: issue.ID, RepoID: issue.RepoID, @@ -106,7 +111,7 @@ func getIssueIndexerData(ctx context.Context, issueID int64) (*internal.IndexerD NoLabel: len(labels) == 0, MilestoneID: issue.MilestoneID, ProjectID: projectID, - ProjectColumnID: issue.ProjectColumnID(ctx), + ProjectColumnID: projectColumnID, PosterID: issue.PosterID, AssigneeID: issue.AssigneeID, MentionIDs: mentionIDs, diff --git a/modules/issue/template/unmarshal.go b/modules/issue/template/unmarshal.go index 1d8e9dd02d995..0fc13d7ddfc42 100644 --- a/modules/issue/template/unmarshal.go +++ b/modules/issue/template/unmarshal.go @@ -109,7 +109,7 @@ func unmarshal(filename string, content []byte) (*api.IssueTemplate, error) { it.Content = string(content) it.Name = path.Base(it.FileName) // paths in Git are always '/' separated - do not use filepath! - it.About = util.EllipsisDisplayString(it.Content, 80) + it.About, _ = util.SplitStringAtByteN(it.Content, 80) } else { it.Content = templateBody if it.About == "" { diff --git a/modules/lfs/http_client.go b/modules/lfs/http_client.go index 3acd23b8f73cf..0a27fb0c867cf 100644 --- a/modules/lfs/http_client.go +++ b/modules/lfs/http_client.go @@ -72,10 +72,14 @@ func (c *HTTPClient) batch(ctx context.Context, operation string, objects []Poin url := fmt.Sprintf("%s/objects/batch", c.endpoint) + // Original: In some lfs server implementations, they require the ref attribute. #32838 // `ref` is an "optional object describing the server ref that the objects belong to" - // but some (incorrect) lfs servers require it, so maybe adding an empty ref here doesn't break the correct ones. + // but some (incorrect) lfs servers like aliyun require it, so maybe adding an empty ref here doesn't break the correct ones. // https://github.com/git-lfs/git-lfs/blob/a32a02b44bf8a511aa14f047627c49e1a7fd5021/docs/api/batch.md?plain=1#L37 - request := &BatchRequest{operation, c.transferNames(), &Reference{}, objects} + // + // UPDATE: it can't use "empty ref" here because it breaks others like https://github.com/go-gitea/gitea/issues/33453 + request := &BatchRequest{operation, c.transferNames(), nil, objects} + payload := new(bytes.Buffer) err := json.NewEncoder(payload).Encode(request) if err != nil { diff --git a/modules/lfstransfer/backend/backend.go b/modules/lfstransfer/backend/backend.go index 2b1fe49fdab28..1328d93a48fef 100644 --- a/modules/lfstransfer/backend/backend.go +++ b/modules/lfstransfer/backend/backend.go @@ -4,7 +4,6 @@ package backend import ( - "bytes" "context" "encoding/base64" "fmt" @@ -29,7 +28,7 @@ var Capabilities = []string{ "locking", } -var _ transfer.Backend = &GiteaBackend{} +var _ transfer.Backend = (*GiteaBackend)(nil) // GiteaBackend is an adapter between git-lfs-transfer library and Gitea's internal LFS API type GiteaBackend struct { @@ -71,24 +70,23 @@ func (g *GiteaBackend) Batch(_ string, pointers []transfer.BatchItem, args trans g.logger.Log("json marshal error", err) return nil, err } - url := g.server.JoinPath("objects/batch").String() headers := map[string]string{ headerAuthorization: g.authToken, headerGiteaInternalAuth: g.internalAuth, headerAccept: mimeGitLFS, headerContentType: mimeGitLFS, } - req := newInternalRequest(g.ctx, url, http.MethodPost, headers, bodyBytes) + req := newInternalRequestLFS(g.ctx, g.server.JoinPath("objects/batch").String(), http.MethodPost, headers, bodyBytes) resp, err := req.Response() if err != nil { g.logger.Log("http request error", err) return nil, err } + defer resp.Body.Close() if resp.StatusCode != http.StatusOK { g.logger.Log("http statuscode error", resp.StatusCode, statusCodeToErr(resp.StatusCode)) return nil, statusCodeToErr(resp.StatusCode) } - defer resp.Body.Close() respBytes, err := io.ReadAll(resp.Body) if err != nil { g.logger.Log("http read error", err) @@ -158,8 +156,7 @@ func (g *GiteaBackend) Batch(_ string, pointers []transfer.BatchItem, args trans return pointers, nil } -// Download implements transfer.Backend. The returned reader must be closed by the -// caller. +// Download implements transfer.Backend. The returned reader must be closed by the caller. func (g *GiteaBackend) Download(oid string, args transfer.Args) (io.ReadCloser, int64, error) { idMapStr, exists := args[argID] if !exists { @@ -181,31 +178,30 @@ func (g *GiteaBackend) Download(oid string, args transfer.Args) (io.ReadCloser, g.logger.Log("argument id incorrect") return nil, 0, transfer.ErrCorruptData } - url := action.Href headers := map[string]string{ headerAuthorization: g.authToken, headerGiteaInternalAuth: g.internalAuth, headerAccept: mimeOctetStream, } - req := newInternalRequest(g.ctx, url, http.MethodGet, headers, nil) + req := newInternalRequestLFS(g.ctx, toInternalLFSURL(action.Href), http.MethodGet, headers, nil) resp, err := req.Response() if err != nil { - return nil, 0, err + return nil, 0, fmt.Errorf("failed to get response: %w", err) } + // no need to close the body here by "defer resp.Body.Close()", see below if resp.StatusCode != http.StatusOK { return nil, 0, statusCodeToErr(resp.StatusCode) } - defer resp.Body.Close() - respBytes, err := io.ReadAll(resp.Body) + + respSize, err := strconv.ParseInt(resp.Header.Get("X-Gitea-LFS-Content-Length"), 10, 64) if err != nil { - return nil, 0, err + return nil, 0, fmt.Errorf("failed to parse content length: %w", err) } - respSize := int64(len(respBytes)) - respBuf := io.NopCloser(bytes.NewBuffer(respBytes)) - return respBuf, respSize, nil + // transfer.Backend will check io.Closer interface and close this Body reader + return resp.Body, respSize, nil } -// StartUpload implements transfer.Backend. +// Upload implements transfer.Backend. func (g *GiteaBackend) Upload(oid string, size int64, r io.Reader, args transfer.Args) error { idMapStr, exists := args[argID] if !exists { @@ -227,22 +223,20 @@ func (g *GiteaBackend) Upload(oid string, size int64, r io.Reader, args transfer g.logger.Log("argument id incorrect") return transfer.ErrCorruptData } - url := action.Href headers := map[string]string{ headerAuthorization: g.authToken, headerGiteaInternalAuth: g.internalAuth, headerContentType: mimeOctetStream, headerContentLength: strconv.FormatInt(size, 10), } - reqBytes, err := io.ReadAll(r) - if err != nil { - return err - } - req := newInternalRequest(g.ctx, url, http.MethodPut, headers, reqBytes) + + req := newInternalRequestLFS(g.ctx, toInternalLFSURL(action.Href), http.MethodPut, headers, nil) + req.Body(r) resp, err := req.Response() if err != nil { return err } + defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return statusCodeToErr(resp.StatusCode) } @@ -277,18 +271,18 @@ func (g *GiteaBackend) Verify(oid string, size int64, args transfer.Args) (trans // the server sent no verify action return transfer.SuccessStatus(), nil } - url := action.Href headers := map[string]string{ headerAuthorization: g.authToken, headerGiteaInternalAuth: g.internalAuth, headerAccept: mimeGitLFS, headerContentType: mimeGitLFS, } - req := newInternalRequest(g.ctx, url, http.MethodPost, headers, bodyBytes) + req := newInternalRequestLFS(g.ctx, toInternalLFSURL(action.Href), http.MethodPost, headers, bodyBytes) resp, err := req.Response() if err != nil { return transfer.NewStatus(transfer.StatusInternalServerError), err } + defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return transfer.NewStatus(uint32(resp.StatusCode), http.StatusText(resp.StatusCode)), statusCodeToErr(resp.StatusCode) } diff --git a/modules/lfstransfer/backend/lock.go b/modules/lfstransfer/backend/lock.go index f094cce1db657..639f8b184e442 100644 --- a/modules/lfstransfer/backend/lock.go +++ b/modules/lfstransfer/backend/lock.go @@ -43,14 +43,13 @@ func (g *giteaLockBackend) Create(path, refname string) (transfer.Lock, error) { g.logger.Log("json marshal error", err) return nil, err } - url := g.server.String() headers := map[string]string{ headerAuthorization: g.authToken, headerGiteaInternalAuth: g.internalAuth, headerAccept: mimeGitLFS, headerContentType: mimeGitLFS, } - req := newInternalRequest(g.ctx, url, http.MethodPost, headers, bodyBytes) + req := newInternalRequestLFS(g.ctx, g.server.String(), http.MethodPost, headers, bodyBytes) resp, err := req.Response() if err != nil { g.logger.Log("http request error", err) @@ -95,14 +94,13 @@ func (g *giteaLockBackend) Unlock(lock transfer.Lock) error { g.logger.Log("json marshal error", err) return err } - url := g.server.JoinPath(lock.ID(), "unlock").String() headers := map[string]string{ headerAuthorization: g.authToken, headerGiteaInternalAuth: g.internalAuth, headerAccept: mimeGitLFS, headerContentType: mimeGitLFS, } - req := newInternalRequest(g.ctx, url, http.MethodPost, headers, bodyBytes) + req := newInternalRequestLFS(g.ctx, g.server.JoinPath(lock.ID(), "unlock").String(), http.MethodPost, headers, bodyBytes) resp, err := req.Response() if err != nil { g.logger.Log("http request error", err) @@ -176,16 +174,15 @@ func (g *giteaLockBackend) Range(cursor string, limit int, iter func(transfer.Lo } func (g *giteaLockBackend) queryLocks(v url.Values) ([]transfer.Lock, string, error) { - urlq := g.server.JoinPath() // get a copy - urlq.RawQuery = v.Encode() - url := urlq.String() + serverURLWithQuery := g.server.JoinPath() // get a copy + serverURLWithQuery.RawQuery = v.Encode() headers := map[string]string{ headerAuthorization: g.authToken, headerGiteaInternalAuth: g.internalAuth, headerAccept: mimeGitLFS, headerContentType: mimeGitLFS, } - req := newInternalRequest(g.ctx, url, http.MethodGet, headers, nil) + req := newInternalRequestLFS(g.ctx, serverURLWithQuery.String(), http.MethodGet, headers, nil) resp, err := req.Response() if err != nil { g.logger.Log("http request error", err) diff --git a/modules/lfstransfer/backend/util.go b/modules/lfstransfer/backend/util.go index cffefef375c79..98ce0b1e62ad8 100644 --- a/modules/lfstransfer/backend/util.go +++ b/modules/lfstransfer/backend/util.go @@ -5,15 +5,16 @@ package backend import ( "context" - "crypto/tls" "fmt" - "net" + "io" "net/http" - "time" + "net/url" + "strings" "code.gitea.io/gitea/modules/httplib" - "code.gitea.io/gitea/modules/proxyprotocol" + "code.gitea.io/gitea/modules/private" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" "github.com/charmbracelet/git-lfs-transfer/transfer" ) @@ -60,8 +61,7 @@ const ( // Operations enum const ( - opNone = iota - opDownload + opDownload = iota + 1 opUpload ) @@ -89,53 +89,60 @@ func statusCodeToErr(code int) error { } } -func newInternalRequest(ctx context.Context, url, method string, headers map[string]string, body []byte) *httplib.Request { - req := httplib.NewRequest(url, method). - SetContext(ctx). - SetTimeout(10*time.Second, 60*time.Second). - SetTLSClientConfig(&tls.Config{ - InsecureSkipVerify: true, - }) - - if setting.Protocol == setting.HTTPUnix { - req.SetTransport(&http.Transport{ - DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) { - var d net.Dialer - conn, err := d.DialContext(ctx, "unix", setting.HTTPAddr) - if err != nil { - return conn, err - } - if setting.LocalUseProxyProtocol { - if err = proxyprotocol.WriteLocalHeader(conn); err != nil { - _ = conn.Close() - return nil, err - } - } - return conn, err - }, - }) - } else if setting.LocalUseProxyProtocol { - req.SetTransport(&http.Transport{ - DialContext: func(ctx context.Context, network, address string) (net.Conn, error) { - var d net.Dialer - conn, err := d.DialContext(ctx, network, address) - if err != nil { - return conn, err - } - if err = proxyprotocol.WriteLocalHeader(conn); err != nil { - _ = conn.Close() - return nil, err - } - return conn, err - }, - }) +func toInternalLFSURL(s string) string { + pos1 := strings.Index(s, "://") + if pos1 == -1 { + return "" } + appSubURLWithSlash := setting.AppSubURL + "/" + pos2 := strings.Index(s[pos1+3:], appSubURLWithSlash) + if pos2 == -1 { + return "" + } + routePath := s[pos1+3+pos2+len(appSubURLWithSlash):] + fields := strings.SplitN(routePath, "/", 3) + if len(fields) < 3 || !strings.HasPrefix(fields[2], "info/lfs") { + return "" + } + return setting.LocalURL + "api/internal/repo/" + routePath +} + +func isInternalLFSURL(s string) bool { + if !strings.HasPrefix(s, setting.LocalURL) { + return false + } + u, err := url.Parse(s) + if err != nil { + return false + } + routePath := util.PathJoinRelX(u.Path) + subRoutePath, cut := strings.CutPrefix(routePath, "api/internal/repo/") + if !cut { + return false + } + fields := strings.SplitN(subRoutePath, "/", 3) + if len(fields) < 3 || !strings.HasPrefix(fields[2], "info/lfs") { + return false + } + return true +} +func newInternalRequestLFS(ctx context.Context, internalURL, method string, headers map[string]string, body any) *httplib.Request { + if !isInternalLFSURL(internalURL) { + return nil + } + req := private.NewInternalRequest(ctx, internalURL, method) for k, v := range headers { req.Header(k, v) } - - req.Body(body) - + switch body := body.(type) { + case nil: // do nothing + case []byte: + req.Body(body) // []byte + case io.Reader: + req.Body(body) // io.Reader or io.ReadCloser + default: + panic(fmt.Sprintf("unsupported request body type %T", body)) + } return req } diff --git a/modules/lfstransfer/backend/util_test.go b/modules/lfstransfer/backend/util_test.go new file mode 100644 index 0000000000000..0f6d7af803cc8 --- /dev/null +++ b/modules/lfstransfer/backend/util_test.go @@ -0,0 +1,54 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package backend + +import ( + "context" + "testing" + + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" + + "github.com/stretchr/testify/assert" +) + +func TestToInternalLFSURL(t *testing.T) { + defer test.MockVariableValue(&setting.LocalURL, "http://localurl/")() + defer test.MockVariableValue(&setting.AppSubURL, "/sub")() + cases := []struct { + url string + expected string + }{ + {"http://appurl/any", ""}, + {"http://appurl/sub/any", ""}, + {"http://appurl/sub/owner/repo/any", ""}, + {"http://appurl/sub/owner/repo/info/any", ""}, + {"http://appurl/sub/owner/repo/info/lfs/any", "http://localurl/api/internal/repo/owner/repo/info/lfs/any"}, + } + for _, c := range cases { + assert.Equal(t, c.expected, toInternalLFSURL(c.url), c.url) + } +} + +func TestIsInternalLFSURL(t *testing.T) { + defer test.MockVariableValue(&setting.LocalURL, "http://localurl/")() + defer test.MockVariableValue(&setting.InternalToken, "mock-token")() + cases := []struct { + url string + expected bool + }{ + {"", false}, + {"http://otherurl/api/internal/repo/owner/repo/info/lfs/any", false}, + {"http://localurl/api/internal/repo/owner/repo/info/lfs/any", true}, + {"http://localurl/api/internal/repo/owner/repo/info", false}, + {"http://localurl/api/internal/misc/owner/repo/info/lfs/any", false}, + {"http://localurl/api/internal/owner/repo/info/lfs/any", false}, + {"http://localurl/api/internal/foo/bar", false}, + } + for _, c := range cases { + req := newInternalRequestLFS(context.Background(), c.url, "GET", nil, nil) + assert.Equal(t, c.expected, req != nil, c.url) + assert.Equal(t, c.expected, isInternalLFSURL(c.url), c.url) + } +} diff --git a/modules/markup/html_commit.go b/modules/markup/html_commit.go index aa1b7d034a594..358e7b06ba538 100644 --- a/modules/markup/html_commit.go +++ b/modules/markup/html_commit.go @@ -8,7 +8,6 @@ import ( "strings" "code.gitea.io/gitea/modules/base" - "code.gitea.io/gitea/modules/references" "code.gitea.io/gitea/modules/util" "golang.org/x/net/html" @@ -195,21 +194,3 @@ func hashCurrentPatternProcessor(ctx *RenderContext, node *html.Node) { node = node.NextSibling.NextSibling } } - -func commitCrossReferencePatternProcessor(ctx *RenderContext, node *html.Node) { - next := node.NextSibling - - for node != nil && node != next { - found, ref := references.FindRenderizableCommitCrossReference(node.Data) - if !found { - return - } - - reftext := ref.Owner + "/" + ref.Name + "@" + base.ShortSha(ref.CommitSha) - linkHref := ctx.RenderHelper.ResolveLink(util.URLJoin(ref.Owner, ref.Name, "commit", ref.CommitSha), LinkTypeApp) - link := createLink(ctx, linkHref, reftext, "commit") - - replaceContent(node, ref.RefLocation.Start, ref.RefLocation.End, link) - node = node.NextSibling.NextSibling - } -} diff --git a/modules/markup/html_issue.go b/modules/markup/html_issue.go index 7a6f33011a7c0..e64ec76c3d2fc 100644 --- a/modules/markup/html_issue.go +++ b/modules/markup/html_issue.go @@ -4,9 +4,9 @@ package markup import ( - "strconv" "strings" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/httplib" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/references" @@ -16,16 +16,8 @@ import ( "code.gitea.io/gitea/modules/util" "golang.org/x/net/html" - "golang.org/x/net/html/atom" ) -type RenderIssueIconTitleOptions struct { - OwnerName string - RepoName string - LinkHref string - IssueIndex int64 -} - func fullIssuePatternProcessor(ctx *RenderContext, node *html.Node) { if ctx.RenderOptions.Metas == nil { return @@ -74,27 +66,6 @@ func fullIssuePatternProcessor(ctx *RenderContext, node *html.Node) { } } -func createIssueLinkContentWithSummary(ctx *RenderContext, linkHref string, ref *references.RenderizableReference) *html.Node { - if DefaultRenderHelperFuncs.RenderRepoIssueIconTitle == nil { - return nil - } - issueIndex, _ := strconv.ParseInt(ref.Issue, 10, 64) - h, err := DefaultRenderHelperFuncs.RenderRepoIssueIconTitle(ctx, RenderIssueIconTitleOptions{ - OwnerName: ref.Owner, - RepoName: ref.Name, - LinkHref: linkHref, - IssueIndex: issueIndex, - }) - if err != nil { - log.Error("RenderRepoIssueIconTitle failed: %v", err) - return nil - } - if h == "" { - return nil - } - return &html.Node{Type: html.RawNode, Data: string(ctx.RenderInternal.ProtectSafeAttrs(h))} -} - func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) { if ctx.RenderOptions.Metas == nil { return @@ -105,28 +76,32 @@ func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) { // old logic: crossLinkOnly := ctx.RenderOptions.Metas["mode"] == "document" && !ctx.IsWiki crossLinkOnly := ctx.RenderOptions.Metas["markupAllowShortIssuePattern"] != "true" - var ref *references.RenderizableReference + var ( + found bool + ref *references.RenderizableReference + ) next := node.NextSibling + for node != nil && node != next { _, hasExtTrackFormat := ctx.RenderOptions.Metas["format"] // Repos with external issue trackers might still need to reference local PRs // We need to concern with the first one that shows up in the text, whichever it is isNumericStyle := ctx.RenderOptions.Metas["style"] == "" || ctx.RenderOptions.Metas["style"] == IssueNameStyleNumeric - refNumeric := references.FindRenderizableReferenceNumeric(node.Data, hasExtTrackFormat && !isNumericStyle, crossLinkOnly) + foundNumeric, refNumeric := references.FindRenderizableReferenceNumeric(node.Data, hasExtTrackFormat && !isNumericStyle, crossLinkOnly) switch ctx.RenderOptions.Metas["style"] { case "", IssueNameStyleNumeric: - ref = refNumeric + found, ref = foundNumeric, refNumeric case IssueNameStyleAlphanumeric: - ref = references.FindRenderizableReferenceAlphanumeric(node.Data) + found, ref = references.FindRenderizableReferenceAlphanumeric(node.Data) case IssueNameStyleRegexp: pattern, err := regexplru.GetCompiled(ctx.RenderOptions.Metas["regexp"]) if err != nil { return } - ref = references.FindRenderizableReferenceRegexp(node.Data, pattern) + found, ref = references.FindRenderizableReferenceRegexp(node.Data, pattern) } // Repos with external issue trackers might still need to reference local PRs @@ -134,17 +109,17 @@ func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) { if hasExtTrackFormat && !isNumericStyle && refNumeric != nil { // If numeric (PR) was found, and it was BEFORE the non-numeric pattern, use that // Allow a free-pass when non-numeric pattern wasn't found. - if ref == nil || refNumeric.RefLocation.Start < ref.RefLocation.Start { + if found && (ref == nil || refNumeric.RefLocation.Start < ref.RefLocation.Start) { + found = foundNumeric ref = refNumeric } } - - if ref == nil { + if !found { return } var link *html.Node - refText := node.Data[ref.RefLocation.Start:ref.RefLocation.End] + reftext := node.Data[ref.RefLocation.Start:ref.RefLocation.End] if hasExtTrackFormat && !ref.IsPull { ctx.RenderOptions.Metas["index"] = ref.Issue @@ -154,23 +129,18 @@ func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) { log.Error("unable to expand template vars for ref %s, err: %v", ref.Issue, err) } - link = createLink(ctx, res, refText, "ref-issue ref-external-issue") + link = createLink(ctx, res, reftext, "ref-issue ref-external-issue") } else { // Path determines the type of link that will be rendered. It's unknown at this point whether // the linked item is actually a PR or an issue. Luckily it's of no real consequence because // Gitea will redirect on click as appropriate. - issueOwner := util.Iif(ref.Owner == "", ctx.RenderOptions.Metas["user"], ref.Owner) - issueRepo := util.Iif(ref.Owner == "", ctx.RenderOptions.Metas["repo"], ref.Name) issuePath := util.Iif(ref.IsPull, "pulls", "issues") - linkHref := ctx.RenderHelper.ResolveLink(util.URLJoin(issueOwner, issueRepo, issuePath, ref.Issue), LinkTypeApp) - - // at the moment, only render the issue index in a full line (or simple line) as icon+title - // otherwise it would be too noisy for "take #1 as an example" in a sentence - if node.Parent.DataAtom == atom.Li && ref.RefLocation.Start < 20 && ref.RefLocation.End == len(node.Data) { - link = createIssueLinkContentWithSummary(ctx, linkHref, ref) - } - if link == nil { - link = createLink(ctx, linkHref, refText, "ref-issue") + if ref.Owner == "" { + linkHref := ctx.RenderHelper.ResolveLink(util.URLJoin(ctx.RenderOptions.Metas["user"], ctx.RenderOptions.Metas["repo"], issuePath, ref.Issue), LinkTypeApp) + link = createLink(ctx, linkHref, reftext, "ref-issue") + } else { + linkHref := ctx.RenderHelper.ResolveLink(util.URLJoin(ref.Owner, ref.Name, issuePath, ref.Issue), LinkTypeApp) + link = createLink(ctx, linkHref, reftext, "ref-issue") } } @@ -198,3 +168,21 @@ func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) { node = node.NextSibling.NextSibling.NextSibling.NextSibling } } + +func commitCrossReferencePatternProcessor(ctx *RenderContext, node *html.Node) { + next := node.NextSibling + + for node != nil && node != next { + found, ref := references.FindRenderizableCommitCrossReference(node.Data) + if !found { + return + } + + reftext := ref.Owner + "/" + ref.Name + "@" + base.ShortSha(ref.CommitSha) + linkHref := ctx.RenderHelper.ResolveLink(util.URLJoin(ref.Owner, ref.Name, "commit", ref.CommitSha), LinkTypeApp) + link := createLink(ctx, linkHref, reftext, "commit") + + replaceContent(node, ref.RefLocation.Start, ref.RefLocation.End, link) + node = node.NextSibling.NextSibling + } +} diff --git a/modules/markup/html_issue_test.go b/modules/markup/html_issue_test.go deleted file mode 100644 index 8d189fbdf62e0..0000000000000 --- a/modules/markup/html_issue_test.go +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright 2024 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package markup_test - -import ( - "context" - "html/template" - "strings" - "testing" - - "code.gitea.io/gitea/modules/htmlutil" - "code.gitea.io/gitea/modules/markup" - "code.gitea.io/gitea/modules/markup/markdown" - testModule "code.gitea.io/gitea/modules/test" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestRender_IssueList(t *testing.T) { - defer testModule.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, true)() - markup.Init(&markup.RenderHelperFuncs{ - RenderRepoIssueIconTitle: func(ctx context.Context, opts markup.RenderIssueIconTitleOptions) (template.HTML, error) { - return htmlutil.HTMLFormat("
issue #%d
", opts.IssueIndex), nil - }, - }) - - test := func(input, expected string) { - rctx := markup.NewTestRenderContext(markup.TestAppURL, map[string]string{ - "user": "test-user", "repo": "test-repo", - "markupAllowShortIssuePattern": "true", - }) - out, err := markdown.RenderString(rctx, input) - require.NoError(t, err) - assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(out))) - } - - t.Run("NormalIssueRef", func(t *testing.T) { - test( - "#12345", - `

#12345

`, - ) - }) - - t.Run("ListIssueRef", func(t *testing.T) { - test( - "* #12345", - `
    -
  • issue #12345
  • -
`, - ) - }) - - t.Run("ListIssueRefNormal", func(t *testing.T) { - test( - "* foo #12345 bar", - ``, - ) - }) - - t.Run("ListTodoIssueRef", func(t *testing.T) { - test( - "* [ ] #12345", - `
    -
  • issue #12345
  • -
`, - ) - }) -} diff --git a/modules/markup/html_link.go b/modules/markup/html_link.go index 0e7a988d3680e..fea82e50ab0a8 100644 --- a/modules/markup/html_link.go +++ b/modules/markup/html_link.go @@ -173,7 +173,7 @@ func linkProcessor(ctx *RenderContext, node *html.Node) { uri := node.Data[m[0]:m[1]] remaining := node.Data[m[1]:] - if util.IsLikelyEllipsisLeftPart(remaining) { + if util.IsLikelySplitLeftPart(remaining) { return } replaceContent(node, m[0], m[1], createLink(ctx, uri, uri, "" /*link*/)) diff --git a/modules/markup/html_test.go b/modules/markup/html_test.go index 6d8f24184bd8a..f14fe4075c437 100644 --- a/modules/markup/html_test.go +++ b/modules/markup/html_test.go @@ -207,12 +207,12 @@ func TestRender_links(t *testing.T) { "ftps://gitea.com", `

ftps://gitea.com

`) - t.Run("LinkEllipsis", func(t *testing.T) { - input := util.EllipsisDisplayString("http://10.1.2.3", 12) + t.Run("LinkSplit", func(t *testing.T) { + input, _ := util.SplitStringAtByteN("http://10.1.2.3", 12) assert.Equal(t, "http://10…", input) test(input, "

http://10…

") - input = util.EllipsisDisplayString("http://10.1.2.3", 13) + input, _ = util.SplitStringAtByteN("http://10.1.2.3", 13) assert.Equal(t, "http://10.…", input) test(input, "

http://10.…

") }) diff --git a/modules/markup/markdown/ast.go b/modules/markup/markdown/ast.go index ca165b1ba045f..f29f883734915 100644 --- a/modules/markup/markdown/ast.go +++ b/modules/markup/markdown/ast.go @@ -4,6 +4,7 @@ package markdown import ( + "html/template" "strconv" "github.com/yuin/goldmark/ast" @@ -29,9 +30,7 @@ func (n *Details) Kind() ast.NodeKind { // NewDetails returns a new Paragraph node. func NewDetails() *Details { - return &Details{ - BaseBlock: ast.BaseBlock{}, - } + return &Details{} } // Summary is a block that contains the summary of details block @@ -54,9 +53,7 @@ func (n *Summary) Kind() ast.NodeKind { // NewSummary returns a new Summary node. func NewSummary() *Summary { - return &Summary{ - BaseBlock: ast.BaseBlock{}, - } + return &Summary{} } // TaskCheckBoxListItem is a block that represents a list item of a markdown block with a checkbox @@ -95,29 +92,6 @@ type Icon struct { Name []byte } -// Dump implements Node.Dump . -func (n *Icon) Dump(source []byte, level int) { - m := map[string]string{} - m["Name"] = string(n.Name) - ast.DumpHelper(n, source, level, m, nil) -} - -// KindIcon is the NodeKind for Icon -var KindIcon = ast.NewNodeKind("Icon") - -// Kind implements Node.Kind. -func (n *Icon) Kind() ast.NodeKind { - return KindIcon -} - -// NewIcon returns a new Paragraph node. -func NewIcon(name string) *Icon { - return &Icon{ - BaseInline: ast.BaseInline{}, - Name: []byte(name), - } -} - // ColorPreview is an inline for a color preview type ColorPreview struct { ast.BaseInline @@ -175,3 +149,24 @@ func NewAttention(attentionType string) *Attention { AttentionType: attentionType, } } + +var KindRawHTML = ast.NewNodeKind("RawHTML") + +type RawHTML struct { + ast.BaseBlock + rawHTML template.HTML +} + +func (n *RawHTML) Dump(source []byte, level int) { + m := map[string]string{} + m["RawHTML"] = string(n.rawHTML) + ast.DumpHelper(n, source, level, m, nil) +} + +func (n *RawHTML) Kind() ast.NodeKind { + return KindRawHTML +} + +func NewRawHTML(rawHTML template.HTML) *RawHTML { + return &RawHTML{rawHTML: rawHTML} +} diff --git a/modules/markup/markdown/convertyaml.go b/modules/markup/markdown/convertyaml.go index 1675b68be2a9a..04664a9c1db4f 100644 --- a/modules/markup/markdown/convertyaml.go +++ b/modules/markup/markdown/convertyaml.go @@ -4,23 +4,22 @@ package markdown import ( + "strings" + + "code.gitea.io/gitea/modules/htmlutil" + "code.gitea.io/gitea/modules/svg" + "github.com/yuin/goldmark/ast" east "github.com/yuin/goldmark/extension/ast" "gopkg.in/yaml.v3" ) func nodeToTable(meta *yaml.Node) ast.Node { - for { - if meta == nil { - return nil - } - switch meta.Kind { - case yaml.DocumentNode: - meta = meta.Content[0] - continue - default: - } - break + for meta != nil && meta.Kind == yaml.DocumentNode { + meta = meta.Content[0] + } + if meta == nil { + return nil } switch meta.Kind { case yaml.MappingNode: @@ -72,12 +71,28 @@ func sequenceNodeToTable(meta *yaml.Node) ast.Node { return table } -func nodeToDetails(meta *yaml.Node, icon string) ast.Node { +func nodeToDetails(g *ASTTransformer, meta *yaml.Node) ast.Node { + for meta != nil && meta.Kind == yaml.DocumentNode { + meta = meta.Content[0] + } + if meta == nil { + return nil + } + if meta.Kind != yaml.MappingNode { + return nil + } + var keys []string + for i := 0; i < len(meta.Content); i += 2 { + if meta.Content[i].Kind == yaml.ScalarNode { + keys = append(keys, meta.Content[i].Value) + } + } details := NewDetails() + details.SetAttributeString(g.renderInternal.SafeAttr("class"), g.renderInternal.SafeValue("frontmatter-content")) summary := NewSummary() - summary.AppendChild(summary, NewIcon(icon)) + summaryInnerHTML := htmlutil.HTMLFormat("%s %s", svg.RenderHTML("octicon-table", 12), strings.Join(keys, ", ")) + summary.AppendChild(summary, NewRawHTML(summaryInnerHTML)) details.AppendChild(details, summary) details.AppendChild(details, nodeToTable(meta)) - return details } diff --git a/modules/markup/markdown/goldmark.go b/modules/markup/markdown/goldmark.go index 69c2a96ff1fd7..bc37fbfe68500 100644 --- a/modules/markup/markdown/goldmark.go +++ b/modules/markup/markdown/goldmark.go @@ -5,9 +5,6 @@ package markdown import ( "fmt" - "regexp" - "strings" - "sync" "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/markup" @@ -51,7 +48,7 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa tocList := make([]Header, 0, 20) if rc.yamlNode != nil { - metaNode := rc.toMetaNode() + metaNode := rc.toMetaNode(g) if metaNode != nil { node.InsertBefore(node, firstChild, metaNode) } @@ -111,11 +108,6 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa } } -// it is copied from old code, which is quite doubtful whether it is correct -var reValidIconName = sync.OnceValue(func() *regexp.Regexp { - return regexp.MustCompile(`^[-\w]+$`) // old: regexp.MustCompile("^[a-z ]+$") -}) - // NewHTMLRenderer creates a HTMLRenderer to render in the gitea form. func NewHTMLRenderer(renderInternal *internal.RenderInternal, opts ...html.Option) renderer.NodeRenderer { r := &HTMLRenderer{ @@ -140,11 +132,11 @@ func (r *HTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { reg.Register(ast.KindDocument, r.renderDocument) reg.Register(KindDetails, r.renderDetails) reg.Register(KindSummary, r.renderSummary) - reg.Register(KindIcon, r.renderIcon) reg.Register(ast.KindCodeSpan, r.renderCodeSpan) reg.Register(KindAttention, r.renderAttention) reg.Register(KindTaskCheckBoxListItem, r.renderTaskCheckBoxListItem) reg.Register(east.KindTaskCheckBox, r.renderTaskCheckBox) + reg.Register(KindRawHTML, r.renderRawHTML) } func (r *HTMLRenderer) renderDocument(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { @@ -206,30 +198,14 @@ func (r *HTMLRenderer) renderSummary(w util.BufWriter, source []byte, node ast.N return ast.WalkContinue, nil } -func (r *HTMLRenderer) renderIcon(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { +func (r *HTMLRenderer) renderRawHTML(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { if !entering { return ast.WalkContinue, nil } - - n := node.(*Icon) - - name := strings.TrimSpace(strings.ToLower(string(n.Name))) - - if len(name) == 0 { - // skip this - return ast.WalkContinue, nil - } - - if !reValidIconName().MatchString(name) { - // skip this - return ast.WalkContinue, nil - } - - // FIXME: the "icon xxx" is from Fomantic UI, it's really questionable whether it still works correctly - err := r.renderInternal.FormatWithSafeAttrs(w, ``, name) + n := node.(*RawHTML) + _, err := w.WriteString(string(r.renderInternal.ProtectSafeAttrs(n.rawHTML))) if err != nil { return ast.WalkStop, err } - return ast.WalkContinue, nil } diff --git a/modules/markup/markdown/markdown.go b/modules/markup/markdown/markdown.go index b5fffccdb989a..19fd933866232 100644 --- a/modules/markup/markdown/markdown.go +++ b/modules/markup/markdown/markdown.go @@ -159,6 +159,14 @@ func render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error limit: setting.UI.MaxDisplayFileSize * 3, } + // FIXME: Don't read all to memory, but goldmark doesn't support + buf, err := io.ReadAll(input) + if err != nil { + log.Error("Unable to ReadAll: %v", err) + return err + } + buf = giteautil.NormalizeEOL(buf) + // FIXME: should we include a timeout to abort the renderer if it takes too long? defer func() { err := recover() @@ -166,29 +174,17 @@ func render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error return } - log.Warn("Unable to render markdown due to panic in goldmark: %v", err) - if (!setting.IsProd && !setting.IsInTesting) || log.IsDebug() { - log.Error("Panic in markdown: %v\n%s", err, log.Stack(2)) - } + log.Error("Panic in markdown: %v\n%s", err, log.Stack(2)) + escapedHTML := template.HTMLEscapeString(giteautil.UnsafeBytesToString(buf)) + _, _ = output.Write(giteautil.UnsafeStringToBytes(escapedHTML)) }() - // FIXME: Don't read all to memory, but goldmark doesn't support pc := newParserContext(ctx) - buf, err := io.ReadAll(input) - if err != nil { - log.Error("Unable to ReadAll: %v", err) - return err - } - buf = giteautil.NormalizeEOL(buf) // Preserve original length. bufWithMetadataLength := len(buf) - rc := &RenderConfig{ - Meta: markup.RenderMetaAsDetails, - Icon: "table", - Lang: "", - } + rc := &RenderConfig{Meta: markup.RenderMetaAsDetails} buf, _ = ExtractMetadataBytes(buf, rc) metaLength := bufWithMetadataLength - len(buf) diff --git a/modules/markup/markdown/markdown_attention_test.go b/modules/markup/markdown/markdown_attention_test.go index f6ec775b2c929..7b54653ec0ed1 100644 --- a/modules/markup/markdown/markdown_attention_test.go +++ b/modules/markup/markdown/markdown_attention_test.go @@ -23,6 +23,11 @@ func TestAttention(t *testing.T) { defer svg.MockIcon("octicon-alert")() defer svg.MockIcon("octicon-stop")() + test := func(input, expected string) { + result, err := markdown.RenderString(markup.NewTestRenderContext(), input) + assert.NoError(t, err) + assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(result))) + } renderAttention := func(attention, icon string) string { tmpl := `

{Attention}

` tmpl = strings.ReplaceAll(tmpl, "{attention}", attention) @@ -31,12 +36,6 @@ func TestAttention(t *testing.T) { return tmpl } - test := func(input, expected string) { - result, err := markdown.RenderString(markup.NewTestRenderContext(), input) - assert.NoError(t, err) - assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(result))) - } - test(` > [!NOTE] > text @@ -53,4 +52,7 @@ func TestAttention(t *testing.T) { // legacy GitHub style test(`> **warning**`, renderAttention("warning", "octicon-alert")+"\n
") + + // edge case (it used to cause panic) + test(">\ntext", "
\n
\n

text

") } diff --git a/modules/markup/markdown/markdown_test.go b/modules/markup/markdown/markdown_test.go index 7a09be8665827..268a543835db6 100644 --- a/modules/markup/markdown/markdown_test.go +++ b/modules/markup/markdown/markdown_test.go @@ -383,18 +383,74 @@ func TestColorPreview(t *testing.T) { } } -func TestTaskList(t *testing.T) { +func TestMarkdownFrontmatter(t *testing.T) { testcases := []struct { - testcase string + name string + input string expected string }{ + { + "MapInFrontmatter", + `--- +key1: val1 +key2: val2 +--- +test +`, + `
octicon-table(12/) key1, key2 + + + + + + + + + + + + +
key1key2
val1val2
+

test

+`, + }, + + { + "ListInFrontmatter", + `--- +- item1 +- item2 +--- +test +`, + `- item1 +- item2 + +

test

+`, + }, + + { + "StringInFrontmatter", + `--- +anything +--- +test +`, + `anything + +

test

+`, + }, + { // data-source-position should take into account YAML frontmatter. + "ListAfterFrontmatter", `--- foo: bar --- - [ ] task 1`, - `
+ `
octicon-table(12/) foo
@@ -414,9 +470,9 @@ foo: bar } for _, test := range testcases { - res, err := markdown.RenderString(markup.NewTestRenderContext(), test.testcase) - assert.NoError(t, err, "Unexpected error in testcase: %q", test.testcase) - assert.Equal(t, template.HTML(test.expected), res, "Unexpected result in testcase %q", test.testcase) + res, err := markdown.RenderString(markup.NewTestRenderContext(), test.input) + assert.NoError(t, err, "Unexpected error in testcase: %q", test.name) + assert.Equal(t, test.expected, string(res), "Unexpected result in testcase %q", test.name) } } diff --git a/modules/markup/markdown/renderconfig.go b/modules/markup/markdown/renderconfig.go index f4c48d1b3d8f9..d8b1b10ce632a 100644 --- a/modules/markup/markdown/renderconfig.go +++ b/modules/markup/markdown/renderconfig.go @@ -16,7 +16,6 @@ import ( // RenderConfig represents rendering configuration for this file type RenderConfig struct { Meta markup.RenderMetaMode - Icon string TOC string // "false": hide, "side"/empty: in sidebar, "main"/"true": in main view Lang string yamlNode *yaml.Node @@ -74,7 +73,7 @@ func (rc *RenderConfig) UnmarshalYAML(value *yaml.Node) error { type yamlRenderConfig struct { Meta *string `yaml:"meta"` - Icon *string `yaml:"details_icon"` + Icon *string `yaml:"details_icon"` // deprecated, because there is no font icon, so no custom icon TOC *string `yaml:"include_toc"` Lang *string `yaml:"lang"` } @@ -96,10 +95,6 @@ func (rc *RenderConfig) UnmarshalYAML(value *yaml.Node) error { rc.Meta = renderMetaModeFromString(*cfg.Gitea.Meta) } - if cfg.Gitea.Icon != nil { - rc.Icon = strings.TrimSpace(strings.ToLower(*cfg.Gitea.Icon)) - } - if cfg.Gitea.Lang != nil && *cfg.Gitea.Lang != "" { rc.Lang = *cfg.Gitea.Lang } @@ -111,7 +106,7 @@ func (rc *RenderConfig) UnmarshalYAML(value *yaml.Node) error { return nil } -func (rc *RenderConfig) toMetaNode() ast.Node { +func (rc *RenderConfig) toMetaNode(g *ASTTransformer) ast.Node { if rc.yamlNode == nil { return nil } @@ -119,7 +114,7 @@ func (rc *RenderConfig) toMetaNode() ast.Node { case markup.RenderMetaAsTable: return nodeToTable(rc.yamlNode) case markup.RenderMetaAsDetails: - return nodeToDetails(rc.yamlNode, rc.Icon) + return nodeToDetails(g, rc.yamlNode) default: return nil } diff --git a/modules/markup/markdown/renderconfig_test.go b/modules/markup/markdown/renderconfig_test.go index c53acdc77a788..8513bf75b652e 100644 --- a/modules/markup/markdown/renderconfig_test.go +++ b/modules/markup/markdown/renderconfig_test.go @@ -7,6 +7,7 @@ import ( "strings" "testing" + "github.com/stretchr/testify/assert" "gopkg.in/yaml.v3" ) @@ -19,42 +20,36 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) { { "empty", &RenderConfig{ Meta: "table", - Icon: "table", Lang: "", }, "", }, { "lang", &RenderConfig{ Meta: "table", - Icon: "table", Lang: "test", }, "lang: test", }, { "metatable", &RenderConfig{ Meta: "table", - Icon: "table", Lang: "", }, "gitea: table", }, { "metanone", &RenderConfig{ Meta: "none", - Icon: "table", Lang: "", }, "gitea: none", }, { "metadetails", &RenderConfig{ Meta: "details", - Icon: "table", Lang: "", }, "gitea: details", }, { "metawrong", &RenderConfig{ Meta: "details", - Icon: "table", Lang: "", }, "gitea: wrong", }, @@ -62,7 +57,6 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) { "toc", &RenderConfig{ TOC: "true", Meta: "table", - Icon: "table", Lang: "", }, "include_toc: true", }, @@ -70,14 +64,12 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) { "tocfalse", &RenderConfig{ TOC: "false", Meta: "table", - Icon: "table", Lang: "", }, "include_toc: false", }, { "toclang", &RenderConfig{ Meta: "table", - Icon: "table", TOC: "true", Lang: "testlang", }, ` @@ -88,7 +80,6 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) { { "complexlang", &RenderConfig{ Meta: "table", - Icon: "table", Lang: "testlang", }, ` gitea: @@ -98,7 +89,6 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) { { "complexlang2", &RenderConfig{ Meta: "table", - Icon: "table", Lang: "testlang", }, ` lang: notright @@ -109,7 +99,6 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) { { "complexlang", &RenderConfig{ Meta: "table", - Icon: "table", Lang: "testlang", }, ` gitea: @@ -121,7 +110,6 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) { Lang: "two", Meta: "table", TOC: "true", - Icon: "smiley", }, ` lang: one include_toc: true @@ -137,7 +125,6 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) { t.Run(tt.name, func(t *testing.T) { got := &RenderConfig{ Meta: "table", - Icon: "table", Lang: "", } if err := yaml.Unmarshal([]byte(strings.ReplaceAll(tt.args, "\t", " ")), got); err != nil { @@ -145,18 +132,9 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) { return } - if got.Meta != tt.expected.Meta { - t.Errorf("Meta Expected %s Got %s", tt.expected.Meta, got.Meta) - } - if got.Icon != tt.expected.Icon { - t.Errorf("Icon Expected %s Got %s", tt.expected.Icon, got.Icon) - } - if got.Lang != tt.expected.Lang { - t.Errorf("Lang Expected %s Got %s", tt.expected.Lang, got.Lang) - } - if got.TOC != tt.expected.TOC { - t.Errorf("TOC Expected %q Got %q", tt.expected.TOC, got.TOC) - } + assert.Equal(t, tt.expected.Meta, got.Meta) + assert.Equal(t, tt.expected.Lang, got.Lang) + assert.Equal(t, tt.expected.TOC, got.TOC) }) } } diff --git a/modules/markup/markdown/transform_blockquote.go b/modules/markup/markdown/transform_blockquote.go index 2651d44a69ff9..3a8c6fa01869c 100644 --- a/modules/markup/markdown/transform_blockquote.go +++ b/modules/markup/markdown/transform_blockquote.go @@ -115,6 +115,9 @@ func (g *ASTTransformer) transformBlockquote(v *ast.Blockquote, reader text.Read // grab these nodes and make sure we adhere to the attention blockquote structure firstParagraph := v.FirstChild() + if firstParagraph == nil { + return ast.WalkContinue, nil + } g.applyElementDir(firstParagraph) attentionType, processedNodes := g.extractBlockquoteAttentionEmphasis(firstParagraph, reader) diff --git a/modules/markup/orgmode/orgmode_test.go b/modules/markup/orgmode/orgmode_test.go index e3cc05b4f022c..de39bafebeabe 100644 --- a/modules/markup/orgmode/orgmode_test.go +++ b/modules/markup/orgmode/orgmode_test.go @@ -103,8 +103,8 @@ func HelloWorld() { } #+end_src `, `
-
// HelloWorld prints "Hello World"
-func HelloWorld() {
+
// HelloWorld prints "Hello World"
+func HelloWorld() {
 	fmt.Println("Hello World")
 }
`) diff --git a/modules/markup/render_helper.go b/modules/markup/render_helper.go index 8ff0e7d6fb41c..82796ef274558 100644 --- a/modules/markup/render_helper.go +++ b/modules/markup/render_helper.go @@ -38,7 +38,6 @@ type RenderHelper interface { type RenderHelperFuncs struct { IsUsernameMentionable func(ctx context.Context, username string) bool RenderRepoFileCodePreview func(ctx context.Context, options RenderCodePreviewOptions) (template.HTML, error) - RenderRepoIssueIconTitle func(ctx context.Context, options RenderIssueIconTitleOptions) (template.HTML, error) } var DefaultRenderHelperFuncs *RenderHelperFuncs diff --git a/modules/markup/renderer_test.go b/modules/markup/renderer_test.go deleted file mode 100644 index 0791081f94e8f..0000000000000 --- a/modules/markup/renderer_test.go +++ /dev/null @@ -1,4 +0,0 @@ -// Copyright 2017 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package markup_test diff --git a/modules/markup/sanitizer_default.go b/modules/markup/sanitizer_default.go index 5eeafe940a4da..14161eb533794 100644 --- a/modules/markup/sanitizer_default.go +++ b/modules/markup/sanitizer_default.go @@ -48,7 +48,7 @@ func (st *Sanitizer) createDefaultPolicy() *bluemonday.Policy { policy.AllowAttrs("class").Matching(regexp.MustCompile(`^(unchecked|checked|indeterminate)$`)).OnElements("li") // Allow 'color' and 'background-color' properties for the style attribute on text elements. - policy.AllowStyles("color", "background-color").OnElements("span", "p") + policy.AllowStyles("color", "background-color").OnElements("div", "span", "p", "tr", "th", "td") policy.AllowAttrs("src", "autoplay", "controls").OnElements("video") diff --git a/modules/packages/npm/creator.go b/modules/packages/npm/creator.go index 7d3d7cd6b55f3..8ba4dbfba710c 100644 --- a/modules/packages/npm/creator.go +++ b/modules/packages/npm/creator.go @@ -81,6 +81,7 @@ type PackageMetadataVersion struct { BundleDependencies []string `json:"bundleDependencies,omitempty"` DevDependencies map[string]string `json:"devDependencies,omitempty"` PeerDependencies map[string]string `json:"peerDependencies,omitempty"` + PeerDependenciesMeta map[string]any `json:"peerDependenciesMeta,omitempty"` Bin map[string]string `json:"bin,omitempty"` OptionalDependencies map[string]string `json:"optionalDependencies,omitempty"` Readme string `json:"readme,omitempty"` @@ -222,6 +223,7 @@ func ParsePackage(r io.Reader) (*Package, error) { BundleDependencies: meta.BundleDependencies, DevelopmentDependencies: meta.DevDependencies, PeerDependencies: meta.PeerDependencies, + PeerDependenciesMeta: meta.PeerDependenciesMeta, OptionalDependencies: meta.OptionalDependencies, Bin: meta.Bin, Readme: meta.Readme, diff --git a/modules/packages/npm/metadata.go b/modules/packages/npm/metadata.go index 6bb77f302bb16..d1d026338780c 100644 --- a/modules/packages/npm/metadata.go +++ b/modules/packages/npm/metadata.go @@ -19,6 +19,7 @@ type Metadata struct { BundleDependencies []string `json:"bundleDependencies,omitempty"` DevelopmentDependencies map[string]string `json:"development_dependencies,omitempty"` PeerDependencies map[string]string `json:"peer_dependencies,omitempty"` + PeerDependenciesMeta map[string]any `json:"peer_dependencies_meta,omitempty"` OptionalDependencies map[string]string `json:"optional_dependencies,omitempty"` Bin map[string]string `json:"bin,omitempty"` Readme string `json:"readme,omitempty"` diff --git a/modules/pandoc/command.go b/modules/pandoc/command.go index 8f25a12b267f4..58d056cc6dd73 100644 --- a/modules/pandoc/command.go +++ b/modules/pandoc/command.go @@ -1,3 +1,6 @@ +// Copyright 2025 The Bindersnap Authors. All rights reserved. +// SPDX-License-Identifier: LicenseRef-License + package pandoc import ( diff --git a/modules/pandoc/pandoc.go b/modules/pandoc/pandoc.go index 68e9d68b52ed2..5b2d454313354 100644 --- a/modules/pandoc/pandoc.go +++ b/modules/pandoc/pandoc.go @@ -1,3 +1,6 @@ +// Copyright 2025 The Bindersnap Authors. All rights reserved. +// SPDX-License-Identifier: LicenseRef-License + package pandoc import ( diff --git a/modules/private/actions.go b/modules/private/actions.go index 311a28365004b..e68f2f85b0388 100644 --- a/modules/private/actions.go +++ b/modules/private/actions.go @@ -17,7 +17,7 @@ type GenerateTokenRequest struct { func GenerateActionsRunnerToken(ctx context.Context, scope string) (*ResponseText, ResponseExtra) { reqURL := setting.LocalURL + "api/internal/actions/generate_actions_runner_token" - req := newInternalRequest(ctx, reqURL, "POST", GenerateTokenRequest{ + req := newInternalRequestAPI(ctx, reqURL, "POST", GenerateTokenRequest{ Scope: scope, }) diff --git a/modules/private/hook.go b/modules/private/hook.go index 745c200619faa..215996b9b9936 100644 --- a/modules/private/hook.go +++ b/modules/private/hook.go @@ -7,9 +7,9 @@ import ( "context" "fmt" "net/url" - "time" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/httplib" "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" ) @@ -82,29 +82,32 @@ type HookProcReceiveRefResult struct { HeadBranch string } +func newInternalRequestAPIForHooks(ctx context.Context, hookName, ownerName, repoName string, opts HookOptions) *httplib.Request { + reqURL := setting.LocalURL + fmt.Sprintf("api/internal/hook/%s/%s/%s", hookName, url.PathEscape(ownerName), url.PathEscape(repoName)) + req := newInternalRequestAPI(ctx, reqURL, "POST", opts) + // This "timeout" applies to http.Client's timeout: A Timeout of zero means no timeout. + // This "timeout" was previously set to `time.Duration(60+len(opts.OldCommitIDs))` seconds, but it caused unnecessary timeout failures. + // It should be good enough to remove the client side timeout, only respect the "ctx" and server side timeout. + req.SetReadWriteTimeout(0) + return req +} + // HookPreReceive check whether the provided commits are allowed func HookPreReceive(ctx context.Context, ownerName, repoName string, opts HookOptions) ResponseExtra { - reqURL := setting.LocalURL + fmt.Sprintf("api/internal/hook/pre-receive/%s/%s", url.PathEscape(ownerName), url.PathEscape(repoName)) - req := newInternalRequest(ctx, reqURL, "POST", opts) - req.SetReadWriteTimeout(time.Duration(60+len(opts.OldCommitIDs)) * time.Second) + req := newInternalRequestAPIForHooks(ctx, "pre-receive", ownerName, repoName, opts) _, extra := requestJSONResp(req, &ResponseText{}) return extra } // HookPostReceive updates services and users func HookPostReceive(ctx context.Context, ownerName, repoName string, opts HookOptions) (*HookPostReceiveResult, ResponseExtra) { - reqURL := setting.LocalURL + fmt.Sprintf("api/internal/hook/post-receive/%s/%s", url.PathEscape(ownerName), url.PathEscape(repoName)) - req := newInternalRequest(ctx, reqURL, "POST", opts) - req.SetReadWriteTimeout(time.Duration(60+len(opts.OldCommitIDs)) * time.Second) + req := newInternalRequestAPIForHooks(ctx, "post-receive", ownerName, repoName, opts) return requestJSONResp(req, &HookPostReceiveResult{}) } // HookProcReceive proc-receive hook func HookProcReceive(ctx context.Context, ownerName, repoName string, opts HookOptions) (*HookProcReceiveResult, ResponseExtra) { - reqURL := setting.LocalURL + fmt.Sprintf("api/internal/hook/proc-receive/%s/%s", url.PathEscape(ownerName), url.PathEscape(repoName)) - - req := newInternalRequest(ctx, reqURL, "POST", opts) - req.SetReadWriteTimeout(time.Duration(60+len(opts.OldCommitIDs)) * time.Second) + req := newInternalRequestAPIForHooks(ctx, "proc-receive", ownerName, repoName, opts) return requestJSONResp(req, &HookProcReceiveResult{}) } @@ -115,7 +118,7 @@ func SetDefaultBranch(ctx context.Context, ownerName, repoName, branch string) R url.PathEscape(repoName), url.PathEscape(branch), ) - req := newInternalRequest(ctx, reqURL, "POST") + req := newInternalRequestAPI(ctx, reqURL, "POST") _, extra := requestJSONResp(req, &ResponseText{}) return extra } @@ -123,7 +126,7 @@ func SetDefaultBranch(ctx context.Context, ownerName, repoName, branch string) R // SSHLog sends ssh error log response func SSHLog(ctx context.Context, isErr bool, msg string) error { reqURL := setting.LocalURL + "api/internal/ssh/log" - req := newInternalRequest(ctx, reqURL, "POST", &SSHLogOption{IsError: isErr, Message: msg}) + req := newInternalRequestAPI(ctx, reqURL, "POST", &SSHLogOption{IsError: isErr, Message: msg}) _, extra := requestJSONResp(req, &ResponseText{}) return extra.Error } diff --git a/modules/private/internal.go b/modules/private/internal.go index c7e7773524d79..35eed1d6087ef 100644 --- a/modules/private/internal.go +++ b/modules/private/internal.go @@ -34,12 +34,16 @@ func getClientIP() string { return strings.Fields(sshConnEnv)[0] } -func newInternalRequest(ctx context.Context, url, method string, body ...any) *httplib.Request { +func NewInternalRequest(ctx context.Context, url, method string) *httplib.Request { if setting.InternalToken == "" { log.Fatal(`The INTERNAL_TOKEN setting is missing from the configuration file: %q. Ensure you are running in the correct environment or set the correct configuration file with -c.`, setting.CustomConf) } + if !strings.HasPrefix(url, setting.LocalURL) { + log.Fatal("Invalid internal request URL: %q", url) + } + req := httplib.NewRequest(url, method). SetContext(ctx). Header("X-Real-IP", getClientIP()). @@ -82,13 +86,17 @@ Ensure you are running in the correct environment or set the correct configurati }, }) } + return req +} +func newInternalRequestAPI(ctx context.Context, url, method string, body ...any) *httplib.Request { + req := NewInternalRequest(ctx, url, method) if len(body) == 1 { req.Header("Content-Type", "application/json") jsonBytes, _ := json.Marshal(body[0]) req.Body(jsonBytes) } else if len(body) > 1 { - log.Fatal("Too many arguments for newInternalRequest") + log.Fatal("Too many arguments for newInternalRequestAPI") } req.SetTimeout(10*time.Second, 60*time.Second) diff --git a/modules/private/key.go b/modules/private/key.go index dcd17148564cd..114683b34392c 100644 --- a/modules/private/key.go +++ b/modules/private/key.go @@ -14,7 +14,7 @@ import ( func UpdatePublicKeyInRepo(ctx context.Context, keyID, repoID int64) error { // Ask for running deliver hook and test pull request tasks. reqURL := setting.LocalURL + fmt.Sprintf("api/internal/ssh/%d/update/%d", keyID, repoID) - req := newInternalRequest(ctx, reqURL, "POST") + req := newInternalRequestAPI(ctx, reqURL, "POST") _, extra := requestJSONResp(req, &ResponseText{}) return extra.Error } @@ -24,7 +24,7 @@ func UpdatePublicKeyInRepo(ctx context.Context, keyID, repoID int64) error { func AuthorizedPublicKeyByContent(ctx context.Context, content string) (*ResponseText, ResponseExtra) { // Ask for running deliver hook and test pull request tasks. reqURL := setting.LocalURL + "api/internal/ssh/authorized_keys" - req := newInternalRequest(ctx, reqURL, "POST") + req := newInternalRequestAPI(ctx, reqURL, "POST") req.Param("content", content) return requestJSONResp(req, &ResponseText{}) } diff --git a/modules/private/mail.go b/modules/private/mail.go index 08de5b7e28724..3904e37beab26 100644 --- a/modules/private/mail.go +++ b/modules/private/mail.go @@ -23,7 +23,7 @@ type Email struct { func SendEmail(ctx context.Context, subject, message string, to []string) (*ResponseText, ResponseExtra) { reqURL := setting.LocalURL + "api/internal/mail/send" - req := newInternalRequest(ctx, reqURL, "POST", Email{ + req := newInternalRequestAPI(ctx, reqURL, "POST", Email{ Subject: subject, Message: message, To: to, diff --git a/modules/private/manager.go b/modules/private/manager.go index 6055e553bd0f0..e3d5ad57e068f 100644 --- a/modules/private/manager.go +++ b/modules/private/manager.go @@ -18,21 +18,21 @@ import ( // Shutdown calls the internal shutdown function func Shutdown(ctx context.Context) ResponseExtra { reqURL := setting.LocalURL + "api/internal/manager/shutdown" - req := newInternalRequest(ctx, reqURL, "POST") + req := newInternalRequestAPI(ctx, reqURL, "POST") return requestJSONClientMsg(req, "Shutting down") } // Restart calls the internal restart function func Restart(ctx context.Context) ResponseExtra { reqURL := setting.LocalURL + "api/internal/manager/restart" - req := newInternalRequest(ctx, reqURL, "POST") + req := newInternalRequestAPI(ctx, reqURL, "POST") return requestJSONClientMsg(req, "Restarting") } // ReloadTemplates calls the internal reload-templates function func ReloadTemplates(ctx context.Context) ResponseExtra { reqURL := setting.LocalURL + "api/internal/manager/reload-templates" - req := newInternalRequest(ctx, reqURL, "POST") + req := newInternalRequestAPI(ctx, reqURL, "POST") return requestJSONClientMsg(req, "Reloaded") } @@ -45,7 +45,7 @@ type FlushOptions struct { // FlushQueues calls the internal flush-queues function func FlushQueues(ctx context.Context, timeout time.Duration, nonBlocking bool) ResponseExtra { reqURL := setting.LocalURL + "api/internal/manager/flush-queues" - req := newInternalRequest(ctx, reqURL, "POST", FlushOptions{Timeout: timeout, NonBlocking: nonBlocking}) + req := newInternalRequestAPI(ctx, reqURL, "POST", FlushOptions{Timeout: timeout, NonBlocking: nonBlocking}) if timeout > 0 { req.SetReadWriteTimeout(timeout + 10*time.Second) } @@ -55,28 +55,28 @@ func FlushQueues(ctx context.Context, timeout time.Duration, nonBlocking bool) R // PauseLogging pauses logging func PauseLogging(ctx context.Context) ResponseExtra { reqURL := setting.LocalURL + "api/internal/manager/pause-logging" - req := newInternalRequest(ctx, reqURL, "POST") + req := newInternalRequestAPI(ctx, reqURL, "POST") return requestJSONClientMsg(req, "Logging Paused") } // ResumeLogging resumes logging func ResumeLogging(ctx context.Context) ResponseExtra { reqURL := setting.LocalURL + "api/internal/manager/resume-logging" - req := newInternalRequest(ctx, reqURL, "POST") + req := newInternalRequestAPI(ctx, reqURL, "POST") return requestJSONClientMsg(req, "Logging Restarted") } // ReleaseReopenLogging releases and reopens logging files func ReleaseReopenLogging(ctx context.Context) ResponseExtra { reqURL := setting.LocalURL + "api/internal/manager/release-and-reopen-logging" - req := newInternalRequest(ctx, reqURL, "POST") + req := newInternalRequestAPI(ctx, reqURL, "POST") return requestJSONClientMsg(req, "Logging Restarted") } // SetLogSQL sets database logging func SetLogSQL(ctx context.Context, on bool) ResponseExtra { reqURL := setting.LocalURL + "api/internal/manager/set-log-sql?on=" + strconv.FormatBool(on) - req := newInternalRequest(ctx, reqURL, "POST") + req := newInternalRequestAPI(ctx, reqURL, "POST") return requestJSONClientMsg(req, "Log SQL setting set") } @@ -91,7 +91,7 @@ type LoggerOptions struct { // AddLogger adds a logger func AddLogger(ctx context.Context, logger, writer, mode string, config map[string]any) ResponseExtra { reqURL := setting.LocalURL + "api/internal/manager/add-logger" - req := newInternalRequest(ctx, reqURL, "POST", LoggerOptions{ + req := newInternalRequestAPI(ctx, reqURL, "POST", LoggerOptions{ Logger: logger, Writer: writer, Mode: mode, @@ -103,7 +103,7 @@ func AddLogger(ctx context.Context, logger, writer, mode string, config map[stri // RemoveLogger removes a logger func RemoveLogger(ctx context.Context, logger, writer string) ResponseExtra { reqURL := setting.LocalURL + fmt.Sprintf("api/internal/manager/remove-logger/%s/%s", url.PathEscape(logger), url.PathEscape(writer)) - req := newInternalRequest(ctx, reqURL, "POST") + req := newInternalRequestAPI(ctx, reqURL, "POST") return requestJSONClientMsg(req, "Removed") } @@ -111,7 +111,7 @@ func RemoveLogger(ctx context.Context, logger, writer string) ResponseExtra { func Processes(ctx context.Context, out io.Writer, flat, noSystem, stacktraces, json bool, cancel string) ResponseExtra { reqURL := setting.LocalURL + fmt.Sprintf("api/internal/manager/processes?flat=%t&no-system=%t&stacktraces=%t&json=%t&cancel-pid=%s", flat, noSystem, stacktraces, json, url.QueryEscape(cancel)) - req := newInternalRequest(ctx, reqURL, "GET") + req := newInternalRequestAPI(ctx, reqURL, "GET") callback := func(resp *http.Response, extra *ResponseExtra) { _, extra.Error = io.Copy(out, resp.Body) } diff --git a/modules/private/restore_repo.go b/modules/private/restore_repo.go index 496209d3cbda2..9c3a0081427c9 100644 --- a/modules/private/restore_repo.go +++ b/modules/private/restore_repo.go @@ -24,7 +24,7 @@ type RestoreParams struct { func RestoreRepo(ctx context.Context, repoDir, ownerName, repoName string, units []string, validation bool) ResponseExtra { reqURL := setting.LocalURL + "api/internal/restore_repo" - req := newInternalRequest(ctx, reqURL, "POST", RestoreParams{ + req := newInternalRequestAPI(ctx, reqURL, "POST", RestoreParams{ RepoDir: repoDir, OwnerName: ownerName, RepoName: repoName, diff --git a/modules/private/serv.go b/modules/private/serv.go index 480a44695496d..2ccc6c112915a 100644 --- a/modules/private/serv.go +++ b/modules/private/serv.go @@ -23,7 +23,7 @@ type KeyAndOwner struct { // ServNoCommand returns information about the provided key func ServNoCommand(ctx context.Context, keyID int64) (*asymkey_model.PublicKey, *user_model.User, error) { reqURL := setting.LocalURL + fmt.Sprintf("api/internal/serv/none/%d", keyID) - req := newInternalRequest(ctx, reqURL, "GET") + req := newInternalRequestAPI(ctx, reqURL, "GET") keyAndOwner, extra := requestJSONResp(req, &KeyAndOwner{}) if extra.HasError() { return nil, nil, extra.Error @@ -58,6 +58,6 @@ func ServCommand(ctx context.Context, keyID int64, ownerName, repoName string, m reqURL += fmt.Sprintf("&verb=%s", url.QueryEscape(verb)) } } - req := newInternalRequest(ctx, reqURL, "GET") + req := newInternalRequestAPI(ctx, reqURL, "GET") return requestJSONResp(req, &ServCommandResults{}) } diff --git a/modules/process/context.go b/modules/process/context.go index 1854988bce2ba..26a80ebd6207c 100644 --- a/modules/process/context.go +++ b/modules/process/context.go @@ -32,7 +32,7 @@ func (c *Context) Value(key any) any { } // ProcessContextKey is the key under which process contexts are stored -var ProcessContextKey any = "process_context" +var ProcessContextKey any = "process-context" // GetContext will return a process context if one exists func GetContext(ctx context.Context) *Context { diff --git a/modules/process/manager.go b/modules/process/manager.go index 661511ce8d6b3..bdc4931810d1c 100644 --- a/modules/process/manager.go +++ b/modules/process/manager.go @@ -11,8 +11,6 @@ import ( "sync" "sync/atomic" "time" - - "code.gitea.io/gitea/modules/gtprof" ) // TODO: This packages still uses a singleton for the Manager. @@ -27,6 +25,18 @@ var ( DefaultContext = context.Background() ) +// DescriptionPProfLabel is a label set on goroutines that have a process attached +const DescriptionPProfLabel = "process-description" + +// PIDPProfLabel is a label set on goroutines that have a process attached +const PIDPProfLabel = "pid" + +// PPIDPProfLabel is a label set on goroutines that have a process attached +const PPIDPProfLabel = "ppid" + +// ProcessTypePProfLabel is a label set on goroutines that have a process attached +const ProcessTypePProfLabel = "process-type" + // IDType is a pid type type IDType string @@ -177,12 +187,7 @@ func (pm *Manager) Add(ctx context.Context, description string, cancel context.C Trace(true, pid, description, parentPID, processType) - pprofCtx := pprof.WithLabels(ctx, pprof.Labels( - gtprof.LabelProcessDescription, description, - gtprof.LabelPpid, string(parentPID), - gtprof.LabelPid, string(pid), - gtprof.LabelProcessType, processType, - )) + pprofCtx := pprof.WithLabels(ctx, pprof.Labels(DescriptionPProfLabel, description, PPIDPProfLabel, string(parentPID), PIDPProfLabel, string(pid), ProcessTypePProfLabel, processType)) if currentlyRunning { pprof.SetGoroutineLabels(pprofCtx) } diff --git a/modules/process/manager_stacktraces.go b/modules/process/manager_stacktraces.go index d83060f6eeca2..e260893113118 100644 --- a/modules/process/manager_stacktraces.go +++ b/modules/process/manager_stacktraces.go @@ -10,8 +10,6 @@ import ( "sort" "time" - "code.gitea.io/gitea/modules/gtprof" - "github.com/google/pprof/profile" ) @@ -204,7 +202,7 @@ func (pm *Manager) ProcessStacktraces(flat, noSystem bool) ([]*Process, int, int // Add the non-process associated labels from the goroutine sample to the Stack for name, value := range sample.Label { - if name == gtprof.LabelProcessDescription || name == gtprof.LabelPid || (!flat && name == gtprof.LabelPpid) || name == gtprof.LabelProcessType { + if name == DescriptionPProfLabel || name == PIDPProfLabel || (!flat && name == PPIDPProfLabel) || name == ProcessTypePProfLabel { continue } @@ -226,7 +224,7 @@ func (pm *Manager) ProcessStacktraces(flat, noSystem bool) ([]*Process, int, int var process *Process // Try to get the PID from the goroutine labels - if pidvalue, ok := sample.Label[gtprof.LabelPid]; ok && len(pidvalue) == 1 { + if pidvalue, ok := sample.Label[PIDPProfLabel]; ok && len(pidvalue) == 1 { pid := IDType(pidvalue[0]) // Now try to get the process from our map @@ -240,20 +238,20 @@ func (pm *Manager) ProcessStacktraces(flat, noSystem bool) ([]*Process, int, int // get the parent PID ppid := IDType("") - if value, ok := sample.Label[gtprof.LabelPpid]; ok && len(value) == 1 { + if value, ok := sample.Label[PPIDPProfLabel]; ok && len(value) == 1 { ppid = IDType(value[0]) } // format the description description := "(dead process)" - if value, ok := sample.Label[gtprof.LabelProcessDescription]; ok && len(value) == 1 { + if value, ok := sample.Label[DescriptionPProfLabel]; ok && len(value) == 1 { description = value[0] + " " + description } // override the type of the process to "code" but add the old type as a label on the first stack ptype := NoneProcessType - if value, ok := sample.Label[gtprof.LabelProcessType]; ok && len(value) == 1 { - stack.Labels = append(stack.Labels, &Label{Name: gtprof.LabelProcessType, Value: value[0]}) + if value, ok := sample.Label[ProcessTypePProfLabel]; ok && len(value) == 1 { + stack.Labels = append(stack.Labels, &Label{Name: ProcessTypePProfLabel, Value: value[0]}) } process = &Process{ PID: pid, diff --git a/modules/queue/workerqueue.go b/modules/queue/workerqueue.go index 0f5b105551dcb..672e9a4114cc1 100644 --- a/modules/queue/workerqueue.go +++ b/modules/queue/workerqueue.go @@ -6,7 +6,6 @@ package queue import ( "context" "fmt" - "runtime/pprof" "sync" "sync/atomic" "time" @@ -242,9 +241,6 @@ func NewWorkerPoolQueueWithContext[T any](ctx context.Context, name string, queu w.origHandler = handler w.safeHandler = func(t ...T) (unhandled []T) { defer func() { - // FIXME: there is no ctx support in the handler, so process manager is unable to restore the labels - // so here we explicitly set the "queue ctx" labels again after the handler is done - pprof.SetGoroutineLabels(w.ctxRun) err := recover() if err != nil { log.Error("Recovered from panic in queue %q handler: %v\n%s", name, err, log.Stack(2)) diff --git a/modules/references/references.go b/modules/references/references.go index a5b102b7f2e48..dcb70a33d0f6d 100644 --- a/modules/references/references.go +++ b/modules/references/references.go @@ -330,22 +330,22 @@ func FindAllIssueReferences(content string) []IssueReference { } // FindRenderizableReferenceNumeric returns the first unvalidated reference found in a string. -func FindRenderizableReferenceNumeric(content string, prOnly, crossLinkOnly bool) *RenderizableReference { +func FindRenderizableReferenceNumeric(content string, prOnly, crossLinkOnly bool) (bool, *RenderizableReference) { var match []int if !crossLinkOnly { match = issueNumericPattern.FindStringSubmatchIndex(content) } if match == nil { if match = crossReferenceIssueNumericPattern.FindStringSubmatchIndex(content); match == nil { - return nil + return false, nil } } r := getCrossReference(util.UnsafeStringToBytes(content), match[2], match[3], false, prOnly) if r == nil { - return nil + return false, nil } - return &RenderizableReference{ + return true, &RenderizableReference{ Issue: r.issue, Owner: r.owner, Name: r.name, @@ -372,14 +372,15 @@ func FindRenderizableCommitCrossReference(content string) (bool, *RenderizableRe } // FindRenderizableReferenceRegexp returns the first regexp unvalidated references found in a string. -func FindRenderizableReferenceRegexp(content string, pattern *regexp.Regexp) *RenderizableReference { +func FindRenderizableReferenceRegexp(content string, pattern *regexp.Regexp) (bool, *RenderizableReference) { match := pattern.FindStringSubmatchIndex(content) if len(match) < 4 { - return nil + return false, nil } action, location := findActionKeywords([]byte(content), match[2]) - return &RenderizableReference{ + + return true, &RenderizableReference{ Issue: content[match[2]:match[3]], RefLocation: &RefSpan{Start: match[0], End: match[1]}, Action: action, @@ -389,14 +390,15 @@ func FindRenderizableReferenceRegexp(content string, pattern *regexp.Regexp) *Re } // FindRenderizableReferenceAlphanumeric returns the first alphanumeric unvalidated references found in a string. -func FindRenderizableReferenceAlphanumeric(content string) *RenderizableReference { +func FindRenderizableReferenceAlphanumeric(content string) (bool, *RenderizableReference) { match := issueAlphanumericPattern.FindStringSubmatchIndex(content) if match == nil { - return nil + return false, nil } action, location := findActionKeywords([]byte(content), match[2]) - return &RenderizableReference{ + + return true, &RenderizableReference{ Issue: content[match[2]:match[3]], RefLocation: &RefSpan{Start: match[2], End: match[3]}, Action: action, diff --git a/modules/references/references_test.go b/modules/references/references_test.go index 1b6a968d6a190..27803083c0b74 100644 --- a/modules/references/references_test.go +++ b/modules/references/references_test.go @@ -249,10 +249,11 @@ func TestFindAllIssueReferences(t *testing.T) { } for _, fixture := range alnumFixtures { - ref := FindRenderizableReferenceAlphanumeric(fixture.input) + found, ref := FindRenderizableReferenceAlphanumeric(fixture.input) if fixture.issue == "" { - assert.Nil(t, ref, "Failed to parse: {%s}", fixture.input) + assert.False(t, found, "Failed to parse: {%s}", fixture.input) } else { + assert.True(t, found, "Failed to parse: {%s}", fixture.input) assert.Equal(t, fixture.issue, ref.Issue, "Failed to parse: {%s}", fixture.input) assert.Equal(t, fixture.refLocation, ref.RefLocation, "Failed to parse: {%s}", fixture.input) assert.Equal(t, fixture.action, ref.Action, "Failed to parse: {%s}", fixture.input) diff --git a/modules/setting/config_provider.go b/modules/setting/config_provider.go index 3138f8a63eeda..b34751e9593c2 100644 --- a/modules/setting/config_provider.go +++ b/modules/setting/config_provider.go @@ -26,6 +26,7 @@ type ConfigKey interface { In(defaultVal string, candidates []string) string String() string Strings(delim string) []string + Bool() (bool, error) MustString(defaultVal string) string MustBool(defaultVal ...bool) bool diff --git a/modules/setting/cors.go b/modules/setting/cors.go index 63daaad60ba75..5260887d9d97b 100644 --- a/modules/setting/cors.go +++ b/modules/setting/cors.go @@ -5,8 +5,6 @@ package setting import ( "time" - - "code.gitea.io/gitea/modules/log" ) // CORSConfig defines CORS settings @@ -28,7 +26,4 @@ var CORSConfig = struct { func loadCorsFrom(rootCfg ConfigProvider) { mustMapSetting(rootCfg, "cors", &CORSConfig) - if CORSConfig.Enabled { - log.Info("CORS Service Enabled") - } } diff --git a/modules/setting/indexer.go b/modules/setting/indexer.go index 18585602c3dd2..e34baae012b32 100644 --- a/modules/setting/indexer.go +++ b/modules/setting/indexer.go @@ -31,6 +31,8 @@ var Indexer = struct { IncludePatterns []*GlobMatcher ExcludePatterns []*GlobMatcher ExcludeVendored bool + + TypeBleveMaxFuzzniess int }{ IssueType: "bleve", IssuePath: "indexers/issues.bleve", @@ -88,6 +90,7 @@ func loadIndexerFrom(rootCfg ConfigProvider) { Indexer.ExcludeVendored = sec.Key("REPO_INDEXER_EXCLUDE_VENDORED").MustBool(true) Indexer.MaxIndexerFileSize = sec.Key("MAX_FILE_SIZE").MustInt64(1024 * 1024) Indexer.StartupTimeout = sec.Key("STARTUP_TIMEOUT").MustDuration(30 * time.Second) + Indexer.TypeBleveMaxFuzzniess = sec.Key("TYPE_BLEVE_MAX_FUZZINESS").MustInt(0) } // IndexerGlobFromString parses a comma separated list of patterns and returns a glob.Glob slice suited for repo indexing @@ -97,7 +100,7 @@ func IndexerGlobFromString(globstr string) []*GlobMatcher { expr = strings.TrimSpace(expr) if expr != "" { if g, err := GlobMatcherCompile(expr, '.', '/'); err != nil { - log.Info("Invalid glob expression '%s' (skipped): %v", expr, err) + log.Warn("Invalid glob expression '%s' (skipped): %v", expr, err) } else { extarr = append(extarr, g) } diff --git a/modules/setting/mailer.go b/modules/setting/mailer.go index d4db55dc7bc70..4c3dff6850947 100644 --- a/modules/setting/mailer.go +++ b/modules/setting/mailer.go @@ -255,8 +255,6 @@ func loadMailerFrom(rootCfg ConfigProvider) { MailService.OverrideEnvelopeFrom = true MailService.EnvelopeFrom = parsed.Address } - - log.Info("Mail Service Enabled") } func loadRegisterMailFrom(rootCfg ConfigProvider) { @@ -267,7 +265,6 @@ func loadRegisterMailFrom(rootCfg ConfigProvider) { return } Service.RegisterEmailConfirm = true - log.Info("Register Mail Service Enabled") } func loadNotifyMailFrom(rootCfg ConfigProvider) { @@ -278,7 +275,6 @@ func loadNotifyMailFrom(rootCfg ConfigProvider) { return } Service.EnableNotifyMail = true - log.Info("Notify Mail Service Enabled") } func tryResolveAddr(addr string) []net.IPAddr { diff --git a/modules/setting/pandoc.go b/modules/setting/pandoc.go index 27b70ae65a40c..952eefce634c9 100644 --- a/modules/setting/pandoc.go +++ b/modules/setting/pandoc.go @@ -1,3 +1,6 @@ +// Copyright 2025 The Bindersnap Authors. All rights reserved. +// SPDX-License-Identifier: LicenseRef-License + package setting var Pandoc = struct { diff --git a/modules/setting/repository.go b/modules/setting/repository.go index 3f3de5be9c4f4..c5619d0f04858 100644 --- a/modules/setting/repository.go +++ b/modules/setting/repository.go @@ -217,7 +217,7 @@ var ( TestConflictingPatchesWithGitApply bool RetargetChildrenOnMerge bool }{ - WorkInProgressPrefixes: []string{"Draft:", "DRAFT:", "[Draft]", "[DRAFT]", "WIP:", "[WIP]"}, + WorkInProgressPrefixes: []string{"WIP:", "[WIP]"}, // Same as GitHub. See // https://help.github.com/articles/closing-issues-via-commit-messages CloseKeywords: strings.Split("close,closes,closed,fix,fixes,fixed,resolve,resolves,resolved", ","), diff --git a/modules/setting/server.go b/modules/setting/server.go index d7a71578d4ab6..e15b790906738 100644 --- a/modules/setting/server.go +++ b/modules/setting/server.go @@ -169,20 +169,24 @@ func loadServerFrom(rootCfg ConfigProvider) { HTTPAddr = sec.Key("HTTP_ADDR").MustString("0.0.0.0") HTTPPort = sec.Key("HTTP_PORT").MustString("3000") + // DEPRECATED should not be removed because users maybe upgrade from lower version to the latest version + // if these are removed, the warning will not be shown + if sec.HasKey("ENABLE_ACME") { + EnableAcme = sec.Key("ENABLE_ACME").MustBool(false) + } else { + deprecatedSetting(rootCfg, "server", "ENABLE_LETSENCRYPT", "server", "ENABLE_ACME", "v1.19.0") + EnableAcme = sec.Key("ENABLE_LETSENCRYPT").MustBool(false) + } + Protocol = HTTP protocolCfg := sec.Key("PROTOCOL").String() + if protocolCfg != "https" && EnableAcme { + log.Fatal("ACME could only be used with HTTPS protocol") + } + switch protocolCfg { case "https": Protocol = HTTPS - - // DEPRECATED should not be removed because users maybe upgrade from lower version to the latest version - // if these are removed, the warning will not be shown - if sec.HasKey("ENABLE_ACME") { - EnableAcme = sec.Key("ENABLE_ACME").MustBool(false) - } else { - deprecatedSetting(rootCfg, "server", "ENABLE_LETSENCRYPT", "server", "ENABLE_ACME", "v1.19.0") - EnableAcme = sec.Key("ENABLE_LETSENCRYPT").MustBool(false) - } if EnableAcme { AcmeURL = sec.Key("ACME_URL").MustString("") AcmeCARoot = sec.Key("ACME_CA_ROOT").MustString("") @@ -210,6 +214,9 @@ func loadServerFrom(rootCfg ConfigProvider) { deprecatedSetting(rootCfg, "server", "LETSENCRYPT_EMAIL", "server", "ACME_EMAIL", "v1.19.0") AcmeEmail = sec.Key("LETSENCRYPT_EMAIL").MustString("") } + if AcmeEmail == "" { + log.Fatal("ACME Email is not set (ACME_EMAIL).") + } } else { CertFile = sec.Key("CERT_FILE").String() KeyFile = sec.Key("KEY_FILE").String() diff --git a/modules/setting/service.go b/modules/setting/service.go index 526ad64eb4001..6f0bcb48bbbfa 100644 --- a/modules/setting/service.go +++ b/modules/setting/service.go @@ -43,9 +43,11 @@ var Service = struct { ShowRegistrationButton bool EnablePasswordSignInForm bool ShowMilestonesDashboardPage bool - RequireSignInView bool + RequireSignInViewStrict bool + BlockAnonymousAccessExpensive bool EnableNotifyMail bool EnableBasicAuth bool + EnablePasskeyAuth bool EnableReverseProxyAuth bool EnableReverseProxyAuthAPI bool EnableReverseProxyAutoRegister bool @@ -158,9 +160,21 @@ func loadServiceFrom(rootCfg ConfigProvider) { Service.EmailDomainBlockList = CompileEmailGlobList(sec, "EMAIL_DOMAIN_BLOCKLIST") Service.ShowRegistrationButton = sec.Key("SHOW_REGISTRATION_BUTTON").MustBool(!(Service.DisableRegistration || Service.AllowOnlyExternalRegistration)) Service.ShowMilestonesDashboardPage = sec.Key("SHOW_MILESTONES_DASHBOARD_PAGE").MustBool(true) - Service.RequireSignInView = sec.Key("REQUIRE_SIGNIN_VIEW").MustBool() + + // boolean values are considered as "strict" + var err error + Service.RequireSignInViewStrict, err = sec.Key("REQUIRE_SIGNIN_VIEW").Bool() + if s := sec.Key("REQUIRE_SIGNIN_VIEW").String(); err != nil && s != "" { + // non-boolean value only supports "expensive" at the moment + Service.BlockAnonymousAccessExpensive = s == "expensive" + if !Service.BlockAnonymousAccessExpensive { + log.Error("Invalid config option: REQUIRE_SIGNIN_VIEW = %s", s) + } + } + Service.EnableBasicAuth = sec.Key("ENABLE_BASIC_AUTHENTICATION").MustBool(true) Service.EnablePasswordSignInForm = sec.Key("ENABLE_PASSWORD_SIGNIN_FORM").MustBool(true) + Service.EnablePasskeyAuth = sec.Key("ENABLE_PASSKEY_AUTHENTICATION").MustBool(true) Service.EnableReverseProxyAuth = sec.Key("ENABLE_REVERSE_PROXY_AUTHENTICATION").MustBool() Service.EnableReverseProxyAuthAPI = sec.Key("ENABLE_REVERSE_PROXY_AUTHENTICATION_API").MustBool() Service.EnableReverseProxyAutoRegister = sec.Key("ENABLE_REVERSE_PROXY_AUTO_REGISTRATION").MustBool() diff --git a/modules/setting/service_test.go b/modules/setting/service_test.go index 1647bcec160a1..73736b793a8db 100644 --- a/modules/setting/service_test.go +++ b/modules/setting/service_test.go @@ -7,16 +7,14 @@ import ( "testing" "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/test" "github.com/gobwas/glob" "github.com/stretchr/testify/assert" ) func TestLoadServices(t *testing.T) { - oldService := Service - defer func() { - Service = oldService - }() + defer test.MockVariableValue(&Service)() cfg, err := NewConfigProviderFromData(` [service] @@ -48,10 +46,7 @@ EMAIL_DOMAIN_BLOCKLIST = d3, *.b } func TestLoadServiceVisibilityModes(t *testing.T) { - oldService := Service - defer func() { - Service = oldService - }() + defer test.MockVariableValue(&Service)() kases := map[string]func(){ ` @@ -130,3 +125,33 @@ ALLOWED_USER_VISIBILITY_MODES = public, limit, privated }) } } + +func TestLoadServiceRequireSignInView(t *testing.T) { + defer test.MockVariableValue(&Service)() + + cfg, err := NewConfigProviderFromData(` +[service] +`) + assert.NoError(t, err) + loadServiceFrom(cfg) + assert.False(t, Service.RequireSignInViewStrict) + assert.False(t, Service.BlockAnonymousAccessExpensive) + + cfg, err = NewConfigProviderFromData(` +[service] +REQUIRE_SIGNIN_VIEW = true +`) + assert.NoError(t, err) + loadServiceFrom(cfg) + assert.True(t, Service.RequireSignInViewStrict) + assert.False(t, Service.BlockAnonymousAccessExpensive) + + cfg, err = NewConfigProviderFromData(` +[service] +REQUIRE_SIGNIN_VIEW = expensive +`) + assert.NoError(t, err) + loadServiceFrom(cfg) + assert.False(t, Service.RequireSignInViewStrict) + assert.True(t, Service.BlockAnonymousAccessExpensive) +} diff --git a/modules/setting/session.go b/modules/setting/session.go index afe63bfdb7832..19a05ce2c2a5f 100644 --- a/modules/setting/session.go +++ b/modules/setting/session.go @@ -73,6 +73,4 @@ func loadSessionFrom(rootCfg ConfigProvider) { SessionConfig.ProviderConfig = string(shadowConfig) SessionConfig.OriginalProvider = SessionConfig.Provider SessionConfig.Provider = "VirtualSession" - - log.Info("Session Service Enabled") } diff --git a/modules/setting/setting.go b/modules/setting/setting.go index 20da796b58d3d..c93d199b1b639 100644 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -235,9 +235,3 @@ func checkOverlappedPath(name, path string) { } configuredPaths[path] = name } - -func PanicInDevOrTesting(msg string, a ...any) { - if !IsProd || IsInTesting { - panic(fmt.Sprintf(msg, a...)) - } -} diff --git a/modules/setting/time.go b/modules/setting/time.go index 39acba12ef14e..97988211a989a 100644 --- a/modules/setting/time.go +++ b/modules/setting/time.go @@ -20,7 +20,6 @@ func loadTimeFrom(rootCfg ConfigProvider) { if err != nil { log.Fatal("Load time zone failed: %v", err) } - log.Info("Default UI Location is %v", zone) } if DefaultUILocation == nil { DefaultUILocation = time.Local diff --git a/modules/setting/ui.go b/modules/setting/ui.go index 20fc612b43afe..db0fe9ef79ac0 100644 --- a/modules/setting/ui.go +++ b/modules/setting/ui.go @@ -63,7 +63,6 @@ var UI = struct { } `ini:"ui.admin"` User struct { RepoPagingNum int - OrgPagingNum int } `ini:"ui.user"` Meta struct { Author string @@ -128,10 +127,8 @@ var UI = struct { }, User: struct { RepoPagingNum int - OrgPagingNum int }{ RepoPagingNum: 15, - OrgPagingNum: 15, }, Meta: struct { Author string diff --git a/modules/storage/storage.go b/modules/storage/storage.go index 750ecdfe0db70..b0529941e7da4 100644 --- a/modules/storage/storage.go +++ b/modules/storage/storage.go @@ -93,7 +93,7 @@ func Clean(storage ObjectStorage) error { } // SaveFrom saves data to the ObjectStorage with path p from the callback -func SaveFrom(objStorage ObjectStorage, p string, callback func(w io.Writer) error) error { +func SaveFrom(objStorage ObjectStorage, path string, callback func(w io.Writer) error) error { pr, pw := io.Pipe() defer pr.Close() go func() { @@ -103,7 +103,7 @@ func SaveFrom(objStorage ObjectStorage, p string, callback func(w io.Writer) err } }() - _, err := objStorage.Save(p, pr, -1) + _, err := objStorage.Save(path, pr, -1) return err } diff --git a/modules/structs/hook.go b/modules/structs/hook.go index ce5742e5c7ce9..cef2dbd712912 100644 --- a/modules/structs/hook.go +++ b/modules/structs/hook.go @@ -116,14 +116,7 @@ var ( _ Payloader = &PackagePayload{} ) -// _________ __ -// \_ ___ \_______ ____ _____ _/ |_ ____ -// / \ \/\_ __ \_/ __ \\__ \\ __\/ __ \ -// \ \____| | \/\ ___/ / __ \| | \ ___/ -// \______ /|__| \___ >____ /__| \___ > -// \/ \/ \/ \/ - -// CreatePayload FIXME +// CreatePayload represents a payload information of create event. type CreatePayload struct { Sha string `json:"sha"` Ref string `json:"ref"` @@ -157,13 +150,6 @@ func ParseCreateHook(raw []byte) (*CreatePayload, error) { return hook, nil } -// ________ .__ __ -// \______ \ ____ | | _____/ |_ ____ -// | | \_/ __ \| | _/ __ \ __\/ __ \ -// | ` \ ___/| |_\ ___/| | \ ___/ -// /_______ /\___ >____/\___ >__| \___ > -// \/ \/ \/ \/ - // PusherType define the type to push type PusherType string @@ -186,13 +172,6 @@ func (p *DeletePayload) JSONPayload() ([]byte, error) { return json.MarshalIndent(p, "", " ") } -// ___________ __ -// \_ _____/__________| | __ -// | __)/ _ \_ __ \ |/ / -// | \( <_> ) | \/ < -// \___ / \____/|__| |__|_ \ -// \/ \/ - // ForkPayload represents fork payload type ForkPayload struct { Forkee *Repository `json:"forkee"` @@ -232,13 +211,6 @@ func (p *IssueCommentPayload) JSONPayload() ([]byte, error) { return json.MarshalIndent(p, "", " ") } -// __________ .__ -// \______ \ ____ | | ____ _____ ______ ____ -// | _// __ \| | _/ __ \\__ \ / ___// __ \ -// | | \ ___/| |_\ ___/ / __ \_\___ \\ ___/ -// |____|_ /\___ >____/\___ >____ /____ >\___ > -// \/ \/ \/ \/ \/ \/ - // HookReleaseAction defines hook release action type type HookReleaseAction string @@ -302,13 +274,6 @@ func (p *PushPayload) Branch() string { return strings.ReplaceAll(p.Ref, "refs/heads/", "") } -// .___ -// | | ______ ________ __ ____ -// | |/ ___// ___/ | \_/ __ \ -// | |\___ \ \___ \| | /\ ___/ -// |___/____ >____ >____/ \___ > -// \/ \/ \/ - // HookIssueAction FIXME type HookIssueAction string @@ -371,13 +336,6 @@ type ChangesPayload struct { Ref *ChangesFromPayload `json:"ref,omitempty"` } -// __________ .__ .__ __________ __ -// \______ \__ __| | | | \______ \ ____ ________ __ ____ _______/ |_ -// | ___/ | \ | | | | _// __ \/ ____/ | \_/ __ \ / ___/\ __\ -// | | | | / |_| |__ | | \ ___< <_| | | /\ ___/ \___ \ | | -// |____| |____/|____/____/ |____|_ /\___ >__ |____/ \___ >____ > |__| -// \/ \/ |__| \/ \/ - // PullRequestPayload represents a payload information of pull request event. type PullRequestPayload struct { Action HookIssueAction `json:"action"` @@ -402,13 +360,6 @@ type ReviewPayload struct { Content string `json:"content"` } -// __ __.__ __ .__ -// / \ / \__| | _|__| -// \ \/\/ / | |/ / | -// \ /| | <| | -// \__/\ / |__|__|_ \__| -// \/ \/ - // HookWikiAction an action that happens to a wiki page type HookWikiAction string @@ -435,13 +386,6 @@ func (p *WikiPayload) JSONPayload() ([]byte, error) { return json.MarshalIndent(p, "", " ") } -//__________ .__ __ -//\______ \ ____ ______ ____ _____|__|/ |_ ___________ ___.__. -// | _// __ \\____ \ / _ \/ ___/ \ __\/ _ \_ __ < | | -// | | \ ___/| |_> > <_> )___ \| || | ( <_> ) | \/\___ | -// |____|_ /\___ > __/ \____/____ >__||__| \____/|__| / ____| -// \/ \/|__| \/ \/ - // HookRepoAction an action that happens to a repo type HookRepoAction string @@ -480,7 +424,7 @@ type PackagePayload struct { Action HookPackageAction `json:"action"` Repository *Repository `json:"repository"` Package *Package `json:"package"` - Organization *User `json:"organization"` + Organization *Organization `json:"organization"` Sender *User `json:"sender"` } diff --git a/modules/structs/pull.go b/modules/structs/pull.go index 55831e642c1a0..d0f4eabc9e746 100644 --- a/modules/structs/pull.go +++ b/modules/structs/pull.go @@ -27,9 +27,10 @@ type PullRequest struct { Comments int `json:"comments"` // number of review comments made on the diff of a PR review (not including comments on commits or issues in a PR) ReviewComments int `json:"review_comments"` - Additions int `json:"additions"` - Deletions int `json:"deletions"` - ChangedFiles int `json:"changed_files"` + + Additions *int `json:"additions,omitempty"` + Deletions *int `json:"deletions,omitempty"` + ChangedFiles *int `json:"changed_files,omitempty"` HTMLURL string `json:"html_url"` DiffURL string `json:"diff_url"` diff --git a/modules/structs/repo_branch.go b/modules/structs/repo_branch.go index a9aa1d330a184..55c98d60b9124 100644 --- a/modules/structs/repo_branch.go +++ b/modules/structs/repo_branch.go @@ -133,3 +133,11 @@ type EditBranchProtectionOption struct { type UpdateBranchProtectionPriories struct { IDs []int64 `json:"ids"` } + +type MergeUpstreamRequest struct { + Branch string `json:"branch"` +} + +type MergeUpstreamResponse struct { + MergeStyle string `json:"merge_type"` +} diff --git a/modules/structs/user_app.go b/modules/structs/user_app.go index a7d2e28b41291..8401252bd6591 100644 --- a/modules/structs/user_app.go +++ b/modules/structs/user_app.go @@ -23,9 +23,11 @@ type AccessToken struct { type AccessTokenList []*AccessToken // CreateAccessTokenOption options when create access token +// swagger:model CreateAccessTokenOption type CreateAccessTokenOption struct { // required: true - Name string `json:"name" binding:"Required"` + Name string `json:"name" binding:"Required"` + // example: ["all", "read:activitypub","read:issue", "write:misc", "read:notification", "read:organization", "read:package", "read:repository", "read:user"] Scopes []string `json:"scopes"` } diff --git a/modules/svg/processor.go b/modules/svg/processor.go index 4fcb11a57d0c2..82248fb0c1216 100644 --- a/modules/svg/processor.go +++ b/modules/svg/processor.go @@ -10,7 +10,7 @@ import ( "sync" ) -type globalVarsStruct struct { +type normalizeVarsStruct struct { reXMLDoc, reComment, reAttrXMLNs, @@ -18,23 +18,26 @@ type globalVarsStruct struct { reAttrClassPrefix *regexp.Regexp } -var globalVars = sync.OnceValue(func() *globalVarsStruct { - return &globalVarsStruct{ - reXMLDoc: regexp.MustCompile(`(?s)<\?xml.*?>`), - reComment: regexp.MustCompile(`(?s)`), - - reAttrXMLNs: regexp.MustCompile(`(?s)\s+xmlns\s*=\s*"[^"]*"`), - reAttrSize: regexp.MustCompile(`(?s)\s+(width|height)\s*=\s*"[^"]+"`), - reAttrClassPrefix: regexp.MustCompile(`(?s)\s+class\s*=\s*"`), - } -}) +var ( + normalizeVars *normalizeVarsStruct + normalizeVarsOnce sync.Once +) // Normalize normalizes the SVG content: set default width/height, remove unnecessary tags/attributes // It's designed to work with valid SVG content. For invalid SVG content, the returned content is not guaranteed. func Normalize(data []byte, size int) []byte { - vars := globalVars() - data = vars.reXMLDoc.ReplaceAll(data, nil) - data = vars.reComment.ReplaceAll(data, nil) + normalizeVarsOnce.Do(func() { + normalizeVars = &normalizeVarsStruct{ + reXMLDoc: regexp.MustCompile(`(?s)<\?xml.*?>`), + reComment: regexp.MustCompile(`(?s)`), + + reAttrXMLNs: regexp.MustCompile(`(?s)\s+xmlns\s*=\s*"[^"]*"`), + reAttrSize: regexp.MustCompile(`(?s)\s+(width|height)\s*=\s*"[^"]+"`), + reAttrClassPrefix: regexp.MustCompile(`(?s)\s+class\s*=\s*"`), + } + }) + data = normalizeVars.reXMLDoc.ReplaceAll(data, nil) + data = normalizeVars.reComment.ReplaceAll(data, nil) data = bytes.TrimSpace(data) svgTag, svgRemaining, ok := bytes.Cut(data, []byte(">")) @@ -42,9 +45,9 @@ func Normalize(data []byte, size int) []byte { return data } normalized := bytes.Clone(svgTag) - normalized = vars.reAttrXMLNs.ReplaceAll(normalized, nil) - normalized = vars.reAttrSize.ReplaceAll(normalized, nil) - normalized = vars.reAttrClassPrefix.ReplaceAll(normalized, []byte(` class="`)) + normalized = normalizeVars.reAttrXMLNs.ReplaceAll(normalized, nil) + normalized = normalizeVars.reAttrSize.ReplaceAll(normalized, nil) + normalized = normalizeVars.reAttrClassPrefix.ReplaceAll(normalized, []byte(` class="`)) normalized = bytes.TrimSpace(normalized) normalized = fmt.Appendf(normalized, ` width="%d" height="%d"`, size, size) if !bytes.Contains(normalized, []byte(` class="`)) { diff --git a/modules/system/appstate_test.go b/modules/system/appstate_test.go index d4b9e167c2f93..911319d00a561 100644 --- a/modules/system/appstate_test.go +++ b/modules/system/appstate_test.go @@ -13,9 +13,7 @@ import ( ) func TestMain(m *testing.M) { - unittest.MainTest(m, &unittest.TestOptions{ - FixtureFiles: []string{""}, // load nothing - }) + unittest.MainTest(m, &unittest.TestOptions{FixtureFiles: []string{ /* load nothing */ }}) } type testItem1 struct { @@ -36,8 +34,6 @@ func (*testItem2) Name() string { } func TestAppStateDB(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - as := &DBStore{} item1 := new(testItem1) diff --git a/modules/templates/helper.go b/modules/templates/helper.go index b5ff5559a285b..1739013f8a99f 100644 --- a/modules/templates/helper.go +++ b/modules/templates/helper.go @@ -9,6 +9,7 @@ import ( "html" "html/template" "net/url" + "reflect" "strings" "time" @@ -68,8 +69,8 @@ func NewFuncMap() template.FuncMap { // ----------------------------------------------------------------- // time / number / format "FileSize": base.FileSize, - "CountFmt": countFmt, - "Sec2Time": util.SecToTime, + "CountFmt": base.FormatNumberSI, + "Sec2Time": util.SecToHours, "TimeEstimateString": timeEstimateString, @@ -239,8 +240,29 @@ func iif(condition any, vals ...any) any { } func isTemplateTruthy(v any) bool { - truth, _ := template.IsTrue(v) - return truth + if v == nil { + return false + } + + rv := reflect.ValueOf(v) + switch rv.Kind() { + case reflect.Bool: + return rv.Bool() + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return rv.Int() != 0 + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return rv.Uint() != 0 + case reflect.Float32, reflect.Float64: + return rv.Float() != 0 + case reflect.Complex64, reflect.Complex128: + return rv.Complex() != 0 + case reflect.String, reflect.Slice, reflect.Array, reflect.Map: + return rv.Len() > 0 + case reflect.Struct: + return true + default: + return !rv.IsNil() + } } // evalTokens evaluates the expression by tokens and returns the result, see the comment of eval.Expr for details. @@ -265,6 +287,14 @@ func userThemeName(user *user_model.User) string { return setting.UI.DefaultTheme } +func timeEstimateString(timeSec any) string { + v, _ := util.ToInt64(timeSec) + if v == 0 { + return "" + } + return util.TimeEstimateString(v) +} + // QueryBuild builds a query string from a list of key-value pairs. // It omits the nil and empty strings, but it doesn't omit other zero values, // because the zero value of number types may have a meaning. @@ -332,5 +362,7 @@ func QueryBuild(a ...any) template.URL { } func panicIfDevOrTesting() { - setting.PanicInDevOrTesting("legacy template functions are for backward compatibility only, do not use them in new code") + if !setting.IsProd || setting.IsInTesting { + panic("legacy template functions are for backward compatibility only, do not use them in new code") + } } diff --git a/modules/templates/helper_test.go b/modules/templates/helper_test.go index a530d484bc4ee..3e17e86c66256 100644 --- a/modules/templates/helper_test.go +++ b/modules/templates/helper_test.go @@ -8,7 +8,6 @@ import ( "strings" "testing" - "code.gitea.io/gitea/modules/htmlutil" "code.gitea.io/gitea/modules/util" "github.com/stretchr/testify/assert" @@ -66,12 +65,31 @@ func TestSanitizeHTML(t *testing.T) { assert.Equal(t, template.HTML(`link xss
inline
`), SanitizeHTML(`link xss
inline
`)) } -func TestTemplateIif(t *testing.T) { +func TestTemplateTruthy(t *testing.T) { tmpl := template.New("test") tmpl.Funcs(template.FuncMap{"Iif": iif}) template.Must(tmpl.Parse(`{{if .Value}}true{{else}}false{{end}}:{{Iif .Value "true" "false"}}`)) - cases := []any{nil, false, true, "", "string", 0, 1} + cases := []any{ + nil, false, true, "", "string", 0, 1, + byte(0), byte(1), int64(0), int64(1), float64(0), float64(1), + complex(0, 0), complex(1, 0), + (chan int)(nil), make(chan int), + (func())(nil), func() {}, + util.ToPointer(0), util.ToPointer(util.ToPointer(0)), + util.ToPointer(1), util.ToPointer(util.ToPointer(1)), + [0]int{}, + [1]int{0}, + []int(nil), + []int{}, + []int{0}, + map[any]any(nil), + map[any]any{}, + map[any]any{"k": "v"}, + (*struct{})(nil), + struct{}{}, + util.ToPointer(struct{}{}), + } w := &strings.Builder{} truthyCount := 0 for i, v := range cases { @@ -84,37 +102,3 @@ func TestTemplateIif(t *testing.T) { } assert.True(t, truthyCount != 0 && truthyCount != len(cases)) } - -func TestTemplateEscape(t *testing.T) { - execTmpl := func(code string) string { - tmpl := template.New("test") - tmpl.Funcs(template.FuncMap{"QueryBuild": QueryBuild, "HTMLFormat": htmlutil.HTMLFormat}) - template.Must(tmpl.Parse(code)) - w := &strings.Builder{} - assert.NoError(t, tmpl.Execute(w, nil)) - return w.String() - } - - t.Run("Golang URL Escape", func(t *testing.T) { - // Golang template considers "href", "*src*", "*uri*", "*url*" (and more) ... attributes as contentTypeURL and does auto-escaping - actual := execTmpl(``) - assert.Equal(t, ``, actual) - actual = execTmpl(``) - assert.Equal(t, ``, actual) - }) - t.Run("Golang URL No-escape", func(t *testing.T) { - // non-URL content isn't auto-escaped - actual := execTmpl(``) - assert.Equal(t, ``, actual) - }) - t.Run("QueryBuild", func(t *testing.T) { - actual := execTmpl(``) - assert.Equal(t, ``, actual) - actual = execTmpl(``) - assert.Equal(t, ``, actual) - }) - t.Run("HTMLFormat", func(t *testing.T) { - actual := execTmpl("{{HTMLFormat `%s` `\"` `<>`}}") - assert.Equal(t, `<>`, actual) - }) -} diff --git a/modules/templates/htmlrenderer.go b/modules/templates/htmlrenderer.go index 529284f7e8e19..e7e805ed3023d 100644 --- a/modules/templates/htmlrenderer.go +++ b/modules/templates/htmlrenderer.go @@ -29,8 +29,6 @@ import ( type TemplateExecutor scopedtmpl.TemplateExecutor -type TplName string - type HTMLRender struct { templates atomic.Pointer[scopedtmpl.ScopedTemplate] } @@ -42,8 +40,7 @@ var ( var ErrTemplateNotInitialized = errors.New("template system is not initialized, check your log for errors") -func (h *HTMLRender) HTML(w io.Writer, status int, tplName TplName, data any, ctx context.Context) error { //nolint:revive - name := string(tplName) +func (h *HTMLRender) HTML(w io.Writer, status int, name string, data any, ctx context.Context) error { //nolint:revive if respWriter, ok := w.(http.ResponseWriter); ok { if respWriter.Header().Get("Content-Type") == "" { respWriter.Header().Set("Content-Type", "text/html; charset=utf-8") diff --git a/modules/templates/mailer.go b/modules/templates/mailer.go index 310d645328782..ace81bf4a500f 100644 --- a/modules/templates/mailer.go +++ b/modules/templates/mailer.go @@ -11,9 +11,9 @@ import ( "strings" texttmpl "text/template" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/util" ) var mailSubjectSplit = regexp.MustCompile(`(?m)^-{3,}\s*$`) @@ -24,7 +24,7 @@ func mailSubjectTextFuncMap() texttmpl.FuncMap { "dict": dict, "Eval": evalTokens, - "EllipsisString": util.EllipsisDisplayString, + "EllipsisString": base.EllipsisString, "AppName": func() string { return setting.AppName }, diff --git a/modules/templates/util_format.go b/modules/templates/util_format.go deleted file mode 100644 index bee6fb7b75b3d..0000000000000 --- a/modules/templates/util_format.go +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright 2024 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package templates - -import ( - "fmt" - - "code.gitea.io/gitea/modules/util" -) - -func timeEstimateString(timeSec any) string { - v, _ := util.ToInt64(timeSec) - if v == 0 { - return "" - } - return util.TimeEstimateString(v) -} - -func countFmt(data any) string { - // legacy code, not ideal, still used in some places - num, err := util.ToInt64(data) - if err != nil { - return "" - } - if num < 1000 { - return fmt.Sprintf("%d", num) - } else if num < 1_000_000 { - num2 := float32(num) / 1000.0 - return fmt.Sprintf("%.1fk", num2) - } else if num < 1_000_000_000 { - num2 := float32(num) / 1_000_000.0 - return fmt.Sprintf("%.1fM", num2) - } - num2 := float32(num) / 1_000_000_000.0 - return fmt.Sprintf("%.1fG", num2) -} diff --git a/modules/templates/util_format_test.go b/modules/templates/util_format_test.go deleted file mode 100644 index 8d466faff0c52..0000000000000 --- a/modules/templates/util_format_test.go +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright 2024 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package templates - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestCountFmt(t *testing.T) { - assert.Equal(t, "125", countFmt(125)) - assert.Equal(t, "1.3k", countFmt(int64(1317))) - assert.Equal(t, "21.3M", countFmt(21317675)) - assert.Equal(t, "45.7G", countFmt(45721317675)) - assert.Equal(t, "", countFmt("test")) -} diff --git a/modules/templates/util_string.go b/modules/templates/util_string.go index 683c77a87042d..382e2de13f1e2 100644 --- a/modules/templates/util_string.go +++ b/modules/templates/util_string.go @@ -8,7 +8,7 @@ import ( "html/template" "strings" - "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/modules/base" ) type StringUtils struct{} @@ -54,7 +54,7 @@ func (su *StringUtils) Cut(s, sep string) []any { } func (su *StringUtils) EllipsisString(s string, maxLength int) string { - return util.EllipsisDisplayString(s, maxLength) + return base.EllipsisString(s, maxLength) } func (su *StringUtils) ToUpper(s string) string { diff --git a/modules/test/utils.go b/modules/test/utils.go index ec4c9763881f8..8dee92fbce49c 100644 --- a/modules/test/utils.go +++ b/modules/test/utils.go @@ -4,16 +4,11 @@ package test import ( - "fmt" "net/http" "net/http/httptest" - "os" - "path/filepath" - "runtime" "strings" "code.gitea.io/gitea/modules/json" - "code.gitea.io/gitea/modules/util" ) // RedirectURL returns the redirect URL of a http response. @@ -46,19 +41,3 @@ func MockVariableValue[T any](p *T, v ...T) (reset func()) { } return func() { *p = old } } - -// SetupGiteaRoot Sets GITEA_ROOT if it is not already set and returns the value -func SetupGiteaRoot() string { - giteaRoot := os.Getenv("GITEA_ROOT") - if giteaRoot != "" { - return giteaRoot - } - _, filename, _, _ := runtime.Caller(0) - giteaRoot = filepath.Dir(filepath.Dir(filepath.Dir(filename))) - fixturesDir := filepath.Join(giteaRoot, "models", "fixtures") - if exist, _ := util.IsDir(fixturesDir); !exist { - panic(fmt.Sprintf("fixtures directory not found: %s", fixturesDir)) - } - _ = os.Setenv("GITEA_ROOT", giteaRoot) - return giteaRoot -} diff --git a/modules/test/utils_test.go b/modules/test/utils_test.go deleted file mode 100644 index 0469ce97f2a60..0000000000000 --- a/modules/test/utils_test.go +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright 2024 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package test - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestSetupGiteaRoot(t *testing.T) { - t.Setenv("GITEA_ROOT", "test") - assert.Equal(t, "test", SetupGiteaRoot()) - t.Setenv("GITEA_ROOT", "") - assert.NotEqual(t, "test", SetupGiteaRoot()) -} diff --git a/modules/util/sec_to_time.go b/modules/util/sec_to_time.go index ad0fb1a68b4a7..73667d723ef76 100644 --- a/modules/util/sec_to_time.go +++ b/modules/util/sec_to_time.go @@ -8,59 +8,17 @@ import ( "strings" ) -// SecToTime converts an amount of seconds to a human-readable string. E.g. -// 66s -> 1 minute 6 seconds -// 52410s -> 14 hours 33 minutes -// 563418 -> 6 days 12 hours -// 1563418 -> 2 weeks 4 days -// 3937125s -> 1 month 2 weeks -// 45677465s -> 1 year 6 months -func SecToTime(durationVal any) string { +// SecToHours converts an amount of seconds to a human-readable hours string. +// This is stable for planning and managing timesheets. +// Here it only supports hours and minutes, because a work day could contain 6 or 7 or 8 hours. +func SecToHours(durationVal any) string { duration, _ := ToInt64(durationVal) - - formattedTime := "" - - // The following four variables are calculated by taking - // into account the previously calculated variables, this avoids - // pitfalls when using remainders. As that could lead to incorrect - // results when the calculated number equals the quotient number. - remainingDays := duration / (60 * 60 * 24) - years := remainingDays / 365 - remainingDays -= years * 365 - months := remainingDays * 12 / 365 - remainingDays -= months * 365 / 12 - weeks := remainingDays / 7 - remainingDays -= weeks * 7 - days := remainingDays - - // The following three variables are calculated without depending - // on the previous calculated variables. - hours := (duration / 3600) % 24 + hours := duration / 3600 minutes := (duration / 60) % 60 - seconds := duration % 60 - // Extract only the relevant information of the time - // If the time is greater than a year, it makes no sense to display seconds. - switch { - case years > 0: - formattedTime = formatTime(years, "year", formattedTime) - formattedTime = formatTime(months, "month", formattedTime) - case months > 0: - formattedTime = formatTime(months, "month", formattedTime) - formattedTime = formatTime(weeks, "week", formattedTime) - case weeks > 0: - formattedTime = formatTime(weeks, "week", formattedTime) - formattedTime = formatTime(days, "day", formattedTime) - case days > 0: - formattedTime = formatTime(days, "day", formattedTime) - formattedTime = formatTime(hours, "hour", formattedTime) - case hours > 0: - formattedTime = formatTime(hours, "hour", formattedTime) - formattedTime = formatTime(minutes, "minute", formattedTime) - default: - formattedTime = formatTime(minutes, "minute", formattedTime) - formattedTime = formatTime(seconds, "second", formattedTime) - } + formattedTime := "" + formattedTime = formatTime(hours, "hour", formattedTime) + formattedTime = formatTime(minutes, "minute", formattedTime) // The formatTime() function always appends a space at the end. This will be trimmed return strings.TrimRight(formattedTime, " ") @@ -76,6 +34,5 @@ func formatTime(value int64, name, formattedTime string) string { } else if value > 1 { formattedTime = fmt.Sprintf("%s%d %ss ", formattedTime, value, name) } - return formattedTime } diff --git a/modules/util/sec_to_time_test.go b/modules/util/sec_to_time_test.go index 4d1213a52c05e..71a8801d4f4aa 100644 --- a/modules/util/sec_to_time_test.go +++ b/modules/util/sec_to_time_test.go @@ -9,22 +9,17 @@ import ( "github.com/stretchr/testify/assert" ) -func TestSecToTime(t *testing.T) { +func TestSecToHours(t *testing.T) { second := int64(1) minute := 60 * second hour := 60 * minute day := 24 * hour - year := 365 * day - assert.Equal(t, "1 minute 6 seconds", SecToTime(minute+6*second)) - assert.Equal(t, "1 hour", SecToTime(hour)) - assert.Equal(t, "1 hour", SecToTime(hour+second)) - assert.Equal(t, "14 hours 33 minutes", SecToTime(14*hour+33*minute+30*second)) - assert.Equal(t, "6 days 12 hours", SecToTime(6*day+12*hour+30*minute+18*second)) - assert.Equal(t, "2 weeks 4 days", SecToTime((2*7+4)*day+2*hour+16*minute+58*second)) - assert.Equal(t, "4 weeks", SecToTime(4*7*day)) - assert.Equal(t, "4 weeks 1 day", SecToTime((4*7+1)*day)) - assert.Equal(t, "1 month 2 weeks", SecToTime((6*7+3)*day+13*hour+38*minute+45*second)) - assert.Equal(t, "11 months", SecToTime(year-25*day)) - assert.Equal(t, "1 year 5 months", SecToTime(year+163*day+10*hour+11*minute+5*second)) + assert.Equal(t, "1 minute", SecToHours(minute+6*second)) + assert.Equal(t, "1 hour", SecToHours(hour)) + assert.Equal(t, "1 hour", SecToHours(hour+second)) + assert.Equal(t, "14 hours 33 minutes", SecToHours(14*hour+33*minute+30*second)) + assert.Equal(t, "156 hours 30 minutes", SecToHours(6*day+12*hour+30*minute+18*second)) + assert.Equal(t, "98 hours 16 minutes", SecToHours(4*day+2*hour+16*minute+58*second)) + assert.Equal(t, "672 hours", SecToHours(4*7*day)) } diff --git a/modules/util/slice.go b/modules/util/slice.go index 9c878c24bea7c..da6886491e5d8 100644 --- a/modules/util/slice.go +++ b/modules/util/slice.go @@ -71,3 +71,10 @@ func KeysOfMap[K comparable, V any](m map[K]V) []K { } return keys } + +func SliceNilAsEmpty[T any](a []T) []T { + if a == nil { + return []T{} + } + return a +} diff --git a/modules/util/truncate.go b/modules/util/truncate.go index 2bce248281358..9f932facc9e71 100644 --- a/modules/util/truncate.go +++ b/modules/util/truncate.go @@ -5,7 +5,6 @@ package util import ( "strings" - "unicode" "unicode/utf8" ) @@ -15,110 +14,31 @@ const ( asciiEllipsis = "..." ) -func IsLikelyEllipsisLeftPart(s string) bool { +func IsLikelySplitLeftPart(s string) bool { return strings.HasSuffix(s, utf8Ellipsis) || strings.HasSuffix(s, asciiEllipsis) } -func ellipsisGuessDisplayWidth(r rune) int { - // To make the truncated string as long as possible, - // CJK/emoji chars are considered as 2-ASCII width but not 3-4 bytes width. - // Here we only make the best guess (better than counting them in bytes), - // it's impossible to 100% correctly determine the width of a rune without a real font and render. - // - // ATTENTION: the guessed width can't be zero, more details in ellipsisDisplayString's comment - if r <= 255 { - return 1 +// SplitStringAtByteN splits a string at byte n accounting for rune boundaries. (Combining characters are not accounted for.) +func SplitStringAtByteN(input string, n int) (left, right string) { + if len(input) <= n { + return input, "" } - switch { - case r == '\u3000': /* ideographic (CJK) characters, still use 2 */ - return 2 - case unicode.Is(unicode.M, r), /* (Mark) */ - unicode.Is(unicode.Cf, r), /* (Other, format) */ - unicode.Is(unicode.Cs, r), /* (Other, surrogate) */ - unicode.Is(unicode.Z /* (Space) */, r): - return 1 - default: - return 2 - } -} - -// EllipsisDisplayString returns a truncated short string for display purpose. -// The length is the approximate number of ASCII-width in the string (CJK/emoji are 2-ASCII width) -// It appends "…" or "..." at the end of truncated string. -// It guarantees the length of the returned runes doesn't exceed the limit. -func EllipsisDisplayString(str string, limit int) string { - s, _, _, _ := ellipsisDisplayString(str, limit) - return s -} - -// EllipsisDisplayStringX works like EllipsisDisplayString while it also returns the right part -func EllipsisDisplayStringX(str string, limit int) (left, right string) { - left, offset, truncated, encounterInvalid := ellipsisDisplayString(str, limit) - if truncated { - right = str[offset:] - r, _ := utf8.DecodeRune(UnsafeStringToBytes(right)) - encounterInvalid = encounterInvalid || r == utf8.RuneError - ellipsis := utf8Ellipsis - if encounterInvalid { - ellipsis = asciiEllipsis + if !utf8.ValidString(input) { + if n-3 < 0 { + return input, "" } - right = ellipsis + right - } - return left, right -} - -func ellipsisDisplayString(str string, limit int) (res string, offset int, truncated, encounterInvalid bool) { - if len(str) <= limit { - return str, len(str), false, false + return input[:n-3] + asciiEllipsis, asciiEllipsis + input[n-3:] } - // To future maintainers: this logic must guarantee that the length of the returned runes doesn't exceed the limit, - // because the returned string will also be used as database value. UTF-8 VARCHAR(10) could store 10 rune characters, - // So each rune must be countered as at least 1 width. - // Even if there are some special Unicode characters (zero-width, combining, etc.), they should NEVER be counted as zero. - pos, used := 0, 0 - for i, r := range str { - encounterInvalid = encounterInvalid || r == utf8.RuneError - pos = i - runeWidth := ellipsisGuessDisplayWidth(r) - if used+runeWidth+3 > limit { + end := 0 + for end <= n-3 { + _, size := utf8.DecodeRuneInString(input[end:]) + if end+size > n-3 { break } - used += runeWidth - offset += utf8.RuneLen(r) + end += size } - // if the remaining are fewer than 3 runes, then maybe we could add them, no need to ellipse - if len(str)-pos <= 12 { - var nextCnt, nextWidth int - for _, r := range str[pos:] { - if nextCnt >= 4 { - break - } - nextWidth += ellipsisGuessDisplayWidth(r) - nextCnt++ - } - if nextCnt <= 3 && used+nextWidth <= limit { - return str, len(str), false, false - } - } - if limit < 3 { - // if the limit is so small, do not add ellipsis - return str[:offset], offset, true, false - } - ellipsis := utf8Ellipsis - if encounterInvalid { - ellipsis = asciiEllipsis - } - return str[:offset] + ellipsis, offset, true, encounterInvalid -} - -// TruncateRunes returns a truncated string with given rune limit, -// it returns input string if its rune length doesn't exceed the limit. -func TruncateRunes(str string, limit int) string { - if utf8.RuneCountInString(str) < limit { - return str - } - return string([]rune(str)[:limit]) + return input[:end] + utf8Ellipsis, utf8Ellipsis + input[end:] } diff --git a/modules/util/truncate_test.go b/modules/util/truncate_test.go index 8789c824f5065..dfe1230fd4473 100644 --- a/modules/util/truncate_test.go +++ b/modules/util/truncate_test.go @@ -4,127 +4,43 @@ package util import ( - "fmt" - "strings" "testing" "github.com/stretchr/testify/assert" ) -func TestEllipsisGuessDisplayWidth(t *testing.T) { - cases := []struct { - r string - want int - }{ - {r: "a", want: 1}, - {r: "é", want: 1}, - {r: "测", want: 2}, - {r: "âš½", want: 2}, - {r: "â˜ï¸", want: 3}, // 2 runes, it has a mark - {r: "\u200B", want: 1}, // ZWSP - {r: "\u3000", want: 2}, // ideographic space +func TestSplitString(t *testing.T) { + type testCase struct { + input string + n int + leftSub string + ellipsis string } - for _, c := range cases { - t.Run(c.r, func(t *testing.T) { - w := 0 - for _, r := range c.r { - w += ellipsisGuessDisplayWidth(r) + + test := func(tc []*testCase, f func(input string, n int) (left, right string)) { + for _, c := range tc { + l, r := f(c.input, c.n) + if c.ellipsis != "" { + assert.Equal(t, c.leftSub+c.ellipsis, l, "test split %q at %d, expected leftSub: %q", c.input, c.n, c.leftSub) + assert.Equal(t, c.ellipsis+c.input[len(c.leftSub):], r, "test split %s at %d, expected rightSub: %q", c.input, c.n, c.input[len(c.leftSub):]) + } else { + assert.Equal(t, c.leftSub, l, "test split %q at %d, expected leftSub: %q", c.input, c.n, c.leftSub) + assert.Empty(t, r, "test split %q at %d, expected rightSub: %q", c.input, c.n, "") } - assert.Equal(t, c.want, w, "hex=% x", []byte(c.r)) - }) + } } -} - -func TestEllipsisString(t *testing.T) { - cases := []struct { - limit int - - input, left, right string - }{ - {limit: 0, input: "abcde", left: "", right: "…abcde"}, - {limit: 1, input: "abcde", left: "", right: "…abcde"}, - {limit: 2, input: "abcde", left: "", right: "…abcde"}, - {limit: 3, input: "abcde", left: "…", right: "…abcde"}, - {limit: 4, input: "abcde", left: "a…", right: "…bcde"}, - {limit: 5, input: "abcde", left: "abcde", right: ""}, - {limit: 6, input: "abcde", left: "abcde", right: ""}, - {limit: 7, input: "abcde", left: "abcde", right: ""}, - // a CJK char or emoji is considered as 2-ASCII width, the ellipsis is 3-ASCII width - {limit: 0, input: "测试文本", left: "", right: "…测试文本"}, - {limit: 1, input: "测试文本", left: "", right: "…测试文本"}, - {limit: 2, input: "测试文本", left: "", right: "…测试文本"}, - {limit: 3, input: "测试文本", left: "…", right: "…测试文本"}, - {limit: 4, input: "测试文本", left: "…", right: "…测试文本"}, - {limit: 5, input: "测试文本", left: "测…", right: "…试文本"}, - {limit: 6, input: "测试文本", left: "测…", right: "…试文本"}, - {limit: 7, input: "测试文本", left: "测试…", right: "…文本"}, - {limit: 8, input: "测试文本", left: "测试文本", right: ""}, - {limit: 9, input: "测试文本", left: "测试文本", right: ""}, - - {limit: 6, input: "测试abc", left: "测…", right: "…试abc"}, - {limit: 7, input: "测试abc", left: "测试abc", right: ""}, // exactly 7-width - {limit: 8, input: "测试abc", left: "测试abc", right: ""}, - - {limit: 7, input: "测abc试啊", left: "测ab…", right: "…c试啊"}, - {limit: 8, input: "测abc试啊", left: "测abc…", right: "…试啊"}, - {limit: 9, input: "测abc试啊", left: "测abc试啊", right: ""}, // exactly 9-width - {limit: 10, input: "测abc试啊", left: "测abc试啊", right: ""}, + tc := []*testCase{ + {"abc123xyz", 0, "", utf8Ellipsis}, + {"abc123xyz", 1, "", utf8Ellipsis}, + {"abc123xyz", 4, "a", utf8Ellipsis}, + {"啊bc123xyz", 4, "", utf8Ellipsis}, + {"啊bc123xyz", 6, "啊", utf8Ellipsis}, + {"啊bc", 5, "啊bc", ""}, + {"啊bc", 6, "啊bc", ""}, + {"abc\xef\x03\xfe", 3, "", asciiEllipsis}, + {"abc\xef\x03\xfe", 4, "a", asciiEllipsis}, + {"\xef\x03", 1, "\xef\x03", ""}, } - for _, c := range cases { - t.Run(fmt.Sprintf("%s(%d)", c.input, c.limit), func(t *testing.T) { - left, right := EllipsisDisplayStringX(c.input, c.limit) - assert.Equal(t, c.left, left, "left") - assert.Equal(t, c.right, right, "right") - }) - } - - t.Run("LongInput", func(t *testing.T) { - left, right := EllipsisDisplayStringX(strings.Repeat("abc", 240), 90) - assert.Equal(t, strings.Repeat("abc", 29)+"…", left) - assert.Equal(t, "…"+strings.Repeat("abc", 211), right) - }) - - t.Run("InvalidUtf8", func(t *testing.T) { - invalidCases := []struct { - limit int - left, right string - }{ - {limit: 0, left: "", right: "...\xef\x03\xfe\xef\x03\xfe"}, - {limit: 1, left: "", right: "...\xef\x03\xfe\xef\x03\xfe"}, - {limit: 2, left: "", right: "...\xef\x03\xfe\xef\x03\xfe"}, - {limit: 3, left: "...", right: "...\xef\x03\xfe\xef\x03\xfe"}, - {limit: 4, left: "...", right: "...\xef\x03\xfe\xef\x03\xfe"}, - {limit: 5, left: "\xef\x03\xfe...", right: "...\xef\x03\xfe"}, - {limit: 6, left: "\xef\x03\xfe\xef\x03\xfe", right: ""}, - {limit: 7, left: "\xef\x03\xfe\xef\x03\xfe", right: ""}, - } - for _, c := range invalidCases { - t.Run(fmt.Sprintf("%d", c.limit), func(t *testing.T) { - left, right := EllipsisDisplayStringX("\xef\x03\xfe\xef\x03\xfe", c.limit) - assert.Equal(t, c.left, left, "left") - assert.Equal(t, c.right, right, "right") - }) - } - }) - - t.Run("IsLikelyEllipsisLeftPart", func(t *testing.T) { - assert.True(t, IsLikelyEllipsisLeftPart("abcde…")) - assert.True(t, IsLikelyEllipsisLeftPart("abcde...")) - }) -} - -func TestTruncateRunes(t *testing.T) { - assert.Equal(t, "", TruncateRunes("", 0)) - assert.Equal(t, "", TruncateRunes("", 1)) - - assert.Equal(t, "", TruncateRunes("ab", 0)) - assert.Equal(t, "a", TruncateRunes("ab", 1)) - assert.Equal(t, "ab", TruncateRunes("ab", 2)) - assert.Equal(t, "ab", TruncateRunes("ab", 3)) - - assert.Equal(t, "", TruncateRunes("测试", 0)) - assert.Equal(t, "测", TruncateRunes("测试", 1)) - assert.Equal(t, "测试", TruncateRunes("测试", 2)) - assert.Equal(t, "测试", TruncateRunes("测试", 3)) + test(tc, SplitStringAtByteN) } diff --git a/modules/web/handler.go b/modules/web/handler.go index 9a3e4a7f17660..1812c664b34a3 100644 --- a/modules/web/handler.go +++ b/modules/web/handler.go @@ -4,6 +4,7 @@ package web import ( + goctx "context" "fmt" "net/http" "reflect" @@ -50,6 +51,7 @@ func (r *responseWriter) WriteHeader(statusCode int) { var ( httpReqType = reflect.TypeOf((*http.Request)(nil)) respWriterType = reflect.TypeOf((*http.ResponseWriter)(nil)).Elem() + cancelFuncType = reflect.TypeOf((*goctx.CancelFunc)(nil)).Elem() ) // preCheckHandler checks whether the handler is valid, developers could get first-time feedback, all mistakes could be found at startup @@ -63,8 +65,11 @@ func preCheckHandler(fn reflect.Value, argsIn []reflect.Value) { if !hasStatusProvider { panic(fmt.Sprintf("handler should have at least one ResponseStatusProvider argument, but got %s", fn.Type())) } - if fn.Type().NumOut() != 0 { - panic(fmt.Sprintf("handler should have no return value other than registered ones, but got %s", fn.Type())) + if fn.Type().NumOut() != 0 && fn.Type().NumIn() != 1 { + panic(fmt.Sprintf("handler should have no return value or only one argument, but got %s", fn.Type())) + } + if fn.Type().NumOut() == 1 && fn.Type().Out(0) != cancelFuncType { + panic(fmt.Sprintf("handler should return a cancel function, but got %s", fn.Type())) } } @@ -100,10 +105,16 @@ func prepareHandleArgsIn(resp http.ResponseWriter, req *http.Request, fn reflect return argsIn } -func handleResponse(fn reflect.Value, ret []reflect.Value) { - if len(ret) != 0 { +func handleResponse(fn reflect.Value, ret []reflect.Value) goctx.CancelFunc { + if len(ret) == 1 { + if cancelFunc, ok := ret[0].Interface().(goctx.CancelFunc); ok { + return cancelFunc + } + panic(fmt.Sprintf("unsupported return type: %s", ret[0].Type())) + } else if len(ret) > 1 { panic(fmt.Sprintf("unsupported return values: %s", fn.Type())) } + return nil } func hasResponseBeenWritten(argsIn []reflect.Value) bool { @@ -160,8 +171,11 @@ func toHandlerProvider(handler any) func(next http.Handler) http.Handler { routing.UpdateFuncInfo(req.Context(), funcInfo) ret := fn.Call(argsIn) - // handle the return value (no-op at the moment) - handleResponse(fn, ret) + // handle the return value, and defer the cancel function if there is one + cancelFunc := handleResponse(fn, ret) + if cancelFunc != nil { + defer cancelFunc() + } // if the response has not been written, call the next handler if next != nil && !hasResponseBeenWritten(argsIn) { diff --git a/modules/web/middleware/data.go b/modules/web/middleware/data.go index a47da0f836b36..08d83f94be7f1 100644 --- a/modules/web/middleware/data.go +++ b/modules/web/middleware/data.go @@ -7,21 +7,46 @@ import ( "context" "time" - "code.gitea.io/gitea/modules/reqctx" "code.gitea.io/gitea/modules/setting" ) +// ContextDataStore represents a data store +type ContextDataStore interface { + GetData() ContextData +} + +type ContextData map[string]any + +func (ds ContextData) GetData() ContextData { + return ds +} + +func (ds ContextData) MergeFrom(other ContextData) ContextData { + for k, v := range other { + ds[k] = v + } + return ds +} + const ContextDataKeySignedUser = "SignedUser" -func GetContextData(c context.Context) reqctx.ContextData { - if rc := reqctx.GetRequestDataStore(c); rc != nil { - return rc.GetData() +type contextDataKeyType struct{} + +var contextDataKey contextDataKeyType + +func WithContextData(c context.Context) context.Context { + return context.WithValue(c, contextDataKey, make(ContextData, 10)) +} + +func GetContextData(c context.Context) ContextData { + if ds, ok := c.Value(contextDataKey).(ContextData); ok { + return ds } return nil } -func CommonTemplateContextData() reqctx.ContextData { - return reqctx.ContextData{ +func CommonTemplateContextData() ContextData { + return ContextData{ "IsLandingPageOrganizations": setting.LandingPageURL == setting.LandingPageOrganizations, "ShowRegistrationButton": setting.Service.ShowRegistrationButton, diff --git a/modules/web/middleware/flash.go b/modules/web/middleware/flash.go index 0caaa8c03640e..88da2049a41cf 100644 --- a/modules/web/middleware/flash.go +++ b/modules/web/middleware/flash.go @@ -7,13 +7,11 @@ import ( "fmt" "html/template" "net/url" - - "code.gitea.io/gitea/modules/reqctx" ) // Flash represents a one time data transfer between two requests. type Flash struct { - DataStore reqctx.RequestDataStore + DataStore ContextDataStore url.Values ErrorMsg, WarningMsg, InfoMsg, SuccessMsg string } diff --git a/modules/web/router.go b/modules/web/route.go similarity index 85% rename from modules/web/router.go rename to modules/web/route.go index da06b955b13af..787521dfb07f5 100644 --- a/modules/web/router.go +++ b/modules/web/route.go @@ -10,7 +10,6 @@ import ( "strings" "code.gitea.io/gitea/modules/htmlutil" - "code.gitea.io/gitea/modules/reqctx" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/web/middleware" @@ -30,18 +29,18 @@ func Bind[T any](_ T) http.HandlerFunc { } // SetForm set the form object -func SetForm(dataStore reqctx.ContextDataProvider, obj any) { +func SetForm(dataStore middleware.ContextDataStore, obj any) { dataStore.GetData()["__form"] = obj } // GetForm returns the validate form information -func GetForm(dataStore reqctx.RequestDataStore) any { +func GetForm(dataStore middleware.ContextDataStore) any { return dataStore.GetData()["__form"] } // Router defines a route based on chi's router type Router struct { - chiRouter *chi.Mux + chiRouter chi.Router curGroupPrefix string curMiddlewares []any } @@ -93,21 +92,16 @@ func isNilOrFuncNil(v any) bool { return r.Kind() == reflect.Func && r.IsNil() } -func wrapMiddlewareAndHandler(curMiddlewares, h []any) ([]func(http.Handler) http.Handler, http.HandlerFunc) { - handlerProviders := make([]func(http.Handler) http.Handler, 0, len(curMiddlewares)+len(h)+1) - for _, m := range curMiddlewares { +func (r *Router) wrapMiddlewareAndHandler(h []any) ([]func(http.Handler) http.Handler, http.HandlerFunc) { + handlerProviders := make([]func(http.Handler) http.Handler, 0, len(r.curMiddlewares)+len(h)+1) + for _, m := range r.curMiddlewares { if !isNilOrFuncNil(m) { handlerProviders = append(handlerProviders, toHandlerProvider(m)) } } - if len(h) == 0 { - panic("no endpoint handler provided") - } - for i, m := range h { + for _, m := range h { if !isNilOrFuncNil(m) { handlerProviders = append(handlerProviders, toHandlerProvider(m)) - } else if i == len(h)-1 { - panic("endpoint handler can't be nil") } } middlewares := handlerProviders[:len(handlerProviders)-1] @@ -122,7 +116,7 @@ func wrapMiddlewareAndHandler(curMiddlewares, h []any) ([]func(http.Handler) htt // Methods adds the same handlers for multiple http "methods" (separated by ","). // If any method is invalid, the lower level router will panic. func (r *Router) Methods(methods, pattern string, h ...any) { - middlewares, handlerFunc := wrapMiddlewareAndHandler(r.curMiddlewares, h) + middlewares, handlerFunc := r.wrapMiddlewareAndHandler(h) fullPattern := r.getPattern(pattern) if strings.Contains(methods, ",") { methods := strings.Split(methods, ",") @@ -142,7 +136,7 @@ func (r *Router) Mount(pattern string, subRouter *Router) { // Any delegate requests for all methods func (r *Router) Any(pattern string, h ...any) { - middlewares, handlerFunc := wrapMiddlewareAndHandler(r.curMiddlewares, h) + middlewares, handlerFunc := r.wrapMiddlewareAndHandler(h) r.chiRouter.With(middlewares...).HandleFunc(r.getPattern(pattern), handlerFunc) } @@ -248,11 +242,39 @@ func (r *Router) Combo(pattern string, h ...any) *Combo { return &Combo{r, pattern, h} } -// PathGroup creates a group of paths which could be matched by regexp. -// It is only designed to resolve some special cases which chi router can't handle. -// For most cases, it shouldn't be used because it needs to iterate all rules to find the matched one (inefficient). -func (r *Router) PathGroup(pattern string, fn func(g *RouterPathGroup), h ...any) { - g := &RouterPathGroup{r: r, pathParam: "*"} - fn(g) - r.Any(pattern, append(h, g.ServeHTTP)...) +// Combo represents a tiny group routes with same pattern +type Combo struct { + r *Router + pattern string + h []any +} + +// Get delegates Get method +func (c *Combo) Get(h ...any) *Combo { + c.r.Get(c.pattern, append(c.h, h...)...) + return c +} + +// Post delegates Post method +func (c *Combo) Post(h ...any) *Combo { + c.r.Post(c.pattern, append(c.h, h...)...) + return c +} + +// Delete delegates Delete method +func (c *Combo) Delete(h ...any) *Combo { + c.r.Delete(c.pattern, append(c.h, h...)...) + return c +} + +// Put delegates Put method +func (c *Combo) Put(h ...any) *Combo { + c.r.Put(c.pattern, append(c.h, h...)...) + return c +} + +// Patch delegates Patch method +func (c *Combo) Patch(h ...any) *Combo { + c.r.Patch(c.pattern, append(c.h, h...)...) + return c } diff --git a/modules/web/route_test.go b/modules/web/route_test.go new file mode 100644 index 0000000000000..6e4c309293b9e --- /dev/null +++ b/modules/web/route_test.go @@ -0,0 +1,222 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package web + +import ( + "bytes" + "net/http" + "net/http/httptest" + "strconv" + "testing" + + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" + + chi "github.com/go-chi/chi/v5" + "github.com/stretchr/testify/assert" +) + +func TestRoute1(t *testing.T) { + buff := bytes.NewBufferString("") + recorder := httptest.NewRecorder() + recorder.Body = buff + + r := NewRouter() + r.Get("/{username}/{reponame}/{type:issues|pulls}", func(resp http.ResponseWriter, req *http.Request) { + username := chi.URLParam(req, "username") + assert.EqualValues(t, "gitea", username) + reponame := chi.URLParam(req, "reponame") + assert.EqualValues(t, "gitea", reponame) + tp := chi.URLParam(req, "type") + assert.EqualValues(t, "issues", tp) + }) + + req, err := http.NewRequest("GET", "http://localhost:8000/gitea/gitea/issues", nil) + assert.NoError(t, err) + r.ServeHTTP(recorder, req) + assert.EqualValues(t, http.StatusOK, recorder.Code) +} + +func TestRoute2(t *testing.T) { + buff := bytes.NewBufferString("") + recorder := httptest.NewRecorder() + recorder.Body = buff + + hit := -1 + + r := NewRouter() + r.Group("/{username}/{reponame}", func() { + r.Group("", func() { + r.Get("/{type:issues|pulls}", func(resp http.ResponseWriter, req *http.Request) { + username := chi.URLParam(req, "username") + assert.EqualValues(t, "gitea", username) + reponame := chi.URLParam(req, "reponame") + assert.EqualValues(t, "gitea", reponame) + tp := chi.URLParam(req, "type") + assert.EqualValues(t, "issues", tp) + hit = 0 + }) + + r.Get("/{type:issues|pulls}/{index}", func(resp http.ResponseWriter, req *http.Request) { + username := chi.URLParam(req, "username") + assert.EqualValues(t, "gitea", username) + reponame := chi.URLParam(req, "reponame") + assert.EqualValues(t, "gitea", reponame) + tp := chi.URLParam(req, "type") + assert.EqualValues(t, "issues", tp) + index := chi.URLParam(req, "index") + assert.EqualValues(t, "1", index) + hit = 1 + }) + }, func(resp http.ResponseWriter, req *http.Request) { + if stop, err := strconv.Atoi(req.FormValue("stop")); err == nil { + hit = stop + resp.WriteHeader(http.StatusOK) + } + }) + + r.Group("/issues/{index}", func() { + r.Get("/view", func(resp http.ResponseWriter, req *http.Request) { + username := chi.URLParam(req, "username") + assert.EqualValues(t, "gitea", username) + reponame := chi.URLParam(req, "reponame") + assert.EqualValues(t, "gitea", reponame) + index := chi.URLParam(req, "index") + assert.EqualValues(t, "1", index) + hit = 2 + }) + }) + }) + + req, err := http.NewRequest("GET", "http://localhost:8000/gitea/gitea/issues", nil) + assert.NoError(t, err) + r.ServeHTTP(recorder, req) + assert.EqualValues(t, http.StatusOK, recorder.Code) + assert.EqualValues(t, 0, hit) + + req, err = http.NewRequest("GET", "http://localhost:8000/gitea/gitea/issues/1", nil) + assert.NoError(t, err) + r.ServeHTTP(recorder, req) + assert.EqualValues(t, http.StatusOK, recorder.Code) + assert.EqualValues(t, 1, hit) + + req, err = http.NewRequest("GET", "http://localhost:8000/gitea/gitea/issues/1?stop=100", nil) + assert.NoError(t, err) + r.ServeHTTP(recorder, req) + assert.EqualValues(t, http.StatusOK, recorder.Code) + assert.EqualValues(t, 100, hit) + + req, err = http.NewRequest("GET", "http://localhost:8000/gitea/gitea/issues/1/view", nil) + assert.NoError(t, err) + r.ServeHTTP(recorder, req) + assert.EqualValues(t, http.StatusOK, recorder.Code) + assert.EqualValues(t, 2, hit) +} + +func TestRoute3(t *testing.T) { + buff := bytes.NewBufferString("") + recorder := httptest.NewRecorder() + recorder.Body = buff + + hit := -1 + + m := NewRouter() + r := NewRouter() + r.Mount("/api/v1", m) + + m.Group("/repos", func() { + m.Group("/{username}/{reponame}", func() { + m.Group("/branch_protections", func() { + m.Get("", func(resp http.ResponseWriter, req *http.Request) { + hit = 0 + }) + m.Post("", func(resp http.ResponseWriter, req *http.Request) { + hit = 1 + }) + m.Group("/{name}", func() { + m.Get("", func(resp http.ResponseWriter, req *http.Request) { + hit = 2 + }) + m.Patch("", func(resp http.ResponseWriter, req *http.Request) { + hit = 3 + }) + m.Delete("", func(resp http.ResponseWriter, req *http.Request) { + hit = 4 + }) + }) + }) + }) + }) + + req, err := http.NewRequest("GET", "http://localhost:8000/api/v1/repos/gitea/gitea/branch_protections", nil) + assert.NoError(t, err) + r.ServeHTTP(recorder, req) + assert.EqualValues(t, http.StatusOK, recorder.Code) + assert.EqualValues(t, 0, hit) + + req, err = http.NewRequest("POST", "http://localhost:8000/api/v1/repos/gitea/gitea/branch_protections", nil) + assert.NoError(t, err) + r.ServeHTTP(recorder, req) + assert.EqualValues(t, http.StatusOK, recorder.Code, http.StatusOK) + assert.EqualValues(t, 1, hit) + + req, err = http.NewRequest("GET", "http://localhost:8000/api/v1/repos/gitea/gitea/branch_protections/master", nil) + assert.NoError(t, err) + r.ServeHTTP(recorder, req) + assert.EqualValues(t, http.StatusOK, recorder.Code) + assert.EqualValues(t, 2, hit) + + req, err = http.NewRequest("PATCH", "http://localhost:8000/api/v1/repos/gitea/gitea/branch_protections/master", nil) + assert.NoError(t, err) + r.ServeHTTP(recorder, req) + assert.EqualValues(t, http.StatusOK, recorder.Code) + assert.EqualValues(t, 3, hit) + + req, err = http.NewRequest("DELETE", "http://localhost:8000/api/v1/repos/gitea/gitea/branch_protections/master", nil) + assert.NoError(t, err) + r.ServeHTTP(recorder, req) + assert.EqualValues(t, http.StatusOK, recorder.Code) + assert.EqualValues(t, 4, hit) +} + +func TestRouteNormalizePath(t *testing.T) { + type paths struct { + EscapedPath, RawPath, Path string + } + testPath := func(reqPath string, expectedPaths paths) { + recorder := httptest.NewRecorder() + recorder.Body = bytes.NewBuffer(nil) + + actualPaths := paths{EscapedPath: "(none)", RawPath: "(none)", Path: "(none)"} + r := NewRouter() + r.Get("/*", func(resp http.ResponseWriter, req *http.Request) { + actualPaths.EscapedPath = req.URL.EscapedPath() + actualPaths.RawPath = req.URL.RawPath + actualPaths.Path = req.URL.Path + }) + + req, err := http.NewRequest("GET", reqPath, nil) + assert.NoError(t, err) + r.ServeHTTP(recorder, req) + assert.Equal(t, expectedPaths, actualPaths, "req path = %q", reqPath) + } + + // RawPath could be empty if the EscapedPath is the same as escape(Path) and it is already normalized + testPath("/", paths{EscapedPath: "/", RawPath: "", Path: "/"}) + testPath("//", paths{EscapedPath: "/", RawPath: "/", Path: "/"}) + testPath("/%2f", paths{EscapedPath: "/%2f", RawPath: "/%2f", Path: "//"}) + testPath("///a//b/", paths{EscapedPath: "/a/b", RawPath: "/a/b", Path: "/a/b"}) + + defer test.MockVariableValue(&setting.UseSubURLPath, true)() + defer test.MockVariableValue(&setting.AppSubURL, "/sub-path")() + testPath("/", paths{EscapedPath: "(none)", RawPath: "(none)", Path: "(none)"}) // 404 + testPath("/sub-path", paths{EscapedPath: "/", RawPath: "/", Path: "/"}) + testPath("/sub-path/", paths{EscapedPath: "/", RawPath: "/", Path: "/"}) + testPath("/sub-path//a/b///", paths{EscapedPath: "/a/b", RawPath: "/a/b", Path: "/a/b"}) + testPath("/sub-path/%2f/", paths{EscapedPath: "/%2f", RawPath: "/%2f", Path: "//"}) + // "/v2" is special for OCI container registry, it should always be in the root of the site + testPath("/v2", paths{EscapedPath: "/v2", RawPath: "/v2", Path: "/v2"}) + testPath("/v2/", paths{EscapedPath: "/v2", RawPath: "/v2", Path: "/v2"}) + testPath("/v2/%2f", paths{EscapedPath: "/v2/%2f", RawPath: "/v2/%2f", Path: "/v2//"}) +} diff --git a/modules/web/router_combo.go b/modules/web/router_combo.go deleted file mode 100644 index 4478689027fe0..0000000000000 --- a/modules/web/router_combo.go +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright 2024 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package web - -// Combo represents a tiny group routes with same pattern -type Combo struct { - r *Router - pattern string - h []any -} - -// Get delegates Get method -func (c *Combo) Get(h ...any) *Combo { - c.r.Get(c.pattern, append(c.h, h...)...) - return c -} - -// Post delegates Post method -func (c *Combo) Post(h ...any) *Combo { - c.r.Post(c.pattern, append(c.h, h...)...) - return c -} - -// Delete delegates Delete method -func (c *Combo) Delete(h ...any) *Combo { - c.r.Delete(c.pattern, append(c.h, h...)...) - return c -} - -// Put delegates Put method -func (c *Combo) Put(h ...any) *Combo { - c.r.Put(c.pattern, append(c.h, h...)...) - return c -} - -// Patch delegates Patch method -func (c *Combo) Patch(h ...any) *Combo { - c.r.Patch(c.pattern, append(c.h, h...)...) - return c -} diff --git a/modules/web/router_path.go b/modules/web/router_path.go deleted file mode 100644 index 39082c072455c..0000000000000 --- a/modules/web/router_path.go +++ /dev/null @@ -1,135 +0,0 @@ -// Copyright 2024 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package web - -import ( - "fmt" - "net/http" - "regexp" - "strings" - - "code.gitea.io/gitea/modules/container" - "code.gitea.io/gitea/modules/util" - - "github.com/go-chi/chi/v5" -) - -type RouterPathGroup struct { - r *Router - pathParam string - matchers []*routerPathMatcher -} - -func (g *RouterPathGroup) ServeHTTP(resp http.ResponseWriter, req *http.Request) { - chiCtx := chi.RouteContext(req.Context()) - path := chiCtx.URLParam(g.pathParam) - for _, m := range g.matchers { - if m.matchPath(chiCtx, path) { - handler := m.handlerFunc - for i := len(m.middlewares) - 1; i >= 0; i-- { - handler = m.middlewares[i](handler).ServeHTTP - } - handler(resp, req) - return - } - } - g.r.chiRouter.NotFoundHandler().ServeHTTP(resp, req) -} - -// MatchPath matches the request method, and uses regexp to match the path. -// The pattern uses "<...>" to define path parameters, for example: "/" (different from chi router) -// It is only designed to resolve some special cases which chi router can't handle. -// For most cases, it shouldn't be used because it needs to iterate all rules to find the matched one (inefficient). -func (g *RouterPathGroup) MatchPath(methods, pattern string, h ...any) { - g.matchers = append(g.matchers, newRouterPathMatcher(methods, pattern, h...)) -} - -type routerPathParam struct { - name string - captureGroup int -} - -type routerPathMatcher struct { - methods container.Set[string] - re *regexp.Regexp - params []routerPathParam - middlewares []func(http.Handler) http.Handler - handlerFunc http.HandlerFunc -} - -func (p *routerPathMatcher) matchPath(chiCtx *chi.Context, path string) bool { - if !p.methods.Contains(chiCtx.RouteMethod) { - return false - } - if !strings.HasPrefix(path, "/") { - path = "/" + path - } - pathMatches := p.re.FindStringSubmatchIndex(path) // Golang regexp match pairs [start, end, start, end, ...] - if pathMatches == nil { - return false - } - var paramMatches [][]int - for i := 2; i < len(pathMatches); { - paramMatches = append(paramMatches, []int{pathMatches[i], pathMatches[i+1]}) - pmIdx := len(paramMatches) - 1 - end := pathMatches[i+1] - i += 2 - for ; i < len(pathMatches); i += 2 { - if pathMatches[i] >= end { - break - } - paramMatches[pmIdx] = append(paramMatches[pmIdx], pathMatches[i], pathMatches[i+1]) - } - } - for i, pm := range paramMatches { - groupIdx := p.params[i].captureGroup * 2 - chiCtx.URLParams.Add(p.params[i].name, path[pm[groupIdx]:pm[groupIdx+1]]) - } - return true -} - -func newRouterPathMatcher(methods, pattern string, h ...any) *routerPathMatcher { - middlewares, handlerFunc := wrapMiddlewareAndHandler(nil, h) - p := &routerPathMatcher{methods: make(container.Set[string]), middlewares: middlewares, handlerFunc: handlerFunc} - for _, method := range strings.Split(methods, ",") { - p.methods.Add(strings.TrimSpace(method)) - } - re := []byte{'^'} - lastEnd := 0 - for lastEnd < len(pattern) { - start := strings.IndexByte(pattern[lastEnd:], '<') - if start == -1 { - re = append(re, pattern[lastEnd:]...) - break - } - end := strings.IndexByte(pattern[lastEnd+start:], '>') - if end == -1 { - panic(fmt.Sprintf("invalid pattern: %s", pattern)) - } - re = append(re, pattern[lastEnd:lastEnd+start]...) - partName, partExp, _ := strings.Cut(pattern[lastEnd+start+1:lastEnd+start+end], ":") - lastEnd += start + end + 1 - - // TODO: it could support to specify a "capture group" for the name, for example: "/" - // it is not used so no need to implement it now - param := routerPathParam{} - if partExp == "*" { - re = append(re, "(.*?)/?"...) - if lastEnd < len(pattern) && pattern[lastEnd] == '/' { - lastEnd++ // the "*" pattern is able to handle the last slash, so skip it - } - } else { - partExp = util.IfZero(partExp, "[^/]+") - re = append(re, '(') - re = append(re, partExp...) - re = append(re, ')') - } - param.name = partName - p.params = append(p.params, param) - } - re = append(re, '$') - reStr := string(re) - p.re = regexp.MustCompile(reStr) - return p -} diff --git a/modules/web/router_test.go b/modules/web/router_test.go deleted file mode 100644 index bdcf623b951b6..0000000000000 --- a/modules/web/router_test.go +++ /dev/null @@ -1,245 +0,0 @@ -// Copyright 2021 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package web - -import ( - "bytes" - "net/http" - "net/http/httptest" - "strings" - "testing" - - "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/test" - "code.gitea.io/gitea/modules/util" - - "github.com/go-chi/chi/v5" - "github.com/stretchr/testify/assert" -) - -func chiURLParamsToMap(chiCtx *chi.Context) map[string]string { - pathParams := chiCtx.URLParams - m := make(map[string]string, len(pathParams.Keys)) - for i, key := range pathParams.Keys { - if key == "*" && pathParams.Values[i] == "" { - continue // chi router will add an empty "*" key if there is a "Mount" - } - m[key] = pathParams.Values[i] - } - return util.Iif(len(m) == 0, nil, m) -} - -func TestPathProcessor(t *testing.T) { - testProcess := func(pattern, uri string, expectedPathParams map[string]string) { - chiCtx := chi.NewRouteContext() - chiCtx.RouteMethod = "GET" - p := newRouterPathMatcher("GET", pattern, http.NotFound) - assert.True(t, p.matchPath(chiCtx, uri), "use pattern %s to process uri %s", pattern, uri) - assert.Equal(t, expectedPathParams, chiURLParamsToMap(chiCtx), "use pattern %s to process uri %s", pattern, uri) - } - - // the "<...>" is intentionally designed to distinguish from chi's path parameters, because: - // 1. their behaviors are totally different, we do not want to mislead developers - // 2. we can write regexp in "" easily and parse it easily - testProcess("//", "/a/b", map[string]string{"p1": "a", "p2": "b"}) - testProcess("/", "", map[string]string{"p1": ""}) // this is a special case, because chi router could use empty path - testProcess("/", "/", map[string]string{"p1": ""}) - testProcess("//", "/a", map[string]string{"p1": "", "p2": "a"}) - testProcess("//", "/a/b", map[string]string{"p1": "a", "p2": "b"}) - testProcess("//", "/a/b/c", map[string]string{"p1": "a/b", "p2": "c"}) -} - -func TestRouter(t *testing.T) { - buff := bytes.NewBufferString("") - recorder := httptest.NewRecorder() - recorder.Body = buff - - type resultStruct struct { - method string - pathParams map[string]string - handlerMark string - } - var res resultStruct - - h := func(optMark ...string) func(resp http.ResponseWriter, req *http.Request) { - mark := util.OptionalArg(optMark, "") - return func(resp http.ResponseWriter, req *http.Request) { - res.method = req.Method - res.pathParams = chiURLParamsToMap(chi.RouteContext(req.Context())) - res.handlerMark = mark - } - } - - stopMark := func(optMark ...string) func(resp http.ResponseWriter, req *http.Request) { - mark := util.OptionalArg(optMark, "") - return func(resp http.ResponseWriter, req *http.Request) { - if stop := req.FormValue("stop"); stop != "" && (mark == "" || mark == stop) { - h(stop)(resp, req) - resp.WriteHeader(http.StatusOK) - } - } - } - - r := NewRouter() - r.NotFound(h("not-found:/")) - r.Get("/{username}/{reponame}/{type:issues|pulls}", h("list-issues-a")) // this one will never be called - r.Group("/{username}/{reponame}", func() { - r.Get("/{type:issues|pulls}", h("list-issues-b")) - r.Group("", func() { - r.Get("/{type:issues|pulls}/{index}", h("view-issue")) - }, stopMark()) - r.Group("/issues/{index}", func() { - r.Post("/update", h("update-issue")) - }) - }) - - m := NewRouter() - m.NotFound(h("not-found:/api/v1")) - r.Mount("/api/v1", m) - m.Group("/repos", func() { - m.Group("/{username}/{reponame}", func() { - m.Group("/branches", func() { - m.Get("", h()) - m.Post("", h()) - m.Group("/{name}", func() { - m.Get("", h()) - m.Patch("", h()) - m.Delete("", h()) - }) - m.PathGroup("/*", func(g *RouterPathGroup) { - g.MatchPath("GET", `//`, stopMark("s2"), h("match-path")) - }, stopMark("s1")) - }) - }) - }) - - testRoute := func(t *testing.T, methodPath string, expected resultStruct) { - t.Run(methodPath, func(t *testing.T) { - res = resultStruct{} - methodPathFields := strings.Fields(methodPath) - req, err := http.NewRequest(methodPathFields[0], methodPathFields[1], nil) - assert.NoError(t, err) - r.ServeHTTP(recorder, req) - assert.EqualValues(t, expected, res) - }) - } - - t.Run("RootRouter", func(t *testing.T) { - testRoute(t, "GET /the-user/the-repo/other", resultStruct{method: "GET", handlerMark: "not-found:/"}) - testRoute(t, "GET /the-user/the-repo/pulls", resultStruct{ - method: "GET", - pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "type": "pulls"}, - handlerMark: "list-issues-b", - }) - testRoute(t, "GET /the-user/the-repo/issues/123", resultStruct{ - method: "GET", - pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "type": "issues", "index": "123"}, - handlerMark: "view-issue", - }) - testRoute(t, "GET /the-user/the-repo/issues/123?stop=hijack", resultStruct{ - method: "GET", - pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "type": "issues", "index": "123"}, - handlerMark: "hijack", - }) - testRoute(t, "POST /the-user/the-repo/issues/123/update", resultStruct{ - method: "POST", - pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "index": "123"}, - handlerMark: "update-issue", - }) - }) - - t.Run("Sub Router", func(t *testing.T) { - testRoute(t, "GET /api/v1/other", resultStruct{method: "GET", handlerMark: "not-found:/api/v1"}) - testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches", resultStruct{ - method: "GET", - pathParams: map[string]string{"username": "the-user", "reponame": "the-repo"}, - }) - - testRoute(t, "POST /api/v1/repos/the-user/the-repo/branches", resultStruct{ - method: "POST", - pathParams: map[string]string{"username": "the-user", "reponame": "the-repo"}, - }) - - testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/master", resultStruct{ - method: "GET", - pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "name": "master"}, - }) - - testRoute(t, "PATCH /api/v1/repos/the-user/the-repo/branches/master", resultStruct{ - method: "PATCH", - pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "name": "master"}, - }) - - testRoute(t, "DELETE /api/v1/repos/the-user/the-repo/branches/master", resultStruct{ - method: "DELETE", - pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "name": "master"}, - }) - }) - - t.Run("MatchPath", func(t *testing.T) { - testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/d1/d2/fn", resultStruct{ - method: "GET", - pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1/d2/fn", "dir": "d1/d2", "file": "fn"}, - handlerMark: "match-path", - }) - testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/d1/d2/000", resultStruct{ - method: "GET", - pathParams: map[string]string{"reponame": "the-repo", "username": "the-user", "*": "d1/d2/000"}, - handlerMark: "not-found:/api/v1", - }) - - testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/d1/d2/fn?stop=s1", resultStruct{ - method: "GET", - pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1/d2/fn"}, - handlerMark: "s1", - }) - - testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/d1/d2/fn?stop=s2", resultStruct{ - method: "GET", - pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1/d2/fn", "dir": "d1/d2", "file": "fn"}, - handlerMark: "s2", - }) - }) -} - -func TestRouteNormalizePath(t *testing.T) { - type paths struct { - EscapedPath, RawPath, Path string - } - testPath := func(reqPath string, expectedPaths paths) { - recorder := httptest.NewRecorder() - recorder.Body = bytes.NewBuffer(nil) - - actualPaths := paths{EscapedPath: "(none)", RawPath: "(none)", Path: "(none)"} - r := NewRouter() - r.Get("/*", func(resp http.ResponseWriter, req *http.Request) { - actualPaths.EscapedPath = req.URL.EscapedPath() - actualPaths.RawPath = req.URL.RawPath - actualPaths.Path = req.URL.Path - }) - - req, err := http.NewRequest("GET", reqPath, nil) - assert.NoError(t, err) - r.ServeHTTP(recorder, req) - assert.Equal(t, expectedPaths, actualPaths, "req path = %q", reqPath) - } - - // RawPath could be empty if the EscapedPath is the same as escape(Path) and it is already normalized - testPath("/", paths{EscapedPath: "/", RawPath: "", Path: "/"}) - testPath("//", paths{EscapedPath: "/", RawPath: "/", Path: "/"}) - testPath("/%2f", paths{EscapedPath: "/%2f", RawPath: "/%2f", Path: "//"}) - testPath("///a//b/", paths{EscapedPath: "/a/b", RawPath: "/a/b", Path: "/a/b"}) - - defer test.MockVariableValue(&setting.UseSubURLPath, true)() - defer test.MockVariableValue(&setting.AppSubURL, "/sub-path")() - testPath("/", paths{EscapedPath: "(none)", RawPath: "(none)", Path: "(none)"}) // 404 - testPath("/sub-path", paths{EscapedPath: "/", RawPath: "/", Path: "/"}) - testPath("/sub-path/", paths{EscapedPath: "/", RawPath: "/", Path: "/"}) - testPath("/sub-path//a/b///", paths{EscapedPath: "/a/b", RawPath: "/a/b", Path: "/a/b"}) - testPath("/sub-path/%2f/", paths{EscapedPath: "/%2f", RawPath: "/%2f", Path: "//"}) - // "/v2" is special for OCI container registry, it should always be in the root of the site - testPath("/v2", paths{EscapedPath: "/v2", RawPath: "/v2", Path: "/v2"}) - testPath("/v2/", paths{EscapedPath: "/v2", RawPath: "/v2", Path: "/v2"}) - testPath("/v2/%2f", paths{EscapedPath: "/v2/%2f", RawPath: "/v2/%2f", Path: "/v2//"}) -} diff --git a/modules/webhook/structs.go b/modules/webhook/structs.go index 927a91a74c545..a9f8e33ae28f7 100644 --- a/modules/webhook/structs.go +++ b/modules/webhook/structs.go @@ -26,6 +26,7 @@ type HookEvents struct { Repository bool `json:"repository"` Release bool `json:"release"` Package bool `json:"package"` + Status bool `json:"status"` } // HookEvent represents events that will delivery hook. diff --git a/modules/webhook/type.go b/modules/webhook/type.go index fbec8892722b7..281f7e653b2f6 100644 --- a/modules/webhook/type.go +++ b/modules/webhook/type.go @@ -38,14 +38,6 @@ const ( // Event returns the HookEventType as an event string func (h HookEventType) Event() string { switch h { - case HookEventCreate: - return "create" - case HookEventDelete: - return "delete" - case HookEventFork: - return "fork" - case HookEventPush: - return "push" case HookEventIssues, HookEventIssueAssign, HookEventIssueLabel, HookEventIssueMilestone: return "issues" case HookEventPullRequest, HookEventPullRequestAssign, HookEventPullRequestLabel, HookEventPullRequestMilestone, @@ -59,14 +51,9 @@ func (h HookEventType) Event() string { return "pull_request_rejected" case HookEventPullRequestReviewComment: return "pull_request_comment" - case HookEventWiki: - return "wiki" - case HookEventRepository: - return "repository" - case HookEventRelease: - return "release" + default: + return string(h) } - return "" } // HookType is the type of a webhook diff --git a/options/gitignore/Processing b/options/gitignore/Processing index 942ebbccb5de8..2d243c96bd24c 100644 --- a/options/gitignore/Processing +++ b/options/gitignore/Processing @@ -2,6 +2,7 @@ applet application.linux-arm64 application.linux-armv6hf +application.linux-riscv64 application.linux32 application.linux64 application.windows32 diff --git a/options/gitignore/Python b/options/gitignore/Python index 15201acc113da..c2fb773388edb 100644 --- a/options/gitignore/Python +++ b/options/gitignore/Python @@ -166,6 +166,3 @@ cython_debug/ # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ - -# PyPI configuration file -.pypirc diff --git a/options/gitignore/TeX b/options/gitignore/TeX index 45e1706c28466..a1f52120909b5 100644 --- a/options/gitignore/TeX +++ b/options/gitignore/TeX @@ -26,7 +26,6 @@ ## Bibliography auxiliary files (bibtex/biblatex/biber): *.bbl -*.bbl-SAVE-ERROR *.bcf *.blg *-blx.aux diff --git a/options/license/Elastic-2.0 b/options/license/Elastic-2.0 index 9496955678085..809108b857ffd 100644 --- a/options/license/Elastic-2.0 +++ b/options/license/Elastic-2.0 @@ -2,18 +2,18 @@ Elastic License 2.0 URL: https://www.elastic.co/licensing/elastic-license -Acceptance +## Acceptance By using the software, you agree to all of the terms and conditions below. -Copyright License +## Copyright License The licensor grants you a non-exclusive, royalty-free, worldwide, non-sublicensable, non-transferable license to use, copy, distribute, make available, and prepare derivative works of the software, in each case subject to the limitations and conditions below. -Limitations +## Limitations You may not provide the software to third parties as a hosted or managed service, where the service provides users with access to any substantial set of @@ -27,7 +27,7 @@ You may not alter, remove, or obscure any licensing, copyright, or other notices of the licensor in the software. Any use of the licensor’s trademarks is subject to applicable law. -Patents +## Patents The licensor grants you a license, under any patent claims the licensor can license, or becomes able to license, to make, have made, use, sell, offer for @@ -40,7 +40,7 @@ the software granted under these terms ends immediately. If your company makes such a claim, your patent license ends immediately for work on behalf of your company. -Notices +## Notices You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these terms. @@ -53,7 +53,7 @@ software prominent notices stating that you have modified the software. These terms do not imply any licenses other than those expressly granted in these terms. -Termination +## Termination If you use the software in violation of these terms, such use is not licensed, and your licenses will automatically terminate. If the licensor provides you @@ -63,31 +63,31 @@ reinstated retroactively. However, if you violate these terms after such reinstatement, any additional violation of these terms will cause your licenses to terminate automatically and permanently. -No Liability +## No Liability -As far as the law allows, the software comes as is, without any warranty or +*As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will not be liable to you for any damages arising out of these terms or the use or nature of the software, under any kind of -legal claim. +legal claim.* -Definitions +## Definitions -The licensor is the entity offering these terms, and the software is the +The **licensor** is the entity offering these terms, and the **software** is the software the licensor makes available under these terms, including any portion of it. -you refers to the individual or entity agreeing to these terms. +**you** refers to the individual or entity agreeing to these terms. -your company is any legal entity, sole proprietorship, or other kind of +**your company** is any legal entity, sole proprietorship, or other kind of organization that you work for, plus all organizations that have control over, are under the control of, or are under common control with that -organization. control means ownership of substantially all the assets of an +organization. **control** means ownership of substantially all the assets of an entity, or the power to direct its management and policies by vote, contract, or otherwise. Control can be direct or indirect. -your licenses are all the licenses granted to you for the software under +**your licenses** are all the licenses granted to you for the software under these terms. -use means anything you do with the software requiring one of your licenses. +**use** means anything you do with the software requiring one of your licenses. -trademark means trademarks, service marks, and similar rights. +**trademark** means trademarks, service marks, and similar rights. diff --git a/options/license/MIT b/options/license/MIT index fc2cf8e6b667b..2071b23b0e085 100644 --- a/options/license/MIT +++ b/options/license/MIT @@ -2,17 +2,8 @@ MIT License Copyright (c) -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and -associated documentation files (the "Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the -following conditions: +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all copies or substantial -portions of the Software. +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT -LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO -EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -USE OR OTHER DEALINGS IN THE SOFTWARE. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/options/locale/locale_cs-CZ.ini b/options/locale/locale_cs-CZ.ini index 57b209aaf9755..2abe3672cd834 100644 --- a/options/locale/locale_cs-CZ.ini +++ b/options/locale/locale_cs-CZ.ini @@ -647,7 +647,6 @@ joined_on=PÅ™idal/a se %s repositories=Repozitáře activity=VeÅ™ejná aktivita followers=Sledující -show_more=Zobrazit více starred=Oblíbené repozitáře watched=Sledované repozitáře code=Kód @@ -3743,7 +3742,6 @@ variables.creation.success=PromÄ›nná „%s“ byla pÅ™idána. variables.update.failed=Úprava promÄ›nné se nezdaÅ™ila. variables.update.success=PromÄ›nná byla upravena. - [projects] deleted.display_name=OdstranÄ›ný projekt type-1.display_name=Samostatný projekt diff --git a/options/locale/locale_de-DE.ini b/options/locale/locale_de-DE.ini index 5f2f16bfdf34c..c8dd3f71a23ba 100644 --- a/options/locale/locale_de-DE.ini +++ b/options/locale/locale_de-DE.ini @@ -93,7 +93,6 @@ remove_all=Alle entfernen remove_label_str=Element "%s " entfernen edit=Bearbeiten view=Anzeigen -test=Test enabled=Aktiviert disabled=Deaktiviert @@ -104,7 +103,6 @@ copy_url=URL kopieren copy_hash=Hash kopieren copy_content=Inhalt kopieren copy_branch=Branchnamen kopieren -copy_path=Pfad kopieren copy_success=Kopiert! copy_error=Kopieren fehlgeschlagen copy_type_unsupported=Dieser Dateityp kann nicht kopiert werden @@ -145,7 +143,6 @@ confirm_delete_selected=Alle ausgewählten Elemente löschen? name=Name value=Wert -readme=Readme filter=Filter filter.clear=Filter leeren @@ -161,15 +158,12 @@ filter.public=Öffentlich filter.private=Privat no_results_found=Es wurden keine Ergebnisse gefunden. -internal_error_skipped=Ein interner Fehler ist aufgetreten, wurde aber übersprungen: %s [search] search=Suche ... type_tooltip=Suchmodus fuzzy=Ähnlich fuzzy_tooltip=Ergebnisse einbeziehen, die dem Suchbegriff ähnlich sind -exact=Exakt -exact_tooltip=Nur Suchbegriffe einbeziehen, die dem exakten Suchbegriff entsprechen repo_kind=Repositories durchsuchen ... user_kind=Benutzer durchsuchen ... org_kind=Organisationen durchsuchen ... @@ -180,13 +174,9 @@ code_search_by_git_grep=Aktuelle Code-Suchergebnisse werden von "git grep" berei package_kind=Pakete durchsuchen ... project_kind=Projekte durchsuchen ... branch_kind=Branches durchsuchen ... -tag_kind=Tags durchsuchen... -tag_tooltip=Suche nach passenden Tags. Benutze '%', um jede Sequenz von Zahlen zu treffen. commit_kind=Commits durchsuchen ... runner_kind=Runner durchsuchen ... no_results=Es wurden keine passenden Ergebnisse gefunden. -issue_kind=Issues durchsuchen ... -pull_kind=Pull-Requests durchsuchen... keyword_search_unavailable=Zurzeit ist die Stichwort-Suche nicht verfügbar. Bitte wende dich an den Website-Administrator. [aria] @@ -211,10 +201,7 @@ buttons.link.tooltip=Link hinzufügen buttons.list.unordered.tooltip=Liste hinzufügen buttons.list.ordered.tooltip=Nummerierte Liste hinzufügen buttons.list.task.tooltip=Aufgabenliste hinzufügen -buttons.table.add.tooltip=Tabelle hinzufügen buttons.table.add.insert=Hinzufügen -buttons.table.rows=Zeilen -buttons.table.cols=Spalten buttons.mention.tooltip=Benutzer oder Team erwähnen buttons.ref.tooltip=Issue oder Pull-Request referenzieren buttons.switch_to_legacy.tooltip=Legacy-Editor verwenden @@ -227,20 +214,16 @@ string.desc=Z–A [error] occurred=Ein Fehler ist aufgetreten -report_message=Wenn du glaubst, dass dies ein Fehler von Gitea ist, suche bitte auf GitHub nach diesem Fehler und erstelle gegebenenfalls einen neuen Bugreport. not_found=Das Ziel konnte nicht gefunden werden. network_error=Netzwerkfehler [startpage] app_desc=Ein einfacher, selbst gehosteter Git-Service install=Einfach zu installieren -install_desc=Starte einfach die Anwendung für deine Plattform oder nutze Docker. Es existieren auch paketierte Versionen. platform=Plattformübergreifend -platform_desc=Gitea läuft überall, wo Go kompiliert: Windows, macOS, Linux, ARM, etc. Wähle das System, das dir am meisten gefällt! lightweight=Leichtgewicht lightweight_desc=Gitea hat minimale Systemanforderungen und kann selbst auf einem günstigen und stromsparenden Raspberry Pi betrieben werden! license=Quelloffen -license_desc=Hol dir den Code unter %[2]s! Leiste deinen Beitrag bei der Verbesserung dieses Projekts. Trau dich! [install] install=Installation @@ -354,7 +337,6 @@ enable_update_checker=Aktualisierungsprüfung aktivieren enable_update_checker_helper=Stellt regelmäßig eine Verbindung zu gitea.io her, um nach neuen Versionen zu prüfen. env_config_keys=Umgebungskonfiguration env_config_keys_prompt=Die folgenden Umgebungsvariablen werden auch auf Ihre Konfigurationsdatei angewendet: -config_write_file_prompt=Diese Konfigurationsoptionen werden in %s geschrieben [home] nav_menu=Navigationsmenü @@ -395,8 +377,6 @@ relevant_repositories=Es werden nur relevante Repositories angezeigt, Liste gestohlener Passwörter, die öffentlich verfügbar sind. Bitte versuche es erneut mit einem anderen Passwort und ziehe in Erwägung, auch anderswo deine Passwörter zu ändern. password_pwned_err=Anfrage an HaveIBeenPwned konnte nicht abgeschlossen werden last_admin=Du kannst den letzten Admin nicht entfernen. Es muss mindestens einen Administrator geben. -signin_passkey=Mit einem Passkey anmelden -back_to_sign_in=Zurück zum Anmelden [mail] view_it_on=Auf %s ansehen @@ -486,7 +459,6 @@ activate_email=Bestätige deine E-Mail-Adresse activate_email.title=%s, bitte verifiziere deine E-Mail-Adresse activate_email.text=Bitte klicke innerhalb von %s auf folgenden Link, um dein Konto zu aktivieren: -register_notify=Willkommen bei %s register_notify.title=%[1]s, willkommen bei %[2]s register_notify.text_1=dies ist deine Bestätigungs-E-Mail für %s! register_notify.text_2=Du kannst dich jetzt mit dem Benutzernamen "%s" anmelden. @@ -588,8 +560,6 @@ lang_select_error=Wähle eine Sprache aus der Liste aus. username_been_taken=Der Benutzername ist bereits vergeben. username_change_not_local_user=Nicht-lokale Benutzer dürfen ihren Nutzernamen nicht ändern. -change_username_disabled=Ändern des Benutzernamens ist deaktiviert. -change_full_name_disabled=Ändern des vollständigen Namens ist deaktiviert. username_has_not_been_changed=Benutzername wurde nicht geändert repo_name_been_taken=Der Repository-Name wird schon verwendet. repository_force_private=Privat erzwingen ist aktiviert: Private Repositories können nicht veröffentlicht werden. @@ -639,7 +609,6 @@ org_still_own_repo=Diese Organisation besitzt noch ein oder mehrere Repositories org_still_own_packages=Diese Organisation besitzt noch ein oder mehrere Pakete, lösche diese zuerst. target_branch_not_exist=Der Ziel-Branch existiert nicht. -target_ref_not_exist=Zielreferenz existiert nicht %s admin_cannot_delete_self=Du kannst dich nicht selbst löschen, wenn du ein Administrator bist. Bitte entferne zuerst deine Administratorrechte. @@ -649,7 +618,6 @@ joined_on=Beigetreten am %s repositories=Repositories activity=Öffentliche Aktivität followers=Follower -show_more=Mehr anzeigen starred=Favoriten watched=Beobachtete Repositories code=Quelltext @@ -716,8 +684,6 @@ public_profile=Öffentliches Profil biography_placeholder=Erzähle uns ein wenig über Dich selbst! (Du kannst Markdown verwenden) location_placeholder=Teile Deinen ungefähren Standort mit anderen profile_desc=Lege fest, wie dein Profil anderen Benutzern angezeigt wird. Deine primäre E-Mail-Adresse wird für Benachrichtigungen, Passwort-Wiederherstellung und webbasierte Git-Operationen verwendet. -password_username_disabled=Du bist nicht berechtigt, den Benutzernamen zu ändern. Bitte kontaktiere Deinen Seitenadministrator für weitere Details. -password_full_name_disabled=Du bist nicht berechtigt, den vollständigen Namen zu ändern. Bitte kontaktiere Deinen Seitenadministrator für weitere Details. full_name=Vollständiger Name website=Webseite location=Standort @@ -735,7 +701,6 @@ cancel=Abbrechen language=Sprache ui=Theme hidden_comment_types=Ausgeblendeter Kommentartypen -hidden_comment_types_description=Die hier markierten Kommentartypen werden nicht innerhalb der Issue-Seiten angezeigt. Beispielsweise entfernt das Markieren von "Label" alle "{user} hat {label} hinzugefügt/entfernt"-Kommentare. hidden_comment_types.ref_tooltip=Kommentare, in denen dieses Issue von einem anderen Issue/Commit referenziert wurde hidden_comment_types.issue_ref_tooltip=Kommentare, bei denen der Benutzer den Branch/Tag des Issues ändert comment_type_group_reference=Verweis auf Mitglieder @@ -767,7 +732,6 @@ uploaded_avatar_not_a_image=Die hochgeladene Datei ist kein Bild. uploaded_avatar_is_too_big=Die hochgeladene Dateigröße (%d KiB) überschreitet die maximale Größe (%d KiB). update_avatar_success=Dein Profilbild wurde geändert. update_user_avatar_success=Der Avatar des Benutzers wurde aktualisiert. -cropper_prompt=Sie können das Bild vor dem Speichern bearbeiten. Das bearbeitete Bild wird als PNG-Datei gespeichert. change_password=Passwort aktualisieren old_password=Aktuelles Passwort @@ -783,8 +747,6 @@ manage_themes=Standard-Theme auswählen manage_openid=OpenID-Adressen verwalten email_desc=Deine primäre E-Mail-Adresse wird für Benachrichtigungen, Passwort-Wiederherstellung und, sofern sie nicht versteckt ist, web-basierte Git-Operationen verwendet. theme_desc=Dies wird dein Standard-Theme auf der Seite sein. -theme_colorblindness_help=Hilfe zum Theme für Farbenblinde -theme_colorblindness_prompt=Gitea erhält aktuell einfache Unterstützung für Farbenblinde durch einige Themes, die nur wenige Farben definiert haben. Die Arbeit ist noch im Gange. Weitere Verbesserungen können durch die Definition von mehr Farben in den CSS-Theme-Dateien vorgenommen werden. primary=Primär activated=Aktiviert requires_activation=Erfordert Aktivierung @@ -909,9 +871,8 @@ repo_and_org_access=Repository- und Organisationszugriff permissions_public_only=Nur öffentlich permissions_access_all=Alle (öffentlich, privat und begrenzt) select_permissions=Berechtigungen auswählen -permission_not_set=Nicht festgelegt permission_no_access=Kein Zugriff -permission_read=Lesen +permission_read=Gelesen permission_write=Lesen und Schreiben access_token_desc=Ausgewählte Token-Berechtigungen beschränken die Authentifizierung auf die entsprechenden API-Routen. Lies die Dokumentation für mehr Informationen. at_least_one_permission=Du musst mindestens eine Berechtigung auswählen, um ein Token zu erstellen @@ -929,7 +890,6 @@ create_oauth2_application_success=Du hast erfolgreich eine neue OAuth2-Anwendung update_oauth2_application_success=Du hast die OAuth2-Anwendung erfolgreich aktualisiert. oauth2_application_name=Name der Anwendung oauth2_confidential_client=Vertraulicher Client. Für Anwendungen aktivieren, die das Geheimnis sicher speichern, z. B. Webanwendungen. Wähle diese Option nicht für native Anwendungen für PCs und Mobilgeräte. -oauth2_skip_secondary_authorization=Autorisierung für öffentliche Clients nach einmaliger Gewährung des Zugriffs überspringen. Dies kann ein Sicherheitsrisiko darstellen. oauth2_redirect_uris=URIs für die Weiterleitung. Bitte verwende eine neue Zeile für jede URI. save_application=Speichern oauth2_client_id=Client-ID @@ -940,7 +900,6 @@ oauth2_client_secret_hint=Das Secret wird nach dem Verlassen oder Aktualisieren oauth2_application_edit=Bearbeiten oauth2_application_create_description=OAuth2 Anwendungen geben deiner Drittanwendung Zugriff auf Benutzeraccounts dieser Gitea-Instanz. oauth2_application_remove_description=Das Entfernen einer OAuth2-Anwendung hat zur Folge, dass diese nicht mehr auf autorisierte Benutzeraccounts auf dieser Instanz zugreifen kann. Möchtest Du fortfahren? -oauth2_application_locked=Wenn es in der Konfiguration aktiviert ist, registriert Gitea einige OAuth2-Anwendungen beim Starten vor. Um unerwartetes Verhalten zu verhindern, können diese weder bearbeitet noch entfernt werden. Weitere Informationen findest Du in der OAuth2-Dokumentation. authorized_oauth2_applications=Autorisierte OAuth2-Anwendungen authorized_oauth2_applications_description=Den folgenden Drittanbieter-Apps hast Du Zugriff auf Deinen persönlichen Gitea-Account gewährt. Bitte widerrufe die Autorisierung für Apps, die Du nicht mehr nutzt. @@ -949,26 +908,20 @@ revoke_oauth2_grant=Autorisierung widerrufen revoke_oauth2_grant_description=Wenn du die Autorisierung widerrufst, kann die Anwendung nicht mehr auf deine Daten zugreifen. Bist du dir sicher? revoke_oauth2_grant_success=Zugriff erfolgreich widerrufen. -twofa_desc=Um dein Konto vor Passwortdiebstahl zu schützen, kannst du ein Smartphone oder ein anderes Gerät verwenden, um zeitbasierte Einmalpasswörter ("TOTP") zu erhalten. twofa_recovery_tip=Wenn du dein Gerät verlierst, kannst du einen einmalig verwendbaren Wiederherstellungsschlüssel nutzen, um den Zugriff auf dein Konto wiederherzustellen. twofa_is_enrolled=Für dein Konto ist die Zwei-Faktor-Authentifizierung eingeschaltet. twofa_not_enrolled=Für dein Konto ist die Zwei-Faktor-Authentifizierung momentan nicht eingeschaltet. twofa_disable=Zwei-Faktor-Authentifizierung deaktivieren -twofa_scratch_token_regenerate=Einweg-Wiederherstellungsschlüssel neu generieren -twofa_scratch_token_regenerated=Dein Einweg-Wiederherstellungsschlüssel ist jetzt %s. Speichere ihn an einem sicheren Ort, er wird nie wieder angezeigt. twofa_enroll=Zwei-Faktor-Authentifizierung aktivieren twofa_disable_note=Du kannst die Zwei-Faktor-Authentifizierung auch wieder deaktivieren. twofa_disable_desc=Wenn du die Zwei-Faktor-Authentifizierung deaktivierst, wird die Sicherheit deines Kontos verringert. Fortfahren? -regenerate_scratch_token_desc=Wenn du deinen Wiederherstellungsschlüssel verlegt oder bereits benutzt hast, kannst du ihn hier zurücksetzen. twofa_disabled=Zwei-Faktor-Authentifizierung wurde deaktiviert. scan_this_image=Scanne diese Grafik mit deiner Authentifizierungs-App: or_enter_secret=Oder gib das Secret ein: %s then_enter_passcode=Und gebe dann die angezeigte PIN der Anwendung ein: passcode_invalid=Die PIN ist falsch. Probiere es erneut. -twofa_enrolled=Die Zwei-Faktor-Authentifizierung wurde für dein Konto aktiviert. Bewahre deinen Einweg-Wiederherstellungsschlüssel (%s) an einem sicheren Ort auf, da er nicht wieder angezeigt werden wird. twofa_failed_get_secret=Fehler beim Abrufen des Secrets. -webauthn_desc=Sicherheitsschlüssel sind Geräte, die kryptografische Schlüssel beeinhalten. Diese können für die Zwei-Faktor-Authentifizierung verwendet werden. Der Sicherheitsschlüssel muss den Standard "WebAuthn" unterstützen. webauthn_register_key=Sicherheitsschlüssel hinzufügen webauthn_nickname=Nickname webauthn_delete_key=Sicherheitsschlüssel entfernen @@ -1034,8 +987,6 @@ fork_to_different_account=Fork in ein anderes Konto erstellen fork_visibility_helper=Die Sichtbarkeit eines geforkten Repositories kann nicht geändert werden. fork_branch=Branch, der zum Fork geklont werden soll all_branches=Alle Branches -view_all_branches=Alle Branches anzeigen -view_all_tags=Alle Tags anzeigen fork_no_valid_owners=Dieses Repository kann nicht geforkt werden, da keine gültigen Besitzer vorhanden sind. fork.blocked_user=Das Repository kann nicht geforkt werden, da du vom Repository-Eigentümer blockiert wurdest. use_template=Dieses Template verwenden @@ -1047,8 +998,6 @@ generate_repo=Repository erstellen generate_from=Erstelle aus repo_desc=Beschreibung repo_desc_helper=Gib eine kurze Beschreibung an (optional) -repo_no_desc=Keine Beschreibung vorhanden -repo_lang=Sprachen repo_gitignore_helper=Wähle eine .gitignore-Vorlage aus. repo_gitignore_helper_desc=Wähle aus einer Liste an Vorlagen für bekannte Sprachen, welche Dateien ignoriert werden sollen. Typische Artefakte, die durch die Build Tools der gewählten Sprache generiert werden, sind standardmäßig Bestandteil der .gitignore. issue_labels=Issue Label @@ -1056,7 +1005,6 @@ issue_labels_helper=Wähle ein Issue-Label-Set. license=Lizenz license_helper=Wähle eine Lizenz aus. license_helper_desc=Eine Lizenz regelt, was Andere mit deinem Code (nicht) tun können. Unsicher, welches für dein Projekt die Richtige ist? Siehe eine Lizenz wählen. -multiple_licenses=Mehrere Lizenzen object_format=Objektformat object_format_helper=Objektformat des Repositories. Es kann später nicht geändert werden. SHA1 ist am meisten kompatibel. readme=README @@ -1110,16 +1058,13 @@ delete_preexisting_success=Nicht übernommene Dateien in %s gelöscht blame_prior=Blame vor dieser Änderung anzeigen blame.ignore_revs=Revisionen in .git-blame-ignore-revs werden ignoriert. Klicke hier, um das zu umgehen und die normale Blame-Ansicht zu sehen. blame.ignore_revs.failed=Fehler beim Ignorieren der Revisionen in .git-blame-ignore-revs. -user_search_tooltip=Zeigt maximal 30 Benutzer tree_path_not_found_commit=Pfad %[1]s existiert nicht in Commit%[2]s tree_path_not_found_branch=Pfad %[1]s existiert nicht in Branch %[2]s tree_path_not_found_tag=Pfad %[1]s existiert nicht in Tag %[2]s transfer.accept=Übertragung Akzeptieren -transfer.accept_desc=`Übertragung nach "%s"` transfer.reject=Übertragung Ablehnen -transfer.reject_desc=Übertragung nach "%s " abbrechen transfer.no_permission_to_accept=Du hast keine Berechtigung, diesen Transfer anzunehmen. transfer.no_permission_to_reject=Du hast keine Berechtigung, diesen Transfer abzulehnen. @@ -1194,11 +1139,6 @@ migrate.gogs.description=Daten von notabug.org oder anderen Gogs Instanzen migri migrate.onedev.description=Daten von code.onedev.io oder anderen OneDev Instanzen migrieren. migrate.codebase.description=Daten von codebasehq.com migrieren. migrate.gitbucket.description=Daten von GitBucket Instanzen migrieren. -migrate.codecommit.description=Daten von AWS CodeCommit migrieren. -migrate.codecommit.aws_access_key_id=AWS Access Key ID -migrate.codecommit.aws_secret_access_key=AWS Secret Access Key -migrate.codecommit.https_git_credentials_username=HTTPS-Git-Nutzername -migrate.codecommit.https_git_credentials_password=HTTPS-Git-Passwort migrate.migrating_git=Git-Daten werden migriert migrate.migrating_topics=Themen werden migriert migrate.migrating_milestones=Meilensteine werden migriert @@ -1259,7 +1199,6 @@ releases=Releases tag=Tag released_this=hat released tagged_this=hat getaggt -file.title=%s in %s file_raw=Originalformat file_history=Verlauf file_view_source=Quelltext anzeigen @@ -1267,16 +1206,12 @@ file_view_rendered=Ansicht rendern file_view_raw=Originalformat anzeigen file_permalink=Permalink file_too_large=Die Datei ist zu groß zum Anzeigen. -file_is_empty=Die Datei ist leer. -code_preview_line_from_to=Zeilen %[1]d bis %[2]d in %[3]s -code_preview_line_in=Zeile %[1]d in %[2]s invisible_runes_header=`Diese Datei enthält unsichtbare Unicode-Zeichen` invisible_runes_description=`Diese Datei enthält unsichtbare Unicode-Zeichen, die für Menschen nicht unterscheidbar sind, aber von einem Computer unterschiedlich verarbeitet werden können. Wenn du glaubst, dass das absichtlich so ist, kannst du diese Warnung ignorieren. Benutze den „Escape“-Button, um versteckte Zeichen anzuzeigen.` ambiguous_runes_header=`Diese Datei enthält mehrdeutige Unicode-Zeichen` ambiguous_runes_description=`Diese Datei enthält Unicode-Zeichen, die mit anderen Zeichen verwechselt werden können. Wenn du glaubst, dass das absichtlich so ist, kannst du diese Warnung ignorieren. Benutze den „Escape“-Button, um versteckte Zeichen anzuzeigen.` invisible_runes_line=`Diese Zeile enthält unsichtbare Unicode-Zeichen` ambiguous_runes_line=`Diese Zeile enthält mehrdeutige Unicode-Zeichen` -ambiguous_character=`%[1]c [U+%04[1]X] kann mit %[2]c [U+%04[2]X] verwechselt werden` escape_control_characters=Escapen unescape_control_characters=Unescapen @@ -1324,7 +1259,6 @@ editor.or=oder editor.cancel_lower=Abbrechen editor.commit_signed_changes=Committe signierte Änderungen editor.commit_changes=Änderungen committen -editor.add_tmpl='{filename}' hinzufügen editor.add=%s hinzugefügt editor.update=%s aktualisiert editor.delete=%s gelöscht @@ -1408,7 +1342,6 @@ commitstatus.success=Erfolg ext_issues=Zugriff auf Externe Issues ext_issues.desc=Link zu externem Issuetracker. -projects.desc=Verwalte Issues und Pull-Requests in Projekten. projects.description=Beschreibung (optional) projects.description_placeholder=Beschreibung projects.create=Projekt erstellen @@ -1468,9 +1401,7 @@ issues.new.clear_milestone=Meilenstein entfernen issues.new.assignees=Zuständig issues.new.clear_assignees=Zuständige entfernen issues.new.no_assignees=Niemand zuständig -issues.new.no_reviewers=Keine Reviewer issues.new.blocked_user=Das Issue kann nicht erstellt werden, da du vom Repository-Eigentümer blockiert wurdest. -issues.edit.already_changed=Änderungen zum Issue konnten nicht gespeichert werden. Es scheint, dass der Inhalt bereits von einem anderen Benutzer geändert wurde. Bitte aktualisiere die Seite und bearbeite diese erneut, um zu verhindern, dass die Änderungen des anderen Benutzers überschrieben werden issues.edit.blocked_user=Der Inhalt kann nicht bearbeitet werden, da du vom Repository-Eigentümer blockiert wurdest. issues.choose.get_started=Los geht's issues.choose.open_external_link=Öffnen @@ -1497,7 +1428,6 @@ issues.remove_labels=hat die Labels %s %s entfernt issues.add_remove_labels=hat %s hinzugefügt, und %s %s entfernt issues.add_milestone_at=`hat diesen Issue %[2]s zum %[1]s Meilenstein hinzugefügt` issues.add_project_at=`hat dieses zum %s projekt %s hinzugefügt` -issues.move_to_column_of_project=`hat dies zu %s in %s %s verschoben` issues.change_milestone_at=`hat den Meilenstein %[3]s von %[1]s zu %[2]s geändert` issues.change_project_at=`hat das Projekt %[3]s von %[1]s zu %[2]s geändert` issues.remove_milestone_at=`hat dieses Issue %[2]s vom %[1]s Meilenstein entfernt` @@ -1529,8 +1459,6 @@ issues.filter_assignee=Zuständig issues.filter_assginee_no_select=Alle Zuständigen issues.filter_assginee_no_assignee=Niemand zuständig issues.filter_poster=Autor -issues.filter_user_placeholder=Benutzer suchen -issues.filter_user_no_select=Alle Benutzer issues.filter_type=Typ issues.filter_type.all_issues=Alle Issues issues.filter_type.assigned_to_you=Dir zugewiesen @@ -1584,9 +1512,7 @@ issues.no_content=Keine Beschreibung angegeben. issues.close=Issue schließen issues.comment_pull_merged_at=hat Commit %[1]s in %[2]s %[3]s gemerged issues.comment_manually_pull_merged_at=hat Commit %[1]s in %[2]s %[3]s manuell gemerged -issues.close_comment_issue=Kommentieren und schließen issues.reopen_issue=Wieder öffnen -issues.reopen_comment_issue=Kommentieren und wieder öffnen issues.create_comment=Kommentieren issues.comment.blocked_user=Der Kommentar kann nicht erstellt oder bearbeitet werden, da du vom Repository-Eigentümer blockiert wurdest. issues.closed_at=`hat diesen Issue %[2]s geschlossen` @@ -1675,25 +1601,12 @@ issues.delete.title=Dieses Issue löschen? issues.delete.text=Möchtest du dieses Issue wirklich löschen? (Dadurch wird der Inhalt dauerhaft gelöscht. Denke daran, es stattdessen zu schließen, wenn du es archivieren willst) issues.tracker=Zeiterfassung -issues.timetracker_timer_start=Timer starten -issues.timetracker_timer_stop=Timer stoppen -issues.timetracker_timer_discard=Timer verwerfen -issues.timetracker_timer_manually_add=Zeit hinzufügen - -issues.time_estimate_set=Geschätzte Zeit festlegen -issues.time_estimate_display=Schätzung: %s -issues.change_time_estimate_at=Zeitschätzung geändert zu %s %s -issues.remove_time_estimate_at=Zeitschätzung %s entfernt -issues.time_estimate_invalid=Format der Zeitschätzung ist ungültig -issues.start_tracking_history=hat die Zeiterfassung %s gestartet + issues.tracker_auto_close=Der Timer wird automatisch gestoppt, wenn dieser Issue geschlossen wird issues.tracking_already_started=`Du hast die Zeiterfassung bereits in diesem Issue gestartet!` -issues.stop_tracking_history=hat für %s gearbeitet %s issues.cancel_tracking_history=`hat die Zeiterfassung %s abgebrochen` issues.del_time=Diese Zeiterfassung löschen -issues.add_time_history=hat %s gearbeitete Zeit hinzugefügt %s issues.del_time_history=`hat %s gearbeitete Zeit gelöscht` -issues.add_time_manually=Zeit manuell hinzufügen issues.add_time_hours=Stunden issues.add_time_minutes=Minuten issues.add_time_sum_to_small=Es wurde keine Zeit eingegeben. @@ -1753,7 +1666,6 @@ issues.dependency.add_error_dep_not_same_repo=Beide Issues müssen sich im selbe issues.review.self.approval=Du kannst nicht dein eigenen Pull-Request genehmigen. issues.review.self.rejection=Du kannst keine Änderungen an deinem eigenen Pull-Request anfragen. issues.review.approve=hat die Änderungen %s genehmigt -issues.review.comment=hat %s überprüft issues.review.dismissed=verwarf %ss Review %s issues.review.dismissed_label=Verworfen issues.review.left_comment=hat einen Kommentar hinterlassen @@ -1779,11 +1691,6 @@ issues.review.resolve_conversation=Diskussion als "erledigt" markieren issues.review.un_resolve_conversation=Diskussion als "nicht-erledigt" markieren issues.review.resolved_by=markierte diese Unterhaltung als gelöst issues.review.commented=Kommentieren -issues.review.official=Genehmigt -issues.review.requested=Prüfung ausstehend -issues.review.rejected=Änderungen angefordert -issues.review.stale=Aktualisiert seit der Genehmigung -issues.review.unofficial=Ungezählte Genehmigung issues.assignee.error=Aufgrund eines unerwarteten Fehlers konnten nicht alle Beauftragten hinzugefügt werden. issues.reference_issue.body=Beschreibung issues.content_history.deleted=gelöscht @@ -1800,8 +1707,6 @@ compare.compare_head=vergleichen pulls.desc=Pull-Requests und Code-Reviews aktivieren. pulls.new=Neuer Pull-Request pulls.new.blocked_user=Der Pull Request kann nicht erstellt werden, da du vom Repository-Eigentümer blockiert wurdest. -pulls.new.must_collaborator=Du musst Mitarbeiter sein, um Pull-Requests zu erstellen. -pulls.edit.already_changed=Änderungen zum Pull-Request konnten nicht gespeichert werden. Es scheint, dass der Inhalt bereits von einem anderen Benutzer geändert wurde. Bitte aktualisieren die Seite und bearbeite diesen erneut, um zu verhindern, dass die Änderungen des anderen Benutzers überschrieben werden pulls.view=Pull-Request ansehen pulls.compare_changes=Neuer Pull-Request pulls.allow_edits_from_maintainers=Änderungen von Maintainern erlauben @@ -1857,8 +1762,6 @@ pulls.is_empty=Die Änderungen an diesem Branch sind bereits auf dem Zielbranch. pulls.required_status_check_failed=Einige erforderliche Prüfungen waren nicht erfolgreich. pulls.required_status_check_missing=Einige erforderliche Prüfungen fehlen. pulls.required_status_check_administrator=Als Administrator kannst du diesen Pull-Request weiterhin mergen. -pulls.blocked_by_approvals=Dieser Pull-Request hat noch nicht genügend Genehmigungen. %d von %d Genehmigungen erteilt. -pulls.blocked_by_approvals_whitelisted=Dieser Pull-Request hat noch nicht genug erforderliche Genehmigungen. %d von %d Genehmigungen von Benutzern oder Teams auf der Berechtigungsliste. pulls.blocked_by_rejection=Dieser Pull-Request hat Änderungen, die von einem offiziellen Reviewer angefragt wurden. pulls.blocked_by_official_review_requests=Dieser Pull Request hat offizielle Review-Anfragen. pulls.blocked_by_outdated_branch=Dieser Pull Request ist blockiert, da er veraltet ist. @@ -1900,9 +1803,7 @@ pulls.unrelated_histories=Merge fehlgeschlagen: Der Head des Merges und die Basi pulls.merge_out_of_date=Merge fehlgeschlagen: Während des Mergens wurde die Basis aktualisiert. Hinweis: Versuche es erneut. pulls.head_out_of_date=Mergen fehlgeschlagen: Der Head wurde aktualisiert während der Merge erstellt wurde. Tipp: Versuche es erneut. pulls.has_merged=Fehler: Der Pull-Request wurde gemerged, du kannst den Zielbranch nicht wieder mergen oder ändern. -pulls.push_rejected=Push fehlgeschlagen: Der Push wurde abgelehnt. Überprüfe die Git Hooks für dieses Repository. pulls.push_rejected_summary=Vollständige Ablehnungsmeldung -pulls.push_rejected_no_message=Push fehlgeschlagen: Der Push wurde abgelehnt, aber es gab keine Fehlermeldung. Überprüfe die Git Hooks für dieses Repository pulls.open_unmerged_pull_exists=`Du kannst diesen Pull-Request nicht erneut öffnen, da noch ein anderer (#%d) mit identischen Eigenschaften offen ist.` pulls.status_checking=Einige Prüfungen sind noch ausstehend pulls.status_checks_success=Alle Prüfungen waren erfolgreich @@ -1926,7 +1827,6 @@ pulls.cmd_instruction_checkout_title=Checkout pulls.cmd_instruction_checkout_desc=Wechsle auf einen neuen Branch in deinem lokalen Repository und teste die Änderungen. pulls.cmd_instruction_merge_title=Mergen pulls.cmd_instruction_merge_desc=Die Änderungen mergen und auf Gitea aktualisieren. -pulls.cmd_instruction_merge_warning=Warnung: Dieser Vorgang kann den Pull-Request nicht mergen, da "manueller Merge" nicht aktiviert wurde pulls.clear_merge_message=Merge-Nachricht löschen pulls.clear_merge_message_hint=Das Löschen der Merge-Nachricht wird nur den Inhalt der Commit-Nachricht entfernen und generierte Git-Trailer wie "Co-Authored-By …" erhalten. @@ -1946,15 +1846,9 @@ pulls.delete.title=Diesen Pull-Request löschen? pulls.delete.text=Willst du diesen Pull-Request wirklich löschen? (Dies wird den Inhalt unwiderruflich löschen. Überlege, ob du ihn nicht lieber schließen willst, um ihn zu archivieren) pulls.recently_pushed_new_branches=Du hast auf den Branch %[1]s %[2]s gepusht -pulls.upstream_diverging_prompt_behind_1=Dieser Branch ist %[1]d Commit hinter %[2]s -pulls.upstream_diverging_prompt_behind_n=Dieser Branch ist %[1]d Commits hinter %[2]s -pulls.upstream_diverging_prompt_base_newer=Der Basis-Branch %s hat neue Änderungen -pulls.upstream_diverging_merge=Fork synchronisieren pull.deleted_branch=(gelöscht):%s -pull.agit_documentation=Dokumentation zu AGit durchschauen -comments.edit.already_changed=Änderungen zum Kommentar konnten nicht gespeichert werden. Es scheint, dass der Inhalt bereits von einem anderen Benutzer geändert wurde. Bitte aktualisiere die Seite und bearbeite diesen erneut, um zu verhindern, dass die Änderungen des anderen Benutzers überschrieben werden milestones.new=Neuer Meilenstein milestones.closed=Geschlossen %s @@ -1963,7 +1857,6 @@ milestones.no_due_date=Kein Fälligkeitsdatum milestones.open=Öffnen milestones.close=Schließen milestones.new_subheader=Benutze Meilensteine, um Issues zu organisieren und den Fortschritt darzustellen. -milestones.completeness=%d%% abgeschlossen milestones.create=Meilenstein erstellen milestones.title=Titel milestones.desc=Beschreibung @@ -2148,14 +2041,12 @@ settings.push_mirror_sync_in_progress=Aktuell werden Änderungen auf %s gepusht. settings.site=Webseite settings.update_settings=Einstellungen speichern settings.update_mirror_settings=Mirror-Einstellungen aktualisieren -settings.branches.switch_default_branch=Standardbranch wechseln settings.branches.update_default_branch=Standardbranch aktualisieren settings.branches.add_new_rule=Neue Regel hinzufügen settings.advanced_settings=Erweiterte Einstellungen settings.wiki_desc=Repository-Wiki aktivieren settings.use_internal_wiki=Eingebautes Wiki verwenden settings.default_wiki_branch_name=Standardbezeichnung für Wiki-Branch -settings.default_wiki_everyone_access=Standard-Zugriffsberechtigung für angemeldete Benutzer: settings.failed_to_change_default_wiki_branch=Das Ändern des Standard-Wiki-Branches ist fehlgeschlagen. settings.use_external_wiki=Externes Wiki verwenden settings.external_wiki_url=Externe Wiki-URL @@ -2186,7 +2077,6 @@ settings.pulls.default_delete_branch_after_merge=Standardmäßig bei Pull-Reques settings.pulls.default_allow_edits_from_maintainers=Änderungen von Maintainern standardmäßig erlauben settings.releases_desc=Repository-Releases aktivieren settings.packages_desc=Repository Packages Registry aktivieren -settings.projects_desc=Projekte aktivieren settings.projects_mode_desc=Projekte-Modus (welche Art Projekte angezeigt werden sollen) settings.projects_mode_repo=Nur Repo-Projekte settings.projects_mode_owner=Nur Benutzer- oder Organisations-Projekte @@ -2226,7 +2116,6 @@ settings.transfer_in_progress=Es gibt derzeit eine laufende Übertragung. Bitte settings.transfer_notices_1=– Du wirst keinen Zugriff mehr haben, wenn der neue Besitzer ein individueller Benutzer ist. settings.transfer_notices_2=– Du wirst weiterhin Zugriff haben, wenn der neue Besitzer eine Organisation ist und du einer der Besitzer bist. settings.transfer_notices_3=- Wenn das Repository privat ist und an einen einzelnen Benutzer übertragen wird, wird sichergestellt, dass der Benutzer mindestens Leserechte hat (und die Berechtigungen werden gegebenenfalls ändert). -settings.transfer_notices_4=- Wenn das Repository einer Organisation gehört und du es an eine andere Organisation oder eine andere Person überträgst, verlierst du die Verlinkungen zwischen den Issues des Repositorys und dem Projektboard der Organisation. settings.transfer_owner=Neuer Besitzer settings.transfer_perform=Übertragung durchführen settings.transfer_started=`Für dieses Repository wurde eine Übertragung eingeleitet und wartet nun auf die Bestätigung von "%s"` @@ -2326,7 +2215,6 @@ settings.event_wiki_desc=Wiki-Seite erstellt, umbenannt, bearbeitet oder gelösc settings.event_release=Release settings.event_release_desc=Release in einem Repository veröffentlicht, aktualisiert oder gelöscht. settings.event_push=Push -settings.event_force_push=Force Push settings.event_push_desc=Git push in ein Repository. settings.event_repository=Repository settings.event_repository_desc=Repository erstellt oder gelöscht. @@ -2363,7 +2251,6 @@ settings.event_pull_request_merge=Pull-Request-Merge settings.event_package=Paket settings.event_package_desc=Paket wurde in einem Repository erstellt oder gelöscht. settings.branch_filter=Branch-Filter -settings.branch_filter_desc=Whitelist für Branches für Push-, Erzeugungs- und Löschevents, als Glob-Pattern beschrieben. Es werden Events für alle Branches gemeldet, falls das Pattern * ist, oder falls es leer ist. Siehe die %[2]s Dokumentation für die Syntax (Englisch). Beispiele: master, {master,release*}. settings.authorization_header=Authorization-Header settings.authorization_header_desc=Wird, falls vorhanden, als Authorization-Header mitgesendet. Beispiele: %s. settings.active=Aktiv @@ -2412,50 +2299,22 @@ settings.branches=Branches settings.protected_branch=Branch-Schutz settings.protected_branch.save_rule=Regel speichern settings.protected_branch.delete_rule=Regel löschen -settings.protected_branch_can_push=Push erlauben? -settings.protected_branch_can_push_yes=Du kannst pushen -settings.protected_branch_can_push_no=Du kannst nicht pushen -settings.branch_protection=Branch-Schutz für Branch '%s' settings.protect_this_branch=Branch-Schutz aktivieren settings.protect_this_branch_desc=Verhindert das Löschen und schränkt Git auf Push- und Merge-Änderungen auf dem Branch ein. settings.protect_disable_push=Push deaktivieren settings.protect_disable_push_desc=Kein Push auf diesen Branch erlauben. -settings.protect_disable_force_push=Force-Push deaktivieren -settings.protect_disable_force_push_desc=Force-Push auf diesen Branch nicht erlauben. settings.protect_enable_push=Push aktivieren settings.protect_enable_push_desc=Jeder, der Schreibzugriff hat, darf in diesen Branch Pushen (aber kein Force-Push). -settings.protect_enable_force_push_all=Force-Push aktivieren -settings.protect_enable_force_push_all_desc=Jeder mit Push-Zugriff wird in diesen Branch force-pushen können. -settings.protect_enable_force_push_allowlist=Force-Push beschränkt auf Genehmigungsliste -settings.protect_enable_force_push_allowlist_desc=Nur Benutzer oder Teams auf der Genehmigungsliste mit Push-Zugriff werden in diesen Branch force-pushen können. settings.protect_enable_merge=Merge aktivieren settings.protect_enable_merge_desc=Jeder mit Schreibzugriff darf die Pull-Requests in diesen Branch mergen. -settings.protect_whitelist_committers=Genehmigungsliste für eingeschränkten Push -settings.protect_whitelist_committers_desc=Jeder, der auf der Genehmigungsliste steht, darf in diesen Branch pushen (aber kein Force-Push). -settings.protect_whitelist_deploy_keys=Genehmigungsliste für Deploy-Schlüssel mit Schreibzugriff zum Pushen. -settings.protect_whitelist_users=Nutzer, die pushen dürfen: -settings.protect_whitelist_teams=Teams, die pushen dürfen: -settings.protect_force_push_allowlist_users=Erlaubte Benutzer für Force-Push: -settings.protect_force_push_allowlist_teams=Erlaubte Teams für Force-Push: -settings.protect_force_push_allowlist_deploy_keys=Genehmigungsliste für Deploy-Schlüssel mit Schreibzugriff zum Force-Push. -settings.protect_merge_whitelist_committers=Merge-Genehmigungsliste aktivieren -settings.protect_merge_whitelist_committers_desc=Erlaube Nutzern oder Teams auf der Genehmigungsliste Pull-Requests in diesen Branch zu mergen. -settings.protect_merge_whitelist_users=Nutzer, die mergen dürfen: -settings.protect_merge_whitelist_teams=Teams, die mergen dürfen: settings.protect_check_status_contexts=Statusprüfungen aktivieren settings.protect_status_check_patterns=Statuscheck-Muster: settings.protect_status_check_patterns_desc=Gib Muster ein, um festzulegen, welche Statusüberprüfungen durchgeführt werden müssen, bevor Branches in einen Branch, der dieser Regel entspricht, gemerged werden können. Jede Zeile gibt ein Muster an. Muster dürfen nicht leer sein. -settings.protect_check_status_contexts_desc=Vor dem Mergen müssen Statusprüfungen bestanden werden. Wähle aus, welche Statusprüfungen erfolgreich durchgeführt werden müssen, bevor Branches in einen anderen gemergt werden können, der dieser Regel entspricht. Wenn aktiviert, müssen Commits zuerst auf einen anderen Branch gepusht werden, dann nach bestandener Statusprüfung gemergt oder direkt auf einen Branch gepusht werden, der dieser Regel entspricht. Wenn kein Kontext ausgewählt ist, muss der letzte Commit unabhängig vom Kontext erfolgreich sein. settings.protect_check_status_contexts_list=Statusprüfungen, die in der letzten Woche für dieses Repository gefunden wurden settings.protect_status_check_matched=Übereinstimmung settings.protect_invalid_status_check_pattern=Ungültiges Muster: "%s". settings.protect_no_valid_status_check_patterns=Keine gültigen Statuscheck-Muster. settings.protect_required_approvals=Erforderliche Zustimmungen: -settings.protect_required_approvals_desc=Erlaube das Mergen des Pull-Requests nur mit genügend Genehmigungen. -settings.protect_approvals_whitelist_enabled=Genehmigungen auf Benutzer oder Teams auf der Genehmigungsliste beschränken -settings.protect_approvals_whitelist_enabled_desc=Nur Bewertungen von Benutzern auf der Genehmigungsliste oder Teams zählen zu den erforderlichen Genehmigungen. Gibt es keine Genehmigungsliste, so zählen Reviews von jedem mit Schreibzugriff zu den erforderlichen Genehmigungen. -settings.protect_approvals_whitelist_users=Freigeschaltete Reviewer: -settings.protect_approvals_whitelist_teams=Freigeschaltete Teams: settings.dismiss_stale_approvals=Entferne alte Genehmigungen settings.dismiss_stale_approvals_desc=Wenn neue Commits gepusht werden, die den Inhalt des Pull-Requests ändern, werden alte Genehmigungen entfernt. settings.ignore_stale_approvals=Veraltete Genehmigungen ignorieren @@ -2463,18 +2322,12 @@ settings.ignore_stale_approvals_desc=Genehmigungen, die für ältere Commits ert settings.require_signed_commits=Signierte Commits erforderlich settings.require_signed_commits_desc=Pushes auf diesen Branch ablehnen, wenn Commits nicht signiert oder nicht überprüfbar sind. settings.protect_branch_name_pattern=Muster für geschützte Branchnamen -settings.protect_branch_name_pattern_desc=Geschützte Branch-Namensmuster. Siehe die Dokumentation für die Pattern-Syntax. Beispiele: main, release/** settings.protect_patterns=Muster settings.protect_protected_file_patterns=Geschützte Dateimuster (durch Semikolon ';' getrennt): -settings.protect_protected_file_patterns_desc=Geschützte Dateien dürfen nicht direkt geändert werden, auch wenn der Benutzer Rechte hat, Dateien in diesem Branch hinzuzufügen, zu bearbeiten oder zu löschen. Mehrere Muster können mit Semikolon (';') getrennt werden. Siehe %[2]s Dokumentation zur Pattern-Syntax. Beispiele: .drone.yml, /docs/**/*.txt. settings.protect_unprotected_file_patterns=Ungeschützte Dateimuster (durch Semikolon ';' getrennt): -settings.protect_unprotected_file_patterns_desc=Ungeschützte Dateien, die direkt geändert werden dürfen, wenn der Benutzer Schreibzugriff hat, können die Push-Beschränkung umgehen. Mehrere Muster können mit Semikolon (';') getrennt werden. Siehe %[2]s Dokumentation zur Mustersyntax. Beispiele: .drone.yml, /docs/**/*.txt. -settings.add_protected_branch=Schutz aktivieren -settings.delete_protected_branch=Schutz deaktivieren settings.update_protect_branch_success=Branchschutzregel "%s" wurde geändert. settings.remove_protected_branch_success=Branchschutzregel "%s" wurde deaktiviert. settings.remove_protected_branch_failed=Entfernen der Branchschutzregel "%s" fehlgeschlagen. -settings.protected_branch_deletion=Branch-Schutz deaktivieren settings.protected_branch_deletion_desc=Wenn du den Branch-Schutz deaktivierst, können alle Nutzer mit Schreibrechten auf den Branch pushen. Fortfahren? settings.block_rejected_reviews=Merge bei abgelehnten Reviews blockieren settings.block_rejected_reviews_desc=Mergen ist nicht möglich, wenn Änderungen durch offizielle Reviewer angefragt werden, auch wenn es genügend Zustimmungen gibt. @@ -2482,11 +2335,8 @@ settings.block_on_official_review_requests=Mergen bei offiziellen Review-Anfrage settings.block_on_official_review_requests_desc=Mergen ist nicht möglich wenn offizielle Review-Anfrangen vorliegen, selbst wenn es genügend Zustimmungen gibt. settings.block_outdated_branch=Merge blockieren, wenn der Pull-Request veraltet ist settings.block_outdated_branch_desc=Mergen ist nicht möglich, wenn der Head-Branch hinter dem Basis-Branch ist. -settings.block_admin_merge_override=Administratoren müssen die Schutzregeln für Branches befolgen -settings.block_admin_merge_override_desc=Administratoren müssen die Schutzregeln für Branches befolgen und können sie nicht umgehen. settings.default_branch_desc=Wähle einen Standardbranch für Pull-Requests und Code-Commits: settings.merge_style_desc=Merge-Styles -settings.default_merge_style_desc=Standard-Mergeverhalten für Pull-Requests settings.choose_branch=Branch wählen… settings.no_protected_branch=Es gibt keine geschützten Branches. settings.edit_protected_branch=Bearbeiten @@ -2502,25 +2352,12 @@ settings.tags.protection.allowed.teams=Erlaubte Teams settings.tags.protection.allowed.noone=Niemand settings.tags.protection.create=Tag schützen settings.tags.protection.none=Es gibt keine geschützten Tags. -settings.tags.protection.pattern.description=Du kannst einen einzigen Namen oder ein globales Schema oder einen regulären Ausdruck verwenden, um mehrere Tags zu schützen. Mehr dazu im Guide für geschützte Tags (Englisch). settings.bot_token=Bot-Token settings.chat_id=Chat-ID settings.thread_id=Thread-ID settings.matrix.homeserver_url=Homeserver-URL settings.matrix.room_id=Raum-ID settings.matrix.message_type=Nachrichtentyp -settings.visibility.private.button=Auf privat setzen -settings.visibility.private.text=Das Ändern der Sichtbarkeit auf privat wird das Repository nicht nur für erlaubte Mitglieder sichtbar machen, sondern kann auch die Beziehung zwischen ihm und Forks, Beobachtern und Sternen entfernen. -settings.visibility.private.bullet_title=Das Ändern der Sichtbarkeit auf privat wird: -settings.visibility.private.bullet_one=Das Repository nur für zugelassene Mitglieder sichtbar machen. -settings.visibility.private.bullet_two=Kann die Beziehung zwischen ihm und Forks, Beobachternund Sternen entfernen. -settings.visibility.public.button=Auf öffentlich setzen -settings.visibility.public.text=Das Ändern der Sichtbarkeit auf öffentlich macht das Repository für jeden sichtbar. -settings.visibility.public.bullet_title=Das Ändern der Sichtbarkeit auf öffentlich wird: -settings.visibility.public.bullet_one=Das Repository für jeden sichtbar machen. -settings.visibility.success=Die Sichtbarkeit des Repositorys wurde geändert. -settings.visibility.error=Beim Versuch, die Sichtbarkeit des Repositorys zu ändern, ist ein Fehler aufgetreten. -settings.visibility.fork_error=Die Sichtbarkeit von geforkten Repositories ist nicht veränderbar. settings.archive.button=Repo archivieren settings.archive.header=Dieses Repo archivieren settings.archive.text=Durch das Archivieren wird ein Repo vollständig schreibgeschützt. Es wird vom Dashboard versteckt. Niemand (nicht einmal du!) wird in der Lage sein, neue Commits zu erstellen oder Issues oder Pull-Requests zu öffnen. @@ -2632,7 +2469,6 @@ release.new_release=Neues Release release.draft=Entwurf release.prerelease=Pre-Release release.stable=Stabil -release.latest=Aktuell release.compare=Vergleichen release.edit=bearbeiten release.ahead.commits=%d Commits @@ -2717,7 +2553,6 @@ tag.create_success=Tag "%s" wurde erstellt. topic.manage_topics=Themen verwalten topic.done=Fertig -topic.count_prompt=Du kannst nicht mehr als 25 Themen auswählen topic.format_prompt=Themen müssen entweder mit einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) und Punkte ('.') enthalten und bis zu 35 Zeichen lang sein. Nur Kleinbuchstaben sind zulässig. find_file.go_to_file=Datei suchen @@ -2815,7 +2650,6 @@ teams.leave.detail=%s verlassen? teams.can_create_org_repo=Repositories erstellen teams.can_create_org_repo_helper=Mitglieder können neue Repositories in der Organisation erstellen. Der Ersteller erhält Administrator-Zugriff auf das neue Repository. teams.none_access=Kein Zugriff -teams.none_access_helper=Mitglieder können keine anderen Aktionen für diese Einheit anzeigen oder durchführen. Dies hat keine Wirkung auf öffentliche Repositories. teams.general_access=Allgemeiner Zugriff teams.general_access_helper=Mitgliederberechtigungen werden durch folgende Berechtigungstabelle festgelegt. teams.read_access=Lesen @@ -2862,7 +2696,6 @@ teams.invite.by=Von %s eingeladen teams.invite.description=Bitte klicke auf die folgende Schaltfläche, um dem Team beizutreten. [admin] -maintenance=Wartung dashboard=Dashboard self_check=Selbstprüfung identity_access=Identität & Zugriff @@ -2884,9 +2717,7 @@ last_page=Letzte total=Gesamt: %d settings=Administratoreinstellungen -dashboard.new_version_hint=Gitea %s ist jetzt verfügbar, deine derzeitige Version ist %s. Weitere Details findest du im Blog. dashboard.statistic=Übersicht -dashboard.maintenance_operations=Wartungsoperationen dashboard.system_status=System-Status dashboard.operation_name=Name der Operation dashboard.operation_switch=Wechseln @@ -2927,7 +2758,6 @@ dashboard.reinit_missing_repos=Alle Git-Repositories neu einlesen, für die Eint dashboard.sync_external_users=Externe Benutzerdaten synchronisieren dashboard.cleanup_hook_task_table=Hook-Task-Tabelle bereinigen dashboard.cleanup_packages=Veraltete Pakete löschen -dashboard.cleanup_actions=Abgelaufene Ressourcen von Actions bereinigen dashboard.server_uptime=Server-Uptime dashboard.current_goroutine=Aktuelle Goroutinen dashboard.current_memory_usage=Aktuelle Speichernutzung @@ -2957,19 +2787,12 @@ dashboard.total_gc_time=Gesamte GC-Pause dashboard.total_gc_pause=Gesamte GC-Pause dashboard.last_gc_pause=Letzte GC-Pause dashboard.gc_times=Anzahl GC -dashboard.delete_old_actions=Alle alten Aktionen aus der Datenbank löschen -dashboard.delete_old_actions.started=Löschen aller alten Aktionen in der Datenbank gestartet. dashboard.update_checker=Update-Checker dashboard.delete_old_system_notices=Alle alten Systemmeldungen aus der Datenbank löschen dashboard.gc_lfs=Garbage-Collection für LFS Meta-Objekte ausführen -dashboard.stop_zombie_tasks=Zombie-Aufgaben stoppen -dashboard.stop_endless_tasks=Endlose Aktionen stoppen -dashboard.cancel_abandoned_jobs=Aufgegebene Jobs abbrechen -dashboard.start_schedule_tasks=Terminierte Aufgaben starten dashboard.sync_branch.started=Synchronisierung der Branches gestartet dashboard.sync_tag.started=Tag-Synchronisierung gestartet dashboard.rebuild_issue_indexer=Issue-Indexer neu bauen -dashboard.sync_repo_licenses=Repo-Lizenzen synchronisieren users.user_manage_panel=Benutzerkontenverwaltung users.new_account=Benutzerkonto erstellen @@ -3041,10 +2864,6 @@ emails.not_updated=Fehler beim Aktualisieren der angeforderten E-Mail-Adresse: % emails.duplicate_active=Diese E-Mail-Adresse wird bereits von einem Nutzer verwendet. emails.change_email_header=E-Mail-Eigenschaften aktualisieren emails.change_email_text=Bist du dir sicher, dass du diese E-Mail-Adresse aktualisieren möchtest? -emails.delete=E-Mail löschen -emails.delete_desc=Willst du diese E-Mail-Adresse wirklich löschen? -emails.deletion_success=Die E-Mail-Adresse wurde gelöscht. -emails.delete_primary_email_error=Du kannst die primäre E-Mail-Adresse nicht löschen. orgs.org_manage_panel=Organisationsverwaltung orgs.name=Name @@ -3077,12 +2896,10 @@ packages.size=Größe packages.published=Veröffentlicht defaulthooks=Standard-Webhooks -defaulthooks.desc=Webhooks senden automatisch eine HTTP-POST-Anfrage an einen Server, wenn bestimmte Gitea-Events ausgelöst werden. Hier definierte Webhooks sind die Standardwerte, die in alle neuen Repositories kopiert werden. Mehr Infos findest du in der Webhooks-Anleitung (auf Englisch). defaulthooks.add_webhook=Standard-Webhook hinzufügen defaulthooks.update_webhook=Standard-Webhook aktualisieren systemhooks=System-Webhooks -systemhooks.desc=Webhooks senden automatisch HTTP-POST-Anfragen an einen Server, wenn bestimmte Gitea-Events ausgelöst werden. Hier definierte Webhooks werden auf alle Repositories des Systems übertragen, beachte daher mögliche Performance-Einbrüche. Mehr Infos findest du in der Webhooks-Anleitung (auf Englisch). systemhooks.add_webhook=System-Webhook hinzufügen systemhooks.update_webhook=System-Webhook aktualisieren @@ -3177,18 +2994,7 @@ auths.tips=Tipps auths.tips.oauth2.general=OAuth2-Authentifizierung auths.tips.oauth2.general.tip=Beim Registrieren einer OAuth2-Anwendung sollte die Callback-URL folgendermaßen lauten: auths.tip.oauth2_provider=OAuth2-Anbieter -auths.tip.bitbucket=Registriere einen neuen OAuth-Consumer unter %s und füge die Berechtigung 'Account' - 'Read' hinzu auths.tip.nextcloud=Registriere über das "Settings -> Security -> OAuth 2.0 client"-Menü einen neuen "OAuth consumer" auf der Nextcloud-Instanz -auths.tip.dropbox=Erstelle eine neue App auf %s -auths.tip.facebook=Erstelle eine neue Anwendung auf %s und füge das Produkt "Facebook Login“ hinzu -auths.tip.github=Erstelle unter %s eine neue OAuth-Anwendung -auths.tip.gitlab_new=Erstelle eine neue Anwendung unter %s -auths.tip.google_plus=Du erhältst die OAuth2-Client-Zugangsdaten in der Google-API-Konsole unter %s -auths.tip.openid_connect=Benutze die OpenID-Connect-Discovery-URL "https://{server}/.well-known/openid-configuration", um die Endpunkte zu spezifizieren -auths.tip.twitter=Gehe zu %s, erstelle eine Anwendung und stelle sicher, dass die Option „Allow this application to be used to Sign in with Twitter“ aktiviert ist -auths.tip.discord=Erstelle unter %s eine neue Anwendung -auths.tip.gitea=Registriere eine neue OAuth2-Anwendung. Eine Anleitung findest du unter %s -auths.tip.yandex=`Erstelle eine neue Anwendung auf %s. Wähle folgende Berechtigungen aus dem "Yandex.Passport API" Bereich: "Zugriff auf E-Mail-Adresse", "Zugriff auf Benutzeravatar" und "Zugriff auf Benutzername, Vor- und Nachname, Geschlecht"` auths.tip.mastodon=Gebe eine benutzerdefinierte URL für die Mastodon-Instanz ein, mit der du dich authentifizieren möchtest (oder benutze die standardmäßige) auths.edit=Authentifikationsquelle bearbeiten auths.activated=Diese Authentifikationsquelle ist aktiviert @@ -3304,10 +3110,6 @@ config.cache_adapter=Cache-Adapter config.cache_interval=Cache-Intervall config.cache_conn=Cache-Anbindung config.cache_item_ttl=Cache Item-TTL -config.cache_test=Cache testen -config.cache_test_failed=Fehler beim Prüfen des Caches: %v. -config.cache_test_slow=Cache-Test erfolgreich, aber die Antwortzeit ist langsam: %s. -config.cache_test_succeeded=Cache-Test erfolgreich, Antwort in %s erhalten. config.session_config=Session-Konfiguration config.session_provider=Session-Provider @@ -3354,7 +3156,6 @@ monitor.next=Nächste Ausführung monitor.previous=Letzte Ausführung monitor.execute_times=Ausführungen monitor.process=Laufende Prozesse -monitor.stacktrace=Stacktraces monitor.processes_count=%d Prozesse monitor.download_diagnosis_report=Diagnosebericht herunterladen monitor.desc=Beschreibung @@ -3362,8 +3163,6 @@ monitor.start=Startzeit monitor.execute_time=Ausführungszeit monitor.last_execution_result=Ergebnis monitor.process.cancel=Prozess abbrechen -monitor.process.cancel_desc=Abbrechen eines Prozesses kann Datenverlust verursachen -monitor.process.cancel_notices=Abbrechen: %s? monitor.process.children=Subprozesse monitor.queues=Warteschlangen @@ -3402,13 +3201,11 @@ notices.op=Aktion notices.delete_success=Diese Systemmeldung wurde gelöscht. self_check.no_problem_found=Bisher wurde kein Problem festgestellt. -self_check.startup_warnings=Warnungen beim Start: self_check.database_collation_mismatch=Erwarte Datenbank-Kollation: %s self_check.database_collation_case_insensitive=Die Datenbank verwendet die Kollation %s, was eine unsensible Kollation ist. Obwohl Gitea damit arbeiten könnte, gibt es vielleicht einige seltene Fälle, die nicht wie erwartet funktionieren. self_check.database_inconsistent_collation_columns=Die Datenbank verwendet die Kollation %s, aber diese Spalten verwenden unzutreffende Kollationen. Dies könnte zu unerwarteten Problemen führen. self_check.database_fix_mysql=Für MySQL/MariaDB-Benutzer kann man den Befehl "gitea doctor convert" oder manuell auch "ALTER ... COLLATE ..."-SQLs verwenden, um die Sortierprobleme zu beheben. self_check.database_fix_mssql=Für MSSQL-Benutzer kann das Problem im Moment nur durch "ALTER ... COLLATE ..." SQLs manuell behoben werden. -self_check.location_origin_mismatch=Aktuelle URL (%[1]s) stimmt nicht mit der URL überein, die Gitea (%[2]s) sieht. Wenn du einen Reverse-Proxy verwendest, stelle bitte sicher, dass die Header "Host" und "X-Forwarded-Proto" korrekt gesetzt sind. [action] create_repo=hat das Repository %s erstellt @@ -3436,7 +3233,6 @@ mirror_sync_create=neue Referenz %[3]s bei % mirror_sync_delete=hat die Referenz des Mirrors %[2]s in %[3]s synchronisiert und gelöscht approve_pull_request=`hat %[3]s#%[2]s approved` reject_pull_request=`schlug Änderungen für %[3]s#%[2]s vor` -publish_release=`veröffentlichte Release "%[4]s" in %[3]s` review_dismissed=`verwarf das Review von %[4]s in %[3]s#%[2]s` review_dismissed_reason=Grund: create_branch=legte den Branch %[3]s in %[4]s an @@ -3465,8 +3261,6 @@ raw_minutes=Minuten [dropzone] default_message=Zum Hochladen hier klicken oder Datei ablegen. -invalid_input_type=Dateien dieses Dateityps können nicht hochgeladen werden. -file_too_big=Dateigröße ({{filesize}} MB) überschreitet die Maximalgröße ({{maxFilesize}} MB). remove_file=Datei entfernen [notification] @@ -3503,7 +3297,6 @@ error.unit_not_allowed=Du hast keine Berechtigung, um auf diesen Repository-Bere title=Pakete desc=Repository-Pakete verwalten. empty=Noch keine Pakete vorhanden. -no_metadata=Keine Metadaten. empty.documentation=Weitere Informationen zur Paket-Registry findest Du in der Dokumentation. empty.repo=Hast du ein Paket hochgeladen, das hier nicht angezeigt wird? Gehe zu den Paketeinstellungen und verlinke es mit diesem Repo. registry.documentation=Für weitere Informationen zur %s-Registry, schaue in der Dokumentation nach. @@ -3538,8 +3331,6 @@ alpine.repository=Repository-Informationen alpine.repository.branches=Branches alpine.repository.repositories=Repositories alpine.repository.architectures=Architekturen -arch.registry=Server mit gebrauchtem Repository und Architektur zu /etc/pacman.conf hinzufügen: -arch.install=Paket mit pacman synchronisieren: arch.repository=Repository-Informationen arch.repository.repositories=Repositories arch.repository.architectures=Architekturen @@ -3590,7 +3381,6 @@ npm.install=Um das Paket mit npm zu installieren, führe den folgenden Befehl au npm.install2=oder füge es zur package.json-Datei hinzu: npm.dependencies=Abhängigkeiten npm.dependencies.development=Entwicklungsabhängigkeiten -npm.dependencies.bundle=Gebündelte Abhängigkeiten npm.dependencies.peer=Peer Abhängigkeiten npm.dependencies.optional=Optionale Abhängigkeiten npm.details.tag=Tag @@ -3722,7 +3512,6 @@ runners.status.active=Aktiv runners.status.offline=Offline runners.version=Version runners.reset_registration_token=Registrierungs-Token zurücksetzen -runners.reset_registration_token_confirm=Möchtest du den aktuellen Token invalidieren und einen neuen generieren? runners.reset_registration_token_success=Runner-Registrierungstoken erfolgreich zurückgesetzt runs.all_workflows=Alle Workflows @@ -3732,7 +3521,6 @@ runs.pushed_by=gepusht von runs.invalid_workflow_helper=Die Workflow-Konfigurationsdatei ist ungültig. Bitte überprüfe Deine Konfigurationsdatei: %s runs.no_matching_online_runner_helper=Kein passender Runner online mit Label: %s runs.no_job_without_needs=Der Workflow muss mindestens einen Job ohne Abhängigkeiten enthalten. -runs.no_job=Der Workflow muss mindestens einen Job enthalten runs.actor=Initiator runs.status=Status runs.actors_no_select=Alle Initiatoren @@ -3743,18 +3531,12 @@ runs.no_workflows.quick_start=Du weißt nicht, wie du mit Gitea Actions loslegst runs.no_workflows.documentation=Weitere Informationen zu Gitea Actions findest du in der Dokumentation. runs.no_runs=Der Workflow hat noch keine Ausführungen. runs.empty_commit_message=(leere Commit-Nachricht) -runs.expire_log_message=Protokolle wurden geleert, weil sie zu alt waren. workflow.disable=Workflow deaktivieren workflow.disable_success=Workflow '%s' erfolgreich deaktiviert. workflow.enable=Workflow aktivieren workflow.enable_success=Workflow '%s' erfolgreich aktiviert. workflow.disabled=Workflow ist deaktiviert. -workflow.run=Workflow ausführen -workflow.not_found=Workflow '%s' wurde nicht gefunden. -workflow.run_success=Workflow '%s' erfolgreich ausgeführt. -workflow.from_ref=Nutze Workflow von -workflow.has_workflow_dispatch=Dieser Workflow hat einen workflow_dispatch Event-Trigger. need_approval_desc=Um Workflows für den Pull-Request eines Forks auszuführen, ist eine Genehmigung erforderlich. @@ -3774,11 +3556,7 @@ variables.creation.success=Die Variable „%s“ wurde hinzugefügt. variables.update.failed=Fehler beim Bearbeiten der Variable. variables.update.success=Die Variable wurde bearbeitet. -logs.always_auto_scroll=Autoscroll für Logs immer aktivieren -logs.always_expand_running=Laufende Logs immer erweitern - [projects] -deleted.display_name=Gelöschtes Projekt type-1.display_name=Individuelles Projekt type-2.display_name=Repository-Projekt type-3.display_name=Organisationsprojekt diff --git a/options/locale/locale_el-GR.ini b/options/locale/locale_el-GR.ini index fa9c41d5de049..193441828ae1e 100644 --- a/options/locale/locale_el-GR.ini +++ b/options/locale/locale_el-GR.ini @@ -580,7 +580,6 @@ joined_on=ΕγγÏάφηκε την %s repositories=ΑποθετήÏια activity=Δημόσια ΔÏαστηÏιότητα followers=Ακόλουθοι -show_more=Εμφάνιση ΠεÏισσότεÏων starred=Αγαπημένα ΑποθετήÏια watched=ΑκολουθοÏμενα ΑποθετήÏια code=Κώδικας @@ -3440,7 +3439,6 @@ variables.creation.success=Η μεταβλητή "%s" έχει Ï€Ïοστεθε variables.update.failed=Αποτυχία επεξεÏγασίας μεταβλητής. variables.update.success=Η μεταβλητή έχει Ï„Ïοποποιηθεί. - [projects] type-1.display_name=Ατομικό ΈÏγο type-2.display_name=ΈÏγο ΑποθετηÏίου diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 151afb8e9e36f..e28840277a3d6 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -17,7 +17,7 @@ template = Template language = Language notifications = Notifications active_stopwatch = Active Time Tracker -tracked_time_summary = Summary of tracked time based on filters of change request list +tracked_time_summary = Summary of tracked time based on filters of issue list create_new = Create… user_profile_and_more = Profile and Settings… signed_in_as = Signed in as @@ -51,15 +51,13 @@ webauthn_error_empty = You must set a name for this key. webauthn_error_timeout = Timeout reached before your key could be read. Please reload this page and retry. webauthn_reload = Reload -repository = Binder -binder = Binder +repository = Repository organization = Organization mirror = Mirror -new_repo = New Binder -new_binder = New Binder +new_repo = New Repository new_migrate = New Migration new_mirror = New Mirror -new_fork = New Binder Fork +new_fork = New Repository Fork new_org = New Organization new_project = New Project new_project_column = New Column @@ -68,7 +66,7 @@ admin_panel = Site Administration account_settings = Account Settings settings = Settings your_profile = Profile -your_starred = Favorites +your_starred = Starred your_settings = Settings all = All @@ -78,8 +76,8 @@ collaborative = Collaborative forks = Forks activities = Activities -pull_requests = Revisions -issues = Change Requests +pull_requests = Pull Requests +issues = Issues milestones = Milestones ok = OK @@ -135,7 +133,7 @@ archived = Archived concept_system_global = Global concept_user_individual = Individual -concept_code_repository = Binder +concept_code_repository = Repository concept_user_organization = Organization show_timestamps = Show timestamps @@ -172,14 +170,13 @@ fuzzy = Fuzzy fuzzy_tooltip = Include results that also match the search term closely exact = Exact exact_tooltip = Include only results that match the exact search term -repo_kind = Search binders... -binder_kind = Search binders... +repo_kind = Search repos... user_kind = Search users... org_kind = Search orgs... team_kind = Search teams... -code_kind = Search documents... -code_search_unavailable = Document search is currently not available. Please contact the site administrator. -code_search_by_git_grep = Current document search results are provided by "git grep". There might be better results if site administrator enables Binder Indexer. +code_kind = Search code... +code_search_unavailable = Code search is currently not available. Please contact the site administrator. +code_search_by_git_grep = Current code search results are provided by "git grep". There might be better results if site administrator enables Repository Indexer. package_kind = Search packages... project_kind = Search projects... branch_kind = Search branches... @@ -188,7 +185,7 @@ tag_tooltip = Search for matching tags. Use '%' to match any sequence of numbers commit_kind = Search commits... runner_kind = Search runners... no_results = No matching results found. -issue_kind = Search change requests... +issue_kind = Search issues... pull_kind = Search pulls... keyword_search_unavailable = Searching by keyword is currently not available. Please contact the site administrator. @@ -209,7 +206,7 @@ buttons.heading.tooltip = Add heading buttons.bold.tooltip = Add bold text buttons.italic.tooltip = Add italic text buttons.quote.tooltip = Quote text -buttons.code.tooltip = Add document +buttons.code.tooltip = Add code buttons.link.tooltip = Add a link buttons.list.unordered.tooltip = Add a bullet list buttons.list.ordered.tooltip = Add a numbered list @@ -219,7 +216,7 @@ buttons.table.add.insert = Add buttons.table.rows = Rows buttons.table.cols = Columns buttons.mention.tooltip = Mention a user or team -buttons.ref.tooltip = Reference a change request or revision +buttons.ref.tooltip = Reference an issue or pull request buttons.switch_to_legacy.tooltip = Use the legacy editor instead buttons.enable_monospace_font = Enable monospace font buttons.disable_monospace_font = Disable monospace font @@ -230,12 +227,12 @@ string.desc = Z - A [error] occurred = An error occurred -report_message = If you believe that this is a Gitea bug, please search for change requests on GitHub or open a new change request if necessary. +report_message = If you believe that this is a Gitea bug, please search for issues on GitHub or open a new issue if necessary. not_found = The target couldn't be found. network_error = Network error [startpage] -app_desc = A painless, fully-managed document service +app_desc = A painless, self-hosted Git service install = Easy to install install_desc = Simply run the binary for your platform, ship it with Docker, or get it packaged. platform = Cross-platform @@ -264,7 +261,7 @@ sqlite_helper = File path for the SQLite3 database.
Enter an absolute path if reinstall_error = You are trying to install into an existing Gitea database reinstall_confirm_message = Re-installing with an existing Gitea database can cause multiple problems. In most cases, you should use your existing "app.ini" to run Gitea. If you know what you are doing, confirm the following: reinstall_confirm_check_1 = The data encrypted by the SECRET_KEY in app.ini may be lost: users may not be able to log in with 2FA/OTP & mirrors may not function correctly. By checking this box you confirm that the current app.ini file contains the correct the SECRET_KEY. -reinstall_confirm_check_2 = The binders and settings may need to be re-synchronized. By checking this box you confirm that you will resynchronize the hooks for the binders and authorized_keys file manually. You confirm that you will ensure that binder and mirror settings are correct. +reinstall_confirm_check_2 = The repositories and settings may need to be re-synchronized. By checking this box you confirm that you will resynchronize the hooks for the repositories and authorized_keys file manually. You confirm that you will ensure that repository and mirror settings are correct. reinstall_confirm_check_3 = You confirm that you are absolutely sure that this Gitea is running with the correct app.ini location and that you are sure that you have to re-install. You confirm that you acknowledge the above risks. err_empty_db_path = The SQLite3 database path cannot be empty. no_admin_and_disable_registration = You cannot disable user self-registration without creating an administrator account. @@ -277,12 +274,12 @@ err_admin_name_is_invalid = Administrator Username is invalid general_title = General Settings app_name = Site Title app_name_helper = You can enter your company name here. -repo_path = Binder Root Path -repo_path_helper = Remote Git binders will be saved to this directory. +repo_path = Repository Root Path +repo_path_helper = Remote Git repositories will be saved to this directory. lfs_path = Git LFS Root Path lfs_path_helper = Files tracked by Git LFS will be stored in this directory. Leave empty to disable. run_user = Run As Username -run_user_helper = The operating system username that Gitea runs as. Note that this user must have access to the binder root path. +run_user_helper = The operating system username that Gitea runs as. Note that this user must have access to the repository root path. domain = Server Domain domain_helper = Domain or host address for the server. ssh_port = SSH Server Port @@ -334,7 +331,7 @@ test_git_failed = Could not test 'git' command: %v sqlite3_not_available = This Gitea version does not support SQLite3. Please download the official binary version from %s (not the 'gobuild' version). invalid_db_setting = The database settings are invalid: %v invalid_db_table = The database table "%s" is invalid: %v -invalid_repo_path = The binder root path is invalid: %v +invalid_repo_path = The repository root path is invalid: %v invalid_app_data_path = The app data path is invalid: %v run_user_not_match = The 'run as' username is not the current username: %s -> %s internal_token_failed = Failed to generate internal token: %v @@ -347,7 +344,7 @@ default_keep_email_private_popup = Hide email addresses of new user accounts by default_allow_create_organization = Allow Creation of Organizations by Default default_allow_create_organization_popup = Allow new user accounts to create organizations by default. default_enable_timetracking = Enable Time Tracking by Default -default_enable_timetracking_popup = Enable time tracking for new binders by default. +default_enable_timetracking_popup = Enable time tracking for new repositories by default. no_reply_address = Hidden Email Domain no_reply_address_helper = Domain name for users with a hidden email address. For example, the username 'joe' will be logged in Git as 'joe@noreply.example.org' if the hidden email domain is set to 'noreply.example.org'. password_algorithm = Password Hash Algorithm @@ -364,18 +361,14 @@ nav_menu = Navigation Menu uname_holder = Username or Email Address password_holder = Password switch_dashboard_context = Switch Dashboard Context -my_repos = Binders -my_binders = Binders -show_more_repos = Show more binders… -show_more_repos = Show more binders… -collaborative_repos = Collaborative Binders -collaborative_binder = Collaborative Binders +my_repos = Repositories +show_more_repos = Show more repositories… +collaborative_repos = Collaborative Repositories my_orgs = My Organizations my_mirrors = My Mirrors view_home = View %s filter = Other Filters -filter_by_team_repositories = Filter by team binders -filter_by_team_binders = Filter by team binders +filter_by_team_repositories = Filter by team repositories feed_of = Feed of "%s" show_archived = Archived @@ -388,21 +381,17 @@ show_both_private_public = Showing both public and private show_only_private = Showing only private show_only_public = Showing only public -issues.in_your_repos = In your binders -issues.in_your_binders = In your binders +issues.in_your_repos = In your repositories [explore] -repos = Binders -binders = Binders +repos = Repositories users = Users organizations = Organizations go_to = Go to code = Code code_last_indexed_at = Last indexed %s -relevant_repositories_tooltip = Binders that are forks or that have no topic, no icon, and no description are hidden. -relevant_binders_tooltip = Binders that are forks or that have no topic, no icon, and no description are hidden. -relevant_repositories = Only relevant binders are being shown, show unfiltered results. -relevant_binders = Only relevant binders are being shown, show unfiltered results. +relevant_repositories_tooltip = Repositories that are forks or that have no topic, no icon, and no description are hidden. +relevant_repositories = Only relevant repositories are being shown, show unfiltered results. [auth] create_new_account = Register Account @@ -509,21 +498,21 @@ reset_password.text = Please click the following link to recover your account wi register_success = Registration successful -issue_assigned.pull = @%[1]s assigned you to %[2]s %[3]s in %[4]s %[5]s. -issue_assigned.issue = @%[1]s assigned you to %[2]s %[3]s in %[4]s %[5]s. +issue_assigned.pull = @%[1]s assigned you to pull request %[2]s in repository %[3]s. +issue_assigned.issue = @%[1]s assigned you to issue %[2]s in repository %[3]s. issue.x_mentioned_you = @%s mentioned you: -issue.action.force_push = %[1]s updated the %[2]s from %[3]s to %[4]s. +issue.action.force_push = %[1]s force-pushed the %[2]s from %[3]s to %[4]s. issue.action.push_1 = @%[1]s pushed %[3]d commit to %[2]s issue.action.push_n = @%[1]s pushed %[3]d commits to %[2]s issue.action.close = @%[1]s closed #%[2]d. issue.action.reopen = @%[1]s reopened #%[2]d. issue.action.merge = @%[1]s merged #%[2]d into %[3]s. -issue.action.approve = @%[1]s approved this revision. -issue.action.reject = @%[1]s requested changes on this revision. -issue.action.review = @%[1]s commented on this revision. -issue.action.review_dismissed = @%[1]s dismissed last review from %[2]s for this revision. -issue.action.ready_for_review = @%[1]s marked this revision ready for review. +issue.action.approve = @%[1]s approved this pull request. +issue.action.reject = @%[1]s requested changes on this pull request. +issue.action.review = @%[1]s commented on this pull request. +issue.action.review_dismissed = @%[1]s dismissed last review from %[2]s for this pull request. +issue.action.ready_for_review = @%[1]s marked this pull request ready for review. issue.action.new = @%[1]s created #%[2]d. issue.in_tree_path = In %s: @@ -541,7 +530,7 @@ repo.transfer.to_you = you repo.transfer.body = To accept or reject it visit %s or just ignore it. repo.collaborator.added.subject = %s added you to %s -repo.collaborator.added.text = You have been added as a collaborator of binder: +repo.collaborator.added.text = You have been added as a collaborator of repository: team_invite.subject = %[1]s has invited you to join the %[2]s organization team_invite.text_1 = %[1]s has invited you to join team %[2]s in organization %[3]s. @@ -557,7 +546,7 @@ modify = Update [form] UserName = Username -RepoName = Binder name +RepoName = Repository name Email = Email address Password = Password Retype = Confirm Password @@ -602,17 +591,17 @@ username_change_not_local_user = Non-local users are not allowed to change their change_username_disabled = Changing username is disabled. change_full_name_disabled = Changing full name is disabled. username_has_not_been_changed = Username has not been changed -repo_name_been_taken = The binder name is already used. -repository_force_private = Force Private is enabled: private binders cannot be made public. -repository_files_already_exist = Files already exist for this binder. Contact the system administrator. -repository_files_already_exist.adopt = Files already exist for this binder and can only be Adopted. -repository_files_already_exist.delete = Files already exist for this binder. You must delete them. -repository_files_already_exist.adopt_or_delete = Files already exist for this binder. Either adopt them or delete them. +repo_name_been_taken = The repository name is already used. +repository_force_private = Force Private is enabled: private repositories cannot be made public. +repository_files_already_exist = Files already exist for this repository. Contact the system administrator. +repository_files_already_exist.adopt = Files already exist for this repository and can only be Adopted. +repository_files_already_exist.delete = Files already exist for this repository. You must delete them. +repository_files_already_exist.adopt_or_delete = Files already exist for this repository. Either adopt them or delete them. visit_rate_limit = Remote visit addressed rate limitation. 2fa_auth_required = Remote visit required two factors authentication. org_name_been_taken = The organization name is already taken. team_name_been_taken = The team name is already taken. -team_no_units_error = Allow access to at least one binder section. +team_no_units_error = Allow access to at least one repository section. email_been_used = The email address is already used. email_invalid = The email address is invalid. email_domain_is_not_allowed = The domain of user email %s conflicts with EMAIL_DOMAIN_ALLOWLIST or EMAIL_DOMAIN_BLOCKLIST. Please ensure your operation is expected. @@ -623,7 +612,7 @@ password_lowercase_one = At least one lowercase character password_uppercase_one = At least one uppercase character password_digit_one = At least one digit password_special_one = At least one special character (punctuation, brackets, quotes, etc.) -enterred_invalid_repo_name = The binder name you entered is incorrect. +enterred_invalid_repo_name = The repository name you entered is incorrect. enterred_invalid_org_name = The organization name you entered is incorrect. enterred_invalid_owner_name = The new owner name is not valid. enterred_invalid_password = The password you entered is incorrect. @@ -643,10 +632,10 @@ must_use_public_key = The key you provided is a private key. Please do not uploa unable_verify_ssh_key = "Cannot verify the SSH key, double-check it for mistakes." auth_failed = Authentication failed: %v -still_own_repo = "Your account owns one or more binders, delete or transfer them first." +still_own_repo = "Your account owns one or more repositories, delete or transfer them first." still_has_org = "Your account is a member of one or more organizations, leave them first." still_own_packages = "Your account owns one or more packages, delete them first." -org_still_own_repo = "This organization still owns one or more binders, delete or transfer them first." +org_still_own_repo = "This organization still owns one or more repositories, delete or transfer them first." org_still_own_packages = "This organization still owns one or more packages, delete them first." target_branch_not_exist = Target branch does not exist. @@ -657,13 +646,12 @@ admin_cannot_delete_self = You cannot delete yourself when you are an admin. Ple [user] change_avatar = Change your avatar… joined_on = Joined on %s -repositories = Binders +repositories = Repositories activity = Public Activity followers = Followers -show_more = Show More -starred = Favorite Binders -watched = Watched Binders -code = Documents +starred = Starred Repositories +watched = Watched Repositories +code = Code projects = Projects overview = Overview following = Following @@ -688,14 +676,14 @@ block.unblock = Unblock block.unblock.failure = Failed to unblock user: %s block.blocked = You have blocked this user. block.title = Block a user -block.info = Blocking a user prevents them from interacting with binders, such as opening or commenting on revisions or change requests. Learn more about blocking a user. -block.info_1 = Blocking a user prevents the following actions on your account and your binders: +block.info = Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user. +block.info_1 = Blocking a user prevents the following actions on your account and your repositories: block.info_2 = following your account block.info_3 = send you notifications by @mentioning your username -block.info_4 = inviting you as a collaborator to their binders -block.info_5 = starring, forking or watching on binders -block.info_6 = opening and commenting on change requests or revisions -block.info_7 = reacting on your comments in change requests or revisions +block.info_4 = inviting you as a collaborator to their repositories +block.info_5 = starring, forking or watching on repositories +block.info_6 = opening and commenting on issues or pull requests +block.info_7 = reacting on your comments in issues or pull requests block.user_to_block = User to block block.note = Note block.note.title = Optional note: @@ -715,7 +703,7 @@ ssh_gpg_keys = SSH / GPG Keys social = Social Accounts applications = Applications orgs = Manage Organizations -repos = Binders +repos = Repositories delete = Delete Account twofa = Two-Factor Authentication (TOTP) account_link = Linked Accounts @@ -746,9 +734,9 @@ cancel = Cancel language = Language ui = Theme hidden_comment_types = Hidden comment types -hidden_comment_types_description = Comment types checked here will not be shown inside change request pages. Checking "Label" for example removes all "{user} added/removed {label}" comments. -hidden_comment_types.ref_tooltip = Comments where this change request was referenced from another change request/commit/… -hidden_comment_types.issue_ref_tooltip = Comments where the user changes the branch/tag associated with the change request +hidden_comment_types_description = Comment types checked here will not be shown inside issue pages. Checking "Label" for example removes all "{user} added/removed {label}" comments. +hidden_comment_types.ref_tooltip = Comments where this issue was referenced from another issue/commit/… +hidden_comment_types.issue_ref_tooltip = Comments where the user changes the branch/tag associated with the issue comment_type_group_reference = Reference comment_type_group_label = Label comment_type_group_milestone = Milestone @@ -762,7 +750,7 @@ comment_type_group_lock = Lock Status comment_type_group_review_request = Review request comment_type_group_pull_request_push = Added commits comment_type_group_project = Project -comment_type_group_issue_ref = Change request reference +comment_type_group_issue_ref = Issue reference saved_successfully = Your settings were saved successfully. privacy = Privacy keep_activity_private = Hide Activity from profile page @@ -821,15 +809,15 @@ add_email_success = The new email address has been added. email_preference_set_success = Email preference has been set successfully. add_openid_success = The new OpenID address has been added. keep_email_private = Hide Email Address -keep_email_private_popup = This will hide your email address from your profile, as well as when you make a revision or edit a document using the web interface. Pushed commits will not be modified. Use %s in commits to associate them with your account. +keep_email_private_popup = This will hide your email address from your profile, as well as when you make a pull request or edit a file using the web interface. Pushed commits will not be modified. Use %s in commits to associate them with your account. openid_desc = OpenID lets you delegate authentication to an external provider. manage_ssh_keys = Manage SSH Keys manage_ssh_principals = Manage SSH Certificate Principals manage_gpg_keys = Manage GPG Keys add_key = Add Key -ssh_desc = These public SSH keys are associated with your account. The corresponding private keys allow full access to your binders. -principal_desc = These SSH certificate principals are associated with your account and allow full access to your binders. +ssh_desc = These public SSH keys are associated with your account. The corresponding private keys allow full access to your repositories. +principal_desc = These SSH certificate principals are associated with your account and allow full access to your repositories. gpg_desc = These public GPG keys are associated with your account. Keep your private keys safe as they allow commits to be verified. ssh_helper = Need help? Have a look at GitHub's guide to create your own SSH keys or solve common problems you may encounter using SSH. gpg_helper = Need help? Have a look at GitHub's guide about GPG. @@ -916,7 +904,7 @@ access_token_deletion_cancel_action = Cancel access_token_deletion_confirm_action = Delete access_token_deletion_desc = Deleting a token will revoke access to your account for applications using it. This cannot be undone. Continue? delete_token_success = The token has been deleted. Applications using it no longer have access to your account. -repo_and_org_access = Binder and Organization Access +repo_and_org_access = Repository and Organization Access permissions_public_only = Public only permissions_access_all = All (public, private, and limited) select_permissions = Select permissions @@ -995,14 +983,14 @@ remove_account_link = Remove Linked Account remove_account_link_desc = Removing a linked account will revoke its access to your Gitea account. Continue? remove_account_link_success = The linked account has been removed. -hooks.desc = Add webhooks which will be triggered for all binders that you own. +hooks.desc = Add webhooks which will be triggered for all repositories that you own. orgs_none = You are not a member of any organizations. -repos_none = You do not own any binders. +repos_none = You do not own any repositories. delete_account = Delete Your Account delete_prompt = This operation will permanently delete your user account. It CANNOT be undone. -delete_with_all_comments = Your account is younger than %s. To avoid ghost comments, all change request/PR comments will be deleted with it. +delete_with_all_comments = Your account is younger than %s. To avoid ghost comments, all issue/PR comments will be deleted with it. confirm_delete_account = Confirm Deletion delete_account_title = Delete User Account delete_account_desc = Are you sure you want to permanently delete this user account? @@ -1022,39 +1010,39 @@ visibility.private = Private visibility.private_tooltip = Visible only to members of organizations you have joined [repo] -new_repo_helper = A binder contains all project files, including revision history. Already hosting one elsewhere? Migrate binder. +new_repo_helper = A repository contains all project files, including revision history. Already hosting one elsewhere? Migrate repository. owner = Owner -owner_helper = Some organizations may not show up in the dropdown due to a maximum binder count limit. -repo_name = Binder Name -repo_name_helper = Good binder names use short, memorable and unique keywords. -repo_size = Binder Size +owner_helper = Some organizations may not show up in the dropdown due to a maximum repository count limit. +repo_name = Repository Name +repo_name_helper = Good repository names use short, memorable and unique keywords. +repo_size = Repository Size template = Template template_select = Select a template. -template_helper = Make binder a template -template_description = Template binders let users generate new binders with the same directory structure, files, and optional settings. +template_helper = Make repository a template +template_description = Template repositories let users generate new repositories with the same directory structure, files, and optional settings. visibility = Visibility visibility_description = Only the owner or the organization members if they have rights, will be able to see it. -visibility_helper = Make binder private -visibility_helper_forced = Your site administrator forces new binders to be private. +visibility_helper = Make repository private +visibility_helper_forced = Your site administrator forces new repositories to be private. visibility_fork_helper = (Changing this will affect all forks.) clone_helper = Need help cloning? Visit Help. -fork_repo = Fork Binder +fork_repo = Fork Repository fork_from = Fork From already_forked = You've already forked %s fork_to_different_account = Fork to a different account -fork_visibility_helper = The visibility of a forked binder cannot be changed. +fork_visibility_helper = The visibility of a forked repository cannot be changed. fork_branch = Branch to be cloned to the fork all_branches = All branches view_all_branches = View all branches view_all_tags = View all tags -fork_no_valid_owners = This binder can not be forked because there are no valid owners. -fork.blocked_user = Cannot fork the binder because you are blocked by the binder owner. +fork_no_valid_owners = This repository can not be forked because there are no valid owners. +fork.blocked_user = Cannot fork the repository because you are blocked by the repository owner. use_template = Use this template open_with_editor = Open with %s download_zip = Download ZIP download_tar = Download TAR.GZ download_bundle = Download BUNDLE -generate_repo = Generate Binder +generate_repo = Generate Repository generate_from = Generate From repo_desc = Description repo_desc_helper = Enter short description (optional) @@ -1062,27 +1050,27 @@ repo_no_desc = No description provided repo_lang = Languages repo_gitignore_helper = Select .gitignore templates. repo_gitignore_helper_desc = Choose which files not to track from a list of templates for common languages. Typical artifacts generated by each language's build tools are included on .gitignore by default. -issue_labels = Change Request Labels -issue_labels_helper = Select a change request label set. +issue_labels = Issue Labels +issue_labels_helper = Select an issue label set. license = License license_helper = Select a license file. -license_helper_desc = A license governs what others can and can't do with your documents. Not sure which one is right for your project? See Choose a license. +license_helper_desc = A license governs what others can and can't do with your code. Not sure which one is right for your project? See Choose a license. multiple_licenses = Multiple Licenses object_format = Object Format -object_format_helper = Object format of the binder. Cannot be changed later. SHA1 is most compatible. +object_format_helper = Object format of the repository. Cannot be changed later. SHA1 is most compatible. readme = README readme_helper = Select a README file template. readme_helper_desc = This is the place where you can write a complete description for your project. -auto_init = Initialize Binder (Adds .gitignore, License and README) +auto_init = Initialize Repository (Adds .gitignore, License and README) trust_model_helper = Select trust model for signature verification. Possible options are: trust_model_helper_collaborator = Collaborator: Trust signatures by collaborators trust_model_helper_committer = Committer: Trust signatures that match committers trust_model_helper_collaborator_committer = Collaborator+Committer: Trust signatures by collaborators which match the committer trust_model_helper_default = Default: Use the default trust model for this installation -create_repo = Create Binder +create_repo = Create Repository default_branch = Default Branch default_branch_label = default -default_branch_helper = The default branch is the base branch for revisions and document commits. +default_branch_helper = The default branch is the base branch for pull requests and code commits. mirror_prune = Prune mirror_prune_desc = Remove obsolete remote-tracking references mirror_interval = Mirror Interval (valid time units are 'h', 'm', 's'). 0 to disable periodic sync. (Minimum interval: %s) @@ -1096,24 +1084,24 @@ mirror_address_protocol_invalid = The provided URL is invalid. Only http(s):// o mirror_lfs = Large File Storage (LFS) mirror_lfs_desc = Activate mirroring of LFS data. mirror_lfs_endpoint = LFS Endpoint -mirror_lfs_endpoint_desc = Sync will attempt to use the clone url to determine the LFS server. You can also specify a custom endpoint if the binder LFS data is stored somewhere else. +mirror_lfs_endpoint_desc = Sync will attempt to use the clone url to determine the LFS server. You can also specify a custom endpoint if the repository LFS data is stored somewhere else. mirror_last_synced = Last Synchronized mirror_password_placeholder = (Unchanged) mirror_password_blank_placeholder = (Unset) mirror_password_help = Change the username to erase a stored password. watchers = Watchers stargazers = Stargazers -stars_remove_warning = This will remove all stars from this binder. +stars_remove_warning = This will remove all stars from this repository. forks = Forks stars = Stars reactions_more = and %d more -unit_disabled = The site administrator has disabled this binder section. +unit_disabled = The site administrator has disabled this repository section. language_other = Other -adopt_search = Enter username to search for unadopted binders... (leave blank to find all) +adopt_search = Enter username to search for unadopted repositories... (leave blank to find all) adopt_preexisting_label = Adopt Files adopt_preexisting = Adopt pre-existing files -adopt_preexisting_content = Create binder from %s -adopt_preexisting_success = Adopted files and created binder from %s +adopt_preexisting_content = Create repository from %s +adopt_preexisting_success = Adopted files and created repository from %s delete_preexisting_label = Delete delete_preexisting = Delete pre-existing files delete_preexisting_content = Delete files in %s @@ -1144,47 +1132,47 @@ desc.sha256 = SHA256 template.items = Template Items template.git_content = Git Content (Default Branch) template.git_hooks = Git Hooks -template.git_hooks_tooltip = You are currently unable to modify or remove Git Hooks once added. Select this only if you trust the template binder. +template.git_hooks_tooltip = You are currently unable to modify or remove Git Hooks once added. Select this only if you trust the template repository. template.webhooks = Webhooks template.topics = Topics template.avatar = Avatar -template.issue_labels = Change Request Labels +template.issue_labels = Issue Labels template.one_item = Must select at least one template item -template.invalid = Must select a template binder +template.invalid = Must select a template repository -archive.title = This repo is archived. You can view files and clone it, but cannot push or open change requests or revisions. -archive.title_date = This binder has been archived on %s. You can view files and clone it, but cannot push or open change requests or revisions. -archive.issue.nocomment = This repo is archived. You cannot comment on change requests. -archive.pull.nocomment = This repo is archived. You cannot comment on revisions. +archive.title = This repo is archived. You can view files and clone it, but cannot push or open issues or pull requests. +archive.title_date = This repository has been archived on %s. You can view files and clone it, but cannot push or open issues or pull requests. +archive.issue.nocomment = This repo is archived. You cannot comment on issues. +archive.pull.nocomment = This repo is archived. You cannot comment on pull requests. -form.reach_limit_of_creation_1 = The owner has already reached the limit of %d binder. -form.reach_limit_of_creation_n = The owner has already reached the limit of %d binders. -form.name_reserved = The binder name "%s" is reserved. -form.name_pattern_not_allowed = The pattern "%s" is not allowed in a binder name. +form.reach_limit_of_creation_1 = The owner has already reached the limit of %d repository. +form.reach_limit_of_creation_n = The owner has already reached the limit of %d repositories. +form.name_reserved = The repository name "%s" is reserved. +form.name_pattern_not_allowed = The pattern "%s" is not allowed in a repository name. need_auth = Authorization migrate_options = Migration Options migrate_service = Migration Service -migrate_options_mirror_helper = This binder will be a mirror +migrate_options_mirror_helper = This repository will be a mirror migrate_options_lfs = Migrate LFS files migrate_options_lfs_endpoint.label = LFS Endpoint -migrate_options_lfs_endpoint.description = Migration will attempt to use your Git remote to determine the LFS server. You can also specify a custom endpoint if the binder LFS data is stored somewhere else. +migrate_options_lfs_endpoint.description = Migration will attempt to use your Git remote to determine the LFS server. You can also specify a custom endpoint if the repository LFS data is stored somewhere else. migrate_options_lfs_endpoint.description.local = A local server path is supported too. migrate_options_lfs_endpoint.placeholder = If left blank, the endpoint will be derived from the clone URL migrate_items = Migration Items migrate_items_wiki = Wiki migrate_items_milestones = Milestones migrate_items_labels = Labels -migrate_items_issues = Change Requests -migrate_items_pullrequests = Revisions +migrate_items_issues = Issues +migrate_items_pullrequests = Pull Requests migrate_items_merge_requests = Merge Requests migrate_items_releases = Releases -migrate_repo = Migrate Binder +migrate_repo = Migrate Repository migrate.clone_address = Migrate / Clone From URL -migrate.clone_address_desc = The HTTP(S) or Git 'clone' URL of an existing binder +migrate.clone_address_desc = The HTTP(S) or Git 'clone' URL of an existing repository migrate.github_token_desc = You can put one or more tokens with comma separated here to make migrating faster because of GitHub API rate limit. WARN: Abusing this feature may violate the service provider's policy and lead to account blocking. migrate.clone_local_path = or a local server path -migrate.permission_denied = You are not allowed to import local binders. +migrate.permission_denied = You are not allowed to import local repositories. migrate.permission_denied_blocked = You cannot import from disallowed hosts, please ask the admin to check ALLOWED_DOMAINS/ALLOW_LOCALNETWORKS/BLOCKED_DOMAINS settings. migrate.invalid_local_path = "The local path is invalid. It doesn't exist or is not a directory." migrate.invalid_lfs_endpoint = The LFS endpoint is not valid. @@ -1198,7 +1186,7 @@ migrate.migrating_failed = Migrating from %s failed. migrate.migrating_failed.error = Failed to migrate: %s migrate.migrating_failed_no_addr = Migration failed. migrate.github.description = Migrate data from github.com or other GitHub instances. -migrate.git.description = Migrate a binder only from any Git service. +migrate.git.description = Migrate a repository only from any Git service. migrate.gitlab.description = Migrate data from gitlab.com or other GitLab instances. migrate.gitea.description = Migrate data from gitea.com or other Gitea instances. migrate.gogs.description = Migrate data from notabug.org or other Gogs instances. @@ -1215,39 +1203,38 @@ migrate.migrating_topics = Migrating Topics migrate.migrating_milestones = Migrating Milestones migrate.migrating_labels = Migrating Labels migrate.migrating_releases = Migrating Releases -migrate.migrating_issues = Migrating Change Requests -migrate.migrating_pulls = Migrating Revisions +migrate.migrating_issues = Migrating Issues +migrate.migrating_pulls = Migrating Pull Requests migrate.cancel_migrating_title = Cancel Migration migrate.cancel_migrating_confirm = Do you want to cancel this migration? mirror_from = mirror of forked_from = forked from generated_from = generated from -fork_from_self = You cannot fork a binder you own. -fork_guest_user = Sign in to fork this binder. -watch_guest_user = Sign in to watch this binder. -star_guest_user = Sign in to favorite this binder. -view_guest_user = Sign in to view this binder. +fork_from_self = You cannot fork a repository you own. +fork_guest_user = Sign in to fork this repository. +watch_guest_user = Sign in to watch this repository. +star_guest_user = Sign in to star this repository. unwatch = Unwatch watch = Watch -unstar = Unfavorite -star = Favorite +unstar = Unstar +star = Star fork = Fork -view = View -action.blocked_user = Cannot perform action because you are blocked by the binder owner. -download_archive = Download Binder +action.blocked_user = Cannot perform action because you are blocked by the repository owner. +download_archive = Download Repository more_operations = More Operations quick_guide = Quick Guide -clone_this_repo = Clone this binder -cite_this_repo = Cite this binder -create_new_repo_command = Creating a new binder on the command line -push_exist_repo = Pushing an existing binder from the command line -empty_message = This binder does not contain any content. -broken_message = The Git data underlying this binder cannot be read. Contact the administrator of this instance or delete this binder. - -code = Documents -code.desc = Access documents, commits and branches. +clone_this_repo = Clone this repository +cite_this_repo = Cite this repository +create_new_repo_command = Creating a new repository on the command line +push_exist_repo = Pushing an existing repository from the command line +empty_message = This repository does not contain any content. +broken_message = The Git data underlying this repository cannot be read. Contact the administrator of this instance or delete this repository. +no_branch = This repository doesn’t have any branches. + +code = Code +code.desc = Access source code, files, commits and branches. branch = Branch tree = Tree clear_ref = `Clear current reference` @@ -1255,13 +1242,13 @@ filter_branch_and_tag = Filter branch or tag find_tag = Find tag branches = Branches tags = Tags -issues = Change Requests -pulls = Revisions +issues = Issues +pulls = Pull Requests projects = Projects packages = Packages actions = Actions labels = Labels -org_labels_desc = Organization level labels that can be used with all binders under this organization +org_labels_desc = Organization level labels that can be used with all repositories under this organization org_labels_desc_manage = manage milestones = Milestones @@ -1304,34 +1291,33 @@ vendored = Vendored generated = Generated commit_graph = Commit Graph commit_graph.select = Select branches -commit_graph.hide_pr_refs = Hide Revisions +commit_graph.hide_pr_refs = Hide Pull Requests commit_graph.monochrome = Mono commit_graph.color = Color commit.contained_in = This commit is contained in: commit.contained_in_default_branch = This commit is part of the default branch commit.load_referencing_branches_and_tags = Load branches and tags referencing this commit blame = Blame -download_file = Download document +download_file = Download file normal_view = Normal View line = line lines = lines from_comment = (comment) -editor.add_file = Add document -editor.new_file = New document -editor.upload_file = Upload document -editor.edit_file = Edit document -editor.new_directory = New folder +editor.add_file = Add File +editor.new_file = New File +editor.upload_file = Upload File +editor.edit_file = Edit File editor.preview_changes = Preview Changes editor.cannot_edit_lfs_files = LFS files cannot be edited in the web interface. editor.cannot_edit_non_text_files = Binary files cannot be edited in the web interface. -editor.edit_this_file = Edit Document -editor.this_file_locked = Document is locked +editor.edit_this_file = Edit File +editor.this_file_locked = File is locked editor.must_be_on_a_branch = You must be on a branch to make or propose changes to this file. -editor.fork_before_edit = You must fork this binder to make or propose changes to this file. -editor.delete_this_file = Delete Document +editor.fork_before_edit = You must fork this repository to make or propose changes to this file. +editor.delete_this_file = Delete File editor.must_have_write_access = You must have write access to make or propose changes to this file. -editor.file_delete_success = Document "%s" has been deleted. +editor.file_delete_success = File "%s" has been deleted. editor.name_your_file = Name your file… editor.filename_help = Add a directory by typing its name followed by a slash ('/'). Remove a directory by typing backspace at the beginning of the input field. editor.or = or @@ -1349,36 +1335,36 @@ editor.new_patch = New Patch editor.commit_message_desc = Add an optional extended description… editor.signoff_desc = Add a Signed-off-by trailer by the committer at the end of the commit log message. editor.commit_directly_to_this_branch = Commit directly to the %s branch. -editor.create_new_branch = Create a new branch for this commit and start a revision. +editor.create_new_branch = Create a new branch for this commit and start a pull request. editor.create_new_branch_np = Create a new branch for this commit. -editor.propose_file_change = Propose document change +editor.propose_file_change = Propose file change editor.new_branch_name = Name the new branch for this commit editor.new_branch_name_desc = New branch name… editor.cancel = Cancel editor.filename_cannot_be_empty = The filename cannot be empty. editor.filename_is_invalid = The filename is invalid: "%s". -editor.branch_does_not_exist = Branch "%s" does not exist in this binder. -editor.branch_already_exists = Branch "%s" already exists in this binder. -editor.directory_is_a_file = Directory name "%s" is already used as a filename in this binder. +editor.branch_does_not_exist = Branch "%s" does not exist in this repository. +editor.branch_already_exists = Branch "%s" already exists in this repository. +editor.directory_is_a_file = Directory name "%s" is already used as a filename in this repository. editor.file_is_a_symlink = `"%s" is a symbolic link. Symbolic links cannot be edited in the web editor` -editor.filename_is_a_directory = Filename "%s" is already used as a directory name in this binder. -editor.file_editing_no_longer_exists = The document being edited, "%s", no longer exists in this binder. -editor.file_deleting_no_longer_exists = The document being deleted, "%s", no longer exists in this binder. -editor.file_changed_while_editing = The document contents have changed since you started editing. Click here to see them or Commit Changes again to overwrite them. -editor.file_already_exists = A document named "%s" already exists in this binder. +editor.filename_is_a_directory = Filename "%s" is already used as a directory name in this repository. +editor.file_editing_no_longer_exists = The file being edited, "%s", no longer exists in this repository. +editor.file_deleting_no_longer_exists = The file being deleted, "%s", no longer exists in this repository. +editor.file_changed_while_editing = The file contents have changed since you started editing. Click here to see them or Commit Changes again to overwrite them. +editor.file_already_exists = A file named "%s" already exists in this repository. editor.commit_id_not_matching = The Commit ID does not match the ID when you began editing. Commit into a patch branch and then merge. editor.push_out_of_date = The push appears to be out of date. -editor.commit_empty_file_header = Commit an empty document -editor.commit_empty_file_text = The document you're about to commit is empty. Proceed? +editor.commit_empty_file_header = Commit an empty file +editor.commit_empty_file_text = The file you're about to commit is empty. Proceed? editor.no_changes_to_show = There are no changes to show. -editor.fail_to_update_file = Failed to update/create document "%s". +editor.fail_to_update_file = Failed to update/create file "%s". editor.fail_to_update_file_summary = Error Message: editor.push_rejected_no_message = The change was rejected by the server without a message. Please check Git Hooks. editor.push_rejected = The change was rejected by the server. Please check Git Hooks. editor.push_rejected_summary = Full Rejection Message: editor.add_subdir = Add a directory… editor.unable_to_upload_files = Failed to upload files to "%s" with error: %v -editor.upload_file_is_locked = Document "%s" is locked by %s. +editor.upload_file_is_locked = File "%s" is locked by %s. editor.upload_files_to_dir = Upload files to "%s" editor.cannot_commit_to_protected_branch = Cannot commit to protected branch "%s". editor.no_commit_to_branch = Unable to commit directly to branch because: @@ -1387,7 +1373,7 @@ editor.require_signed_commit = Branch requires a signed commit editor.cherry_pick = Cherry-pick %s onto: editor.revert = Revert %s onto: -commits.desc = Browse document change history. +commits.desc = Browse source code change history. commits.commits = Commits commits.no_commits = No commits in common. "%s" and "%s" have entirely different histories. commits.nothing_to_compare = These branches are equal. @@ -1419,11 +1405,11 @@ commitstatus.failure = Failure commitstatus.pending = Pending commitstatus.success = Success -ext_issues = Access to External Change Requests -ext_issues.desc = Link to an external change request tracker. +ext_issues = Access to External Issues +ext_issues.desc = Link to an external issue tracker. projects = Projects -projects.desc = Manage change requests and pulls in projects. +projects.desc = Manage issues and pulls in projects. projects.description = Description (optional) projects.description_placeholder = Description projects.create = Create Project @@ -1432,10 +1418,10 @@ projects.new = New Project projects.new_subheader = Coordinate, track, and update your work in one place, so projects stay transparent and on schedule. projects.create_success = The project "%s" has been created. projects.deletion = Delete Project -projects.deletion_desc = Deleting a project removes it from all related change requests. Continue? +projects.deletion_desc = Deleting a project removes it from all related issues. Continue? projects.deletion_success = The project has been deleted. projects.edit = Edit Project -projects.edit_subheader = Projects organize change requests and track progress. +projects.edit_subheader = Projects organize issues and track progress. projects.modify = Edit Project projects.edit_success = Project "%s" has been updated. projects.type.none = "None" @@ -1449,9 +1435,9 @@ projects.column.new_title = "Name" projects.column.new_submit = "Create Column" projects.column.new = "New Column" projects.column.set_default = "Set Default" -projects.column.set_default_desc = "Set this column as default for uncategorized change requests and pulls" +projects.column.set_default_desc = "Set this column as default for uncategorized issues and pulls" projects.column.delete = "Delete Column" -projects.column.deletion_desc = "Deleting a project column moves all related change requests to the default column. Continue?" +projects.column.deletion_desc = "Deleting a project column moves all related issues to the default column. Continue?" projects.column.color = "Color" projects.open = Open projects.close = Close @@ -1466,7 +1452,7 @@ issues.filter_milestones = Filter Milestone issues.filter_projects = Filter Project issues.filter_labels = Filter Label issues.filter_reviewers = Filter Reviewer -issues.new = New Change Request +issues.new = New Issue issues.new.title_empty = Title cannot be empty issues.new.labels = Labels issues.new.no_label = No Label @@ -1484,18 +1470,18 @@ issues.new.assignees = Assignees issues.new.clear_assignees = Clear assignees issues.new.no_assignees = No Assignees issues.new.no_reviewers = No Reviewers -issues.new.blocked_user = Cannot create change request because you are blocked by the binder owner. -issues.edit.already_changed = Unable to save changes to the change request. It appears the content has already been changed by another user. Please refresh the page and try editing again to avoid overwriting their changes -issues.edit.blocked_user = Cannot edit content because you are blocked by the poster or binder owner. +issues.new.blocked_user = Cannot create issue because you are blocked by the repository owner. +issues.edit.already_changed = Unable to save changes to the issue. It appears the content has already been changed by another user. Please refresh the page and try editing again to avoid overwriting their changes +issues.edit.blocked_user = Cannot edit content because you are blocked by the poster or repository owner. issues.choose.get_started = Get Started issues.choose.open_external_link = Open issues.choose.blank = Default -issues.choose.blank_about = Create a change request from default template. +issues.choose.blank_about = Create an issue from default template. issues.choose.ignore_invalid_templates = Invalid templates have been ignored issues.choose.invalid_templates = %v invalid template(s) found -issues.choose.invalid_config = The change request config contains errors: +issues.choose.invalid_config = The issue config contains errors: issues.no_ref = No Branch/Tag Specified -issues.create = Create Change Request +issues.create = Create Issue issues.new_label = New Label issues.new_label_placeholder = Label name issues.new_label_desc_placeholder = Description @@ -1547,7 +1533,7 @@ issues.filter_poster = Author issues.filter_user_placeholder = Search users issues.filter_user_no_select = All users issues.filter_type = Type -issues.filter_type.all_issues = All change requests +issues.filter_type.all_issues = All issues issues.filter_type.assigned_to_you = Assigned to you issues.filter_type.created_by_you = Created by you issues.filter_type.mentioning_you = Mentioning you @@ -1576,8 +1562,8 @@ issues.action_assignee_no_select = No assignee issues.action_check = Check/Uncheck issues.action_check_all = Check/Uncheck all items issues.opened_by = opened %[1]s by %[3]s -pulls.merged_by = by %[3]s was accepted %[1]s -pulls.merged_by_fake = by %[2]s was accepted %[1]s +pulls.merged_by = by %[3]s was merged %[1]s +pulls.merged_by_fake = by %[2]s was merged %[1]s issues.closed_by = by %[3]s was closed %[1]s issues.opened_by_fake = opened %[1]s by %[2]s issues.closed_by_fake = by %[2]s was closed %[1]s @@ -1592,40 +1578,40 @@ issues.commented_at = `commented %s` issues.delete_comment_confirm = Are you sure you want to delete this comment? issues.context.copy_link = Copy Link issues.context.quote_reply = Quote Reply -issues.context.reference_issue = Reference in New Change Request +issues.context.reference_issue = Reference in New Issue issues.context.edit = Edit issues.context.delete = Delete issues.no_content = No description provided. -issues.close = Close Change Request +issues.close = Close Issue issues.comment_pull_merged_at = merged commit %[1]s into %[2]s %[3]s issues.comment_manually_pull_merged_at = manually merged commit %[1]s into %[2]s %[3]s issues.close_comment_issue = Close with Comment issues.reopen_issue = Reopen issues.reopen_comment_issue = Reopen with Comment issues.create_comment = Comment -issues.comment.blocked_user = Cannot create or edit comment because you are blocked by the poster or binder owner. -issues.closed_at = `closed this change request %[2]s` -issues.reopened_at = `reopened this change request %[2]s` -issues.commit_ref_at = `referenced this change request from a commit %[2]s` -issues.ref_issue_from = `referenced this change request %[4]s %[2]s` -issues.ref_pull_from = `referenced this revision %[4]s %[2]s` -issues.ref_closing_from = `referenced a revision %[4]s that will close this change request %[2]s` -issues.ref_reopening_from = `referenced a revision %[4]s that will reopen this change request %[2]s` -issues.ref_closed_from = `closed this change request %[4]s %[2]s` -issues.ref_reopened_from = `reopened this change request %[4]s %[2]s` +issues.comment.blocked_user = Cannot create or edit comment because you are blocked by the poster or repository owner. +issues.closed_at = `closed this issue %[2]s` +issues.reopened_at = `reopened this issue %[2]s` +issues.commit_ref_at = `referenced this issue from a commit %[2]s` +issues.ref_issue_from = `referenced this issue %[4]s %[2]s` +issues.ref_pull_from = `referenced this pull request %[4]s %[2]s` +issues.ref_closing_from = `referenced a pull request %[4]s that will close this issue %[2]s` +issues.ref_reopening_from = `referenced a pull request %[4]s that will reopen this issue %[2]s` +issues.ref_closed_from = `closed this issue %[4]s %[2]s` +issues.ref_reopened_from = `reopened this issue %[4]s %[2]s` issues.ref_from = `from %[1]s` issues.author = Author issues.author_helper = This user is the author. issues.role.owner = Owner -issues.role.owner_helper = This user is the owner of this binder. +issues.role.owner_helper = This user is the owner of this repository. issues.role.member = Member -issues.role.member_helper = This user is a member of the organization owning this binder. +issues.role.member_helper = This user is a member of the organization owning this repository. issues.role.collaborator = Collaborator -issues.role.collaborator_helper = This user has been invited to collaborate on the binder. +issues.role.collaborator_helper = This user has been invited to collaborate on the repository. issues.role.first_time_contributor = First-time contributor -issues.role.first_time_contributor_helper = This is the first contribution of this user to the binder. +issues.role.first_time_contributor_helper = This is the first contribution of this user to the repository. issues.role.contributor = Contributor -issues.role.contributor_helper = This user has previously committed to the binder. +issues.role.contributor_helper = This user has previously committed to the repository. issues.re_request_review=Re-request review issues.is_stale = There have been changes to this PR since this review issues.remove_request_review=Remove review request @@ -1644,14 +1630,14 @@ issues.label_archive = Archive Label issues.label_archived_filter = Show archived labels issues.label_archive_tooltip = Archived labels are excluded by default from the suggestions when searching by label. issues.label_exclusive_desc = Name the label scope/item to make it mutually exclusive with other scope/ labels. -issues.label_exclusive_warning = Any conflicting scoped labels will be removed when editing the labels of a change request or revision. +issues.label_exclusive_warning = Any conflicting scoped labels will be removed when editing the labels of an issue or pull request. issues.label_count = %d labels -issues.label_open_issues = %d open change requests/revisions +issues.label_open_issues = %d open issues/pull requests issues.label_edit = Edit issues.label_delete = Delete issues.label_modify = Edit Label issues.label_deletion = Delete Label -issues.label_deletion_desc = Deleting a label removes it from all change requests. Continue? +issues.label_deletion_desc = Deleting a label removes it from all issues. Continue? issues.label_deletion_success = The label has been deleted. issues.label.filter_sort.alphabetically = Alphabetically issues.label.filter_sort.reverse_alphabetically = Reverse alphabetically @@ -1662,32 +1648,32 @@ issues.attachment.open_tab = `Click to see "%s" in a new tab` issues.attachment.download = `Click to download "%s"` issues.subscribe = Subscribe issues.unsubscribe = Unsubscribe -issues.unpin_issue = Unpin Change Request -issues.max_pinned = "You can't pin more change requests" +issues.unpin_issue = Unpin Issue +issues.max_pinned = "You can't pin more issues" issues.pin_comment = "pinned this %s" issues.unpin_comment = "unpinned this %s" issues.lock = Lock conversation issues.unlock = Unlock conversation -issues.lock.unknown_reason = Cannot lock a change request with an unknown reason. -issues.lock_duplicate = A change request cannot be locked twice. -issues.unlock_error = Cannot unlock a change request that is not locked. +issues.lock.unknown_reason = Cannot lock an issue with an unknown reason. +issues.lock_duplicate = An issue cannot be locked twice. +issues.unlock_error = Cannot unlock an issue that is not locked. issues.lock_with_reason = "locked as %s and limited conversation to collaborators %s" issues.lock_no_reason = "locked and limited conversation to collaborators %s" issues.unlock_comment = "unlocked this conversation %s" issues.lock_confirm = Lock issues.unlock_confirm = Unlock -issues.lock.notice_1 = - Other users can’t add new comments to this change request. -issues.lock.notice_2 = - You and other collaborators with access to this binder can still leave comments that others can see. -issues.lock.notice_3 = - You can always unlock this change request again in the future. -issues.unlock.notice_1 = - Everyone would be able to comment on this change request once more. -issues.unlock.notice_2 = - You can always lock this change request again in the future. +issues.lock.notice_1 = - Other users can’t add new comments to this issue. +issues.lock.notice_2 = - You and other collaborators with access to this repository can still leave comments that others can see. +issues.lock.notice_3 = - You can always unlock this issue again in the future. +issues.unlock.notice_1 = - Everyone would be able to comment on this issue once more. +issues.unlock.notice_2 = - You can always lock this issue again in the future. issues.lock.reason = Reason for locking -issues.lock.title = Lock conversation on this change request. -issues.unlock.title = Unlock conversation on this change request. -issues.comment_on_locked = You cannot comment on a locked change request. +issues.lock.title = Lock conversation on this issue. +issues.unlock.title = Unlock conversation on this issue. +issues.comment_on_locked = You cannot comment on a locked issue. issues.delete = Delete -issues.delete.title = Delete this change request? -issues.delete.text = Do you really want to delete this change request? (This will permanently remove all content. Consider closing it instead, if you intend to keep it archived) +issues.delete.title = Delete this issue? +issues.delete.text = Do you really want to delete this issue? (This will permanently remove all content. Consider closing it instead, if you intend to keep it archived) issues.tracker = Time Tracker issues.timetracker_timer_start = Start timer @@ -1701,9 +1687,11 @@ issues.change_time_estimate_at = changed time estimate to %s %s issues.remove_time_estimate_at = removed time estimate %s issues.time_estimate_invalid = Time estimate format is invalid issues.start_tracking_history = started working %s -issues.tracker_auto_close = Timer will be stopped automatically when this change request gets closed -issues.tracking_already_started = `You have already started time tracking on another change request!` +issues.tracker_auto_close = Timer will be stopped automatically when this issue gets closed +issues.tracking_already_started = `You have already started time tracking on another issue!` issues.stop_tracking_history = worked for %s %s +issues.stop_tracking = Stop Timer +issues.cancel_tracking = Discard issues.cancel_tracking_history = `canceled time tracking %s` issues.del_time = Delete this time log issues.add_time_history = added spent time %s %s @@ -1721,13 +1709,13 @@ issues.error_modifying_due_date = "Failed to modify the due date." issues.error_removing_due_date = "Failed to remove the due date." issues.push_commit_1 = "added %d commit %s" issues.push_commits_n = "added %d commits %s" -issues.force_push_codes = `updated %[1]s from %[2]s to %[4]s %[6]s` +issues.force_push_codes = `force-pushed %[1]s from %[2]s to %[4]s %[6]s` issues.force_push_compare = Compare issues.due_date_form = "yyyy-mm-dd" issues.due_date_form_add = "Add due date" issues.due_date_form_edit = "Edit" issues.due_date_form_remove = "Remove" -issues.due_date_not_writer = "You need write access to this binder in order to update the due date of a change request." +issues.due_date_not_writer = "You need write access to this repository in order to update the due date of an issue." issues.due_date_not_set = "No due date set." issues.due_date_added = "added the due date %s %s" issues.due_date_modified = "modified the due date from %[2]s to %[1]s %[3]s" @@ -1746,27 +1734,27 @@ issues.dependency.remove = Remove issues.dependency.remove_info = Remove this dependency issues.dependency.added_dependency = `added a new dependency %s` issues.dependency.removed_dependency = `removed a dependency %s` -issues.dependency.pr_closing_blockedby = Closing this revision is blocked by the following change requests -issues.dependency.issue_closing_blockedby = Closing this change request is blocked by the following change requests -issues.dependency.issue_close_blocks = This change request blocks closing of the following change requests -issues.dependency.pr_close_blocks = This revision blocks closing of the following change requests -issues.dependency.issue_close_blocked = You need to close all change requests blocking this change request before you can close it. -issues.dependency.issue_batch_close_blocked = "Cannot batch close change requests that you choose, because change request #%d still has open dependencies" -issues.dependency.pr_close_blocked = You need to close all change requests blocking this revision before you can accept it. +issues.dependency.pr_closing_blockedby = Closing this pull request is blocked by the following issues +issues.dependency.issue_closing_blockedby = Closing this issue is blocked by the following issues +issues.dependency.issue_close_blocks = This issue blocks closing of the following issues +issues.dependency.pr_close_blocks = This pull request blocks closing of the following issues +issues.dependency.issue_close_blocked = You need to close all issues blocking this issue before you can close it. +issues.dependency.issue_batch_close_blocked = "Cannot batch close issues that you choose, because issue #%d still has open dependencies" +issues.dependency.pr_close_blocked = You need to close all issues blocking this pull request before you can merge it. issues.dependency.blocks_short = Blocks issues.dependency.blocked_by_short = Depends on issues.dependency.remove_header = Remove Dependency -issues.dependency.issue_remove_text = This will remove the dependency from this change request. Continue? -issues.dependency.pr_remove_text = This will remove the dependency from this revision. Continue? -issues.dependency.setting = Enable Dependencies For Change Requests and Revisions -issues.dependency.add_error_same_issue = You cannot make a change request depend on itself. -issues.dependency.add_error_dep_issue_not_exist = Dependent change request does not exist. +issues.dependency.issue_remove_text = This will remove the dependency from this issue. Continue? +issues.dependency.pr_remove_text = This will remove the dependency from this pull request. Continue? +issues.dependency.setting = Enable Dependencies For Issues and Pull Requests +issues.dependency.add_error_same_issue = You cannot make an issue depend on itself. +issues.dependency.add_error_dep_issue_not_exist = Dependent issue does not exist. issues.dependency.add_error_dep_not_exist = Dependency does not exist. issues.dependency.add_error_dep_exists = Dependency already exists. -issues.dependency.add_error_cannot_create_circular = You cannot create a dependency with two change requests blocking each other. -issues.dependency.add_error_dep_not_same_repo = Both change requests must be in the same binder. -issues.review.self.approval = You cannot approve your own revision. -issues.review.self.rejection = You cannot request changes on your own revision. +issues.dependency.add_error_cannot_create_circular = You cannot create a dependency with two issues blocking each other. +issues.dependency.add_error_dep_not_same_repo = Both issues must be in the same repository. +issues.review.self.approval = You cannot approve your own pull request. +issues.review.self.rejection = You cannot request changes on your own pull request. issues.review.approve = "approved these changes %s" issues.review.comment = "reviewed %s" issues.review.dismissed = "dismissed %s’s review %s" @@ -1812,13 +1800,13 @@ issues.reference_link = Reference: %s compare.compare_base = base compare.compare_head = compare -pulls.desc = Enable revisions and document reviews. -pulls.new = New Revision -pulls.new.blocked_user = Cannot create revision because you are blocked by the binder owner. -pulls.new.must_collaborator = You must be a collaborator to create revision. -pulls.edit.already_changed = Unable to save changes to the revision. It appears the content has already been changed by another user. Please refresh the page and try editing again to avoid overwriting their changes -pulls.view = View Revision -pulls.compare_changes = New Revision +pulls.desc = Enable pull requests and code reviews. +pulls.new = New Pull Request +pulls.new.blocked_user = Cannot create pull request because you are blocked by the repository owner. +pulls.new.must_collaborator = You must be a collaborator to create pull request. +pulls.edit.already_changed = Unable to save changes to the pull request. It appears the content has already been changed by another user. Please refresh the page and try editing again to avoid overwriting their changes +pulls.view = View Pull Request +pulls.compare_changes = New Pull Request pulls.allow_edits_from_maintainers = Allow edits from maintainers pulls.allow_edits_from_maintainers_desc = Users with write access to the base branch can also push to this branch pulls.allow_edits_from_maintainers_err = Updating failed @@ -1840,49 +1828,49 @@ pulls.showing_specified_commit_range = Showing only changes between %[1]s..%[2]s pulls.select_commit_hold_shift_for_range = Select commit. Hold shift + click to select a range pulls.review_only_possible_for_full_diff = Review is only possible when viewing the full diff pulls.filter_changes_by_commit = Filter by commit -pulls.nothing_to_compare = These branches are equal. There is no need to create a revision. +pulls.nothing_to_compare = These branches are equal. There is no need to create a pull request. pulls.nothing_to_compare_have_tag = The selected branch/tag are equal. pulls.nothing_to_compare_and_allow_empty_pr = These branches are equal. This PR will be empty. -pulls.has_pull_request = `A revision between these branches already exists: %[2]s#%[3]d` -pulls.create = Create Revision -pulls.title_desc = wants approval on %[1]d commits from %[2]s into %[3]s -pulls.merged_title_desc = accepted %[1]d commits from %[2]s into %[3]s %[4]s +pulls.has_pull_request = `A pull request between these branches already exists: %[2]s#%[3]d` +pulls.create = Create Pull Request +pulls.title_desc = wants to merge %[1]d commits from %[2]s into %[3]s +pulls.merged_title_desc = merged %[1]d commits from %[2]s into %[3]s %[4]s pulls.change_target_branch_at = `changed target branch from %s to %s %s` pulls.tab_conversation = Conversation pulls.tab_commits = Commits pulls.tab_files = Files Changed -pulls.reopen_to_merge = Please reopen this revision to perform a accept. -pulls.cant_reopen_deleted_branch = This revision cannot be reopened because the branch was deleted. -pulls.merged = Accepted -pulls.merged_success = Revision successfully accepted and closed -pulls.closed = Revision closed +pulls.reopen_to_merge = Please reopen this pull request to perform a merge. +pulls.cant_reopen_deleted_branch = This pull request cannot be reopened because the branch was deleted. +pulls.merged = Merged +pulls.merged_success = Pull request successfully merged and closed +pulls.closed = Pull request closed pulls.manually_merged = Manually merged pulls.merged_info_text = The branch %s can now be deleted. -pulls.is_closed = The revision has been closed. -pulls.title_wip_desc = `Start the title with %s to prevent the revision from being accepted accidentally.` -pulls.cannot_merge_work_in_progress = This revision is marked as a draft. +pulls.is_closed = The pull request has been closed. +pulls.title_wip_desc = `Start the title with %s to prevent the pull request from being merged accidentally.` +pulls.cannot_merge_work_in_progress = This pull request is marked as a work in progress. pulls.still_in_progress = Still in progress? pulls.add_prefix = Add %s prefix pulls.remove_prefix = Remove %s prefix -pulls.data_broken = This revision is broken due to missing fork information. -pulls.files_conflicted = This revision has changes conflicting with the target branch. +pulls.data_broken = This pull request is broken due to missing fork information. +pulls.files_conflicted = This pull request has changes conflicting with the target branch. pulls.is_checking = "Merge conflict checking is in progress. Try again in few moments." -pulls.is_ancestor = "This branch is already included in the target branch. There is nothing to accept." +pulls.is_ancestor = "This branch is already included in the target branch. There is nothing to merge." pulls.is_empty = "The changes on this branch are already on the target branch. This will be an empty commit." pulls.required_status_check_failed = Some required checks were not successful. pulls.required_status_check_missing = Some required checks are missing. -pulls.required_status_check_administrator = As an administrator, you may still accept this revision. -pulls.blocked_by_approvals = "This revision doesn't have enough required approvals yet. %d of %d official approvals granted." -pulls.blocked_by_approvals_whitelisted = "This revision doesn't have enough required approvals yet. %d of %d approvals granted from users or teams on the allowlist." -pulls.blocked_by_rejection = "This revision has changes requested by an official reviewer." -pulls.blocked_by_official_review_requests = "This revision has official review requests." -pulls.blocked_by_outdated_branch = "This revision is blocked because it's outdated." -pulls.blocked_by_changed_protected_files_1= "This revision is blocked because it changes a protected document:" -pulls.blocked_by_changed_protected_files_n= "This revision is blocked because it changes protected files:" -pulls.can_auto_merge_desc = This revision can be accepted automatically. -pulls.cannot_auto_merge_desc = This revision cannot be accepted automatically due to conflicts. +pulls.required_status_check_administrator = As an administrator, you may still merge this pull request. +pulls.blocked_by_approvals = "This pull request doesn't have enough required approvals yet. %d of %d official approvals granted." +pulls.blocked_by_approvals_whitelisted = "This pull request doesn't have enough required approvals yet. %d of %d approvals granted from users or teams on the allowlist." +pulls.blocked_by_rejection = "This pull request has changes requested by an official reviewer." +pulls.blocked_by_official_review_requests = "This pull request has official review requests." +pulls.blocked_by_outdated_branch = "This pull request is blocked because it's outdated." +pulls.blocked_by_changed_protected_files_1= "This pull request is blocked because it changes a protected file:" +pulls.blocked_by_changed_protected_files_n= "This pull request is blocked because it changes protected files:" +pulls.can_auto_merge_desc = This pull request can be merged automatically. +pulls.cannot_auto_merge_desc = This pull request cannot be merged automatically due to conflicts. pulls.cannot_auto_merge_helper = Merge manually to resolve the conflicts. -pulls.num_conflicting_files_1 = "%d conflicting document" +pulls.num_conflicting_files_1 = "%d conflicting file" pulls.num_conflicting_files_n = "%d conflicting files" pulls.approve_count_1 = "%d approval" pulls.approve_count_n = "%d approvals" @@ -1892,11 +1880,11 @@ pulls.waiting_count_1 = "%d waiting review" pulls.waiting_count_n = "%d waiting reviews" pulls.wrong_commit_id = "commit id must be a commit id on the target branch" -pulls.no_merge_desc = This revision cannot be accepted because all binder accept options are disabled. -pulls.no_merge_helper = Enable accept options in the binder settings or accept the revision manually. -pulls.no_merge_wip = This revision cannot be accepted because it is marked as being a draft. -pulls.no_merge_not_ready = This revision is not ready to be accepted, check review status and status checks. -pulls.no_merge_access = You are not authorized to accept this revision. +pulls.no_merge_desc = This pull request cannot be merged because all repository merge options are disabled. +pulls.no_merge_helper = Enable merge options in the repository settings or merge the pull request manually. +pulls.no_merge_wip = This pull request cannot be merged because it is marked as being a work in progress. +pulls.no_merge_not_ready = This pull request is not ready to be merged, check review status and status checks. +pulls.no_merge_access = You are not authorized to merge this pull request. pulls.merge_pull_request = Create merge commit pulls.rebase_merge_pull_request = Rebase then fast-forward pulls.rebase_merge_commit_pull_request = Rebase then create merge commit @@ -1906,7 +1894,7 @@ pulls.merge_manually = Manually merged pulls.merge_commit_id = The merge commit ID pulls.require_signed_wont_sign = The branch requires signed commits but this merge will not be signed -pulls.invalid_merge_option = You cannot use this merge option for this revision. +pulls.invalid_merge_option = You cannot use this merge option for this pull request. pulls.merge_conflict = Merge Failed: There was a conflict whilst merging. Hint: Try a different strategy pulls.merge_conflict_summary = Error Message pulls.rebase_conflict = Merge Failed: There was a conflict whilst rebasing commit: %[1]s. Hint: Try a different strategy @@ -1914,11 +1902,11 @@ pulls.rebase_conflict_summary = Error Message pulls.unrelated_histories = Merge Failed: The merge head and base do not share a common history. Hint: Try a different strategy pulls.merge_out_of_date = Merge Failed: Whilst generating the merge, the base was updated. Hint: Try again. pulls.head_out_of_date = Merge Failed: Whilst generating the merge, the head was updated. Hint: Try again. -pulls.has_merged = Failed: The revision has been merged, you cannot merge again or change the target branch. -pulls.push_rejected = Push Failed: The push was rejected. Review the Git Hooks for this binder. +pulls.has_merged = Failed: The pull request has been merged, you cannot merge again or change the target branch. +pulls.push_rejected = Push Failed: The push was rejected. Review the Git Hooks for this repository. pulls.push_rejected_summary = Full Rejection Message -pulls.push_rejected_no_message = Push Failed: The push was rejected but there was no remote message. Review the Git Hooks for this binder -pulls.open_unmerged_pull_exists = `You cannot perform a reopen operation because there is a pending revision (#%d) with identical properties.` +pulls.push_rejected_no_message = Push Failed: The push was rejected but there was no remote message. Review the Git Hooks for this repository +pulls.open_unmerged_pull_exists = `You cannot perform a reopen operation because there is a pending pull request (#%d) with identical properties.` pulls.status_checking = Some checks are pending pulls.status_checks_success = All checks were successful pulls.status_checks_warning = Some checks reported warnings @@ -1933,38 +1921,39 @@ pulls.update_branch_rebase = Update branch by rebase pulls.update_branch_success = Branch update was successful pulls.update_not_allowed = You are not allowed to update branch pulls.outdated_with_base_branch = This branch is out-of-date with the base branch -pulls.close = Close Revision -pulls.closed_at = `closed this revision %[2]s` -pulls.reopened_at = `reopened this revision %[2]s` +pulls.close = Close Pull Request +pulls.closed_at = `closed this pull request %[2]s` +pulls.reopened_at = `reopened this pull request %[2]s` pulls.cmd_instruction_hint = `View command line instructions.` pulls.cmd_instruction_checkout_title = Checkout -pulls.cmd_instruction_checkout_desc = From your project binder, check out a new branch and test the changes. -pulls.cmd_instruction_merge_title = Accept +pulls.cmd_instruction_checkout_desc = From your project repository, check out a new branch and test the changes. +pulls.cmd_instruction_merge_title = Merge pulls.cmd_instruction_merge_desc = Merge the changes and update on Gitea. -pulls.cmd_instruction_merge_warning = Warning: This operation can not accept revision because "autodetect manual merge" was not enable +pulls.cmd_instruction_merge_warning = Warning: This operation can not merge pull request because "autodetect manual merge" was not enable pulls.clear_merge_message = Clear merge message pulls.clear_merge_message_hint = Clearing the merge message will only remove the commit message content and keep generated git trailers such as "Co-Authored-By …". pulls.auto_merge_button_when_succeed = (When checks succeed) -pulls.auto_merge_when_succeed = Auto accept when all checks succeed -pulls.auto_merge_newly_scheduled = The revision was scheduled to accept when all checks succeed. -pulls.auto_merge_has_pending_schedule = %[1]s scheduled this revision to auto accept when all checks succeed %[2]s. +pulls.auto_merge_when_succeed = Auto merge when all checks succeed +pulls.auto_merge_newly_scheduled = The pull request was scheduled to merge when all checks succeed. +pulls.auto_merge_has_pending_schedule = %[1]s scheduled this pull request to auto merge when all checks succeed %[2]s. -pulls.auto_merge_cancel_schedule = Cancel auto accept -pulls.auto_merge_not_scheduled = This revision is not scheduled to auto accept. -pulls.auto_merge_canceled_schedule = The auto accept was canceled for this revision. +pulls.auto_merge_cancel_schedule = Cancel auto merge +pulls.auto_merge_not_scheduled = This pull request is not scheduled to auto merge. +pulls.auto_merge_canceled_schedule = The auto merge was canceled for this pull request. -pulls.auto_merge_newly_scheduled_comment = `scheduled this revision to auto accept when all checks succeed %[1]s` -pulls.auto_merge_canceled_schedule_comment = `canceled auto merging this revision when all checks succeed %[1]s` +pulls.auto_merge_newly_scheduled_comment = `scheduled this pull request to auto merge when all checks succeed %[1]s` +pulls.auto_merge_canceled_schedule_comment = `canceled auto merging this pull request when all checks succeed %[1]s` -pulls.delete.title = Delete this revision? -pulls.delete.text = Do you really want to delete this revision? (This will permanently remove all content. Consider closing it instead, if you intend to keep it archived) +pulls.delete.title = Delete this pull request? +pulls.delete.text = Do you really want to delete this pull request? (This will permanently remove all content. Consider closing it instead, if you intend to keep it archived) pulls.recently_pushed_new_branches = You pushed on branch %[1]s %[2]s pulls.upstream_diverging_prompt_behind_1 = This branch is %[1]d commit behind %[2]s pulls.upstream_diverging_prompt_behind_n = This branch is %[1]d commits behind %[2]s pulls.upstream_diverging_prompt_base_newer = The base branch %s has new changes pulls.upstream_diverging_merge = Sync fork +pulls.upstream_diverging_merge_confirm = Would you like to merge "%[1]s" onto "%[2]s"? pull.deleted_branch = (deleted):%s pull.agit_documentation = Review documentation about AGit @@ -1977,7 +1966,7 @@ milestones.update_ago = Updated %s milestones.no_due_date = No due date milestones.open = Open milestones.close = Close -milestones.new_subheader = Milestones can help you organize change requests and track their progress. +milestones.new_subheader = Milestones can help you organize issues and track their progress. milestones.completeness = %d%% Completed milestones.create = Create Milestone milestones.title = Title @@ -1987,20 +1976,20 @@ milestones.clear = Clear milestones.invalid_due_date_format = "Due date format must be 'yyyy-mm-dd'." milestones.create_success = The milestone "%s" has been created. milestones.edit = Edit Milestone -milestones.edit_subheader = Milestones organize change requests and track progress. +milestones.edit_subheader = Milestones organize issues and track progress. milestones.cancel = Cancel milestones.modify = Update Milestone milestones.edit_success = Milestone "%s" has been updated. milestones.deletion = Delete Milestone -milestones.deletion_desc = Deleting a milestone removes it from all related change requests. Continue? +milestones.deletion_desc = Deleting a milestone removes it from all related issues. Continue? milestones.deletion_success = The milestone has been deleted. milestones.filter_sort.name = Name milestones.filter_sort.earliest_due_data = Earliest due date milestones.filter_sort.latest_due_date = Latest due date milestones.filter_sort.least_complete = Least complete milestones.filter_sort.most_complete = Most complete -milestones.filter_sort.most_issues = Most change requests -milestones.filter_sort.least_issues = Least change requests +milestones.filter_sort.most_issues = Most issues +milestones.filter_sort.least_issues = Least issues signing.will_sign = This commit will be signed with key "%s". signing.wont_sign.error = There was an error whilst checking if the commit could be signed. @@ -2060,42 +2049,42 @@ activity.period.quarterly = 3 months activity.period.semiyearly = 6 months activity.period.yearly = 1 year activity.overview = Overview -activity.active_prs_count_1 = %d Active Revision -activity.active_prs_count_n = %d Active Revisions -activity.merged_prs_count_1 = Accepted Revision -activity.merged_prs_count_n = Accepted Revisions -activity.opened_prs_count_1 = Proposed Revision -activity.opened_prs_count_n = Proposed Revisions +activity.active_prs_count_1 = %d Active Pull Request +activity.active_prs_count_n = %d Active Pull Requests +activity.merged_prs_count_1 = Merged Pull Request +activity.merged_prs_count_n = Merged Pull Requests +activity.opened_prs_count_1 = Proposed Pull Request +activity.opened_prs_count_n = Proposed Pull Requests activity.title.user_1 = %d user activity.title.user_n = %d users -activity.title.prs_1 = %d Revision -activity.title.prs_n = %d Revisions -activity.title.prs_merged_by = %s accepted by %s +activity.title.prs_1 = %d Pull request +activity.title.prs_n = %d Pull requests +activity.title.prs_merged_by = %s merged by %s activity.title.prs_opened_by = %s proposed by %s -activity.merged_prs_label = Accepted +activity.merged_prs_label = Merged activity.opened_prs_label = Proposed -activity.active_issues_count_1 = %d Active Change Request -activity.active_issues_count_n = %d Active Change Requests -activity.closed_issues_count_1 = Closed Change Request -activity.closed_issues_count_n = Closed Change Requests -activity.title.issues_1 = %d Change Request -activity.title.issues_n = %d Change Requests +activity.active_issues_count_1 = %d Active Issue +activity.active_issues_count_n = %d Active Issues +activity.closed_issues_count_1 = Closed Issue +activity.closed_issues_count_n = Closed Issues +activity.title.issues_1 = %d Issue +activity.title.issues_n = %d Issues activity.title.issues_closed_from = %s closed from %s activity.title.issues_created_by = %s created by %s activity.closed_issue_label = Closed -activity.new_issues_count_1 = New Change Request -activity.new_issues_count_n = New Change Requests +activity.new_issues_count_1 = New Issue +activity.new_issues_count_n = New Issues activity.new_issue_label = Opened activity.title.unresolved_conv_1 = %d Unresolved Conversation activity.title.unresolved_conv_n = %d Unresolved Conversations -activity.unresolved_conv_desc = These recently changed change requests and revisions have not been resolved yet. +activity.unresolved_conv_desc = These recently changed issues and pull requests have not been resolved yet. activity.unresolved_conv_label = Open activity.title.releases_1 = %d Release activity.title.releases_n = %d Releases activity.title.releases_published_by = %s published by %s activity.published_release_label = Published activity.no_git_activity = There has not been any commit activity in this period. -activity.git_stats_exclude_merges = Excluding accepts, +activity.git_stats_exclude_merges = Excluding merges, activity.git_stats_author_1 = %d author activity.git_stats_author_n = %d authors activity.git_stats_pushed_1 = has pushed @@ -2122,8 +2111,8 @@ contributors.contribution_type.additions = Additions contributors.contribution_type.deletions = Deletions settings = Settings -settings.desc = Settings is where you can manage the settings for the binder -settings.options = Binder +settings.desc = Settings is where you can manage the settings for the repository +settings.options = Repository settings.collaboration = Collaborators settings.collaboration.admin = Administrator settings.collaboration.write = Write @@ -2134,26 +2123,26 @@ settings.hooks = Webhooks settings.githooks = Git Hooks settings.basic_settings = Basic Settings settings.mirror_settings = Mirror Settings -settings.mirror_settings.docs = Set up your binder to automatically synchronize commits, tags and branches with another binder. -settings.mirror_settings.docs.disabled_pull_mirror.instructions = Set up your project to automatically push commits, tags and branches to another binder. Pull mirrors have been disabled by your site administrator. -settings.mirror_settings.docs.disabled_push_mirror.instructions = Set up your project to automatically pull commits, tags and branches from another binder. +settings.mirror_settings.docs = Set up your repository to automatically synchronize commits, tags and branches with another repository. +settings.mirror_settings.docs.disabled_pull_mirror.instructions = Set up your project to automatically push commits, tags and branches to another repository. Pull mirrors have been disabled by your site administrator. +settings.mirror_settings.docs.disabled_push_mirror.instructions = Set up your project to automatically pull commits, tags and branches from another repository. settings.mirror_settings.docs.disabled_push_mirror.pull_mirror_warning = Right now, this can only be done in the "New Migration" menu. For more information, please consult: settings.mirror_settings.docs.disabled_push_mirror.info = Push mirrors have been disabled by your site administrator. -settings.mirror_settings.docs.no_new_mirrors = Your binder is mirroring changes to or from another binder. Please keep in mind that you can't create any new mirrors at this time. +settings.mirror_settings.docs.no_new_mirrors = Your repository is mirroring changes to or from another repository. Please keep in mind that you can't create any new mirrors at this time. settings.mirror_settings.docs.can_still_use = Although you can't modify existing mirrors or create new ones, you may still use your existing mirror. settings.mirror_settings.docs.pull_mirror_instructions = To set up a pull mirror, please consult: settings.mirror_settings.docs.more_information_if_disabled = You can find out more about push and pull mirrors here: -settings.mirror_settings.docs.doc_link_title = How do I mirror binders? -settings.mirror_settings.docs.doc_link_pull_section = the "Pulling from a remote binder" section of the documentation. -settings.mirror_settings.docs.pulling_remote_title = Pulling from a remote binder -settings.mirror_settings.mirrored_repository = Mirrored binder -settings.mirror_settings.pushed_repository = Pushed binder +settings.mirror_settings.docs.doc_link_title = How do I mirror repositories? +settings.mirror_settings.docs.doc_link_pull_section = the "Pulling from a remote repository" section of the documentation. +settings.mirror_settings.docs.pulling_remote_title = Pulling from a remote repository +settings.mirror_settings.mirrored_repository = Mirrored repository +settings.mirror_settings.pushed_repository = Pushed repository settings.mirror_settings.direction = Direction settings.mirror_settings.direction.pull = Pull settings.mirror_settings.direction.push = Push settings.mirror_settings.last_update = Last update settings.mirror_settings.push_mirror.none = No push mirrors configured -settings.mirror_settings.push_mirror.remote_url = Git Remote Binder URL +settings.mirror_settings.push_mirror.remote_url = Git Remote Repository URL settings.mirror_settings.push_mirror.add = Add Push Mirror settings.mirror_settings.push_mirror.edit_sync_time = Edit mirror sync interval @@ -2167,7 +2156,7 @@ settings.branches.switch_default_branch = Switch Default Branch settings.branches.update_default_branch = Update Default Branch settings.branches.add_new_rule = Add New Rule settings.advanced_settings = Advanced Settings -settings.wiki_desc = Enable Binder Wiki +settings.wiki_desc = Enable Repository Wiki settings.use_internal_wiki = Use Built-In Wiki settings.default_wiki_branch_name = Default Wiki Branch Name settings.default_wiki_everyone_access = Default Access Permission for signed-in users: @@ -2176,123 +2165,123 @@ settings.use_external_wiki = Use External Wiki settings.external_wiki_url = External Wiki URL settings.external_wiki_url_error = The external wiki URL is not a valid URL. settings.external_wiki_url_desc = Visitors are redirected to the external wiki URL when clicking the wiki tab. -settings.issues_desc = Enable Binder Change Request Tracker -settings.use_internal_issue_tracker = Use Built-In Change Request Tracker -settings.use_external_issue_tracker = Use External Change Request Tracker -settings.external_tracker_url = External Change Request Tracker URL -settings.external_tracker_url_error = The external change request tracker URL is not a valid URL. -settings.external_tracker_url_desc = Visitors are redirected to the external change request tracker URL when clicking on the change requests tab. -settings.tracker_url_format = External Change Request Tracker URL Format -settings.tracker_url_format_error = The external change request tracker URL format is not a valid URL. -settings.tracker_issue_style = External Change Request Tracker Number Format +settings.issues_desc = Enable Repository Issue Tracker +settings.use_internal_issue_tracker = Use Built-In Issue Tracker +settings.use_external_issue_tracker = Use External Issue Tracker +settings.external_tracker_url = External Issue Tracker URL +settings.external_tracker_url_error = The external issue tracker URL is not a valid URL. +settings.external_tracker_url_desc = Visitors are redirected to the external issue tracker URL when clicking on the issues tab. +settings.tracker_url_format = External Issue Tracker URL Format +settings.tracker_url_format_error = The external issue tracker URL format is not a valid URL. +settings.tracker_issue_style = External Issue Tracker Number Format settings.tracker_issue_style.numeric = Numeric settings.tracker_issue_style.alphanumeric = Alphanumeric settings.tracker_issue_style.regexp = Regular Expression settings.tracker_issue_style.regexp_pattern = Regular Expression Pattern settings.tracker_issue_style.regexp_pattern_desc = The first captured group will be used in place of {index}. -settings.tracker_url_format_desc = Use the placeholders {user}, {repo} and {index} for the username, binder name and change request index. +settings.tracker_url_format_desc = Use the placeholders {user}, {repo} and {index} for the username, repository name and issue index. settings.enable_timetracker = Enable Time Tracking settings.allow_only_contributors_to_track_time = Let Only Contributors Track Time -settings.pulls_desc = Enable Binder Revisions +settings.pulls_desc = Enable Repository Pull Requests settings.pulls.ignore_whitespace = Ignore Whitespace for Conflicts settings.pulls.enable_autodetect_manual_merge = Enable autodetect manual merge (Note: In some special cases, misjudgments can occur) -settings.pulls.allow_rebase_update = Enable updating revision branch by rebase -settings.pulls.default_delete_branch_after_merge = Delete revision branch after merge by default +settings.pulls.allow_rebase_update = Enable updating pull request branch by rebase +settings.pulls.default_delete_branch_after_merge = Delete pull request branch after merge by default settings.pulls.default_allow_edits_from_maintainers = Allow edits from maintainers by default -settings.releases_desc = Enable Binder Releases -settings.packages_desc = Enable Binder Packages Registry +settings.releases_desc = Enable Repository Releases +settings.packages_desc = Enable Repository Packages Registry settings.projects_desc = Enable Projects settings.projects_mode_desc = Projects Mode (which kinds of projects to show) settings.projects_mode_repo = Repo projects only settings.projects_mode_owner = Only user or org projects settings.projects_mode_all = All projects -settings.actions_desc = Enable Binder Actions +settings.actions_desc = Enable Repository Actions settings.admin_settings = Administrator Settings -settings.admin_enable_health_check = Enable Binder Health Checks (git fsck) -settings.admin_code_indexer = Document Indexer -settings.admin_stats_indexer = Document Statistics Indexer +settings.admin_enable_health_check = Enable Repository Health Checks (git fsck) +settings.admin_code_indexer = Code Indexer +settings.admin_stats_indexer = Code Statistics Indexer settings.admin_indexer_commit_sha = Last Indexed SHA settings.admin_indexer_unindexed = Unindexed settings.reindex_button = Add to Reindex Queue settings.reindex_requested=Reindex Requested -settings.admin_enable_close_issues_via_commit_in_any_branch = Close a change request via a commit made in a non default branch +settings.admin_enable_close_issues_via_commit_in_any_branch = Close an issue via a commit made in a non default branch settings.danger_zone = Danger Zone -settings.new_owner_has_same_repo = The new owner already has a binder with same name. Please choose another name. -settings.convert = Convert to Regular Binder -settings.convert_desc = You can convert this mirror into a regular binder. This cannot be undone. -settings.convert_notices_1 = This operation will convert the mirror into a regular binder and cannot be undone. -settings.convert_confirm = Convert Binder -settings.convert_succeed = The mirror has been converted into a regular binder. -settings.convert_fork = Convert to Regular Binder -settings.convert_fork_desc = You can convert this fork into a regular binder. This cannot be undone. -settings.convert_fork_notices_1 = This operation will convert the fork into a regular binder and cannot be undone. -settings.convert_fork_confirm = Convert Binder -settings.convert_fork_succeed = The fork has been converted into a regular binder. +settings.new_owner_has_same_repo = The new owner already has a repository with same name. Please choose another name. +settings.convert = Convert to Regular Repository +settings.convert_desc = You can convert this mirror into a regular repository. This cannot be undone. +settings.convert_notices_1 = This operation will convert the mirror into a regular repository and cannot be undone. +settings.convert_confirm = Convert Repository +settings.convert_succeed = The mirror has been converted into a regular repository. +settings.convert_fork = Convert to Regular Repository +settings.convert_fork_desc = You can convert this fork into a regular repository. This cannot be undone. +settings.convert_fork_notices_1 = This operation will convert the fork into a regular repository and cannot be undone. +settings.convert_fork_confirm = Convert Repository +settings.convert_fork_succeed = The fork has been converted into a regular repository. settings.transfer = Transfer Ownership -settings.transfer.rejected = Binder transfer was rejected. -settings.transfer.success = Binder transfer was successful. -settings.transfer.blocked_user = Cannot transfer binder because you are blocked by the new owner. +settings.transfer.rejected = Repository transfer was rejected. +settings.transfer.success = Repository transfer was successful. +settings.transfer.blocked_user = Cannot transfer repository because you are blocked by the new owner. settings.transfer_abort = Cancel transfer -settings.transfer_abort_invalid = You cannot cancel a non existent binder transfer. -settings.transfer_abort_success = The binder transfer to %s was successfully canceled. -settings.transfer_desc = Transfer this binder to a user or to an organization for which you have administrator rights. -settings.transfer_form_title = Enter the binder name as confirmation: -settings.transfer_in_progress = There is currently an ongoing transfer. Please cancel it if you will like to transfer this binder to another user. -settings.transfer_notices_1 = - You will lose access to the binder if you transfer it to an individual user. -settings.transfer_notices_2 = - You will keep access to the binder if you transfer it to an organization that you (co-)own. -settings.transfer_notices_3 = - If the binder is private and is transferred to an individual user, this action makes sure that the user does have at least read permission (and changes permissions if necessary). -settings.transfer_notices_4 = - If the binder belongs to an organization, and you transfer it to another organization or individual, you will lose the links between the binder's change requests and the organization's project board. +settings.transfer_abort_invalid = You cannot cancel a non existent repository transfer. +settings.transfer_abort_success = The repository transfer to %s was successfully canceled. +settings.transfer_desc = Transfer this repository to a user or to an organization for which you have administrator rights. +settings.transfer_form_title = Enter the repository name as confirmation: +settings.transfer_in_progress = There is currently an ongoing transfer. Please cancel it if you will like to transfer this repository to another user. +settings.transfer_notices_1 = - You will lose access to the repository if you transfer it to an individual user. +settings.transfer_notices_2 = - You will keep access to the repository if you transfer it to an organization that you (co-)own. +settings.transfer_notices_3 = - If the repository is private and is transferred to an individual user, this action makes sure that the user does have at least read permission (and changes permissions if necessary). +settings.transfer_notices_4 = - If the repository belongs to an organization, and you transfer it to another organization or individual, you will lose the links between the repository's issues and the organization's project board. settings.transfer_owner = New Owner settings.transfer_perform = Perform Transfer -settings.transfer_started = This binder has been marked for transfer and awaits confirmation from "%s" -settings.transfer_succeed = The binder has been transferred. +settings.transfer_started = This repository has been marked for transfer and awaits confirmation from "%s" +settings.transfer_succeed = The repository has been transferred. settings.signing_settings = Signing Verification Settings settings.trust_model = Signature Trust Model settings.trust_model.default = Default Trust Model -settings.trust_model.default.desc= Use the default binder trust model for this installation. +settings.trust_model.default.desc= Use the default repository trust model for this installation. settings.trust_model.collaborator = Collaborator settings.trust_model.collaborator.long = Collaborator: Trust signatures by collaborators -settings.trust_model.collaborator.desc = Valid signatures by collaborators of this binder will be marked "trusted" - (whether they match the committer or not). Otherwise, valid signatures will be marked "untrusted" if the signature matches the committer and "unmatched" if not. +settings.trust_model.collaborator.desc = Valid signatures by collaborators of this repository will be marked "trusted" - (whether they match the committer or not). Otherwise, valid signatures will be marked "untrusted" if the signature matches the committer and "unmatched" if not. settings.trust_model.committer = Committer settings.trust_model.committer.long = Committer: Trust signatures that match committers (This matches GitHub and will force Gitea signed commits to have Gitea as the committer) settings.trust_model.committer.desc = Valid signatures will only be marked "trusted" if they match the committer, otherwise they will be marked "unmatched". This forces Gitea to be the committer on signed commits with the actual committer marked as Co-authored-by: and Co-committed-by: trailer in the commit. The default Gitea key must match a User in the database. settings.trust_model.collaboratorcommitter = Collaborator+Committer settings.trust_model.collaboratorcommitter.long = Collaborator+Committer: Trust signatures by collaborators which match the committer -settings.trust_model.collaboratorcommitter.desc = Valid signatures by collaborators of this binder will be marked "trusted" if they match the committer. Otherwise, valid signatures will be marked "untrusted" if the signature matches the committer and "unmatched" otherwise. This will force Gitea to be marked as the committer on signed commits with the actual committer marked as Co-Authored-By: and Co-Committed-By: trailer in the commit. The default Gitea key must match a User in the database. +settings.trust_model.collaboratorcommitter.desc = Valid signatures by collaborators of this repository will be marked "trusted" if they match the committer. Otherwise, valid signatures will be marked "untrusted" if the signature matches the committer and "unmatched" otherwise. This will force Gitea to be marked as the committer on signed commits with the actual committer marked as Co-Authored-By: and Co-Committed-By: trailer in the commit. The default Gitea key must match a User in the database. settings.wiki_delete = Delete Wiki Data -settings.wiki_delete_desc = Deleting binder wiki data is permanent and cannot be undone. -settings.wiki_delete_notices_1 = - This will permanently delete and disable the binder wiki for %s. +settings.wiki_delete_desc = Deleting repository wiki data is permanent and cannot be undone. +settings.wiki_delete_notices_1 = - This will permanently delete and disable the repository wiki for %s. settings.confirm_wiki_delete = Delete Wiki Data -settings.wiki_deletion_success = The binder wiki data has been deleted. -settings.delete = Delete This Binder -settings.delete_desc = Deleting a binder is permanent and cannot be undone. +settings.wiki_deletion_success = The repository wiki data has been deleted. +settings.delete = Delete This Repository +settings.delete_desc = Deleting a repository is permanent and cannot be undone. settings.delete_notices_1 = - This operation CANNOT be undone. -settings.delete_notices_2 = - This operation will permanently delete the %s binder including documents, change requests, comments, wiki data and collaborator settings. -settings.delete_notices_fork_1 = - Forks of this binder will become independent after deletion. -settings.deletion_success = The binder has been deleted. -settings.update_settings_success = The binder settings have been updated. -settings.update_settings_no_unit = The binder should allow at least some sort of interaction. -settings.confirm_delete = Delete Binder +settings.delete_notices_2 = - This operation will permanently delete the %s repository including code, issues, comments, wiki data and collaborator settings. +settings.delete_notices_fork_1 = - Forks of this repository will become independent after deletion. +settings.deletion_success = The repository has been deleted. +settings.update_settings_success = The repository settings have been updated. +settings.update_settings_no_unit = The repository should allow at least some sort of interaction. +settings.confirm_delete = Delete Repository settings.add_collaborator = Add Collaborator settings.add_collaborator_success = The collaborator has been added. settings.add_collaborator_inactive_user = Cannot add an inactive user as a collaborator. settings.add_collaborator_owner = Cannot add an owner as a collaborator. -settings.add_collaborator_duplicate = The collaborator is already added to this binder. -settings.add_collaborator.blocked_user = The collaborator is blocked by the binder owner or vice versa. +settings.add_collaborator_duplicate = The collaborator is already added to this repository. +settings.add_collaborator.blocked_user = The collaborator is blocked by the repository owner or vice versa. settings.delete_collaborator = Remove settings.collaborator_deletion = Remove Collaborator -settings.collaborator_deletion_desc = Removing a collaborator will revoke their access to this binder. Continue? +settings.collaborator_deletion_desc = Removing a collaborator will revoke their access to this repository. Continue? settings.remove_collaborator_success = The collaborator has been removed. settings.org_not_allowed_to_be_collaborator = Organizations cannot be added as a collaborator. -settings.change_team_access_not_allowed = Changing team access for binder has been restricted to organization owner -settings.team_not_in_organization = The team is not in the same organization as the binder +settings.change_team_access_not_allowed = Changing team access for repository has been restricted to organization owner +settings.team_not_in_organization = The team is not in the same organization as the repository settings.teams = Teams settings.add_team = Add Team -settings.add_team_duplicate = Team already has the binder -settings.add_team_success = The team now have access to the binder. -settings.change_team_permission_tip = Team's permission is set on the team setting page and can't be changed per binder -settings.delete_team_tip = This team has access to all binders and can't be removed -settings.remove_team_success = The team's access to the binder has been removed. +settings.add_team_duplicate = Team already has the repository +settings.add_team_success = The team now have access to the repository. +settings.change_team_permission_tip = Team's permission is set on the team setting page and can't be changed per repository +settings.delete_team_tip = This team has access to all repositories and can't be removed +settings.remove_team_success = The team's access to the repository has been removed. settings.add_webhook = Add Webhook settings.add_webhook.invalid_channel_name = Webhook channel name cannot be empty and cannot contain only a # character. settings.hooks_desc = Webhooks automatically make HTTP POST requests to a server when certain Gitea events trigger. Read more in the webhooks guide. @@ -2329,54 +2318,56 @@ settings.event_desc = Trigger On: settings.event_push_only = Push Events settings.event_send_everything = All Events settings.event_choose = Custom Events… -settings.event_header_repository = Binder Events +settings.event_header_repository = Repository Events settings.event_create = Create settings.event_create_desc = Branch or tag created. settings.event_delete = Delete settings.event_delete_desc = Branch or tag deleted. settings.event_fork = Fork -settings.event_fork_desc = Binder forked. +settings.event_fork_desc = Repository forked. settings.event_wiki = Wiki settings.event_wiki_desc = Wiki page created, renamed, edited or deleted. +settings.event_statuses = Statuses +settings.event_statuses_desc = Commit Status updated from the API. settings.event_release = Release -settings.event_release_desc = Release published, updated or deleted in a binder. +settings.event_release_desc = Release published, updated or deleted in a repository. settings.event_push = Push settings.event_force_push = Force Push -settings.event_push_desc = Git push to a binder. -settings.event_repository = Binder -settings.event_repository_desc = Binder created or deleted. -settings.event_header_issue = Change Request Events -settings.event_issues = Change Requests -settings.event_issues_desc = Change Request opened, closed, reopened, or edited. -settings.event_issue_assign = Change Request Assigned -settings.event_issue_assign_desc = Change Request assigned or unassigned. -settings.event_issue_label = Change Request Labeled -settings.event_issue_label_desc = Change Request labels updated or cleared. -settings.event_issue_milestone = Change Request Milestoned -settings.event_issue_milestone_desc = Change Request milestoned or demilestoned. -settings.event_issue_comment = Change Request Comment -settings.event_issue_comment_desc = Change Request comment created, edited, or deleted. -settings.event_header_pull_request = Revision Events -settings.event_pull_request = Revision -settings.event_pull_request_desc = Revision opened, closed, reopened, or edited. -settings.event_pull_request_assign = Revision Assigned -settings.event_pull_request_assign_desc = Revision assigned or unassigned. -settings.event_pull_request_label = Revision Labeled -settings.event_pull_request_label_desc = Revision labels updated or cleared. -settings.event_pull_request_milestone = Revision Milestoned -settings.event_pull_request_milestone_desc = Revision milestoned or demilestoned. -settings.event_pull_request_comment = Revision Comment -settings.event_pull_request_comment_desc = Revision comment created, edited, or deleted. -settings.event_pull_request_review = Revision Reviewed -settings.event_pull_request_review_desc = Revision approved, rejected, or review comment. -settings.event_pull_request_sync = Revision Synchronized -settings.event_pull_request_sync_desc = Revision synchronized. -settings.event_pull_request_review_request = Revision Review Requested -settings.event_pull_request_review_request_desc = Revision review requested or review request removed. -settings.event_pull_request_approvals = Revision Approvals -settings.event_pull_request_merge = Revision Accept +settings.event_push_desc = Git push to a repository. +settings.event_repository = Repository +settings.event_repository_desc = Repository created or deleted. +settings.event_header_issue = Issue Events +settings.event_issues = Issues +settings.event_issues_desc = Issue opened, closed, reopened, or edited. +settings.event_issue_assign = Issue Assigned +settings.event_issue_assign_desc = Issue assigned or unassigned. +settings.event_issue_label = Issue Labeled +settings.event_issue_label_desc = Issue labels updated or cleared. +settings.event_issue_milestone = Issue Milestoned +settings.event_issue_milestone_desc = Issue milestoned or demilestoned. +settings.event_issue_comment = Issue Comment +settings.event_issue_comment_desc = Issue comment created, edited, or deleted. +settings.event_header_pull_request = Pull Request Events +settings.event_pull_request = Pull Request +settings.event_pull_request_desc = Pull request opened, closed, reopened, or edited. +settings.event_pull_request_assign = Pull Request Assigned +settings.event_pull_request_assign_desc = Pull request assigned or unassigned. +settings.event_pull_request_label = Pull Request Labeled +settings.event_pull_request_label_desc = Pull request labels updated or cleared. +settings.event_pull_request_milestone = Pull Request Milestoned +settings.event_pull_request_milestone_desc = Pull request milestoned or demilestoned. +settings.event_pull_request_comment = Pull Request Comment +settings.event_pull_request_comment_desc = Pull request comment created, edited, or deleted. +settings.event_pull_request_review = Pull Request Reviewed +settings.event_pull_request_review_desc = Pull request approved, rejected, or review comment. +settings.event_pull_request_sync = Pull Request Synchronized +settings.event_pull_request_sync_desc = Pull request synchronized. +settings.event_pull_request_review_request = Pull Request Review Requested +settings.event_pull_request_review_request_desc = Pull request review requested or review request removed. +settings.event_pull_request_approvals = Pull Request Approvals +settings.event_pull_request_merge = Pull Request Merge settings.event_package = Package -settings.event_package_desc = Package created or deleted in a binder. +settings.event_package_desc = Package created or deleted in a repository. settings.branch_filter = Branch filter settings.branch_filter_desc = Branch whitelist for push, branch creation and branch deletion events, specified as glob pattern. If empty or *, events for all branches are reported. See %[2]s documentation for syntax. Examples: master, {master,release*}. settings.authorization_header = Authorization Header @@ -2392,7 +2383,7 @@ settings.hook_type = Hook Type settings.slack_token = Token settings.slack_domain = Domain settings.slack_channel = Channel -settings.add_web_hook_desc = Integrate %s into your binder. +settings.add_web_hook_desc = Integrate %s into your repository. settings.web_hook_name_gitea = Gitea settings.web_hook_name_gogs = Gogs settings.web_hook_name_slack = Slack @@ -2411,9 +2402,9 @@ settings.packagist_api_token = API token settings.packagist_package_url = Packagist package URL settings.deploy_keys = Deploy Keys settings.add_deploy_key = Add Deploy Key -settings.deploy_key_desc = Deploy keys have read-only pull access to the binder. +settings.deploy_key_desc = Deploy keys have read-only pull access to the repository. settings.is_writable = Enable Write Access -settings.is_writable_info = Allow this deploy key to push to the binder. +settings.is_writable_info = Allow this deploy key to push to the repository. settings.no_deploy_keys = There are no deploy keys yet. settings.title = Title settings.deploy_key_content = Content @@ -2421,7 +2412,7 @@ settings.key_been_used = A deploy key with identical content is already in use. settings.key_name_used = A deploy key with the same name already exists. settings.add_key_success = The deploy key "%s" has been added. settings.deploy_key_deletion = Remove Deploy Key -settings.deploy_key_deletion_desc = Removing a deploy key will revoke its access to this binder. Continue? +settings.deploy_key_deletion_desc = Removing a deploy key will revoke its access to this repository. Continue? settings.deploy_key_deletion_success = The deploy key has been removed. settings.branches = Branches settings.protected_branch = Branch Protection @@ -2443,8 +2434,8 @@ settings.protect_enable_force_push_all = Enable Force Push settings.protect_enable_force_push_all_desc = Anyone with push access will be allowed to force push to this branch. settings.protect_enable_force_push_allowlist = Allowlist Restricted Force Push settings.protect_enable_force_push_allowlist_desc = Only allowlisted users or teams with push access will be allowed to force push to this branch. -settings.protect_enable_merge = Enable Accept -settings.protect_enable_merge_desc = Anyone with write access will be allowed to accept the revisions into this branch. +settings.protect_enable_merge = Enable Merge +settings.protect_enable_merge_desc = Anyone with write access will be allowed to merge the pull requests into this branch. settings.protect_whitelist_committers = Allowlist Restricted Push settings.protect_whitelist_committers_desc = Only allowlisted users or teams will be allowed to push to this branch (but not force push). settings.protect_whitelist_deploy_keys = Allowlist deploy keys with write access to push. @@ -2453,26 +2444,26 @@ settings.protect_whitelist_teams = Allowlisted teams for pushing: settings.protect_force_push_allowlist_users = Allowlisted users for force pushing: settings.protect_force_push_allowlist_teams = Allowlisted teams for force pushing: settings.protect_force_push_allowlist_deploy_keys = Allowlist deploy keys with push access to force push. -settings.protect_merge_whitelist_committers = Enable Accept Allowlist -settings.protect_merge_whitelist_committers_desc = Allow only allowlisted users or teams to accept revisions into this branch. +settings.protect_merge_whitelist_committers = Enable Merge Allowlist +settings.protect_merge_whitelist_committers_desc = Allow only allowlisted users or teams to merge pull requests into this branch. settings.protect_merge_whitelist_users = Allowlisted users for merging: settings.protect_merge_whitelist_teams = Allowlisted teams for merging: settings.protect_check_status_contexts = Enable Status Check settings.protect_status_check_patterns = Status check patterns: settings.protect_status_check_patterns_desc = Enter patterns to specify which status checks must pass before branches can be merged into a branch that matches this rule. Each line specifies a pattern. Patterns cannot be empty. settings.protect_check_status_contexts_desc = Require status checks to pass before merging. When enabled, commits must first be pushed to another branch, then merged or pushed directly to a branch that matches this rule after status checks have passed. If no contexts are matched, the last commit must be successful regardless of context. -settings.protect_check_status_contexts_list = Status checks found in the last week for this binder +settings.protect_check_status_contexts_list = Status checks found in the last week for this repository settings.protect_status_check_matched = Matched settings.protect_invalid_status_check_pattern = Invalid status check pattern: "%s". settings.protect_no_valid_status_check_patterns = No valid status check patterns. settings.protect_required_approvals = Required approvals: -settings.protect_required_approvals_desc = Allow only to accept revision with enough required approvals. Required approvals are either from users or teams who are on the allowlist or anyone with write access. +settings.protect_required_approvals_desc = Allow only to merge pull request with enough required approvals. Required approvals are either from users or teams who are on the allowlist or anyone with write access. settings.protect_approvals_whitelist_enabled = Restrict approvals to allowlisted users or teams settings.protect_approvals_whitelist_enabled_desc = Only reviews from allowlisted users or teams will count to the required approvals. Without approval allowlist, reviews from anyone with write access count to the required approvals. settings.protect_approvals_whitelist_users = Allowlisted reviewers: settings.protect_approvals_whitelist_teams = Allowlisted teams for reviews: settings.dismiss_stale_approvals = Dismiss stale approvals -settings.dismiss_stale_approvals_desc = When new commits that change the content of the revision are pushed to the branch, old approvals will be dismissed. +settings.dismiss_stale_approvals_desc = When new commits that change the content of the pull request are pushed to the branch, old approvals will be dismissed. settings.ignore_stale_approvals = Ignore stale approvals settings.ignore_stale_approvals_desc = Do not count approvals that were made on older commits (stale reviews) towards how many approvals the PR has. Irrelevant if stale reviews are already dismissed. settings.require_signed_commits = Require Signed Commits @@ -2491,15 +2482,15 @@ settings.remove_protected_branch_success = Branch protection for rule "%s" has b settings.remove_protected_branch_failed = Removing branch protection rule "%s" failed. settings.protected_branch_deletion = Delete Branch Protection settings.protected_branch_deletion_desc = Disabling branch protection allows users with write permission to push to the branch. Continue? -settings.block_rejected_reviews = Block accept on rejected reviews +settings.block_rejected_reviews = Block merge on rejected reviews settings.block_rejected_reviews_desc = Merging will not be possible when changes are requested by official reviewers, even if there are enough approvals. -settings.block_on_official_review_requests = Block accept on official review requests +settings.block_on_official_review_requests = Block merge on official review requests settings.block_on_official_review_requests_desc = Merging will not be possible when it has official review requests, even if there are enough approvals. -settings.block_outdated_branch = Block accept if revision is outdated +settings.block_outdated_branch = Block merge if pull request is outdated settings.block_outdated_branch_desc = Merging will not be possible when head branch is behind base branch. settings.block_admin_merge_override = Administrators must follow branch protection rules settings.block_admin_merge_override_desc = Administrators must follow branch protection rules and can not circumvent it. -settings.default_branch_desc = Select a default binder branch for revisions and document commits: +settings.default_branch_desc = Select a default repository branch for pull requests and code commits: settings.merge_style_desc = Merge Styles settings.default_merge_style_desc = Default Merge Style settings.choose_branch = Choose a branch… @@ -2533,12 +2524,12 @@ settings.visibility.public.button = Make Public settings.visibility.public.text = Changing the visibility to public will make the repo visible to anyone. settings.visibility.public.bullet_title= Changing the visibility to public will: settings.visibility.public.bullet_one = Make the repo visible to anyone. -settings.visibility.success = Binder visibility changed. +settings.visibility.success = Repository visibility changed. settings.visibility.error = An error occurred while trying to change the repo visibility. settings.visibility.fork_error = Can't change the visibility of a forked repo. settings.archive.button = Archive Repo settings.archive.header = Archive This Repo -settings.archive.text = Archiving the repo will make it entirely read-only. It will be hidden from the dashboard. Nobody (not even you!) will be able to make new commits, or open any change requests or revisions. +settings.archive.text = Archiving the repo will make it entirely read-only. It will be hidden from the dashboard. Nobody (not even you!) will be able to make new commits, or open any issues or pull requests. settings.archive.success = The repo was successfully archived. settings.archive.error = An error occurred while trying to archive the repo. See the log for more details. settings.archive.error_ismirror = You cannot archive a mirrored repo. @@ -2547,13 +2538,13 @@ settings.archive.tagsettings_unavailable = Tag settings are not available if the settings.archive.mirrors_unavailable = Mirrors are not available if the repo is archived. settings.unarchive.button = Unarchive repo settings.unarchive.header = Unarchive this repo -settings.unarchive.text = Unarchiving the repo will restore its ability to receive commits and pushes, as well as new change requests and pull-requests. +settings.unarchive.text = Unarchiving the repo will restore its ability to receive commits and pushes, as well as new issues and pull-requests. settings.unarchive.success = The repo was successfully unarchived. settings.unarchive.error = An error occurred while trying to unarchive the repo. See the log for more details. -settings.update_avatar_success = The binder avatar has been updated. +settings.update_avatar_success = The repository avatar has been updated. settings.lfs=LFS -settings.lfs_filelist=LFS files stored in this binder -settings.lfs_no_lfs_files=No LFS files stored in this binder +settings.lfs_filelist=LFS files stored in this repository +settings.lfs_no_lfs_files=No LFS files stored in this repository settings.lfs_findcommits=Find commits settings.lfs_lfs_file_no_commits=No Commits found for this LFS file settings.lfs_noattribute=This path does not have the lockable attribute in the default branch @@ -2603,14 +2594,14 @@ diff.stats_desc = %d changed files with %d additionsdocumentation to fix them, then push some commits to refresh the status. +error.csv.too_large = Can't render this file because it is too large. +error.csv.unexpected = Can't render this file because it contains an unexpected character in line %d and column %d. +error.csv.invalid_field_count = Can't render this file because it has a wrong number of fields in line %d. +error.broken_git_hook = Git hooks of this repository seem to be broken. Please follow the documentation to fix them, then push some commits to refresh the status. [graphs] component_loading = Loading %s... @@ -2760,9 +2752,9 @@ create_org = Create Organization repo_updated = Updated members = Members teams = Teams -code = Documents +code = Code lower_members = members -lower_repositories = binders +lower_repositories = repositories create_new_team = New Team create_team = Create Team org_desc = Description @@ -2770,9 +2762,9 @@ team_name = Team Name team_desc = Description team_name_helper = Team names should be short and memorable. team_desc_helper = Describe the purpose or role of the team. -team_access_desc = Binder access +team_access_desc = Repository access team_permission_desc = Permission -team_unit_desc = Allow Access to Binder Sections +team_unit_desc = Allow Access to Repository Sections team_unit_disabled = (Disabled) form.name_reserved = The organization name "%s" is reserved. @@ -2786,7 +2778,7 @@ settings.email = Contact Email settings.website = Website settings.location = Location settings.permission = Permissions -settings.repoadminchangeteam = Binder admin can add and remove access for teams +settings.repoadminchangeteam = Repository admin can add and remove access for teams settings.visibility = Visibility settings.visibility.public = Public settings.visibility.limited = Limited (Visible to authenticated users only) @@ -2805,9 +2797,9 @@ settings.delete_prompt = The organization will be permanently removed. This all binders under this organization. +settings.hooks_desc = Add webhooks which will be triggered for all repositories under this organization. -settings.labels_desc = Add labels which can be used on change requests for all binders under this organization. +settings.labels_desc = Add labels which can be used on issues for all repositories under this organization. members.membership_visibility = Membership Visibility: members.public = Visible @@ -2827,21 +2819,21 @@ members.invite_now = Invite Now teams.join = Join teams.leave = Leave teams.leave.detail = Leave %s? -teams.can_create_org_repo = Create binders -teams.can_create_org_repo_helper = Members can create new binders in organization. Creator will get administrator access to the new binder. +teams.can_create_org_repo = Create repositories +teams.can_create_org_repo_helper = Members can create new repositories in organization. Creator will get administrator access to the new repository. teams.none_access = No Access -teams.none_access_helper = Members cannot view or do any other action on this unit. It has no effect for public binders. +teams.none_access_helper = Members cannot view or do any other action on this unit. It has no effect for public repositories. teams.general_access = General Access teams.general_access_helper = Members permissions will be decided by below permission table. teams.read_access = Read -teams.read_access_helper = Members can view and clone team binders. +teams.read_access_helper = Members can view and clone team repositories. teams.write_access = Write -teams.write_access_helper = Members can read and push to team binders. +teams.write_access_helper = Members can read and push to team repositories. teams.admin_access = Administrator Access -teams.admin_access_helper = Members can pull and push to team binders and add collaborators to them. +teams.admin_access_helper = Members can pull and push to team repositories and add collaborators to them. teams.no_desc = This team has no description teams.settings = Settings -teams.owners_permission_desc = Owners have full access to all binders and have administrator access to the organization. +teams.owners_permission_desc = Owners have full access to all repositories and have administrator access to the organization. teams.members = Team Members teams.update_settings = Update Settings teams.delete_team = Delete Team @@ -2849,29 +2841,29 @@ teams.add_team_member = Add Team Member teams.invite_team_member = Invite to %s teams.invite_team_member.list = Pending Invitations teams.delete_team_title = Delete Team -teams.delete_team_desc = Deleting a team revokes binder access from its members. Continue? +teams.delete_team_desc = Deleting a team revokes repository access from its members. Continue? teams.delete_team_success = The team has been deleted. -teams.read_permission_desc = This team grants Read access: members can view and clone team binders. -teams.write_permission_desc = This team grants Write access: members can read from and push to team binders. -teams.admin_permission_desc = This team grants Admin access: members can read from, push to and add collaborators to team binders. -teams.create_repo_permission_desc = Additionally, this team grants Create binder permission: members can create new binders in organization. -teams.repositories = Team Binders -teams.remove_all_repos_title = Remove all team binders -teams.remove_all_repos_desc = This will remove all binders from the team. -teams.add_all_repos_title = Add all binders -teams.add_all_repos_desc = This will add all the organization's binders to the team. -teams.add_nonexistent_repo = "The binder you're trying to add doesn't exist, please create it first." +teams.read_permission_desc = This team grants Read access: members can view and clone team repositories. +teams.write_permission_desc = This team grants Write access: members can read from and push to team repositories. +teams.admin_permission_desc = This team grants Admin access: members can read from, push to and add collaborators to team repositories. +teams.create_repo_permission_desc = Additionally, this team grants Create repository permission: members can create new repositories in organization. +teams.repositories = Team Repositories +teams.remove_all_repos_title = Remove all team repositories +teams.remove_all_repos_desc = This will remove all repositories from the team. +teams.add_all_repos_title = Add all repositories +teams.add_all_repos_desc = This will add all the organization's repositories to the team. +teams.add_nonexistent_repo = "The repository you're trying to add doesn't exist, please create it first." teams.add_duplicate_users = User is already a team member. -teams.repos.none = No binders could be accessed by this team. +teams.repos.none = No repositories could be accessed by this team. teams.members.none = No members on this team. teams.members.blocked_user = Cannot add the user because it is blocked by the organization. -teams.specific_repositories = Specific binders -teams.specific_repositories_helper = Members will only have access to binders explicitly added to the team. Selecting this will not automatically remove binders already added with All binders. -teams.all_repositories = All binders -teams.all_repositories_helper = Team has access to all binders. Selecting this will add all existing binders to the team. -teams.all_repositories_read_permission_desc = This team grants Read access to all binders: members can view and clone binders. -teams.all_repositories_write_permission_desc = This team grants Write access to all binders: members can read from and push to binders. -teams.all_repositories_admin_permission_desc = This team grants Admin access to all binders: members can read from, push to and add collaborators to binders. +teams.specific_repositories = Specific repositories +teams.specific_repositories_helper = Members will only have access to repositories explicitly added to the team. Selecting this will not automatically remove repositories already added with All repositories. +teams.all_repositories = All repositories +teams.all_repositories_helper = Team has access to all repositories. Selecting this will add all existing repositories to the team. +teams.all_repositories_read_permission_desc = This team grants Read access to all repositories: members can view and clone repositories. +teams.all_repositories_write_permission_desc = This team grants Write access to all repositories: members can read from and push to repositories. +teams.all_repositories_admin_permission_desc = This team grants Admin access to all repositories: members can read from, push to and add collaborators to repositories. teams.invite.title = You have been invited to join team %s in organization %s. teams.invite.by = Invited by %s teams.invite.description = Please click the button below to join the team. @@ -2884,7 +2876,7 @@ identity_access = Identity & Access users = User Accounts organizations = Organizations assets = Code Assets -repositories = Binders +repositories = Repositories hooks = Webhooks integrations = Integrations authentication = Authentication Sources @@ -2921,24 +2913,24 @@ dashboard.cron.error=Error in Cron: %s: %[3]s dashboard.cron.finished=Cron: %[1]s has finished dashboard.delete_inactive_accounts = Delete all unactivated accounts dashboard.delete_inactive_accounts.started = Delete all unactivated accounts task started. -dashboard.delete_repo_archives = "Delete all binders' archives (ZIP, TAR.GZ, etc..)" -dashboard.delete_repo_archives.started = Delete all binder archives task started. -dashboard.delete_missing_repos = Delete all binders missing their Git files -dashboard.delete_missing_repos.started = Delete all binders missing their Git files task started. -dashboard.delete_generated_repository_avatars = Delete generated binder avatars +dashboard.delete_repo_archives = "Delete all repositories' archives (ZIP, TAR.GZ, etc..)" +dashboard.delete_repo_archives.started = Delete all repository archives task started. +dashboard.delete_missing_repos = Delete all repositories missing their Git files +dashboard.delete_missing_repos.started = Delete all repositories missing their Git files task started. +dashboard.delete_generated_repository_avatars = Delete generated repository avatars dashboard.sync_repo_branches = Sync missed branches from git data to databases dashboard.sync_repo_tags = Sync tags from git data to database dashboard.update_mirrors = Update Mirrors -dashboard.repo_health_check = Health check all binders -dashboard.check_repo_stats = Check all binder statistics -dashboard.archive_cleanup = Delete old binder archives +dashboard.repo_health_check = Health check all repositories +dashboard.check_repo_stats = Check all repository statistics +dashboard.archive_cleanup = Delete old repository archives dashboard.deleted_branches_cleanup = Clean-up deleted branches dashboard.update_migration_poster_id = Update migration poster IDs -dashboard.git_gc_repos = Garbage collect all binders +dashboard.git_gc_repos = Garbage collect all repositories dashboard.resync_all_sshkeys = Update the '.ssh/authorized_keys' file with Gitea SSH keys. dashboard.resync_all_sshprincipals = Update the '.ssh/authorized_principals' file with Gitea SSH principals. -dashboard.resync_all_hooks = Resynchronize pre-receive, update and post-receive hooks of all binders. -dashboard.reinit_missing_repos = Reinitialize all missing Git binders for which records exist +dashboard.resync_all_hooks = Resynchronize pre-receive, update and post-receive hooks of all repositories. +dashboard.reinit_missing_repos = Reinitialize all missing Git repositories for which records exist dashboard.sync_external_users = Synchronize external user data dashboard.cleanup_hook_task_table = Cleanup hook_task table dashboard.cleanup_packages = Cleanup expired packages @@ -2983,7 +2975,7 @@ dashboard.cancel_abandoned_jobs = Cancel actions abandoned jobs dashboard.start_schedule_tasks = Start actions schedule tasks dashboard.sync_branch.started = Branches Sync started dashboard.sync_tag.started = Tags Sync started -dashboard.rebuild_issue_indexer = Rebuild change request indexer +dashboard.rebuild_issue_indexer = Rebuild issue indexer dashboard.sync_repo_licenses = Sync repo licenses users.user_manage_panel = User Account Management @@ -3010,23 +3002,23 @@ users.auth_login_name = Authentication Sign-In Name users.password_helper = Leave the password empty to keep it unchanged. users.update_profile_success = The user account has been updated. users.edit_account = Edit User Account -users.max_repo_creation = Maximum Number of Binders +users.max_repo_creation = Maximum Number of Repositories users.max_repo_creation_desc = (Enter -1 to use the global default limit.) users.is_activated = User Account Is Activated users.prohibit_login = Disable Sign-In users.is_admin = Is Administrator users.is_restricted = Is Restricted users.allow_git_hook = May Create Git Hooks -users.allow_git_hook_tooltip = Git Hooks are executed as the OS user running Gitea and will have the same level of host access. As a result, users with this special Git Hook privilege can access and modify all Gitea binders as well as the database used by Gitea. Consequently they are also able to gain Gitea administrator privileges. -users.allow_import_local = May Import Local Binders +users.allow_git_hook_tooltip = Git Hooks are executed as the OS user running Gitea and will have the same level of host access. As a result, users with this special Git Hook privilege can access and modify all Gitea repositories as well as the database used by Gitea. Consequently they are also able to gain Gitea administrator privileges. +users.allow_import_local = May Import Local Repositories users.allow_create_organization = May Create Organizations users.update_profile = Update User Account users.delete_account = Delete User Account users.cannot_delete_self = "You cannot delete yourself" -users.still_own_repo = This user still owns one or more binders. Delete or transfer these binders first. +users.still_own_repo = This user still owns one or more repositories. Delete or transfer these repositories first. users.still_has_org = This user is a member of an organization. Remove the user from any organizations first. users.purge = Purge User -users.purge_help = Forcibly delete user and any binders, organizations, and packages owned by the user. All comments will be deleted too. +users.purge_help = Forcibly delete user and any repositories, organizations, and packages owned by the user. All comments will be deleted too. users.still_own_packages = This user still owns one or more packages, delete these packages first. users.deletion_success = The user account has been deleted. users.reset_2fa = Reset 2FA @@ -3067,13 +3059,13 @@ orgs.teams = Teams orgs.members = Members orgs.new_orga = New Organization -repos.repo_manage_panel = Binder Management -repos.unadopted = Unadopted Binders -repos.unadopted.no_more = No more unadopted binders found +repos.repo_manage_panel = Repository Management +repos.unadopted = Unadopted Repositories +repos.unadopted.no_more = No more unadopted repositories found repos.owner = Owner repos.name = Name repos.private = Private -repos.issues = Change Requests +repos.issues = Issues repos.size = Size repos.lfs_size = LFS Size @@ -3087,17 +3079,17 @@ packages.creator = Creator packages.name = Name packages.version = Version packages.type = Type -packages.repository = Binder +packages.repository = Repository packages.size = Size packages.published = Published defaulthooks = Default Webhooks -defaulthooks.desc = Webhooks automatically make HTTP POST requests to a server when certain Gitea events trigger. Webhooks defined here are defaults and will be copied into all new binders. Read more in the webhooks guide. +defaulthooks.desc = Webhooks automatically make HTTP POST requests to a server when certain Gitea events trigger. Webhooks defined here are defaults and will be copied into all new repositories. Read more in the webhooks guide. defaulthooks.add_webhook = Add Default Webhook defaulthooks.update_webhook = Update Default Webhook systemhooks = System Webhooks -systemhooks.desc = Webhooks automatically make HTTP POST requests to a server when certain Gitea events trigger. Webhooks defined here will act on all binders on the system, so please consider any performance implications this may have. Read more in the webhooks guide. +systemhooks.desc = Webhooks automatically make HTTP POST requests to a server when certain Gitea events trigger. Webhooks defined here will act on all repositories on the system, so please consider any performance implications this may have. Read more in the webhooks guide. systemhooks.add_webhook = Add System Webhook systemhooks.update_webhook = Update System Webhook @@ -3233,7 +3225,7 @@ config.run_user = Run As Username config.run_mode = Run Mode config.git_version = Git Version config.app_data_path = App Data Path -config.repo_root_path = Binder Root Path +config.repo_root_path = Repository Root Path config.lfs_root_path = LFS Root Path config.log_file_root_path = Log Path config.script_type = Script Type @@ -3285,7 +3277,7 @@ config.default_enable_timetracking = Enable Time Tracking by Default config.default_allow_only_contributors_to_track_time = Let Only Contributors Track Time config.no_reply_address = Hidden Email Domain config.default_visibility_organization = Default visibility for new Organizations -config.default_enable_dependencies = Enable Change Request Dependencies by Default +config.default_enable_dependencies = Enable Issue Dependencies by Default config.webhook_config = Webhook Configuration config.queue_length = Queue Length @@ -3410,7 +3402,7 @@ notices.inverse_selection = Inverse Selection notices.delete_selected = Delete Selected notices.delete_all = Delete All Notices notices.type = Type -notices.type_1 = Binder +notices.type_1 = Repository notices.type_2 = Task notices.desc = Description notices.op = Op. @@ -3426,20 +3418,20 @@ self_check.database_fix_mssql = For MSSQL users, you could only fix the problem self_check.location_origin_mismatch = Current URL (%[1]s) doesn't match the URL seen by Gitea (%[2]s). If you are using a reverse proxy, please make sure the "Host" and "X-Forwarded-Proto" headers are set correctly. [action] -create_repo = created binder %s -rename_repo = renamed binder from %[1]s to %[3]s +create_repo = created repository %s +rename_repo = renamed repository from %[1]s to %[3]s commit_repo = pushed to %[3]s at %[4]s -create_issue = `opened change request %[3]s#%[2]s` -close_issue = `closed change request %[3]s#%[2]s` -reopen_issue = `reopened change request %[3]s#%[2]s` -create_pull_request = `created revision %[3]s#%[2]s` -close_pull_request = `closed revision %[3]s#%[2]s` -reopen_pull_request = `reopened revision %[3]s#%[2]s` -comment_issue = `commented on change request %[3]s#%[2]s` -comment_pull = `commented on revision %[3]s#%[2]s` -merge_pull_request = `accepted revision %[3]s#%[2]s` -auto_merge_pull_request = `automatically accepted revision %[3]s#%[2]s` -transfer_repo = transferred binder %s to %s +create_issue = `opened issue %[3]s#%[2]s` +close_issue = `closed issue %[3]s#%[2]s` +reopen_issue = `reopened issue %[3]s#%[2]s` +create_pull_request = `created pull request %[3]s#%[2]s` +close_pull_request = `closed pull request %[3]s#%[2]s` +reopen_pull_request = `reopened pull request %[3]s#%[2]s` +comment_issue = `commented on issue %[3]s#%[2]s` +comment_pull = `commented on pull request %[3]s#%[2]s` +merge_pull_request = `merged pull request %[3]s#%[2]s` +auto_merge_pull_request = `automatically merged pull request %[3]s#%[2]s` +transfer_repo = transferred repository %s to %s push_tag = pushed tag %[3]s to %[4]s delete_tag = deleted tag %[2]s from %[3]s delete_branch = deleted branch %[2]s from %[3]s @@ -3511,12 +3503,12 @@ error.probable_bad_default_signature = "WARNING! Although the default key has th [units] unit = Unit -error.no_unit_allowed_repo = You are not allowed to access any section of this binder. -error.unit_not_allowed = You are not allowed to access this binder section. +error.no_unit_allowed_repo = You are not allowed to access any section of this repository. +error.unit_not_allowed = You are not allowed to access this repository section. [packages] title = Packages -desc = Manage binder packages. +desc = Manage repository packages. empty = There are no packages yet. no_metadata = No metadata. empty.documentation = For more information on the package registry, see the documentation. @@ -3537,7 +3529,7 @@ keywords = Keywords details = Details details.author = Author details.project_site = Project Site -details.repository_site = Binder Site +details.repository_site = Repository Site details.documentation_site = Documentation Site details.license = License assets = Assets @@ -3549,14 +3541,14 @@ alpine.registry = Setup this registry by adding the url in your /etc/apk/r alpine.registry.key = Download the registry public RSA key into the /etc/apk/keys/ folder to verify the index signature: alpine.registry.info = Choose $branch and $repository from the list below. alpine.install = To install the package, run the following command: -alpine.repository = Binder Info +alpine.repository = Repository Info alpine.repository.branches = Branches -alpine.repository.repositories = Binders +alpine.repository.repositories = Repositories alpine.repository.architectures = Architectures -arch.registry = Add server with related binder and architecture to /etc/pacman.conf: +arch.registry = Add server with related repository and architecture to /etc/pacman.conf: arch.install = Sync package with pacman: -arch.repository = Binder Info -arch.repository.repositories = Binders +arch.repository = Repository Info +arch.repository.repositories = Repositories arch.repository.architectures = Architectures cargo.registry = Setup this registry in the Cargo configuration file (for example ~/.cargo/config.toml): cargo.install = To install the package using Cargo, run the following command: @@ -3566,15 +3558,16 @@ composer.registry = Setup this registry in your ~/.composer/config.json.condarc file: +conda.registry = Setup this registry as a Conda repository in your .condarc file: conda.install = To install the package using Conda, run the following command: container.details.type = Image Type container.details.platform = Platform container.pull = Pull the image from the command line: -container.digest = Digest: +container.images = Images +container.digest = Digest container.multi_arch = OS / Arch container.layers = Image Layers container.labels = Labels @@ -3585,7 +3578,7 @@ cran.install = To install the package, run the following command: debian.registry = Setup this registry from the command line: debian.registry.info = Choose $distribution and $component from the list below. debian.install = To install the package, run the following command: -debian.repository = Binder Info +debian.repository = Repository Info debian.repository.distributions = Distributions debian.repository.components = Components debian.repository.architectures = Architectures @@ -3616,7 +3609,7 @@ rpm.registry = Setup this registry from the command line: rpm.distros.redhat = on RedHat based distributions rpm.distros.suse = on SUSE based distributions rpm.install = To install the package, run the following command: -rpm.repository = Binder Info +rpm.repository = Repository Info rpm.repository.architectures = Architectures rpm.repository.multiple_groups = This package is available in multiple groups. rubygems.install = To install the package using gem, run the following command: @@ -3629,12 +3622,12 @@ swift.registry = Setup this registry from the command line: swift.install = Add the package in your Package.swift file: swift.install2 = and run the following command: vagrant.install = To add a Vagrant box, run the following command: -settings.link = Link this package to a binder -settings.link.description = If you link a package with a binder, the package is listed in the binder's package list. -settings.link.select = Select Binder -settings.link.button = Update Binder Link -settings.link.success = Binder link was successfully updated. -settings.link.error = Failed to update binder link. +settings.link = Link this package to a repository +settings.link.description = If you link a package with a repository, the package is listed in the repository's package list. +settings.link.select = Select Repository +settings.link.button = Update Repository Link +settings.link.success = Repository link was successfully updated. +settings.link.error = Failed to update repository link. settings.delete = Delete package settings.delete.description = Deleting a package is permanent and cannot be undone. settings.delete.notice = You are about to delete %s (%s). This operation is irreversible, are you sure? @@ -3642,7 +3635,7 @@ settings.delete.success = The package has been deleted. settings.delete.error = Failed to delete the package. owner.settings.cargo.title = Cargo Registry Index owner.settings.cargo.initialize = Initialize Index -owner.settings.cargo.initialize.description = A special index Git binder is needed to use the Cargo registry. Using this option will (re-)create the binder and configure it automatically. +owner.settings.cargo.initialize.description = A special index Git repository is needed to use the Cargo registry. Using this option will (re-)create the repository and configure it automatically. owner.settings.cargo.initialize.error = Failed to initialize Cargo index: %v owner.settings.cargo.initialize.success = The Cargo index was successfully created. owner.settings.cargo.rebuild = Rebuild Index @@ -3718,7 +3711,7 @@ runners.task_list = Recent tasks on this runner runners.task_list.no_tasks = There is no task yet. runners.task_list.run = Run runners.task_list.status = Status -runners.task_list.repository = Binder +runners.task_list.repository = Repository runners.task_list.commit = Commit runners.task_list.done_at = Done At runners.edit_runner = Edit Runner @@ -3771,7 +3764,7 @@ workflow.run_success = Workflow '%s' run successfully. workflow.from_ref = Use workflow from workflow.has_workflow_dispatch = This workflow has a workflow_dispatch event trigger. -need_approval_desc = Need approval to run workflows for fork revision. +need_approval_desc = Need approval to run workflows for fork pull request. variables = Variables variables.management = Variables Management @@ -3789,13 +3782,10 @@ variables.creation.success = The variable "%s" has been added. variables.update.failed = Failed to edit variable. variables.update.success = The variable has been edited. -logs.always_auto_scroll = Always auto scroll logs -logs.always_expand_running = Always expand running logs - [projects] deleted.display_name = Deleted Project type-1.display_name = Individual Project -type-2.display_name = Binder Project +type-2.display_name = Repository Project type-3.display_name = Organization Project [git.filemode] diff --git a/options/locale/locale_es-ES.ini b/options/locale/locale_es-ES.ini index d7d1cadd089a6..e95513766bb3b 100644 --- a/options/locale/locale_es-ES.ini +++ b/options/locale/locale_es-ES.ini @@ -577,7 +577,6 @@ joined_on=Se unió el %s repositories=Repositorios activity=Actividad pública followers=Seguidores -show_more=Ver más starred=Repositorios Favoritos watched=Repositorios seguidos code=Código @@ -3416,7 +3415,6 @@ variables.creation.success=La variable "%s" ha sido añadida. variables.update.failed=Error al editar la variable. variables.update.success=La variable ha sido editada. - [projects] type-1.display_name=Proyecto individual type-2.display_name=Proyecto repositorio diff --git a/options/locale/locale_fa-IR.ini b/options/locale/locale_fa-IR.ini index 6dcd35560a64d..640592f2bfbb3 100644 --- a/options/locale/locale_fa-IR.ini +++ b/options/locale/locale_fa-IR.ini @@ -463,7 +463,6 @@ change_avatar=تغییر آواتار… repositories=مخازن activity=ÙØ¹Ø§Ù„یت های عمومی followers=دنبال کنندگان -show_more=نمایش بیشتر starred=مخان ستاره دار watched=مخازنی Ú©Ù‡ دنبال می‌شوند projects=پروژه‌ها @@ -2530,7 +2529,6 @@ runs.commit=کامیت - [projects] [git.filemode] diff --git a/options/locale/locale_fi-FI.ini b/options/locale/locale_fi-FI.ini index b4f3869db41eb..375c7b11bf928 100644 --- a/options/locale/locale_fi-FI.ini +++ b/options/locale/locale_fi-FI.ini @@ -1707,7 +1707,6 @@ runs.commit=Commit - [projects] [git.filemode] diff --git a/options/locale/locale_fr-FR.ini b/options/locale/locale_fr-FR.ini index b024be4d882bd..c025ed0aba995 100644 --- a/options/locale/locale_fr-FR.ini +++ b/options/locale/locale_fr-FR.ini @@ -649,7 +649,6 @@ joined_on=Inscrit le %s repositories=Dépôts activity=Activité publique followers=abonnés -show_more=Voir plus starred=Dépôts favoris watched=Dépôts surveillés code=Code @@ -1110,7 +1109,6 @@ delete_preexisting_success=Fichiers dépossédés supprimés dans %s. blame_prior=Voir le blame avant cette modification blame.ignore_revs=Les révisions dans .git-blame-ignore-revs sont ignorées. Vous pouvez quand même voir ces blâmes. blame.ignore_revs.failed=Impossible d'ignorer les révisions dans .git-blame-ignore-revs. -user_search_tooltip=Affiche un maximum de 30 utilisateurs tree_path_not_found_commit=Le chemin %[1]s n’existe pas dans la révision %[2]s. tree_path_not_found_branch=Le chemin %[1]s n’existe pas dans la branche %[2]s. @@ -1529,8 +1527,6 @@ issues.filter_assignee=Assigné issues.filter_assginee_no_select=Tous les assignés issues.filter_assginee_no_assignee=Aucun assigné issues.filter_poster=Auteur -issues.filter_user_placeholder=Rechercher des utilisateurs -issues.filter_user_no_select=Tous les utilisateurs issues.filter_type=Type issues.filter_type.all_issues=Tous les tickets issues.filter_type.assigned_to_you=Qui vous sont assignés @@ -1691,7 +1687,6 @@ issues.tracking_already_started=`Vous avez déjà un minuteur en cours sur %s %s.` issues.cancel_tracking_history=`a abandonné son minuteur %s.` issues.del_time=Supprimer ce minuteur du journal -issues.add_time_history=`a pointé du temps de travail %s.` issues.del_time_history=`a supprimé son temps de travail %s.` issues.add_time_manually=Temps pointé manuellement issues.add_time_hours=Heures @@ -2632,7 +2627,6 @@ release.new_release=Nouvelle publication release.draft=Brouillon release.prerelease=Pré-publication release.stable=Stable -release.latest=Dernière release.compare=Comparer release.edit=Éditer release.ahead.commits=%d révisions @@ -3773,7 +3767,6 @@ variables.creation.success=La variable « %s » a été ajoutée. variables.update.failed=Impossible d’éditer la variable. variables.update.success=La variable a bien été modifiée. - [projects] deleted.display_name=Projet supprimé type-1.display_name=Projet personnel diff --git a/options/locale/locale_ga-IE.ini b/options/locale/locale_ga-IE.ini index 7bf6941e35ec1..0b74776c17419 100644 --- a/options/locale/locale_ga-IE.ini +++ b/options/locale/locale_ga-IE.ini @@ -649,7 +649,6 @@ joined_on=Cláraigh ar %s repositories=Stórais activity=Gníomhaíocht Phoiblí followers=Leantóirí -show_more=Taispeáin Tuilleadh starred=Stórais Réaltaithe watched=Stórais Breathnaithe code=Cód @@ -1110,7 +1109,6 @@ delete_preexisting_success=Scriosta comhaid neamhghlactha i %s blame_prior=Féach ar an milleán roimh an athrú seo blame.ignore_revs=Ag déanamh neamhairde de leasuithe i .git-blame-ignore-revs. Cliceáil anseo chun seachaint agus an gnáth-amharc milleán a fheiceáil. blame.ignore_revs.failed=Theip ar neamhaird a dhéanamh ar leasuithe i .git-blame-ignore-revs. -user_search_tooltip=Taispeáint uasmhéid de 30 úsáideoir tree_path_not_found_commit=Níl cosán %[1]s ann i dtiomantas %[2]s tree_path_not_found_branch=Níl cosán %[1]s ann i mbrainse %[2]s @@ -1529,8 +1527,6 @@ issues.filter_assignee=Sannaitheoir issues.filter_assginee_no_select=Gach sannaithe issues.filter_assginee_no_assignee=Gan sannaitheoir issues.filter_poster=Údar -issues.filter_user_placeholder=Cuardaigh úsáideoirí -issues.filter_user_no_select=Gach úsáideoir issues.filter_type=Cineál issues.filter_type.all_issues=Gach saincheist issues.filter_type.assigned_to_you=Sannta duit @@ -1946,8 +1942,8 @@ pulls.delete.title=Scrios an t-iarratas tarraingthe seo? pulls.delete.text=An bhfuil tú cinnte gur mhaith leat an t-iarratas tarraingthe seo a scriosadh? (Bainfidh sé seo an t-inneachar go léir go buan. Smaoinigh ar é a dhúnadh ina ionad sin, má tá sé i gceist agat é a choinneáil i gcartlann) pulls.recently_pushed_new_branches=Bhrúigh tú ar bhrainse %[1]s %[2]s -pulls.upstream_diverging_prompt_behind_1=Tá an brainse seo %[1]d tiomantas taobh thiar de %[2]s -pulls.upstream_diverging_prompt_behind_n=Tá an brainse seo %[1]d geallta taobh thiar de %[2]s +pulls.upstream_diverging_prompt_behind_1=Tá an brainse seo %d tiomantas taobh thiar de %s +pulls.upstream_diverging_prompt_behind_n=Tá an brainse seo %d geallta taobh thiar de %s pulls.upstream_diverging_prompt_base_newer=Tá athruithe nua ar an mbunbhrainse %s pulls.upstream_diverging_merge=Forc sionc @@ -2632,7 +2628,6 @@ release.new_release=Scaoileadh Nua release.draft=Dréacht release.prerelease=Réamh-eisiúint release.stable=Cobhsaí -release.latest=Is déanaí release.compare=Déan comparáid release.edit=cuir in eagar release.ahead.commits=Geallann %d @@ -3722,7 +3717,6 @@ runners.status.active=Gníomhach runners.status.offline=As líne runners.version=Leagan runners.reset_registration_token=Athshocraigh comhartha clár -runners.reset_registration_token_confirm=Ar mhaith leat an comhartha reatha a neamhbhailiú agus ceann nua a ghiniúint? runners.reset_registration_token_success=D'éirigh le hathshocrú comhartha clárúcháin an dara háit runs.all_workflows=Gach Sreafaí Oibre @@ -3774,9 +3768,6 @@ variables.creation.success=Tá an athróg "%s" curtha leis. variables.update.failed=Theip ar athróg a chur in eagar. variables.update.success=Tá an t-athróg curtha in eagar. -logs.always_auto_scroll=Logchomhaid scrollaithe uathoibríoch i gcónaí -logs.always_expand_running=Leathnaigh logs reatha i gcónaí - [projects] deleted.display_name=Tionscadal scriosta type-1.display_name=Tionscadal Aonair diff --git a/options/locale/locale_hu-HU.ini b/options/locale/locale_hu-HU.ini index 8e11f07d33ab4..88ccc9fac217b 100644 --- a/options/locale/locale_hu-HU.ini +++ b/options/locale/locale_hu-HU.ini @@ -1615,7 +1615,6 @@ runs.commit=Commit - [projects] [git.filemode] diff --git a/options/locale/locale_id-ID.ini b/options/locale/locale_id-ID.ini index 7c8271b98b403..237323a0fc42d 100644 --- a/options/locale/locale_id-ID.ini +++ b/options/locale/locale_id-ID.ini @@ -22,25 +22,11 @@ toc=Daftar Isi username=Nama Pengguna email=Alamat Email password=Kata Sandi -re_type=Konfirmasi Kata Sandi captcha=CAPTCHA twofa=Otentikasi Dua Faktor twofa_scratch=Kode Awal Dua Faktor passcode=Kode Akses -webauthn_insert_key=Masukkan kunci keamanan anda -webauthn_sign_in=Tekan tombol pada kunci keamanan Anda. Jika kunci keamanan Anda tidak memiliki tombol, masukkan kembali. -webauthn_press_button=Silakan tekan tombol pada kunci keamanan Anda… -webauthn_use_twofa=Gunakan kode dua faktor dari telepon Anda -webauthn_error=Tidak dapat membaca kunci keamanan Anda. -webauthn_unsupported_browser=Browser Anda saat ini tidak mendukung WebAuthn. -webauthn_error_unknown=Terdapat kesalahan yang tidak diketahui. Mohon coba lagi. -webauthn_error_insecure=`WebAuthn hanya mendukung koneksi aman. Untuk pengujian melalui HTTP, Anda dapat menggunakan "localhost" atau "127.0.0.1"` -webauthn_error_unable_to_process=Server tidak dapat memproses permintaan Anda. -webauthn_error_duplicated=Kunci keamanan tidak diperbolehkan untuk permintaan ini. Pastikan bahwa kunci ini belum terdaftar sebelumnya. -webauthn_error_empty=Anda harus menetapkan nama untuk kunci ini. -webauthn_error_timeout=Waktu habis sebelum kunci Anda dapat dibaca. Mohon muat ulang halaman ini dan coba lagi. -webauthn_reload=Muat ulang repository=Repositori organization=Organisasi @@ -50,8 +36,6 @@ new_migrate=Migrasi Baru new_mirror=Duplikat Baru new_fork=Fork Repositori Baru new_org=Organisasi Baru -new_project=Proyek Baru -new_project_column=Kolom Baru manage_org=Mengelola Organisasi admin_panel=Administrasi Situs account_settings=Pengaturan Akun @@ -71,58 +55,29 @@ pull_requests=Tarik Permintaan issues=Masalah milestones=Tonggak -ok=Oke cancel=Batal -retry=Coba lagi -rerun=Jalankan ulang -rerun_all=Jalankan ulang semua job save=Simpan add=Tambah add_all=Tambah Semua remove=Buang remove_all=Buang Semua -remove_label_str=`Hapus item "%s"` edit=Edit -view=Tampilan -test=Pengujian enabled=Aktif disabled=Nonaktif -locked=Terkunci -copy=Salin -copy_url=Salin URL -copy_hash=Salin hash -copy_content=Salin konten -copy_branch=Salin nama branch -copy_success=Tersalin! -copy_error=Gagal menyalin -copy_type_unsupported=Tipe berkas ini tidak dapat disalin write=Tulis preview=Pratinjau loading=Memuat… -error=Gangguan -error404=Halaman yang akan kamu akses tidak dapat ditemukan atau kamu tidak memiliki akses untuk melihatnya. -go_back=Kembali -invalid_data=Data invalid: %v -never=Tidak Pernah -unknown=Tidak diketahui -rss_feed=Umpan Berita -pin=Sematkan -unpin=Lepas sematan -artifacts=Artefak -confirm_delete_artifact=Apakah Anda yakin ingin menghapus artefak '%s' ? archived=Diarsipkan -concept_system_global=Global -concept_user_individual=Perorangan concept_code_repository=Repositori show_full_screen=Tampilkan layar penuh @@ -727,16 +682,13 @@ commits.newer=Terbaru commits.signed_by=Ditandai oleh -commitstatus.error=Gangguan projects.description_placeholder=Deskripsi projects.title=Judul -projects.new=Proyek Baru projects.template.desc=Contoh projects.column.edit_title=Nama projects.column.new_title=Nama -projects.column.new=Kolom Baru issues.new=Masalah Baru issues.new.labels=Label @@ -1444,7 +1396,6 @@ variables.creation.success=Variabel "%s" telah ditambahkan. variables.update.failed=Gagal mengedit variabel. variables.update.success=Variabel telah diedit. - [projects] type-1.display_name=Proyek Individu type-2.display_name=Proyek Repositori diff --git a/options/locale/locale_is-IS.ini b/options/locale/locale_is-IS.ini index d3824f83a2146..0564d49b1c0b5 100644 --- a/options/locale/locale_is-IS.ini +++ b/options/locale/locale_is-IS.ini @@ -1342,7 +1342,6 @@ runs.commit=Framlag - [projects] [git.filemode] diff --git a/options/locale/locale_it-IT.ini b/options/locale/locale_it-IT.ini index 1268f156735d4..567e6acdcefc5 100644 --- a/options/locale/locale_it-IT.ini +++ b/options/locale/locale_it-IT.ini @@ -488,7 +488,6 @@ change_avatar=Modifica il tuo avatar… repositories=Repository activity=Attività pubblica followers=Seguaci -show_more=Mostra Altro starred=Repositories votate watched=Repository Osservate projects=Progetti @@ -2808,7 +2807,6 @@ runs.commit=Commit - [projects] [git.filemode] diff --git a/options/locale/locale_ja-JP.ini b/options/locale/locale_ja-JP.ini index efcf34806a4cb..1d8b33bef68aa 100644 --- a/options/locale/locale_ja-JP.ini +++ b/options/locale/locale_ja-JP.ini @@ -145,7 +145,6 @@ confirm_delete_selected=é¸æŠžã—ãŸã™ã¹ã¦ã®ã‚¢ã‚¤ãƒ†ãƒ ã‚’削除ã—ã¦ã‚ˆ name=åç§° value=値 -readme=Readme filter=フィルター filter.clear=フィルターをクリア @@ -649,7 +648,6 @@ joined_on=%sã«ç™»éŒ² repositories=リãƒã‚¸ãƒˆãƒª activity=公開アクティビティ followers=フォロワー -show_more=ã•らã«è¡¨ç¤º starred=スター付ãリãƒã‚¸ãƒˆãƒª watched=ウォッãƒä¸­ãƒªãƒã‚¸ãƒˆãƒª code=コード @@ -1045,8 +1043,6 @@ generate_repo=リãƒã‚¸ãƒˆãƒªã®ç”Ÿæˆ generate_from=ä»–ã‹ã‚‰ã®ç”Ÿæˆ repo_desc=説明 repo_desc_helper=ç°¡å˜ãªèª¬æ˜Žã‚’入力ã—ã¦ãã ã•ã„ (オプション) -repo_no_desc=èª¬æ˜ŽãŒæä¾›ã•れã¦ã„ã¾ã›ã‚“ -repo_lang=言語 repo_gitignore_helper=.gitignoreãƒ†ãƒ³ãƒ—ãƒ¬ãƒ¼ãƒˆã‚’é¸æŠžã—ã¦ãã ã•ã„。 repo_gitignore_helper_desc=一般的ãªè¨€èªžã®ãƒ†ãƒ³ãƒ—レートリストã‹ã‚‰ã€è¿½è·¡ã—ãªã„ファイルã®è¨­å®šã‚’é¸æŠžã—ã¾ã™ã€‚ å„言語ã®ãƒ“ルドツールãŒç”Ÿæˆã™ã‚‹å…¸åž‹çš„ãªãƒ•ァイルãŒã€ãƒ‡ãƒ•ォルトã§.gitignoreã«å«ã¾ã‚Œã¾ã™ã€‚ issue_labels=イシューラベル @@ -1670,25 +1666,12 @@ issues.delete.title=ã“ã®ã‚¤ã‚·ãƒ¥ãƒ¼ã‚’削除ã—ã¾ã™ã‹? issues.delete.text=本当ã«ã“ã®ã‚¤ã‚·ãƒ¥ãƒ¼ã‚’削除ã—ã¾ã™ã‹? (ã“れã¯ã™ã¹ã¦ã®ã‚³ãƒ³ãƒ†ãƒ³ãƒ„を完全ã«å‰Šé™¤ã—ã¾ã™ã€‚ ä¿å­˜ã—ã¦ãŠããŸã„å ´åˆã¯ã€ä»£ã‚りã«ã‚¯ãƒ­ãƒ¼ã‚ºã™ã‚‹ã“ã¨ã‚’検討ã—ã¦ãã ã•ã„) issues.tracker=タイムトラッカー -issues.timetracker_timer_start=タイマー開始 -issues.timetracker_timer_stop=タイマー終了 -issues.timetracker_timer_discard=タイマー破棄 -issues.timetracker_timer_manually_add=時間を追加 - -issues.time_estimate_set=è¦‹ç©æ™‚間を設定 -issues.time_estimate_display=è¦‹ç©æ™‚é–“: %s -issues.change_time_estimate_at=ãŒè¦‹ç©æ™‚é–“ã‚’ %s ã«å¤‰æ›´ %s -issues.remove_time_estimate_at=ãŒè¦‹ç©æ™‚間を削除 %s -issues.time_estimate_invalid=è¦‹ç©æ™‚é–“ã®ãƒ•ォーマットãŒä¸æ­£ã§ã™ -issues.start_tracking_history=ãŒä½œæ¥­ã‚’é–‹å§‹ %s + issues.tracker_auto_close=タイマーã¯ã€ã“ã®ã‚¤ã‚·ãƒ¥ãƒ¼ãŒã‚¯ãƒ­ãƒ¼ã‚ºã•れるã¨è‡ªå‹•çš„ã«çµ‚了ã—ã¾ã™ issues.tracking_already_started=`別ã®ã‚¤ã‚·ãƒ¥ãƒ¼ã§æ—¢ã«ã‚¿ã‚¤ãƒ ãƒˆãƒ©ãƒƒã‚­ãƒ³ã‚°ã‚’é–‹å§‹ã—ã¦ã„ã¾ã™ï¼` -issues.stop_tracking_history=㌠%s ã®ä½œæ¥­ã‚’終了 %s issues.cancel_tracking_history=`ãŒã‚¿ã‚¤ãƒ ãƒˆãƒ©ãƒƒã‚­ãƒ³ã‚°ã‚’中止 %s` issues.del_time=ã“ã®ã‚¿ã‚¤ãƒ ãƒ­ã‚°ã‚’削除 -issues.add_time_history=ãŒä½œæ¥­æ™‚é–“ %s を追加 %s issues.del_time_history=`ãŒä½œæ¥­æ™‚間を削除 %s` -issues.add_time_manually=æ™‚é–“ã®æ‰‹å…¥åŠ› issues.add_time_hours=時間 issues.add_time_minutes=分 issues.add_time_sum_to_small=時間ãŒå…¥åŠ›ã•れã¦ã„ã¾ã›ã‚“。 @@ -1708,15 +1691,15 @@ issues.due_date_form_add=期日ã®è¿½åŠ  issues.due_date_form_edit=変更 issues.due_date_form_remove=削除 issues.due_date_not_writer=ã‚¤ã‚·ãƒ¥ãƒ¼ã®æœŸæ—¥ã‚’変更ã™ã‚‹ã«ã¯ã€ãƒªãƒã‚¸ãƒˆãƒªã¸ã®æ›¸ãè¾¼ã¿æ¨©é™ãŒå¿…è¦ã§ã™ã€‚ -issues.due_date_not_set=期日ã¯è¨­å®šã•れã¦ã„ã¾ã›ã‚“。 +issues.due_date_not_set=æœŸæ—¥ã¯æœªè¨­å®šã§ã™ã€‚ issues.due_date_added=ãŒæœŸæ—¥ %s を追加 %s issues.due_date_modified=ãŒæœŸæ—¥ã‚’ %[2]s ã‹ã‚‰ %[1]s ã«å¤‰æ›´ %[3]s issues.due_date_remove=ãŒæœŸæ—¥ %s を削除 %s issues.due_date_overdue=期日ã¯éŽãŽã¦ã„ã¾ã™ issues.due_date_invalid=æœŸæ—¥ãŒæ­£ã—ããªã„ã‹ç¯„囲を超ãˆã¦ã„ã¾ã™ã€‚ 'yyyy-mm-dd' ã®å½¢å¼ã§å…¥åŠ›ã—ã¦ãã ã•ã„。 issues.dependency.title=ä¾å­˜é–¢ä¿‚ -issues.dependency.issue_no_dependencies=ä¾å­˜é–¢ä¿‚ã¯è¨­å®šã•れã¦ã„ã¾ã›ã‚“。 -issues.dependency.pr_no_dependencies=ä¾å­˜é–¢ä¿‚ã¯è¨­å®šã•れã¦ã„ã¾ã›ã‚“。 +issues.dependency.issue_no_dependencies=ä¾å­˜é–¢ä¿‚ãŒè¨­å®šã•れã¦ã„ã¾ã›ã‚“。 +issues.dependency.pr_no_dependencies=ä¾å­˜é–¢ä¿‚ãŒè¨­å®šã•れã¦ã„ã¾ã›ã‚“。 issues.dependency.no_permission_1=%d 個ã®ä¾å­˜é–¢ä¿‚ã¸ã®èª­ã¿å–り権é™ãŒã‚りã¾ã›ã‚“ issues.dependency.no_permission_n=%d 個ã®ä¾å­˜é–¢ä¿‚ã¸ã®èª­ã¿å–り権é™ãŒã‚りã¾ã›ã‚“ issues.dependency.no_permission.can_remove=ã“ã®ä¾å­˜é–¢ä¿‚ã¸ã®èª­ã¿å–り権é™ã¯ã‚りã¾ã›ã‚“ãŒã€ã“ã®ä¾å­˜é–¢ä¿‚ã¯å‰Šé™¤ã§ãã¾ã™ @@ -1941,8 +1924,6 @@ pulls.delete.title=ã“ã®ãƒ—ルリクエストを削除ã—ã¾ã™ã‹ï¼Ÿ pulls.delete.text=本当ã«ã“ã®ãƒ—ルリクエストを削除ã—ã¾ã™ã‹? (ã“れã¯ã™ã¹ã¦ã®ã‚³ãƒ³ãƒ†ãƒ³ãƒ„を完全ã«å‰Šé™¤ã—ã¾ã™ã€‚ ä¿å­˜ã—ã¦ãŠããŸã„å ´åˆã¯ã€ä»£ã‚りã«ã‚¯ãƒ­ãƒ¼ã‚ºã™ã‚‹ã“ã¨ã‚’検討ã—ã¦ãã ã•ã„) pulls.recently_pushed_new_branches=%[2]s ã€ã‚ãªãŸã¯ãƒ–ランム%[1]s ã«ãƒ—ッシュã—ã¾ã—㟠-pulls.upstream_diverging_prompt_base_newer=ベースブランム%s ã«æ–°ã—ã„変更ãŒã‚りã¾ã™ -pulls.upstream_diverging_merge=ãƒ•ã‚©ãƒ¼ã‚¯ã‚’åŒæœŸ pull.deleted_branch=(削除済ã¿):%s pull.agit_documentation=AGitã«é–¢ã™ã‚‹ãƒ‰ã‚­ãƒ¥ãƒ¡ãƒ³ãƒˆã‚’確èªã™ã‚‹ @@ -3763,7 +3744,6 @@ variables.creation.success=変数 "%s" を追加ã—ã¾ã—ãŸã€‚ variables.update.failed=変数を更新ã§ãã¾ã›ã‚“ã§ã—ãŸã€‚ variables.update.success=変数を更新ã—ã¾ã—ãŸã€‚ - [projects] deleted.display_name=削除ã•れãŸãƒ—ロジェクト type-1.display_name=個人プロジェクト diff --git a/options/locale/locale_ko-KR.ini b/options/locale/locale_ko-KR.ini index 361ab23e4cd81..48220d5c998eb 100644 --- a/options/locale/locale_ko-KR.ini +++ b/options/locale/locale_ko-KR.ini @@ -1563,7 +1563,6 @@ runs.commit=커밋 - [projects] [git.filemode] diff --git a/options/locale/locale_lv-LV.ini b/options/locale/locale_lv-LV.ini index 4aa069b663922..fd412b95b416b 100644 --- a/options/locale/locale_lv-LV.ini +++ b/options/locale/locale_lv-LV.ini @@ -583,7 +583,6 @@ joined_on=PievienojÄs %s repositories=Repozitoriji activity=PubliskÄ aktivitÄte followers=SekotÄji -show_more=RÄdÄ«t vairÄk starred=AtzÄ«mÄ“ti repozitoriji watched=VÄ“rotie repozitoriji code=Kods @@ -3444,7 +3443,6 @@ variables.creation.success=MainÄ«gais "%s" tika pievienots. variables.update.failed=NeizdevÄs labot mainÄ«go. variables.update.success=MainÄ«gais tika labots. - [projects] type-1.display_name=IndividuÄlais projekts type-2.display_name=Repozitorija projekts diff --git a/options/locale/locale_nl-NL.ini b/options/locale/locale_nl-NL.ini index fe41c4529aa95..a4da8177bce30 100644 --- a/options/locale/locale_nl-NL.ini +++ b/options/locale/locale_nl-NL.ini @@ -487,7 +487,6 @@ change_avatar=Wijzig je profielfoto… repositories=repositories activity=Openbare activiteit followers=Volgers -show_more=Meer weergeven starred=Repositories met ster watched=Gevolgde repositories projects=Projecten @@ -2538,7 +2537,6 @@ runs.commit=Commit - [projects] [git.filemode] diff --git a/options/locale/locale_pl-PL.ini b/options/locale/locale_pl-PL.ini index 13c05eebe079b..22f701219d3c3 100644 --- a/options/locale/locale_pl-PL.ini +++ b/options/locale/locale_pl-PL.ini @@ -2430,7 +2430,6 @@ runs.commit=Commit - [projects] [git.filemode] diff --git a/options/locale/locale_pt-BR.ini b/options/locale/locale_pt-BR.ini index 9a8b6aeb62ca5..8fb869898b56c 100644 --- a/options/locale/locale_pt-BR.ini +++ b/options/locale/locale_pt-BR.ini @@ -582,7 +582,6 @@ joined_on=Inscreveu-se em %s repositories=Repositórios activity=Atividade pública followers=Seguidores -show_more=Mostrar mais starred=Repositórios favoritos watched=Repositórios observados code=Código @@ -3354,7 +3353,6 @@ runs.empty_commit_message=(mensagem de commit vazia) need_approval_desc=Precisa de aprovação para executar workflows para pull request do fork. - [projects] type-1.display_name=Projeto individual type-2.display_name=Projeto do repositório diff --git a/options/locale/locale_pt-PT.ini b/options/locale/locale_pt-PT.ini index b59a3cd9b6624..b959e7fdba259 100644 --- a/options/locale/locale_pt-PT.ini +++ b/options/locale/locale_pt-PT.ini @@ -649,7 +649,6 @@ joined_on=Inscreveu-se em %s repositories=Repositórios activity=Trabalho público followers=Seguidores -show_more=Mostrar mais starred=Repositórios favoritos watched=Repositórios sob vigilância code=Código @@ -1922,8 +1921,8 @@ pulls.close=Encerrar pedido de integração pulls.closed_at=`fechou este pedido de integração %[2]s` pulls.reopened_at=`reabriu este pedido de integração %[2]s` pulls.cmd_instruction_hint=`Ver instruções para a linha de comandos.` -pulls.cmd_instruction_checkout_title=Checkout -pulls.cmd_instruction_checkout_desc=A partir do seu repositório, crie um novo ramo e teste nele as modificações. +pulls.cmd_instruction_checkout_title=Conferir +pulls.cmd_instruction_checkout_desc=No seu repositório, irá criar um novo ramo para que possa testar as modificações. pulls.cmd_instruction_merge_title=Integrar pulls.cmd_instruction_merge_desc=Integrar as modificações e enviar para o Gitea. pulls.cmd_instruction_merge_warning=Aviso: Esta operação não pode executar pedidos de integração porque "auto-identificar integração manual" não estava habilitado @@ -1946,8 +1945,8 @@ pulls.delete.title=Eliminar este pedido de integração? pulls.delete.text=Tem a certeza que quer eliminar este pedido de integração? Isso irá remover todo o conteúdo permanentemente. Como alternativa considere fechá-lo, se pretender mantê-lo em arquivo. pulls.recently_pushed_new_branches=Enviou para o ramo %[1]s %[2]s -pulls.upstream_diverging_prompt_behind_1=Este ramo está %[1]d cometimento atrás de %[2]s -pulls.upstream_diverging_prompt_behind_n=Este ramo está %[1]d cometimentos atrás de %[2]s +pulls.upstream_diverging_prompt_behind_1=Este ramo está %d cometimento atrás de %s +pulls.upstream_diverging_prompt_behind_n=Este ramo está %d cometimentos atrás de %s pulls.upstream_diverging_prompt_base_newer=O ramo base %s tem novas modificações pulls.upstream_diverging_merge=Sincronizar derivação @@ -3722,7 +3721,6 @@ runners.status.active=Em funcionamento runners.status.offline=Desconectado runners.version=Versão runners.reset_registration_token=Repor código de registo -runners.reset_registration_token_confirm=Gostaria de invalidar o código vigente e gerar um novo? runners.reset_registration_token_success=O código de incrição do executor foi reposto com sucesso runs.all_workflows=Todas as sequências de trabalho @@ -3774,9 +3772,6 @@ variables.creation.success=A variável "%s" foi adicionada. variables.update.failed=Falha ao editar a variável. variables.update.success=A variável foi editada. -logs.always_auto_scroll=Rolar registos de forma automática e permanente -logs.always_expand_running=Expandir sempre os registos que vão rolando - [projects] deleted.display_name=Planeamento eliminado type-1.display_name=Planeamento individual diff --git a/options/locale/locale_ru-RU.ini b/options/locale/locale_ru-RU.ini index dc72dd7621bb0..735077bd41407 100644 --- a/options/locale/locale_ru-RU.ini +++ b/options/locale/locale_ru-RU.ini @@ -578,7 +578,6 @@ joined_on=ПриÑоединил(ÑÑ/аÑÑŒ) %s repositories=Репозитории activity=ÐктивноÑть followers=ПодпиÑчики -show_more=Показать больше starred=Избранные репозитории watched=ОтÑлеживаемые репозитории code=Код @@ -3374,7 +3373,6 @@ variables.creation.success=ÐŸÐµÑ€ÐµÐ¼ÐµÐ½Ð½Ð°Ñ Â«%s» добавлена. variables.update.failed=Ðе удалоÑÑŒ изменить переменную. variables.update.success=ÐŸÐµÑ€ÐµÐ¼ÐµÐ½Ð½Ð°Ñ Ð¸Ð·Ð¼ÐµÐ½ÐµÐ½Ð°. - [projects] type-1.display_name=Индивидуальный проект type-2.display_name=Проект Ñ€ÐµÐ¿Ð¾Ð·Ð¸Ñ‚Ð¾Ñ€Ð¸Ñ diff --git a/options/locale/locale_si-LK.ini b/options/locale/locale_si-LK.ini index f722d160eb58c..506fa5b492575 100644 --- a/options/locale/locale_si-LK.ini +++ b/options/locale/locale_si-LK.ini @@ -452,7 +452,6 @@ change_avatar=ඔබගේ අවතà·à¶»à¶º වෙනස් කරන්න… repositories=à¶šà·à·‚්ඨ activity=à¶´à·Šâ€à¶»à·ƒà·’ද්ධ à¶šà·Šâ€à¶»à·’යà·à¶šà·à¶»à¶šà¶¸ followers=අනුගà·à¶¸à·’කයන් -show_more=à¶­à·€ පෙන්වන්න starred=තරු ගබඩà·à·€ watched=නරඹන ලද ගබඩà·à·€à¶½à¶¯à·“ projects=ව්â€à¶ºà·à¶´à·˜à¶­à·’ @@ -2471,7 +2470,6 @@ runs.commit=à¶šà·à¶´ - [projects] [git.filemode] diff --git a/options/locale/locale_sk-SK.ini b/options/locale/locale_sk-SK.ini index 39c13c358efb5..b4bf6fb552a8d 100644 --- a/options/locale/locale_sk-SK.ini +++ b/options/locale/locale_sk-SK.ini @@ -1328,7 +1328,6 @@ runners.labels=Å títky - [projects] [git.filemode] diff --git a/options/locale/locale_sv-SE.ini b/options/locale/locale_sv-SE.ini index 2fd8277b76f1c..fc138381db7c1 100644 --- a/options/locale/locale_sv-SE.ini +++ b/options/locale/locale_sv-SE.ini @@ -2005,7 +2005,6 @@ runs.commit=Commit - [projects] [git.filemode] diff --git a/options/locale/locale_tr-TR.ini b/options/locale/locale_tr-TR.ini index 0c3884fd64887..9b7f2cb5c6d13 100644 --- a/options/locale/locale_tr-TR.ini +++ b/options/locale/locale_tr-TR.ini @@ -631,7 +631,6 @@ joined_on=%s tarihinde katıldı repositories=Depolar activity=Genel Aktivite followers=Takipçiler -show_more=Daha Fazla Göster starred=Yıldızlanmış depolar watched=İzlenen Depolar code=Kod @@ -3634,7 +3633,6 @@ variables.creation.success=`"%s" deÄŸiÅŸkeni eklendi.` variables.update.failed=DeÄŸiÅŸken düzenlenemedi. variables.update.success=DeÄŸiÅŸken düzenlendi. - [projects] deleted.display_name=SilinmiÅŸ Proje type-1.display_name=KiÅŸisel Proje diff --git a/options/locale/locale_uk-UA.ini b/options/locale/locale_uk-UA.ini index e8553594a0375..efefeeb436be7 100644 --- a/options/locale/locale_uk-UA.ini +++ b/options/locale/locale_uk-UA.ini @@ -466,7 +466,6 @@ change_avatar=Змінити Ñвій аватар… repositories=Репозиторії activity=Публічна активніÑть followers=Читачі -show_more=Показати більше starred=Обрані Репозиторії watched=ВідÑтежувані репозиторії projects=Проєкт @@ -2539,7 +2538,6 @@ runs.commit=Коміт - [projects] [git.filemode] diff --git a/options/locale/locale_zh-CN.ini b/options/locale/locale_zh-CN.ini index 572ad2d667c7e..c02d4d64b653d 100644 --- a/options/locale/locale_zh-CN.ini +++ b/options/locale/locale_zh-CN.ini @@ -649,7 +649,6 @@ joined_on=加入于 %s repositories=仓库列表 activity=公开活动 followers=关注者 -show_more=显示更多 starred=已点赞 watched=已关注仓库 code=ä»£ç  @@ -1688,10 +1687,8 @@ issues.time_estimate_invalid=é¢„è®¡æ—¶é—´æ ¼å¼æ— æ•ˆ issues.start_tracking_history=`开始工作 %s` issues.tracker_auto_close=当此工å•å…³é—­æ—¶ï¼Œè‡ªåŠ¨åœæ­¢è®¡æ—¶å™¨ issues.tracking_already_started=`ä½ å·²ç»å¼€å§‹å¯¹ å¦ä¸€ä¸ªå·¥å• 进行时间跟踪ï¼` -issues.stop_tracking_history=`åœæ­¢å·¥ä½œ %s` issues.cancel_tracking_history=`å–æ¶ˆæ—¶é—´è·Ÿè¸ª %s` issues.del_time=删除此时间跟踪日志 -issues.add_time_history=`添加计时 %s` issues.del_time_history=`已删除时间 %s` issues.add_time_manually=手动添加时间 issues.add_time_hours=å°æ—¶ @@ -3773,7 +3770,6 @@ variables.creation.success=å˜é‡ “%s†添加æˆåŠŸã€‚ variables.update.failed=编辑å˜é‡å¤±è´¥ã€‚ variables.update.success=该å˜é‡å·²è¢«ç¼–辑。 - [projects] deleted.display_name=已删除项目 type-1.display_name=个人项目 diff --git a/options/locale/locale_zh-HK.ini b/options/locale/locale_zh-HK.ini index e1ca6ebfc83ca..6f37d30efcf56 100644 --- a/options/locale/locale_zh-HK.ini +++ b/options/locale/locale_zh-HK.ini @@ -975,7 +975,6 @@ runners.task_list.repository=儲存庫 - [projects] [git.filemode] diff --git a/options/locale/locale_zh-TW.ini b/options/locale/locale_zh-TW.ini index a3bf6ca88863c..3b1d37a322afc 100644 --- a/options/locale/locale_zh-TW.ini +++ b/options/locale/locale_zh-TW.ini @@ -4,7 +4,6 @@ explore=探索 help=說明 logo=標誌 sign_in=登入 -sign_in_with_provider=使用 %s 帳戶登入 sign_in_or=或 sign_out=登出 sign_up=註冊 @@ -17,7 +16,6 @@ template=æ¨¡æ¿ language=語言 notifications=通知 active_stopwatch=進行中的時間追蹤 -tracked_time_summary=ç›®å‰çš„ issue 累計時長 create_new=建立... user_profile_and_more=個人資料和設定... signed_in_as=已登入 @@ -25,7 +23,6 @@ enable_javascript=æœ¬ç¶²ç«™éœ€è¦ JavaScript。 toc=目錄 licenses=æŽˆæ¬Šæ¢æ¬¾ return_to_gitea=返回 Gitea -more_items=更多項目 username=帳號 email=é›»å­ä¿¡ç®± @@ -83,8 +80,6 @@ milestones=里程碑 ok=ç¢ºèª cancel=å–æ¶ˆ retry=é‡è©¦ -rerun=釿–°åŸ·è¡Œ -rerun_all=釿–°åŸ·è¡Œæ‰€æœ‰å·¥ä½œ save=儲存 add=增加 add_all=全部增加 @@ -92,19 +87,14 @@ remove=移除 remove_all=全部移除 remove_label_str=移除項目「%s〠edit=編輯 -view=檢視 -test=測試 enabled=已啟用 disabled=å·²åœç”¨ -locked=已上鎖 copy=複製 copy_url=複製 URL -copy_hash=複製哈希值 copy_content=複製內容 copy_branch=複製分支å稱 -copy_path=複製路徑 copy_success=複製æˆåŠŸï¼ copy_error=複製失敗 copy_type_unsupported=無法複製此類型的檔案 @@ -115,8 +105,6 @@ loading=載入中… error=錯誤 error404=您正嘗試訪å•çš„é é¢ ä¸å­˜åœ¨ 或 您尚未被授權 查看該é é¢ã€‚ -go_back=返回 -invalid_data=無效的資料: %v never=從來沒有 unknown=未知 @@ -126,67 +114,25 @@ rss_feed=RSS æ‘˜è¦ pin=固定 unpin=å–æ¶ˆå›ºå®š -artifacts=檔案或工件 -confirm_delete_artifact=你確定è¦åˆªé™¤é€™å€‹æª”案 '%s' 嗎? archived=å·²å°å­˜ -concept_system_global=全域 -concept_user_individual=個人 concept_code_repository=儲存庫 concept_user_organization=組織 -show_timestamps=顯示時間戳記 -show_log_seconds=顯示秒數 -show_full_screen=全螢幕顯示 -download_logs=下載記錄 -confirm_delete_selected=確定è¦åˆªé™¤æ‰€æœ‰å·²é¸å–的項目嗎? name=å稱 value=值 filter=ç¯©é¸ -filter.clear=清除篩é¸å™¨ filter.is_archived=å·²å°å­˜ -filter.not_archived=未歸檔 -filter.is_fork=已分支 -filter.not_fork=未分支 -filter.is_mirror=å·²é¡åƒ -filter.not_mirror=䏿˜¯é¡åƒ filter.is_template=æ¨¡æ¿ -filter.not_template=䏿˜¯ç¯„本 filter.public=公開 filter.private=ç§æœ‰ -no_results_found=找ä¸åˆ°çµæžœã€‚ -internal_error_skipped=已略éŽå…§éƒ¨éŒ¯èª¤ï¼š%s [search] -search=æœå°‹â€¦ -type_tooltip=æœå°‹é¡žåž‹ -fuzzy=模糊 -fuzzy_tooltip=包å«èˆ‡æœå°‹è©žæŽ¥è¿‘çš„çµæžœ -exact=精確 -exact_tooltip=åªåŒ…å«å®Œå…¨ç¬¦åˆé—œéµå­—çš„çµæžœ -repo_kind=æœå°‹å„²å­˜åº«â€¦ -user_kind=æœå°‹ä½¿ç”¨è€…… -org_kind=æœå°‹çµ„織… -team_kind=æœå°‹åœ˜éšŠâ€¦ -code_kind=æœå°‹ä»£ç¢¼â€¦ -code_search_unavailable=ç¾åœ¨ç„¡æ³•使用原始碼æœå°‹ã€‚請與網站管ç†å“¡è¯çµ¡ã€‚ -code_search_by_git_grep=ç›®å‰çš„原始碼æœå°‹çµæžœæ˜¯ç”±ã€Œgit grepã€æä¾›ã€‚å¦‚æžœç¶²ç«™ç®¡ç†è€…啟用 Repository Indexer å¯èƒ½å¯ä»¥æä¾›æ›´å¥½çš„çµæžœã€‚ -package_kind=æœå°‹è»Ÿé«”包... -project_kind=æœå°‹å°ˆæ¡ˆâ€¦ -branch_kind=æœå°‹åˆ†æ”¯â€¦ -tag_kind=æœå°‹æ¨™ç±¤â€¦ -tag_tooltip=æœå°‹ç¬¦åˆçš„æ¨™ç±¤ã€‚使用「%ã€ä»¥æ¯”å°ä»»æ„長度的數字。 -commit_kind=æœå°‹æäº¤æ­·å²â€¦ -runner_kind=æœå°‹ Runner... -no_results=找ä¸åˆ°ç¬¦åˆçš„çµæžœã€‚ -issue_kind=æœå°‹è­°é¡Œâ€¦ -pull_kind=æœå°‹åˆä½µè«‹æ±‚... -keyword_search_unavailable=ç¾åœ¨ç„¡æ³•使用關éµå­—æœå°‹ã€‚請與網站管ç†å“¡è¯çµ¡ã€‚ [aria] navbar=導航列 @@ -210,13 +156,9 @@ buttons.link.tooltip=æ–°å¢žé€£çµ buttons.list.unordered.tooltip=新增項目符號清單 buttons.list.ordered.tooltip=新增編號清單 buttons.list.task.tooltip=新增工作項目清單 -buttons.table.add.tooltip=新增表格 buttons.table.add.insert=增加 -buttons.table.rows=行 -buttons.table.cols=列 buttons.mention.tooltip=æåŠä½¿ç”¨è€…或團隊 buttons.ref.tooltip=åƒè€ƒå•題或åˆä½µè«‹æ±‚ -buttons.switch_to_legacy.tooltip=使用舊版編輯器代替 buttons.enable_monospace_font=啟用等寬字型 buttons.disable_monospace_font=åœç”¨ç­‰å¯¬å­—åž‹ @@ -226,20 +168,16 @@ string.desc=Z - A [error] occurred=發生錯誤 -report_message=如果你確定這是一個 Gitea çš„ bug,請去 GitHub æœå°‹ç›¸é—œçš„å•題,如果有需è¦ä½ ä¹Ÿå¯ä»¥é–‹ä¸€å€‹æ–°çš„å•題 not_found=找ä¸åˆ°ç›®æ¨™ã€‚ network_error=網路錯誤 [startpage] app_desc=一套極易架設的 Git æœå‹™ install=安è£å®¹æ˜“ -install_desc=直接用 執行檔安è£ï¼Œé‚„å¯ä»¥é€éŽ Docker部屬,或是å–å¾— 套件。 platform=è·¨å¹³å° -platform_desc=Gitea å¯ä»¥åœ¨æ‰€æœ‰èƒ½ç·¨è­¯ Go 語言的平å°ä¸ŠåŸ·è¡Œ: Windows, macOS, Linux, ARM 等等。挑一個您喜歡的å§ï¼ lightweight=輕é‡ç´š lightweight_desc=一片便宜的 Raspberry Pi å°±å¯ä»¥æ»¿è¶³ Gitea çš„æœ€ä½Žéœ€æ±‚ã€‚ç¯€çœæ‚¨çš„æ©Ÿå™¨è³‡æºï¼ license=開放原始碼 -license_desc=å–å¾— code.gitea.io/gitea ï¼æˆç‚ºä¸€åè²¢ç»è€…和我們一起讓 Gitea 更好,快點加入我們å§ï¼ [install] install=安è£é é¢ @@ -278,7 +216,6 @@ repo_path_helper=所有é ç«¯ Git 儲存庫會儲存到此目錄。 lfs_path=Git LFS 根目錄 lfs_path_helper=以 Git LFS 儲存檔案時會被儲存在此目錄中。請留空以åœç”¨ LFS 功能。 run_user=以使用者å稱執行 -run_user_helper=輸入 Gitea 執行的作業系統使用者å稱。請注æ„ï¼Œæ­¤ä½¿ç”¨è€…å¿…é ˆæ“æœ‰å­˜å„²åº«æ ¹ç›®éŒ„çš„å­˜å–æ¬Šé™ã€‚ domain=伺æœå™¨åŸŸå domain_helper=伺æœå™¨çš„åŸŸåæˆ–主機ä½ç½®ã€‚ ssh_port=SSH 伺æœå™¨åŸ  @@ -295,7 +232,6 @@ email_title=é›»å­éƒµä»¶è¨­å®š smtp_addr=SMTP 主機 smtp_port=SMTP 連接埠 smtp_from=é›»å­éƒµä»¶å¯„件者 -smtp_from_invalid=「以此電å­ä¿¡ç®±å¯„é€ã€çš„地å€ç„¡æ•ˆ smtp_from_helper=Gitea 將會使用的電å­ä¿¡ç®±ï¼Œç›´æŽ¥è¼¸å…¥é›»å­ä¿¡ç®±æˆ–使用「"å稱" ã€çš„æ ¼å¼ã€‚ mailer_user=SMTP 帳號 mailer_password=SMTP 密碼 @@ -351,12 +287,8 @@ invalid_password_algorithm=無效的密碼雜湊演算法 password_algorithm_helper=設定密碼雜湊演算法。演算法有ä¸åŒçš„需求與強度。argon2 演算法雖然較安全但會使用大é‡è¨˜æ†¶é«”,å¯èƒ½ä¸é©ç”¨æ–¼å°åž‹ç³»çµ±ã€‚ enable_update_checker=啟用更新檢查器 enable_update_checker_helper=定期連線到 gitea.io 檢查更新。 -env_config_keys=環境組態設定 -env_config_keys_prompt=下列環境變數也會套用到您的組態檔: -config_write_file_prompt=這些é…ç½®é¸é …將被寫入到: %s [home] -nav_menu=導覽é¸å–® uname_holder=帳號或電å­ä¿¡ç®± password_holder=密碼 switch_dashboard_context=切æ›è³‡è¨Šä¸»é å¸³æˆ¶ @@ -386,7 +318,6 @@ issues.in_your_repos=在您的儲存庫中 repos=儲存庫 users=使用者 organizations=組織 -go_to=å‰å¾€ code=程å¼ç¢¼ code_last_indexed_at=最後索引 %s relevant_repositories_tooltip=已隱è—缺少主題ã€åœ–示ã€èªªæ˜Žã€Fork 的儲存庫。 @@ -394,38 +325,27 @@ relevant_repositories=åªé¡¯ç¤ºç›¸é—œçš„å„²å­˜åº«ï¼Œé¡¯ç¤ºæœªç¯©é¸ [auth] create_new_account=註冊帳戶 -already_have_account=已經有帳號嗎? -sign_in_now=ç«‹å³ç™»å…¥! disable_register_prompt=註冊功能已åœç”¨ã€‚ è«‹è¯ç¹«æ‚¨çš„網站管ç†å“¡ã€‚ disable_register_mail=å·²åœç”¨è¨»å†Šç¢ºèªé›»å­éƒµä»¶ã€‚ manual_activation_only=è«‹è¯çµ¡æ‚¨çš„網站管ç†å“¡ä»¥å®Œæˆå•Ÿç”¨ç¨‹åºã€‚ remember_me=記得這個è£ç½® -remember_me.compromised=這個登入 token 已經失效,å¯èƒ½ä»£è¡¨è‘—å¸³è™Ÿå·²è¢«å…¥ä¾µã€‚è«‹ç¢ºèªæ‚¨çš„å¸³è™Ÿæ˜¯å¦æœ‰ä¸å°‹å¸¸çš„æ´»å‹•。 forgot_password_title=忘記密碼 forgot_password=忘記密碼? -need_account=需è¦ä¸€å€‹å¸³è™Ÿ? -sign_up_now=還沒有帳戶?馬上註冊。 -sign_up_successful=帳戶已æˆåŠŸå»ºç«‹ã€‚æ­¡è¿Žæ‚¨! -confirmation_mail_sent_prompt_ex=新的確èªä¿¡å·²å¯„到%s。請在接下來的 %s ç¢ºèªæ‚¨çš„æ”¶ä»¶å¤¾ä¾†å®Œæˆè¨»å†Šæ‰‹çºŒã€‚å¦‚æžœæ‚¨çš„è¨»å†Šåœ°å€æœ‰éŒ¯èª¤ï¼Œæ‚¨å¯ä»¥å†æ¬¡ç™»å…¥ä¸¦ä¿®æ”¹å®ƒã€‚ must_change_password=更新您的密碼 allow_password_change=è¦æ±‚使用者更改密碼 (推薦) reset_password_mail_sent_prompt=確èªä¿¡å·²ç™¼é€è‡³ %s。請在 %s內檢查您的收件匣並完æˆå¸³æˆ¶æ•‘æ´ä½œæ¥­ã€‚ active_your_account=啟用您的帳戶 account_activated=帳戶已啟用 prohibit_login=ç¦æ­¢ç™»å…¥ -prohibit_login_desc=æ‚¨çš„å¸³æˆ¶è¢«ç¦æ­¢ç™»å…¥ï¼Œè«‹è¯çµ¡ç¶²ç«™ç®¡ç†å“¡ resent_limit_prompt=抱歉,您請求發é€é©—證電å­éƒµä»¶å¤ªéŽé »ç¹ï¼Œè«‹ç­‰å¾… 3 分é˜å¾Œå†è©¦ä¸€æ¬¡ã€‚ has_unconfirmed_mail=%s 您好,您有一å°ç™¼é€è‡³( %s) 但未被確èªçš„郵件。如果您未收到啟用郵件,或需è¦é‡æ–°ç™¼é€ï¼Œè«‹å–®æ“Šä¸‹æ–¹çš„æŒ‰éˆ•。 -change_unconfirmed_mail_address=如果您註冊的電å­éƒµä»¶åœ°å€æœ‰éŒ¯èª¤ï¼Œæ‚¨å¯ä»¥åœ¨é€™é‚Šæ›´æ­£ï¼Œä¸¦é‡æ–°å¯„é€ç¢ºèªéƒµä»¶ã€‚ resend_mail=å–®æ“Šæ­¤è™•é‡æ–°ç™¼é€ç¢ºèªéƒµä»¶ email_not_associate=此電å­ä¿¡ç®±æœªèˆ‡ä»»ä½•å¸³æˆ¶é€£çµ send_reset_mail=發é€å¸³æˆ¶æ•‘æ´ä¿¡ reset_password=å¸³æˆ¶æ•‘æ´ invalid_code=您的確èªä»£ç¢¼ç„¡æ•ˆæˆ–å·²éŽæœŸã€‚ -invalid_code_forgot_password=您的確èªä»£ç¢¼ç„¡æ•ˆæˆ–å·²éŽæœŸã€‚開啟 é€™å€‹é€£çµ é–‹å§‹æ–°çš„ session。 invalid_password=您的密碼和用來建立帳戶的ä¸ç¬¦ã€‚ reset_password_helper=å¸³æˆ¶æ•‘æ´ -reset_password_wrong_user=您已經使用 %s 的帳戶登入,但帳戶救æ´é€£çµæ˜¯çµ¦ %s çš„ password_too_short=密碼長度ä¸èƒ½å°‘æ–¼ %d å€‹å­—ï¼ non_local_account=éžæœ¬åœ°å¸³æˆ¶ç„¡æ³•é€éŽ Gitea 的網é ä»‹é¢æ›´æ”¹å¯†ç¢¼ã€‚ verify=é©—è­‰ @@ -445,13 +365,11 @@ oauth_signin_submit=連çµå¸³æˆ¶ oauth.signin.error=è™•ç†æŽˆæ¬Šè«‹æ±‚æ™‚ç™¼ç”ŸéŒ¯èª¤ã€‚å¦‚æžœé€™å€‹å•題æŒçºŒç™¼ç”Ÿï¼Œè«‹è¯çµ¡ç¶²ç«™ç®¡ç†å“¡ã€‚ oauth.signin.error.access_denied=授權請求被拒絕。 oauth.signin.error.temporarily_unavailable=授權失敗,因為èªè­‰ä¼ºæœå™¨æš«æ™‚無法使用。請ç¨å¾Œå†è©¦ã€‚ -oauth_callback_unable_auto_reg=自助註冊已啟用,但是 OAuth2 æä¾›è€… %[1]s å›žå‚³çš„çµæžœç¼ºå°‘欄ä½ï¼š%[2]s,導致無法自動建立帳號。請建立新帳號或是連çµè‡³æ—¢å­˜çš„帳號,或是è¯çµ¡ç¶²ç«™ç®¡ç†è€…。 openid_connect_submit=連接 openid_connect_title=é€£æŽ¥åˆ°ç¾æœ‰å¸³æˆ¶ openid_connect_desc=所é¸çš„ OpenID URI 未知。在這裡連çµä¸€å€‹æ–°å¸³æˆ¶ã€‚ openid_register_title=建立新帳戶 openid_register_desc=所é¸çš„ OpenID URI 未知。在這裡連çµä¸€å€‹æ–°å¸³æˆ¶ã€‚ -openid_signin_desc=輸入您的 OpenID ä½å€ã€‚例如:alice.openid.example.org 或是 https://openid.example.org/alice。 disable_forgot_password_mail=由於未設定電å­éƒµä»¶åŠŸèƒ½ï¼Œå¸³æˆ¶æ•‘æ´åŠŸèƒ½å·²è¢«åœç”¨ã€‚請與網站管ç†å“¡è¯çµ¡ã€‚ disable_forgot_password_mail_admin=帳戶救æ´åŠŸèƒ½éœ€è¦è¨­å®šé›»å­éƒµä»¶åŠŸèƒ½æ‰èƒ½ä½¿ç”¨ã€‚請設定電å­éƒµä»¶åŠŸèƒ½ä»¥å•Ÿç”¨å¸³æˆ¶æ•‘æ´åŠŸèƒ½ã€‚ email_domain_blacklisted=您無法使用您的電å­ä¿¡ç®±è¨»å†Šå¸³è™Ÿã€‚ @@ -461,13 +379,8 @@ authorize_application_created_by=æ­¤æ‡‰ç”¨ç¨‹å¼æ˜¯ç”± %s 建立的。 authorize_application_description=如果您å…許,它將能夠讀å–å’Œä¿®æ”¹æ‚¨çš„æ‰€æœ‰å¸³æˆ¶è³‡è¨Šï¼ŒåŒ…æ‹¬ç§æœ‰å„²å­˜åº«å’Œçµ„織。 authorize_title=授權「%sã€å­˜å–您的帳戶? authorization_failed=授權失效 -authorization_failed_desc=æŽˆæ¬Šå¤±æ•—ï¼Œå› ç‚ºæˆ‘å€‘åµæ¸¬åˆ°ç„¡æ•ˆçš„請求。請è¯çµ¡æ‚¨æ¬²æŽˆæ¬Šä¹‹æ‡‰ç”¨ç¨‹å¼çš„維護人員。 sspi_auth_failed=SSPI èªè­‰å¤±æ•— -password_pwned=æ‚¨é¸æ“‡çš„密碼已被列於被盜密碼清單中,該清單因公共資料外洩而暴露。請試試其他密碼。 password_pwned_err=無法完æˆå° HaveIBeenPwned 的請求。 -last_admin=您無法移除最後一個管ç†å“¡ã€‚至少需è¦ä¿ç•™ä¸€å€‹ç®¡ç†å“¡å¸³æˆ¶ã€‚ -signin_passkey=使用 Passkey 登入 -back_to_sign_in=返回至登入 [mail] view_it_on=在 %s 上查看 @@ -481,10 +394,8 @@ activate_account.text_1=%[1]s æ‚¨å¥½ï¼Œæ„Ÿè¬æ‚¨è¨»å†Š %[2]sï¼ activate_account.text_2=請在 %s內點擊下列連çµä»¥å•Ÿç”¨æ‚¨çš„帳戶: activate_email=請驗證您的電å­ä¿¡ç®± -activate_email.title=%s,請驗證您的電å­ä¿¡ç®± activate_email.text=請在 %s內點擊下列連çµä»¥é©—證您的電å­ä¿¡ç®±ï¼š -register_notify=歡迎來到 Gitea register_notify.title=%[1]s,歡迎來到 %[2]s register_notify.text_1=這是您在 %s 的註冊確èªä¿¡ï¼ register_notify.text_2=您ç¾åœ¨å¯ä»¥ç”¨å¸³è™Ÿ %s 登入。 @@ -586,9 +497,6 @@ lang_select_error=å¾žæ¸…å–®ä¸­é¸æ“‡ä¸€å€‹èªžè¨€ã€‚ username_been_taken=帳號已被使用 username_change_not_local_user=éžæœ¬åœ°ä½¿ç”¨è€…ä¸å…許更改他們的帳號。詳細資訊請è¯çµ¡æ‚¨çš„系統管ç†å“¡ã€‚ -change_username_disabled=更改使用者å稱功能已被åœç”¨ã€‚ -change_full_name_disabled=更改完整å稱功能已被åœç”¨ã€‚ -username_has_not_been_changed=使用者å稱並未變更 repo_name_been_taken=儲存庫å稱已被使用。 repository_force_private=å·²å•Ÿç”¨ã€Œå¼·åˆ¶ç§æœ‰ã€ï¼šç§æœ‰å„²å­˜åº«ä¸èƒ½è¢«å…¬é–‹ã€‚ repository_files_already_exist=此儲存庫的檔案已存在,請è¯çµ¡ç³»çµ±ç®¡ç†æœ‰ã€‚ @@ -602,7 +510,6 @@ team_name_been_taken=團隊å稱已被使用。 team_no_units_error=è«‹è‡³å°‘é¸æ“‡ä¸€å€‹å„²å­˜åº«å€åŸŸã€‚ email_been_used=此電å­ä¿¡ç®±å·²è¢«ä½¿ç”¨ email_invalid=此電å­ä¿¡ç®±ç„¡æ•ˆã€‚ -email_domain_is_not_allowed=使用者的電å­éƒµä»¶åœ°å€ %s 與電å­éƒµä»¶åŸŸåå…許清單或是電å­éƒµä»¶åŸŸåç¦æ­¢æ¸…單有è¡çªã€‚è«‹ç¢ºèªæ‚¨é æœŸåŸ·è¡Œé€™å€‹å‹•作。 openid_been_used=OpenID ä½å€ã€Œ%sã€å·²è¢«ä½¿ç”¨ã€‚ username_password_incorrect=å¸³è™Ÿæˆ–å¯†ç¢¼ä¸æ­£ç¢º password_complexity=密碼複雜度沒有通éŽä»¥ä¸‹çš„è¦æ±‚: @@ -614,8 +521,6 @@ enterred_invalid_repo_name=您輸入的儲存庫åç¨±ä¸æ­£ç¢ºã€‚ enterred_invalid_org_name=您輸入的組織åç¨±ä¸æ­£ç¢ºã€‚ enterred_invalid_owner_name=æ–°çš„æ“æœ‰è€…å稱無效。 enterred_invalid_password=æ‚¨è¼¸å…¥çš„å¯†ç¢¼ä¸æ­£ç¢ºã€‚ -unset_password=登入的使用者並未設定密碼。 -unsupported_login_type=這個登入方å¼ä¸¦ä¸æ”¯æ´åˆªé™¤å¸³è™Ÿã€‚ user_not_exist=該用戶åä¸å­˜åœ¨ team_not_exist=團隊ä¸å­˜åœ¨ last_org_owner=ä½ ä¸èƒ½å¾žã€ŒOwnersã€åœ˜éšŠä¸­åˆªé™¤æœ€å¾Œä¸€å€‹ä½¿ç”¨è€…。æ¯å€‹çµ„ç¹”ä¸­è‡³å°‘è¦æœ‰ä¸€å€‹æ“有者。 @@ -637,17 +542,13 @@ org_still_own_repo=此組織ä»ç„¶æ“有一個以上的儲存庫,請先刪除 org_still_own_packages=此組織ä»ç„¶æ“有一個以上的套件,請先刪除它們。 target_branch_not_exist=目標分支ä¸å­˜åœ¨ -target_ref_not_exist=目標åƒè€ƒä¸å­˜åœ¨ %s -admin_cannot_delete_self=當您是管ç†è€…時,您無法移除自己。請先移除您的管ç†è€…權é™ã€‚ [user] change_avatar=更改大頭貼... -joined_on=加入於 %s repositories=儲存庫 activity=公開動態 followers=追蹤者 -show_more=顯示更多 starred=已加星號 watched=關注的儲存庫 code=程å¼ç¢¼ @@ -660,36 +561,11 @@ user_bio=個人簡介 disabled_public_activity=這個使用者已å°å¤–éš±è—å‹•æ…‹ email_visibility.limited=所有已驗證的使用者都å¯ä»¥çœ‹åˆ°æ‚¨çš„é›»å­ä¿¡ç®±åœ°å€ email_visibility.private=åªæœ‰æ‚¨å’Œç³»çµ±ç®¡ç†å“¡å¯ä»¥çœ‹åˆ°æ‚¨çš„é›»å­ä¿¡ç®±åœ°å€ -show_on_map=在地圖上顯示此ä½ç½® -settings=使用者設定 form.name_reserved=「%sã€æ˜¯ä¿ç•™çš„帳號。 form.name_pattern_not_allowed=帳號ä¸å¯åŒ…å«å­—元「%sã€ã€‚ form.name_chars_not_allowed=帳號「%sã€åŒ…å«ç„¡æ•ˆå­—元。 -block.block=å°éŽ– -block.block.user=å°éŽ–ä½¿ç”¨è€… -block.block.org=為組織阻擋使用者 -block.block.failure=無法å°éŽ–ä½¿ç”¨è€…: %s -block.unblock=解除å°éŽ– -block.unblock.failure=無法解除å°éŽ–ä½¿ç”¨è€…: %s -block.blocked=您已å°éŽ–è©²ä½¿ç”¨è€…ã€‚ -block.title=å°éŽ–ä½¿ç”¨è€… -block.info=阻擋使用者å¯é¿å…他們與儲存庫互動,如在建立åˆä½µè«‹æ±‚或是å•é¡Œä»¥åŠæ–¼å…¶ä¸Šç•™è¨€ã€‚了解更多關於阻擋一個使用者。 -block.info_1=阻擋一個使用者å¯é¿å…å…¶å°æ‚¨çš„帳號或是儲存庫進行以下動作: -block.info_2=正在追蹤你的帳戶 -block.info_3=é€éŽ @mentioning 標記您的使用者å稱來通知您 -block.info_4=邀請您æˆç‚ºä»–們的儲存庫的å”作者 -block.info_5=å°å„²å­˜åº«åŠ ä¸Šæ˜Ÿè™Ÿã€å»ºç«‹åˆ†ä¹‹æˆ–是關注儲存庫 -block.info_6=建立å•題或是åˆä½µè«‹æ±‚,或是å°å…¶é€²è¡Œç•™è¨€ -block.info_7=å°æ‚¨åœ¨å•題或是åˆä½µè«‹æ±‚中的留言é€å‡ºå應 -block.user_to_block=欲阻擋的使用者 -block.note=備註 -block.note.title=é¸ç”¨é™„註: -block.note.info=è¢«é˜»æ“‹çš„ä½¿ç”¨è€…ä¸æœƒçœ‹åˆ°é€™å€‹é™„註。 -block.note.edit=編輯備註 -block.list=å·²å°éŽ–çš„ä½¿ç”¨è€… -block.list.none=您尚未å°éŽ–ä»»ä½•ä½¿ç”¨è€…ã€‚ [settings] profile=個人資料 @@ -704,17 +580,10 @@ applications=æ‡‰ç”¨ç¨‹å¼ orgs=管ç†çµ„ç¹” repos=儲存庫 delete=刪除帳戶 -twofa=兩步驟驗證 account_link=已連çµå¸³è™Ÿ organization=組織 -webauthn=安全金鑰 public_profile=公開的個人資料 -biography_placeholder=告訴我們一些關於您的事情å§! (您å¯ä»¥ä½¿ç”¨ Markdown) -location_placeholder=與其他人分享您的大概ä½ç½® -profile_desc=控制您的個人檔案會如何呈ç¾çµ¦å…¶å¥¹ä½¿ç”¨è€…。您的主è¦é›»å­éƒµä»¶åœ°å€æœƒè¢«ç”¨æ–¼é€šçŸ¥ã€å¯†ç¢¼æ•‘æ´ä»¥åŠç¶²é ä¸Šçš„ Git æ“作。 -password_username_disabled=éžæœ¬åœ°ä½¿ç”¨è€…ä¸å…許更改他們的帳號。詳細資訊請è¯çµ¡æ‚¨çš„系統管ç†å“¡ã€‚ -password_full_name_disabled=您ä¸è¢«å…許更改他們的全å。詳細資訊請è¯çµ¡æ‚¨çš„系統管ç†å“¡ã€‚ full_name=å…¨å website=個人網站 location=æ‰€åœ¨åœ°å€ @@ -725,16 +594,11 @@ update_language_not_found=無法使用語言「%sã€ã€‚ update_language_success=已更新語言。 update_profile_success=已更新您的個人資料。 change_username=您的帳號已更改。 -change_username_prompt=註:更改您的使用å稱也會更改您的帳號網å€ã€‚ -change_username_redirect_prompt=舊的帳號被領用å‰ï¼Œæœƒé‡æ–°å°Žå‘您的新帳號。 continue=繼續 cancel=å–æ¶ˆ language=語言 ui=佈景主題 hidden_comment_types=éš±è—的留言類型 -hidden_comment_types_description=此處勾é¸çš„è©•è«–é¡žåž‹å°‡ä¸æœƒé¡¯ç¤ºåœ¨å•題é é¢å…§ã€‚例如勾é¸ã€Œæ¨™ç±¤ã€å°‡ç§»é™¤æ‰€æœ‰çš„"{user} 新增/移除{label}" 評論。 -hidden_comment_types.ref_tooltip=當這個å•題在其他的å•é¡Œã€æäº¤â€¦ç­‰åœ°æ–¹è¢«å¼•ç”¨æ™‚çš„ç•™è¨€ -hidden_comment_types.issue_ref_tooltip=當使用者更改與這個å•題相關è¯çš„åˆ†æ”¯ã€æ¨™ç±¤æ™‚的留言 comment_type_group_reference=åƒè€ƒ comment_type_group_label=標籤 comment_type_group_milestone=里程碑 @@ -751,7 +615,6 @@ comment_type_group_project=專案 comment_type_group_issue_ref=å•題åƒè€ƒ saved_successfully=您的設定已æˆåŠŸå„²å­˜ã€‚ privacy=éš±ç§ -keep_activity_private=éš±è—個人檔案上的動態 keep_activity_private_popup=è®“å‹•æ…‹åªæœ‰ä½ å’Œç®¡ç†å“¡çœ‹å¾—到 lookup_avatar_by_mail=以電å­ä¿¡ç®±æŸ¥è©¢å¤§é ­è²¼ @@ -761,10 +624,8 @@ choose_new_avatar=鏿“‡æ–°çš„大頭貼 update_avatar=更新大頭貼 delete_current_avatar=刪除目å‰çš„大頭貼 uploaded_avatar_not_a_image=ä¸Šå‚³çš„æª”æ¡ˆä¸æ˜¯åœ–片 -uploaded_avatar_is_too_big=ä¸Šå‚³æª”æ¡ˆå¤§å° (%d KiB) è¶…éŽå¤§å°ä¸Šé™ (%d KiB) update_avatar_success=您的大頭貼已更新 update_user_avatar_success=已更新使用者的大頭貼。 -cropper_prompt=您å¯ä»¥åœ¨å„²å­˜å‰ç·¨è¼¯åœ–片。編輯後的圖片將以 PNG æ ¼å¼å„²å­˜ã€‚ change_password=更新密碼 old_password=ç›®å‰çš„密碼 @@ -778,17 +639,13 @@ emails=é›»å­ä¿¡ç®± manage_emails=管ç†é›»å­ä¿¡ç®± manage_themes=鏿“‡é è¨­ä½ˆæ™¯ä¸»é¡Œ manage_openid=ç®¡ç† OpenID ä½å€ -email_desc=您的主è¦é›»å­ä¿¡ç®±å°‡ç”¨æ–¼é€šçŸ¥ã€å¯†ç¢¼æ¢å¾©ä»¥åŠï¼ˆå¦‚果未隱è—)基於網é çš„ Git æ“作。 theme_desc=這將是您在整個網站上的é è¨­ä½ˆæ™¯ä¸»é¡Œã€‚ -theme_colorblindness_help=è‰²ç›²ä¸»é¡Œæ”¯æ´ -theme_colorblindness_prompt=Gitea 剛å–得了一些具有基本色盲支æ´çš„主題,其中僅定義了幾種é¡è‰²ã€‚這項工作ä»åœ¨é€²è¡Œä¸­ã€‚é€éŽåœ¨ä¸»é¡Œ CSS 檔案中定義更多é¡è‰²å¯ä»¥å®Œæˆæ›´å¤šæ”¹é€²ã€‚ primary=ä¸»è¦ activated=已啟用 requires_activation=需è¦å•Ÿå‹• primary_email=è¨­ç‚ºä¸»è¦ activate_email=寄出啟用信 activations_pending=等待啟用中 -can_not_add_email_activations_pending=有一個待處ç†çš„å•Ÿç”¨ï¼Œè‹¥è¦æ–°å¢žé›»å­ä¿¡ç®±ï¼Œè«‹å¹¾åˆ†é˜å¾Œå†è©¦ã€‚ delete_email=移除 email_deletion=移除電å­ä¿¡ç®± email_deletion_desc=é›»å­ä¿¡ç®±å’Œç›¸é—œè³‡è¨Šå°‡å¾žæ‚¨çš„帳戶中刪除,由此電å­ä¿¡ç®±æ‰€æäº¤çš„ Git å°‡ä¿æŒä¸è®Šï¼Œæ˜¯å¦ç¹¼çºŒï¼Ÿ @@ -802,13 +659,11 @@ add_new_email=新增電å­ä¿¡ç®± add_new_openid=新增 OpenID URI add_email=新增電å­ä¿¡ç®± add_openid=新增 OpenID URI -add_email_confirmation_sent=確èªä¿¡å·²ç™¼é€è‡³ã€Œ%sã€ï¼Œè«‹åœ¨ %så…§æª¢æŸ¥æ‚¨çš„æ”¶ä»¶åŒ£ä¸¦ç¢ºèªæ‚¨çš„é›»å­ä¿¡ç®±ã€‚ add_email_success=已加入新的電å­ä¿¡ç®±ã€‚ email_preference_set_success=已套用郵件å好設定 add_openid_success=已加入新的 OpenID 地å€ã€‚ keep_email_private=éš±è—é›»å­ä¿¡ç®± -keep_email_private_popup=é€™å°‡éš±è—æ‚¨çš„é›»å­éƒµä»¶åœ°å€ï¼Œç„¡è«–是在您的個人資料中,還是在您使用網é ä»‹é¢é€²è¡Œ pull request 或編輯文件時。推é€çš„æäº¤å°‡ä¸æœƒè¢«ä¿®æ”¹ã€‚使用 %s 在æäº¤ä¸­å°‡å…¶èˆ‡æ‚¨çš„帳戶關è¯ã€‚ -openid_desc=OpenID 讓您å¯ä»¥æŽˆæ¬Šèªè­‰çµ¦å¤–部æœå‹™ã€‚ +openid_desc=OpenID 讓你å¯ä»¥æŽˆæ¬Šèªè­‰çµ¦å¤–部æœå‹™ã€‚ manage_ssh_keys=ç®¡ç† SSH 金鑰 manage_ssh_principals=ç®¡ç† SSH èªè­‰ä¸»é«” @@ -869,8 +724,6 @@ ssh_principal_deletion_desc=移除 SSH èªè­‰ä¸»é«”å°‡æ’¤éŠ·å…¶å°æ‚¨å¸³æˆ¶çš„ ssh_key_deletion_success=SSH 金鑰已被移除。 gpg_key_deletion_success=GPG 金鑰已被移除。 ssh_principal_deletion_success=已移除主體。 -added_on=新增於 %s -valid_until_date=有效直到 %s valid_forever=æ°¸é æœ‰æ•ˆ last_used=上次使用在 no_activity=沒有近期動態 @@ -882,12 +735,9 @@ principal_state_desc=此主體在éŽåŽ» 7 天內曾被使用 show_openid=在個人資料顯示 hide_openid=å¾žå€‹äººè³‡æ–™éš±è— ssh_disabled=å·²åœç”¨ SSH -ssh_signonly=ç›®å‰å·²åœç”¨ SSH,因此這些金鑰僅用於 commit 簽章驗證。 ssh_externally_managed=æ­¤ SSH 金鑰由此使用者的外部æœå‹™æ‰€ç®¡ç† manage_social=管ç†é—œè¯çš„社群帳戶 -social_desc=這些帳號å¯ä»¥ç”¨å·²ç™»å…¥ä½ çš„帳號,請確èªä½ çŸ¥é“它們。 unbind=è§£é™¤é€£çµ -unbind_success=社群帳號刪除æˆåŠŸ manage_access_token=ç®¡ç† Access Token generate_new_token=產生新的 Token @@ -902,17 +752,8 @@ access_token_deletion_cancel_action=å–æ¶ˆ access_token_deletion_confirm_action=刪除 access_token_deletion_desc=刪除 Token 後,使用此 Token 的應用程å¼å°‡ç„¡æ³•å†å­˜å–您的帳戶,此動作ä¸å¯é‚„原。是å¦ç¹¼çºŒï¼Ÿ delete_token_success=已刪除 Token。使用此 Token 的應用程å¼ç„¡æ³•å†å­˜å–您的帳戶。 -repo_and_org_access=å„²å­˜åº«å’Œçµ„ç¹”å­˜å– -permissions_public_only=僅公開 -permissions_access_all=全部 (公開ã€ç§æœ‰èˆ‡å—é™) -select_permissions=鏿“‡æ¬Šé™ -permission_not_set=尚未設定 permission_no_access=æ²’æœ‰æ¬Šé™ permission_read=è®€å– -permission_write=讀å–和寫入 -access_token_desc=鏿“‡çš„ token 權é™åƒ…陿–¼å°æ‡‰çš„ API 路徑授權。閱讀 文件 以了解更多資訊。 -at_least_one_permission=æ‚¨å¿…é ˆé¸æ“‡è‡³å°‘一個權é™ä¾†å»ºç«‹ token -permissions_list=權é™: manage_oauth2_applications=ç®¡ç† OAuth2 æ‡‰ç”¨ç¨‹å¼ edit_oauth2_application=編輯 OAuth2 æ‡‰ç”¨ç¨‹å¼ @@ -922,56 +763,38 @@ remove_oauth2_application_desc=刪除 OAuth2 應用程å¼å°‡æœƒæ’¤éŠ·æ‰€æœ‰å·² remove_oauth2_application_success=已刪除應用程å¼ã€‚ create_oauth2_application=新增 OAuth2 æ‡‰ç”¨ç¨‹å¼ create_oauth2_application_button=å»ºç«‹æ‡‰ç”¨ç¨‹å¼ -create_oauth2_application_success=您已æˆåŠŸå»ºç«‹æ–°çš„ OAuth2 應用程å¼ã€‚ -update_oauth2_application_success=您已æˆåŠŸæ›´æ–° OAuth2 應用程å¼ã€‚ oauth2_application_name=應用程å¼å稱 oauth2_confidential_client=機密客戶端 (Confidential Client)ã€‚è«‹ç‚ºèƒ½ä¿æŒæ©Ÿå¯†æ€§çš„程å¼å‹¾é¸ï¼Œä¾‹å¦‚ç¶²é æ‡‰ç”¨ç¨‹å¼ã€‚ä½¿ç”¨åŽŸç”Ÿç¨‹å¼æ™‚ä¸è¦å‹¾é¸ï¼ŒåŒ…嫿¡Œé¢ã€è¡Œå‹•應用程å¼ã€‚ -oauth2_skip_secondary_authorization=授權一次後,跳éŽå…¬ç”¨å®¢æˆ¶ç«¯çš„二次授權。å¯èƒ½æœƒå¸¶ä¾†å®‰å…¨é¢¨éšªã€‚ -oauth2_redirect_uris=釿–°å°Žå‘ URI,æ¯è¡Œä¸€å€‹ URI。 save_application=儲存 oauth2_client_id=客戶端 ID oauth2_client_secret=客戶端密鑰 oauth2_regenerate_secret=釿–°ç”¢ç”Ÿå¯†é‘° oauth2_regenerate_secret_hint=éºå¤±æ‚¨çš„密鑰? -oauth2_client_secret_hint=é›¢é–‹æˆ–é‡æ–°æ•´ç†æ­¤é é¢å¾Œå°‡ä¸æœƒå†é¡¯ç¤ºå¯†é‘°ã€‚è«‹ç¢ºä¿æ‚¨å·²ä¿å­˜å®ƒã€‚ oauth2_application_edit=編輯 oauth2_application_create_description=OAuth2 應用程å¼è®“您的第三方應用程å¼å¯ä»¥å­˜å–æ­¤ Gitea 上的帳戶。 -oauth2_application_remove_description=移除 OAuth2 應用程å¼å°‡é˜»æ­¢å…¶å­˜å–此實例上的授權使用者帳戶。是å¦ç¹¼çºŒï¼Ÿ -oauth2_application_locked=Gitea 在啟動時會é å…ˆè¨»å†Šä¸€äº› OAuth2 應用程å¼ï¼ˆå¦‚果在é…置中啟用)。為防止æ„外行為,這些應用程å¼ç„¡æ³•編輯或刪除。請åƒé–± OAuth2 文件以ç²å–更多資訊。 authorized_oauth2_applications=已授權的 OAuth2 æ‡‰ç”¨ç¨‹å¼ -authorized_oauth2_applications_description=您已授權這些第三方應用程å¼å­˜å–您的 Gitea 帳戶。請撤銷ä¸å†éœ€è¦çš„æ‡‰ç”¨ç¨‹å¼çš„å­˜å–æ¬Šã€‚ revoke_key=撤銷 revoke_oauth2_grant=æ’¤éŠ·å­˜å–æ¬Š -revoke_oauth2_grant_description=撤銷此第三方應用程å¼çš„å­˜å–æ¬Šï¼Œæ­¤æ‡‰ç”¨ç¨‹å¼å°±ç„¡æ³•å†å­˜å–您的資料。您確定嗎? -revoke_oauth2_grant_success=æˆåŠŸæ’¤éŠ·å­˜å–æ¬Šã€‚ +revoke_oauth2_grant_description=撤銷此第三方應用程å¼çš„å­˜å–æ¬Šï¼Œæ­¤æ‡‰ç”¨ç¨‹å¼å°±ç„¡æ³•å†å­˜å–您的資料?您確定嗎? -twofa_desc=兩步驟驗證å¯ä»¥å¢žå¼·æ‚¨çš„帳戶安全性。 -twofa_recovery_tip=如果您éºå¤±è¨­å‚™ï¼Œæ‚¨å¯ä»¥ä½¿ç”¨ä¸€æ¬¡æ€§æ¢å¾©å¯†é‘°é‡æ–°ç²å–å¸³æˆ¶å­˜å–æ¬Šã€‚ twofa_is_enrolled=您的帳戶已經啟用兩步驟驗證。 twofa_not_enrolled=您的帳戶目å‰å°šæœªå•Ÿç”¨å…©æ­¥é©Ÿé©—證。 twofa_disable=åœç”¨å…©æ­¥é©Ÿé©—è­‰ -twofa_scratch_token_regenerate=釿–°ç”¢ç”Ÿå‚™ç”¨é©—證碼 -twofa_scratch_token_regenerated=您的單次使用æ¢å¾©å¯†é‘°ç¾åœ¨æ˜¯ %sã€‚è«‹å°‡å…¶å­˜æ”¾åœ¨å®‰å…¨çš„åœ°æ–¹ï¼Œå› ç‚ºå®ƒä¸æœƒå†æ¬¡é¡¯ç¤ºã€‚ twofa_enroll=啟用兩步驟驗證 twofa_disable_note=如有需è¦ï¼Œæ‚¨å¯ä»¥åœç”¨å…©æ­¥é©Ÿé©—證。 twofa_disable_desc=關閉兩步驟驗證會使您的帳戶安全性é™ä½Žï¼Œæ˜¯å¦ç¹¼çºŒï¼Ÿ -regenerate_scratch_token_desc=如果您éºå¤±äº†å‚™ç”¨é©—證碼或已經使用它登入,您å¯ä»¥åœ¨æ­¤é‡æ–°è¨­å®šã€‚ twofa_disabled=兩步驟驗證已經被關閉。 scan_this_image=使用您的授權應用程å¼ä¾†æŽƒçž„圖片: or_enter_secret=或者輸入密碼: %s then_enter_passcode=然後輸入應用程å¼ä¸­é¡¯ç¤ºçš„驗證碼: passcode_invalid=無效的驗證碼,請é‡è©¦ã€‚ -twofa_enrolled=您的帳戶已經啟用了兩步驟驗證。請將備用驗證碼 (%s) ä¿å­˜åˆ°å®‰å…¨çš„åœ°æ–¹ï¼Œå®ƒåªæœƒé¡¯ç¤ºé€™éº¼ä¸€æ¬¡ï¼ twofa_failed_get_secret=å–得密鑰 (Secret) 失敗。 -webauthn_desc=安全金鑰是包å«åŠ å¯†å¯†é‘°çš„ç¡¬é«”è¨­å‚™ï¼Œå®ƒå€‘å¯ä»¥ç”¨æ–¼å…©æ­¥é©Ÿé©—è­‰ã€‚å®‰å…¨é‡‘é‘°å¿…é ˆæ”¯æ´ WebAuthn Authenticator 標準。 webauthn_register_key=新增安全金鑰 webauthn_nickname=暱稱 webauthn_delete_key=移除安全金鑰 webauthn_delete_key_desc=如果您移除安全金鑰,將ä¸èƒ½å†ä½¿ç”¨å®ƒç™»å…¥ã€‚是å¦ç¹¼çºŒï¼Ÿ -webauthn_key_loss_warning=如果您éºå¤±äº†å®‰å…¨é‡‘é‘°ï¼Œæ‚¨å°‡ç„¡æ³•å­˜å–æ‚¨çš„帳戶。 -webauthn_alternative_tip=您å¯èƒ½éœ€è¦è¨­å®šå…¶ä»–的驗證方法。 manage_account_links=管ç†å·²é€£çµçš„帳戶 manage_account_links_desc=這些外部帳戶已連çµåˆ°æ‚¨çš„ Gitea 帳戶。 @@ -981,10 +804,8 @@ remove_account_link=刪除已連çµçš„帳戶 remove_account_link_desc=刪除連çµå¸³æˆ¶å°‡æ’¤éŠ·å…¶å° Gitea å¸³æˆ¶çš„å­˜å–æ¬Šé™ã€‚是å¦ç¹¼çºŒï¼Ÿ remove_account_link_success=已移除連çµçš„帳戶。 -hooks.desc=新增 Webhookï¼Œç•¶æ‚¨æ“æœ‰çš„æ‰€æœ‰å„²å­˜åº«è§¸ç™¼äº‹ä»¶æ™‚將會執行。 orgs_none=您尚未æˆç‚ºä»»ä¸€çµ„織的æˆå“¡ã€‚ -repos_none=æ‚¨å°šæœªæ“æœ‰ä»»ä½•儲存庫。 delete_account=刪除您的帳戶 delete_prompt=此動作將永久刪除您的使用者帳戶,而且無法復原。 @@ -1003,12 +824,9 @@ visibility=使用者ç€è¦½æ¬Šé™ visibility.public=公開 visibility.public_tooltip=所有人都å¯ä»¥çœ‹åˆ° visibility.limited=å—é™ -visibility.limited_tooltip=åªæœ‰å·²æŽˆæ¬Šçš„使用者å¯è¦‹ visibility.private=ç§äºº -visibility.private_tooltip=åƒ…å°æ‚¨å·²åŠ å…¥çš„çµ„ç¹”æˆå“¡å¯è¦‹ [repo] -new_repo_helper=å„²å­˜åº«åŒ…å«æ‰€æœ‰å°ˆæ¡ˆæª”案,包括修訂歷å²ã€‚已經在其他地方託管了嗎?é·ç§»å„²å­˜åº«ã€‚ owner=æ“æœ‰è€… owner_helper=組織å¯èƒ½å› ç‚ºå„²å­˜åº«æ•¸é‡ä¸Šé™è€Œæœªåˆ—入此é¸å–®ã€‚ repo_name=儲存庫å稱 @@ -1020,7 +838,6 @@ template_helper=將儲存庫設為範本 template_description=å„²å­˜åº«ç¯„æœ¬è®“ä½¿ç”¨è€…å¯æ–°å¢žç›¸åŒç›®éŒ„çµæ§‹ã€æª”案以åŠè¨­å®šçš„儲存庫。 visibility=ç€è¦½æ¬Šé™ visibility_description=åªæœ‰çµ„ç¹”æ“æœ‰è€…或有權é™çš„組織æˆå“¡æ‰èƒ½çœ‹åˆ°ã€‚ -visibility_helper=å°‡å„²å­˜åº«è¨­ç‚ºç§æœ‰ visibility_helper_forced=您的網站管ç†å“¡å¼·åˆ¶æ–°çš„å­˜å„²åº«å¿…éœ€è¨­å®šç‚ºç§æœ‰ã€‚ visibility_fork_helper=(修改本值將會影響所有 fork 儲存庫) clone_helper=éœ€è¦æœ‰é—œ Clone çš„å”助嗎?查看幫助 。 @@ -1029,14 +846,7 @@ fork_from=Fork 自 already_forked=您已經 fork éŽ %s fork_to_different_account=Fork 到其他帳戶 fork_visibility_helper=無法更改 fork 儲存庫的ç€è¦½æ¬Šé™ã€‚ -fork_branch=è¦å…‹éš†åˆ° fork 的分支 -all_branches=所有分支 -view_all_branches=查看所有分支 -view_all_tags=查看所有標籤 -fork_no_valid_owners=此儲存庫無法 forkï¼Œå› ç‚ºæ²’æœ‰æœ‰æ•ˆçš„æ“æœ‰è€…。 -fork.blocked_user=無法 fork å„²å­˜åº«ï¼Œå› ç‚ºæ‚¨è¢«å„²å­˜åº«æ“æœ‰è€…å°éŽ–ã€‚ use_template=使用此範本 -open_with_editor=以 %s 開啟 download_zip=下載 ZIP download_tar=下載 TAR.GZ download_bundle=下載 BUNDLE @@ -1044,8 +854,6 @@ generate_repo=產生儲存庫 generate_from=產生自 repo_desc=æè¿° repo_desc_helper=輸入簡介 (é¸ç”¨) -repo_no_desc=未æä¾›æè¿° -repo_lang=儲存庫語言 repo_gitignore_helper=鏿“‡ .gitignore 範本 repo_gitignore_helper_desc=從常見語言範本清單中挑é¸å¿½ç•¥è¿½è¹¤çš„æª”案。é è¨­æƒ…æ³ä¸‹å„種語言建置工具產生的特殊檔案都包å«åœ¨ .gitignore 中。 issue_labels=å•題標籤 @@ -1053,9 +861,6 @@ issue_labels_helper=鏿“‡å•題標籤集 license=æŽˆæ¬Šæ¢æ¬¾ license_helper=è«‹é¸æ“‡æŽˆæ¬Šæ¢æ¬¾æª”案 license_helper_desc=æŽˆæ¬Šæ¢æ¬¾å®šç¾©äº†ä»–人使用您原始碼的å…è¨±å’Œç¦æ­¢äº‹é …。ä¸ç¢ºå®šå“ªå€‹é©ç”¨æ–¼æ‚¨çš„å°ˆæ¡ˆï¼ŸæŸ¥çœ‹é¸æ“‡æŽˆæ¬Šæ¢æ¬¾ã€‚ -multiple_licenses=å¤šé‡æŽˆæ¬Š -object_format=ç‰©ä»¶æ ¼å¼ -object_format_helper=儲存庫的物件格å¼ã€‚無法更改。SHA1 是最兼容的。 readme=讀我檔案 readme_helper=鏿“‡è®€æˆ‘檔案範本。 readme_helper_desc=這是您能為專案撰寫完整æè¿°çš„地方。 @@ -1067,19 +872,15 @@ trust_model_helper_collaborator_committer=å”作者 + æäº¤è€…: ä¿¡ä»»å”作者 trust_model_helper_default=é è¨­: 使用此 Gitea çš„é è¨­å„²å­˜åº«ä¿¡ä»»æ¨¡å¼ create_repo=建立儲存庫 default_branch=é è¨­åˆ†æ”¯ -default_branch_label=é è¨­ default_branch_helper=é è¨­åˆ†æ”¯æ˜¯åˆä½µè«‹æ±‚å’Œæäº¤ç¨‹å¼ç¢¼çš„基礎分支。 mirror_prune=è£æ¸› mirror_prune_desc=åˆªé™¤éŽæ™‚çš„é ç«¯è¿½è¹¤åƒè€ƒ mirror_interval=é¡åƒé–“éš” (有效時間單ä½ç‚º 'h'ã€'m'ã€'s'),設為 0 以åœç”¨å®šæœŸåŒæ­¥ã€‚(最å°é–“éš”: %s) mirror_interval_invalid=é¡åƒé€±æœŸç„¡æ•ˆ -mirror_sync=å·²åŒæ­¥ mirror_sync_on_commit=æŽ¨é€æäº¤å¾Œé€²è¡ŒåŒæ­¥ mirror_address=從 URL Clone mirror_address_desc=在授權資訊中填入必è¦çš„資料。 -mirror_address_url_invalid=æä¾›çš„ URL 無效。您必須正確轉義 URL 的所有組件。 -mirror_address_protocol_invalid=æä¾›çš„ URL 無效。僅å¯ä½¿ç”¨ http(s):// 或 git:// ä½ç½®é€²è¡Œé¡åƒã€‚ -mirror_lfs=大型檔案存儲 (LFS) +mirror_lfs=Large File Storage (LFS) mirror_lfs_desc=啟動 LFS 檔案的é¡åƒåŠŸèƒ½ã€‚ mirror_lfs_endpoint=LFS 端點 mirror_lfs_endpoint_desc=åŒæ­¥å°‡æœƒå˜—試使用 Clone URL ä¾†ç¢ºèª LFS 伺æœå™¨ã€‚如果存儲庫的 LFS 資料放在其他地方,您也å¯ä»¥æŒ‡å®šè‡ªè¨‚的端點。 @@ -1089,9 +890,7 @@ mirror_password_blank_placeholder=(未設定) mirror_password_help=修改帳號以清除已儲存的密碼。 watchers=關注者 stargazers=å æ˜Ÿè¡“師 -stars_remove_warning=這將移除此儲存庫的所有星標。 forks=Fork -stars=星 reactions_more=å†å¤šæ·»åŠ  %d個 unit_disabled=網站管ç†å“¡å·²ç¶“åœç”¨é€™å€‹å„²å­˜åº«å€åŸŸã€‚ language_other=å…¶ä»– @@ -1105,20 +904,10 @@ delete_preexisting=刪除既有的檔案 delete_preexisting_content=刪除 %s 中的檔案 delete_preexisting_success=刪除 %s 中未接管的檔案 blame_prior=檢視此變更å‰çš„ Blame -blame.ignore_revs=忽略 .git-blame-ignore-revs 中的修訂。點擊 這裡 以繞éŽä¸¦æŸ¥çœ‹æ­£å¸¸çš„ Blame 視圖。 -blame.ignore_revs.failed=忽略 .git-blame-ignore-revs 中的修訂失敗。 -user_search_tooltip=顯示最多 30 個使用者 -tree_path_not_found_commit=路徑 %[1]s 在æäº¤ %[2]s 中ä¸å­˜åœ¨ -tree_path_not_found_branch=路徑 %[1]s 在分支 %[2]s 中ä¸å­˜åœ¨ -tree_path_not_found_tag=路徑 %[1]s 在標籤 %[2]s 中ä¸å­˜åœ¨ transfer.accept=åŒæ„轉移 -transfer.accept_desc=轉移到「%s〠transfer.reject=拒絕轉移 -transfer.reject_desc=å–æ¶ˆè½‰ç§»åˆ°ã€Œ%s〠-transfer.no_permission_to_accept=æ‚¨æ²’æœ‰æ¬Šé™æŽ¥å—æ­¤è½‰ç§»ã€‚ -transfer.no_permission_to_reject=æ‚¨æ²’æœ‰æ¬Šé™æ‹’絕此轉移。 desc.private=ç§æœ‰ desc.public=公開 @@ -1137,15 +926,12 @@ template.issue_labels=å•題標籤 template.one_item=è‡³å°‘é ˆé¸æ“‡ä¸€å€‹ç¯„本項目 template.invalid=å¿…é ˆé¸æ“‡ä¸€å€‹å„²å­˜åº«ç¯„本 -archive.title=此儲存庫已å°å­˜ã€‚您å¯ä»¥æŸ¥çœ‹æª”案並進行 Cloneï¼Œä½†ç„¡æ³•æŽ¨é€æˆ–開啟å•題或åˆä½µè«‹æ±‚。 -archive.title_date=此儲存庫已於 %s å°å­˜ã€‚您å¯ä»¥æŸ¥çœ‹æª”案並進行 Cloneï¼Œä½†ç„¡æ³•æŽ¨é€æˆ–開啟å•題或åˆä½µè«‹æ±‚。 -archive.issue.nocomment=此儲存庫已å°å­˜ï¼Œæ‚¨ä¸èƒ½åœ¨å•題上留言。 -archive.pull.nocomment=此儲存庫已å°å­˜ï¼Œæ‚¨ä¸èƒ½åœ¨åˆä½µè«‹æ±‚上留言。 +archive.issue.nocomment=此存儲庫已å°å­˜ï¼Œæ‚¨ä¸èƒ½åœ¨å•題上留言。 +archive.pull.nocomment=此存儲庫已å°å­˜ï¼Œæ‚¨ä¸èƒ½åœ¨åˆä½µè«‹æ±‚上留言。 form.reach_limit_of_creation_1=您已經é”到了您儲存庫的數é‡ä¸Šé™ (%d 個)。 form.reach_limit_of_creation_n=您已經é”到了您儲存庫的數é‡ä¸Šé™ (%d 個)。 form.name_reserved=「%sã€æ˜¯ä¿ç•™çš„儲存庫å稱。 -form.name_pattern_not_allowed=儲存庫å稱ä¸å¯åŒ…å«å­—元「%sã€ã€‚ need_auth=授權 migrate_options=é·ç§»é¸é … @@ -1155,7 +941,6 @@ migrate_options_lfs=é·ç§» LFS 檔案 migrate_options_lfs_endpoint.label=LFS 端點 migrate_options_lfs_endpoint.description=é·ç§»å°‡æœƒå˜—試使用您的 Git Remote ä¾†ç¢ºèª LFS 伺æœå™¨ã€‚如果存儲庫的 LFS 資料放在其他地方,您也å¯ä»¥æŒ‡å®šè‡ªè¨‚的端點。 migrate_options_lfs_endpoint.description.local=åŒæ™‚ä¹Ÿæ”¯æ´æœ¬åœ°ä¼ºæœå™¨è·¯å¾‘。 -migrate_options_lfs_endpoint.placeholder=如果留空,端點將從 Clone URL 中推導出來 migrate_items=é·ç§»é …ç›® migrate_items_wiki=Wiki migrate_items_milestones=里程碑 @@ -1180,7 +965,6 @@ migrated_from_fake=已從 %[1]s é·ç§» migrate.migrate=從 %s é·ç§» migrate.migrating=正在從 %s é·ç§»... migrate.migrating_failed=從 %s é·ç§»å¤±æ•— -migrate.migrating_failed.error=é·ç§»å¤±æ•—: %s migrate.migrating_failed_no_addr=é·ç§»å¤±æ•—。 migrate.github.description=從 github.com 或其他 GitHub 執行個體é·ç§»è³‡æ–™ã€‚ migrate.git.description=從任何 Git æœå‹™é·ç§»å„²å­˜åº«ã€‚ @@ -1190,9 +974,6 @@ migrate.gogs.description=從 notabug.org 或其他 Gogs 執行個體é·ç§»è³‡æ–™ migrate.onedev.description=從 code.onedev.io 或其他 OneDev 執行個體é·ç§»è³‡æ–™ã€‚ migrate.codebase.description=從 codebasehq.com é·ç§»è³‡æ–™ã€‚ migrate.gitbucket.description=從 GitBucket 執行個體é·ç§»è³‡æ–™ã€‚ -migrate.codecommit.description=從 AWS CodeCommit é·ç§»è³‡æ–™ã€‚ -migrate.codecommit.https_git_credentials_username=HTTPS Git 憑證使用者å稱 -migrate.codecommit.https_git_credentials_password=HTTPS Git 憑證密碼 migrate.migrating_git=正在é·ç§» Git 資料 migrate.migrating_topics=正在é·ç§»ä¸»é¡Œ migrate.migrating_milestones=正在é·ç§»é‡Œç¨‹ç¢‘ @@ -1200,8 +981,6 @@ migrate.migrating_labels=正在é·ç§»æ¨™ç±¤ migrate.migrating_releases=正在é·ç§»ç‰ˆæœ¬ç™¼å¸ƒ migrate.migrating_issues=正在é·ç§»å•題 migrate.migrating_pulls=正在é·ç§»åˆä½µè«‹æ±‚ -migrate.cancel_migrating_title=å–æ¶ˆé·ç§» -migrate.cancel_migrating_confirm=您è¦å–消é·ç§»å—Žï¼Ÿ mirror_from=é¡åƒè‡ª forked_from=fork 自 @@ -1215,7 +994,6 @@ watch=關注 unstar=移除星號 star=加上星號 fork=Fork -action.blocked_user=無法執行æ“ä½œï¼Œå› ç‚ºæ‚¨è¢«å„²å­˜åº«æ“æœ‰è€…å°éŽ–ã€‚ download_archive=下載此儲存庫 more_operations=更多æ“作 @@ -1253,7 +1031,6 @@ releases=版本發布 tag=標籤 released_this=發布了此版本 tagged_this=標記了此標籤 -file.title=%s æ–¼ %s file_raw=原始文件 file_history=æ­·å²è¨˜éŒ„ file_view_source=檢視原始碼 @@ -1261,36 +1038,22 @@ file_view_rendered=檢視渲染圖 file_view_raw=查看原始文件 file_permalink=æ°¸ä¹…é€£çµ file_too_large=檔案太大,無法顯示。 -file_is_empty=檔案是空的。 -code_preview_line_from_to=第 %[1]d 行到第 %[2]d 行在 %[3]s -code_preview_line_in=第 %[1]d 行在 %[2]s -invisible_runes_header=此檔案包å«ä¸å¯è¦‹çš„ Unicode å­—å…ƒ -invisible_runes_description=此檔案包å«ä¸å¯è¦‹çš„ Unicode 字元,這些字元å°äººé¡žä¾†èªªæ˜¯ç„¡æ³•å€åˆ†çš„,但電腦å¯èƒ½æœƒä»¥ä¸åŒæ–¹å¼è™•ç†ã€‚如果您èªç‚ºé€™æ˜¯æœ‰æ„的,å¯ä»¥å®‰å…¨åœ°å¿½ç•¥æ­¤è­¦å‘Šã€‚使用 Escape éµä¾†é¡¯ç¤ºå®ƒå€‘。 -ambiguous_runes_header=æ­¤æª”æ¡ˆåŒ…å«æ˜“混淆的 Unicode å­—å…ƒ -ambiguous_runes_description=此檔案包å«å¯èƒ½èˆ‡å…¶ä»–字元混淆的 Unicode 字元。如果您èªç‚ºé€™æ˜¯æœ‰æ„的,å¯ä»¥å®‰å…¨åœ°å¿½ç•¥æ­¤è­¦å‘Šã€‚使用 Escape éµä¾†é¡¯ç¤ºå®ƒå€‘。 -invisible_runes_line=這一行有看ä¸è¦‹çš„ Unicode å­—å…ƒ -ambiguous_runes_line=這一行有易混淆的 Unicode å­—å…ƒ -ambiguous_character=%[1]c [U+%04[1]X] 容易與 %[2]c [U+%04[2]X] æ··æ·† - -escape_control_characters=轉義控制字元 -unescape_control_characters=å–æ¶ˆè½‰ç¾©æŽ§åˆ¶å­—å…ƒ +invisible_runes_line=`這一行有看ä¸è¦‹çš„ Unicode å­—å…ƒ` +ambiguous_runes_line=`這一行有易混淆的 Unicode å­—å…ƒ` + +escape_control_characters=Escape +unescape_control_characters=Unescape file_copy_permalink=è¤‡è£½å›ºå®šé€£çµ view_git_blame=檢視 Git Blame video_not_supported_in_browser=您的ç€è¦½å™¨ä¸æ”¯æ´ä½¿ç”¨ HTML5 播放影片。 audio_not_supported_in_browser=您的ç€è¦½å™¨ä¸æ”¯æ´ HTML5 的「audioã€æ¨™ç±¤ stored_lfs=已使用 Git LFS 儲存 symbolic_link=ç¬¦è™Ÿé€£çµ -executable_file=å¯åŸ·è¡Œæª” -vendored=已供應 -generated=已產生 commit_graph=æäº¤ç·šåœ– commit_graph.select=鏿“‡åˆ†æ”¯ commit_graph.hide_pr_refs=éš±è—åˆä½µè«‹æ±‚ commit_graph.monochrome=單色 commit_graph.color=彩色 -commit.contained_in=æ­¤æäº¤åŒ…å«åœ¨ï¼š -commit.contained_in_default_branch=æ­¤æäº¤æ˜¯é è¨­åˆ†æ”¯çš„一部分 -commit.load_referencing_branches_and_tags=載入引用此æäº¤çš„分支和標籤 blame=Blame download_file=下載檔案 normal_view=標準檢視 @@ -1318,7 +1081,6 @@ editor.or=或 editor.cancel_lower=å–æ¶ˆ editor.commit_signed_changes=æäº¤ç°½ç½²éŽçš„變更 editor.commit_changes=æäº¤è®Šæ›´ -editor.add_tmpl=新增「〠editor.add=新增 %s editor.update=æ›´æ–° %s editor.delete=刪除 %s @@ -1339,15 +1101,8 @@ editor.filename_cannot_be_empty=檔案å稱ä¸èƒ½ç‚ºç©ºã€‚ editor.filename_is_invalid=檔å無效:「%sã€ã€‚ editor.branch_does_not_exist=此儲存庫沒有å為「%sã€çš„分支。 editor.branch_already_exists=此儲存庫已有å為「%sã€çš„分支。 -editor.directory_is_a_file=目錄å稱「%sã€å·²è¢«æ­¤å„²å­˜åº«çš„æª”案使用。 -editor.file_is_a_symlink=`"%s" 是一個符號連çµã€‚符號連çµç„¡æ³•在網é ç·¨è¼¯å™¨ä¸­ç·¨è¼¯` -editor.filename_is_a_directory=檔å「%sã€å·²è¢«æ­¤å„²å­˜åº«çš„目錄å稱使用。 -editor.file_editing_no_longer_exists=æ­£è¦ç·¨è¼¯çš„æª”案「%sã€å·²ä¸å­˜åœ¨æ­¤å„²å­˜åº«ä¸­ã€‚ -editor.file_deleting_no_longer_exists=æ­£è¦åˆªé™¤çš„æª”案「%sã€å·²ä¸å­˜åœ¨æ­¤å„²å­˜åº«ä¸­ã€‚ editor.file_changed_while_editing=æª”æ¡ˆå…§å®¹åœ¨æ‚¨ç·¨è¼¯çš„é€”ä¸­å·²è¢«è®Šæ›´ã€‚æŒ‰ä¸€ä¸‹æ­¤è™•æŸ¥çœ‹æ›´å‹•çš„åœ°æ–¹æˆ–å†æ¬¡æäº¤ä»¥è¦†è“‹é€™äº›è®Šæ›´ã€‚ editor.file_already_exists=此儲存庫已有å為「%sã€çš„æª”案。 -editor.commit_id_not_matching=æäº¤ ID 與您開始編輯時的 ID ä¸åŒ¹é…。請æäº¤åˆ°ä¸€å€‹è£œä¸åˆ†æ”¯ç„¶å¾Œåˆä½µã€‚ -editor.push_out_of_date=推é€ä¼¼ä¹Žå·²éŽæ™‚。 editor.commit_empty_file_header=æäº¤ç©ºç™½æª”案 editor.commit_empty_file_text=你準備æäº¤çš„æª”案是空白的,是å¦ç¹¼çºŒï¼Ÿ editor.no_changes_to_show=沒有å¯ä»¥é¡¯ç¤ºçš„變更。 @@ -1372,7 +1127,6 @@ commits.commits=次程å¼ç¢¼æäº¤ commits.no_commits=沒有共åŒçš„æäº¤ã€‚「%sã€å’Œã€Œ%sã€çš„æ­·å²å®Œå…¨ä¸åŒã€‚ commits.nothing_to_compare=這些分支是相åŒçš„。 commits.search.tooltip=ä½ å¯ä»¥ç”¨ã€Œauthor:ã€ã€ã€Œcommitter:ã€ã€ã€Œafter:ã€ã€ã€Œbefore:ã€ç­‰ä½œç‚ºé—œéµå­—çš„å‰ç¶´ï¼Œä¾‹å¦‚: 「revert author:Alice before:2019-01-13ã€ã€‚ -commits.search_branch=此分支 commits.search_all=所有分支 commits.author=作者 commits.message=備註 @@ -1384,7 +1138,6 @@ commits.signed_by_untrusted_user=ç”±ä¸ä¿¡ä»»çš„使用者簽署 commits.signed_by_untrusted_user_unmatched=ç”±ä¸å—信任且與æäº¤è€…ä¸ç›¸ç¬¦çš„使用者簽署 commits.gpg_key_id=GPG 金鑰 ID commits.ssh_key_fingerprint=SSH 金鑰指紋 -commits.view_path=æª¢è¦–æ­¤æ­·å²æ™‚刻 commit.operations=æ“作 commit.revert=還原 @@ -1402,7 +1155,6 @@ commitstatus.success=æˆåŠŸ ext_issues=å­˜å–外部å•題 ext_issues.desc=連çµåˆ°å¤–部å•題追蹤器。 -projects.desc=在專案看æ¿ä¸­ç®¡ç†å•題與åˆä½µè«‹æ±‚。 projects.description=æè¿° (é¸ç”¨) projects.description_placeholder=æè¿° projects.create=建立專案 @@ -1430,7 +1182,6 @@ projects.column.new=æ–°å¢žæ¬„ä½ projects.column.set_default=設為é è¨­ projects.column.set_default_desc=將此欄ä½è¨­å®šç‚ºæœªåˆ†é¡žå•題åŠåˆä½µè«‹æ±‚çš„é è¨­é è¨­å€¼ projects.column.delete=åˆªé™¤æ¬„ä½ -projects.column.deletion_desc=åˆªé™¤å°ˆæ¡ˆæ¬„ä½æœƒå°‡æ‰€æœ‰ç›¸é—œçš„å•題移動到「未分類ã€ï¼Œæ˜¯å¦ç¹¼çºŒï¼Ÿ projects.column.color=é¡è‰² projects.open=開啟 projects.close=關閉 @@ -1462,10 +1213,6 @@ issues.new.clear_milestone=清除已é¸å–里程碑 issues.new.assignees=負責人 issues.new.clear_assignees=清除負責人 issues.new.no_assignees=沒有負責人 -issues.new.no_reviewers=沒有審核者 -issues.new.blocked_user=無法建立å•é¡Œï¼Œå› ç‚ºæ‚¨è¢«å„²å­˜åº«æ“æœ‰è€…å°éŽ–ã€‚ -issues.edit.already_changed=無法儲存å•é¡Œçš„è®Šæ›´ã€‚çœ‹èµ·ä¾†å…§å®¹å·²è¢«å…¶ä»–ä½¿ç”¨è€…æ›´æ”¹ã€‚è«‹é‡æ–°æ•´ç†é é¢ä¸¦å†æ¬¡å˜—試編輯以é¿å…覆蓋他們的變更。 -issues.edit.blocked_user=ç„¡æ³•ç·¨è¼¯å…§å®¹ï¼Œå› ç‚ºæ‚¨è¢«ç™¼æ–‡è€…æˆ–å„²å­˜åº«æ“æœ‰è€…å°éŽ–ã€‚ issues.choose.get_started=é–‹å§‹ issues.choose.open_external_link=開啟 issues.choose.blank=é è¨­ @@ -1491,7 +1238,6 @@ issues.remove_labels=移除了 %s 標籤 %s issues.add_remove_labels=加入了 %s 並移除了 %s 標籤 %s issues.add_milestone_at=`新增到 %s 里程碑 %s` issues.add_project_at=`將此加入到 %s 專案 %s` -issues.move_to_column_of_project=`將此移動到 %s çš„ %s 中 %s` issues.change_milestone_at=`%[3]s 修改了里程碑 %[1]s 到 %[2]s` issues.change_project_at=`將專案從 %s 修改為 %s %s` issues.remove_milestone_at=`從 %s 里程碑移除 %s` @@ -1512,10 +1258,6 @@ issues.filter_label_exclude=`使用 alt + click/enter issues.filter_label_no_select=所有標籤 issues.filter_label_select_no_label=沒有標籤 issues.filter_milestone=里程碑 -issues.filter_milestone_all=所有里程碑 -issues.filter_milestone_none=無里程碑 -issues.filter_milestone_open=開放中的里程碑 -issues.filter_milestone_closed=已關閉的里程碑 issues.filter_project=專案 issues.filter_project_all=所有專案 issues.filter_project_none=æœªé¸æ“‡å°ˆæ¡ˆ @@ -1523,8 +1265,6 @@ issues.filter_assignee=負責人 issues.filter_assginee_no_select=所有負責人 issues.filter_assginee_no_assignee=沒有負責人 issues.filter_poster=作者 -issues.filter_user_placeholder=æœå°‹ä½¿ç”¨è€… -issues.filter_user_no_select=所有使用者 issues.filter_type=類型 issues.filter_type.all_issues=所有å•題 issues.filter_type.assigned_to_you=指派給您的 @@ -1565,7 +1305,6 @@ issues.next=ä¸‹ä¸€é  issues.open_title=開放中 issues.closed_title=已關閉 issues.draft_title=è‰ç¨¿ -issues.num_comments_1=%d 則評論 issues.num_comments=%d 則留言 issues.commented_at=`已留言 %s` issues.delete_comment_confirm=您確定è¦åˆªé™¤é€™å‰‡ç•™è¨€å—Žï¼Ÿ @@ -1574,15 +1313,9 @@ issues.context.quote_reply=引用回覆 issues.context.reference_issue=新增å•題並åƒè€ƒ issues.context.edit=編輯 issues.context.delete=刪除 -issues.no_content=沒有æä¾›æè¿°ã€‚ issues.close=關閉å•題 -issues.comment_pull_merged_at=åˆä½µæäº¤ %[1]s 到 %[2]s %[3]s -issues.comment_manually_pull_merged_at=手動åˆä½µæäº¤ %[1]s 到 %[2]s %[3]s -issues.close_comment_issue=留言並關閉 issues.reopen_issue=釿–°é–‹æ”¾ -issues.reopen_comment_issue=ç•™è¨€ä¸¦é‡æ–°é–‹æ”¾ issues.create_comment=留言 -issues.comment.blocked_user=ç„¡æ³•å»ºç«‹æˆ–ç·¨è¼¯ç•™è¨€ï¼Œå› ç‚ºæ‚¨è¢«ç™¼æ–‡è€…æˆ–å„²å­˜åº«æ“æœ‰è€…å°éŽ–ã€‚ issues.closed_at=`關閉了這個å•題 %[2]s` issues.reopened_at=`釿–°é–‹æ”¾äº†é€™å€‹å•題 %[2]s` issues.commit_ref_at=`在æäº¤ä¸­é—œè¯äº†é€™å€‹å•題 %[2]s` @@ -1594,17 +1327,8 @@ issues.ref_closed_from=`關閉了這個å•題 %[4]s 釿–°é–‹æ”¾äº†é€™å€‹å•題 %[4]s %[2]s` issues.ref_from=`自 %[1]s` issues.author=作者 -issues.author_helper=此使用者是作者。 issues.role.owner=æ“æœ‰è€… -issues.role.owner_helper=æ­¤ä½¿ç”¨è€…æ˜¯æ­¤å„²å­˜åº«çš„æ“æœ‰è€…。 issues.role.member=普通æˆå“¡ -issues.role.member_helper=æ­¤ä½¿ç”¨è€…æ˜¯æ“æœ‰æ­¤å„²å­˜åº«çš„組織æˆå“¡ã€‚ -issues.role.collaborator=å”作者 -issues.role.collaborator_helper=此使用者已被邀請å”作此儲存庫。 -issues.role.first_time_contributor=首次貢ç»è€… -issues.role.first_time_contributor_helper=æ­¤ä½¿ç”¨è€…æ˜¯é¦–æ¬¡å°æ­¤å„²å­˜åº«é€²è¡Œè²¢ç»ã€‚ -issues.role.contributor=è²¢ç»è€… -issues.role.contributor_helper=此使用者之å‰å·²æäº¤éŽæ­¤å„²å­˜åº«ã€‚ issues.re_request_review=冿¬¡è«‹æ±‚審核 issues.is_stale=ç¶“éŽæ­¤å¯©æ ¸ä»¥å¾Œï¼Œæ­¤åˆä½µè«‹æ±‚有被修改 issues.remove_request_review=移除審核請求 @@ -1619,9 +1343,6 @@ issues.label_title=å稱 issues.label_description=æè¿° issues.label_color=é¡è‰² issues.label_exclusive=互斥 -issues.label_archive=å°å­˜æ¨™ç±¤ -issues.label_archived_filter=顯示å°å­˜æ¨™ç±¤ -issues.label_archive_tooltip=å°å­˜æ¨™ç±¤åœ¨æœå°‹æ¨™ç±¤æ™‚é è¨­æœƒè¢«æŽ’除在建議之外。 issues.label_exclusive_desc=請以此格å¼å‘½å標籤: scope/item,使它和其他 scope/ (相åŒç¯„åœ) 標籤互斥。 issues.label_exclusive_warning=在編輯å•題åŠåˆä½µè«‹æ±‚的標籤時,將會刪除任何有相åŒç¯„åœçš„æ¨™ç±¤ã€‚ issues.label_count=%d 個標籤 @@ -1669,25 +1390,11 @@ issues.delete.title=刪除此å•題? issues.delete.text=您真的è¦åˆªé™¤æ­¤å•題嗎?(這將會永久移除所有內容。若您還想ä¿ç•™ï¼Œè«‹è€ƒæ…®æ”¹ç‚ºé—œé–‰å®ƒã€‚) issues.tracker=時間追蹤 -issues.timetracker_timer_start=開始計時 -issues.timetracker_timer_stop=åœæ­¢è¨ˆæ™‚ -issues.timetracker_timer_discard=æ¨æ£„計時 -issues.timetracker_timer_manually_add=手動新增時間 - -issues.time_estimate_set=設定é ä¼°æ™‚é–“ -issues.time_estimate_display=é ä¼°æ™‚間:%s -issues.change_time_estimate_at=å°‡é ä¼°æ™‚間更改為 %s %s -issues.remove_time_estimate_at=移除é ä¼°æ™‚é–“ %s -issues.time_estimate_invalid=é ä¼°æ™‚é–“æ ¼å¼ç„¡æ•ˆ -issues.start_tracking_history=`開始工作 %s` + issues.tracker_auto_close=當這個å•é¡Œè¢«é—œé–‰æ™‚ï¼Œè‡ªå‹•åœæ­¢è¨ˆæ™‚器 issues.tracking_already_started=`您已在å¦ä¸€å€‹å•題上開始時間追蹤ï¼` -issues.stop_tracking_history=`çµæŸå·¥ä½œ %s` -issues.cancel_tracking_history=`å–æ¶ˆæ™‚間追蹤 %s` issues.del_time=刪除此時間記錄 -issues.add_time_history=`加入了花費時間 %s` issues.del_time_history=`刪除了花費時間 %s` -issues.add_time_manually=手動新增時間 issues.add_time_hours=å°æ™‚ issues.add_time_minutes=åˆ†é˜ issues.add_time_sum_to_small=沒有輸入時間。 @@ -1706,7 +1413,6 @@ issues.due_date_form=yyyyå¹´mm月ddæ—¥ issues.due_date_form_add=新增截止日期 issues.due_date_form_edit=編輯 issues.due_date_form_remove=移除 -issues.due_date_not_writer=您需è¦å°æ­¤å„²å­˜åº«çš„å¯«å…¥æ¬Šé™æ‰èƒ½æ›´æ–°å•題的截止日期。 issues.due_date_not_set=未設定截止日期。 issues.due_date_added=新增了截止日期 %s %s issues.due_date_modified=將截止日期從 %[2]s 修改為 %[1]s %[3]s @@ -1730,7 +1436,6 @@ issues.dependency.issue_closing_blockedby=æ­¤å•題被下列å•題阻擋而無 issues.dependency.issue_close_blocks=因為此å•題的阻擋,下列å•題無法被關閉 issues.dependency.pr_close_blocks=因為此åˆä½µè«‹æ±‚的阻擋,下列å•題無法被關閉 issues.dependency.issue_close_blocked=在您關閉此å•題以å‰ï¼Œæ‚¨å¿…須先關閉所有阻擋它的å•題。 -issues.dependency.issue_batch_close_blocked=ç„¡æ³•æ‰¹æ¬¡é—œé–‰æ‚¨é¸æ“‡çš„å•題,因為å•題 #%d 還有開放中的先決æ¢ä»¶ã€‚ issues.dependency.pr_close_blocked=在您åˆä½µä»¥å‰ï¼Œæ‚¨å¿…須先關閉所有阻擋它的å•題。 issues.dependency.blocks_short=阻擋 issues.dependency.blocked_by_short=先決於 @@ -1747,7 +1452,6 @@ issues.dependency.add_error_dep_not_same_repo=這兩個å•題必須在åŒä¸€å€‹ issues.review.self.approval=您ä¸èƒ½æ ¸å¯è‡ªå·±çš„åˆä½µè«‹æ±‚。 issues.review.self.rejection=您ä¸èƒ½å°è‡ªå·±çš„åˆä½µè«‹æ±‚æå‡ºè«‹æ±‚變更。 issues.review.approve=æ ¸å¯äº†é€™äº›è®Šæ›´ %s -issues.review.comment=已審核 %s issues.review.dismissed=å–æ¶ˆ %s 的審核 %s issues.review.dismissed_label=已喿¶ˆ issues.review.left_comment=留下了回應 @@ -1758,13 +1462,9 @@ issues.review.add_review_request=請求了 %s 來審核 %s issues.review.remove_review_request=ç§»é™¤äº†å° %s 的審核請求 %s issues.review.remove_review_request_self=拒絕了審核 %s issues.review.pending=å¾…è™•ç† -issues.review.pending.tooltip=ç›®å‰å…¶ä»–使用者還ä¸èƒ½çœ‹è¦‹æ­¤ç•™è¨€ã€‚è¦é€å‡ºæ‚¨å¾…定的留言請在é é¢æœ€ä¸Šæ–¹é¸æ“‡ã€Œ%sã€->「%s/%s/%sã€ã€‚ issues.review.review=審核 issues.review.reviewers=審核者 issues.review.outdated=éŽæ™‚çš„ -issues.review.outdated_description=此留言發表後內容已變更 -issues.review.option.show_outdated_comments=é¡¯ç¤ºéŽæ™‚的留言 -issues.review.option.hide_outdated_comments=éš±è—éŽæ™‚的留言 issues.review.show_outdated=é¡¯ç¤ºéŽæ™‚çš„ issues.review.hide_outdated=éš±è—éŽæ™‚çš„ issues.review.show_resolved=顯示已解決 @@ -1773,11 +1473,6 @@ issues.review.resolve_conversation=解決å°è©± issues.review.un_resolve_conversation=å–æ¶ˆè§£æ±ºå°è©± issues.review.resolved_by=標記了此å°è©±ç‚ºå·²è§£æ±º issues.review.commented=留言 -issues.review.official=核准 -issues.review.requested=å¯©æ ¸å¾…è™•ç† -issues.review.rejected=請求變更 -issues.review.stale=核准後已更新 -issues.review.unofficial=未計入的核准 issues.assignee.error=å› ç‚ºæœªé æœŸçš„錯誤,未能æˆåŠŸåŠ å…¥æ‰€æœ‰è² è²¬äººã€‚ issues.reference_issue.body=內容 issues.content_history.deleted=刪除 @@ -1793,9 +1488,6 @@ compare.compare_head=比較 pulls.desc=啟用åˆä½µè«‹æ±‚和程å¼ç¢¼å¯©æ ¸ã€‚ pulls.new=建立åˆä½µè«‹æ±‚ -pulls.new.blocked_user=無法建立åˆä½µè«‹æ±‚ï¼Œå› ç‚ºæ‚¨è¢«å„²å­˜åº«æ“æœ‰è€…å°éŽ–ã€‚ -pulls.new.must_collaborator=您必須是å”作者æ‰èƒ½å»ºç«‹åˆä½µè«‹æ±‚。 -pulls.edit.already_changed=無法儲存åˆä½µè«‹æ±‚çš„è®Šæ›´ã€‚çœ‹èµ·ä¾†å…§å®¹å·²è¢«å…¶ä»–ä½¿ç”¨è€…æ›´æ”¹ã€‚è«‹é‡æ–°æ•´ç†é é¢ä¸¦å†æ¬¡å˜—試編輯以é¿å…覆蓋他們的變更。 pulls.view=檢視åˆä½µè«‹æ±‚ pulls.compare_changes=建立åˆä½µè«‹æ±‚ pulls.allow_edits_from_maintainers=å…許維護者編輯 @@ -1812,31 +1504,20 @@ pulls.compare_compare=拉å–自 pulls.switch_comparison_type=åˆ‡æ›æ¯”較類型 pulls.switch_head_and_base=åˆ‡æ› head å’Œ base pulls.filter_branch=éŽæ¿¾åˆ†æ”¯ -pulls.show_all_commits=顯示所有æäº¤ -pulls.show_changes_since_your_last_review=顯示自上次審核以來的變更 -pulls.showing_only_single_commit=僅顯示æäº¤ %[1]s 的變更 -pulls.showing_specified_commit_range=僅顯示介於 %[1]s å’Œ %[2]s 之間的變更 -pulls.select_commit_hold_shift_for_range=鏿“‡æäº¤ã€‚æŒ‰ä½ Shift ä¸¦é»žæ“Šä»¥é¸æ“‡ç¯„åœ -pulls.review_only_possible_for_full_diff=僅在查看完整差異時æ‰èƒ½é€²è¡Œå¯©æ ¸ -pulls.filter_changes_by_commit=按æäº¤ç¯©é¸è®Šæ›´ pulls.nothing_to_compare=這些分支的內容相åŒï¼Œç„¡éœ€å»ºç«‹åˆä½µè«‹æ±‚。 -pulls.nothing_to_compare_have_tag=所é¸çš„分支/標籤相åŒã€‚ pulls.nothing_to_compare_and_allow_empty_pr=這些分支的內容相åŒï¼Œæ­¤åˆä½µè«‹æ±‚將會是空白的。 pulls.has_pull_request=`已有介於這些分支間的åˆä½µè«‹æ±‚:%[2]s#%[3]d` pulls.create=建立åˆä½µè«‹æ±‚ -pulls.title_desc=請求將 %[1]d 次æäº¤å¾ž %[2]s åˆä½µè‡³ %[3]s +pulls.title_desc=請求將 %[1]d 次程å¼ç¢¼æäº¤å¾ž %[2]s åˆä½µè‡³ %[3]s pulls.merged_title_desc=å°‡ %[1]d 次æäº¤å¾ž %[2]s åˆä½µè‡³ %[3]s %[4]s pulls.change_target_branch_at=`將目標分支從 %s 更改為 %s %s` pulls.tab_conversation=å°è©±å…§å®¹ -pulls.tab_commits=æäº¤ +pulls.tab_commits=程å¼ç¢¼æäº¤ pulls.tab_files=檔案變動 pulls.reopen_to_merge=è«‹é‡æ–°é–‹æ”¾æ­¤åˆä½µè«‹æ±‚以進行åˆä½µä½œæ¥­ã€‚ pulls.cant_reopen_deleted_branch=ç„¡æ³•é‡æ–°é–‹æ”¾æ­¤åˆä½µè«‹æ±‚,因為該分支已刪除。 pulls.merged=å·²åˆä½µ -pulls.merged_success=åˆä½µè«‹æ±‚å·²æˆåŠŸåˆä½µä¸¦é—œé–‰ -pulls.closed=關閉åˆä½µè«‹æ±‚ pulls.manually_merged=手動åˆä½µ -pulls.merged_info_text=ç¾åœ¨å¯ä»¥åˆªé™¤åˆ†æ”¯ %s。 pulls.is_closed=åˆä½µè«‹æ±‚已被關閉。 pulls.title_wip_desc=`標題用 %s 開頭以é¿å…æ„外地åˆä½µæ­¤åˆä½µè«‹æ±‚。` pulls.cannot_merge_work_in_progress=æ­¤åˆä½µè«‹æ±‚被標記為還在進行中 (WIP)。 @@ -1851,13 +1532,6 @@ pulls.is_empty=在這個分支上的更動都已經套用在目標分支上。 pulls.required_status_check_failed=æœªé€šéŽæŸäº›å¿…è¦çš„æª¢æŸ¥ã€‚ pulls.required_status_check_missing=éºå¤±æŸäº›å¿…è¦çš„æª¢æŸ¥ã€‚ pulls.required_status_check_administrator=身為系統管ç†å“¡ï¼Œæ‚¨ä¾ç„¶å¯ä»¥é€²è¡Œåˆä½µã€‚ -pulls.blocked_by_approvals=æ­¤åˆä½µè«‹æ±‚尚未ç²å¾—足夠的核å¯ã€‚å·²ç²å¾— %d 個核å¯ä¸­çš„ %d 個。 -pulls.blocked_by_approvals_whitelisted=æ­¤åˆä½µè«‹æ±‚尚未ç²å¾—足夠的核å¯ã€‚å·²ç²å¾—å…許å單中的 %d 個核å¯ä¸­çš„ %d 個。 -pulls.blocked_by_rejection=æ­¤åˆä½µè«‹æ±‚有官方審核者請求變更。 -pulls.blocked_by_official_review_requests=æ­¤åˆä½µè«‹æ±‚有官方審核請求。 -pulls.blocked_by_outdated_branch=æ­¤åˆä½µè«‹æ±‚è¢«é˜»æ“‹ï¼Œå› ç‚ºå®ƒå·²éŽæ™‚。 -pulls.blocked_by_changed_protected_files_1=æ­¤åˆä½µè«‹æ±‚被阻擋,因為它更改了å—ä¿è­·çš„æª”案: -pulls.blocked_by_changed_protected_files_n=æ­¤åˆä½µè«‹æ±‚被阻擋,因為它更改了å—ä¿è­·çš„æª”案: pulls.can_auto_merge_desc=這個åˆä½µè«‹æ±‚å¯ä»¥è‡ªå‹•åˆä½µã€‚ pulls.cannot_auto_merge_desc=æ­¤åˆä½µè«‹æ±‚無法自動åˆä½µï¼Œå› ç‚ºæœ‰è¡çªã€‚ pulls.cannot_auto_merge_helper=手動åˆä½µä»¥è§£æ±ºæ­¤è¡çªã€‚ @@ -1892,10 +1566,7 @@ pulls.rebase_conflict_summary=éŒ¯èª¤è¨Šæ¯ pulls.unrelated_histories=åˆä½µå¤±æ•—:è¦åˆä½µçš„ HEAD 和基底分支沒有共åŒçš„æ­·å²ã€‚ æç¤ºï¼šè«‹å˜—試ä¸åŒçš„ç­–ç•¥ pulls.merge_out_of_date=åˆä½µå¤±æ•—:產生åˆä½µæ™‚,基底已被更新。æç¤ºï¼šå†è©¦ä¸€æ¬¡ã€‚ pulls.head_out_of_date=åˆä½µå¤±æ•—:產生åˆä½µæ™‚,head 已被更新。æç¤ºï¼šå†è©¦ä¸€æ¬¡ã€‚ -pulls.has_merged=失敗:此åˆä½µè«‹æ±‚已被åˆä½µï¼Œæ‚¨ä¸èƒ½å†æ¬¡åˆä½µæˆ–更改目標分支。 -pulls.push_rejected=åˆä½µå¤±æ•—:此推é€è¢«æ‹’絕。請檢查此儲存庫的 Git Hook。 pulls.push_rejected_summary=å®Œæ•´çš„æ‹’çµ•è¨Šæ¯ -pulls.push_rejected_no_message=åˆä½µå¤±æ•—:此推é€è¢«æ‹’絕但未æä¾›å…¶ä»–資訊。
請檢查此儲存庫的 Git Hook。 pulls.open_unmerged_pull_exists=`您ä¸èƒ½é‡æ–°é–‹æ”¾ï¼Œå› ç‚ºç›®å‰æœ‰ç›¸åŒçš„åˆä½µè«‹æ±‚ (#%d) 正在進行中。` pulls.status_checking=還在進行一些檢查 pulls.status_checks_success=å·²é€šéŽæ‰€æœ‰æª¢æŸ¥ @@ -1904,8 +1575,6 @@ pulls.status_checks_failure=一些檢查失敗了 pulls.status_checks_error=一些檢查回報了錯誤 pulls.status_checks_requested=å¿…è¦ pulls.status_checks_details=詳情 -pulls.status_checks_hide_all=éš±è—æ‰€æœ‰æª¢æŸ¥ -pulls.status_checks_show_all=顯示所有檢查 pulls.update_branch=以åˆä½µæ›´æ–°åˆ†æ”¯ pulls.update_branch_rebase=以 Rebase 更新分支 pulls.update_branch_success=分支更新æˆåŠŸ @@ -1914,12 +1583,6 @@ pulls.outdated_with_base_branch=ç›¸å°æ–¼åŸºåº•åˆ†æ”¯ï¼Œæ­¤åˆ†æ”¯å·²éŽæ™‚ pulls.close=關閉åˆä½µè«‹æ±‚ pulls.closed_at=`關閉了這個åˆä½µè«‹æ±‚ %[2]s` pulls.reopened_at=`釿–°é–‹æ”¾äº†é€™å€‹åˆä½µè«‹æ±‚ %[2]s` -pulls.cmd_instruction_hint=`檢視 命令列指示。` -pulls.cmd_instruction_checkout_title=檢出 -pulls.cmd_instruction_checkout_desc=從您的專案儲存庫中,檢出一個新分支並測試變更。 -pulls.cmd_instruction_merge_title=åˆä½µ -pulls.cmd_instruction_merge_desc=åˆä½µè®Šæ›´ä¸¦åœ¨ Gitea 上更新。 -pulls.cmd_instruction_merge_warning=警告:此æ“作無法åˆä½µåˆä½µè«‹æ±‚,因為未啟用「自動檢測手動åˆä½µã€ pulls.clear_merge_message=清除åˆä½µè¨Šæ¯ pulls.clear_merge_message_hint=清除åˆä½µè¨Šæ¯å°‡åƒ…移除æäº¤è¨Šæ¯å…§å®¹ï¼Œç•™ä¸‹ç”¢ç”Ÿçš„ git çµå°¾ï¼Œå¦‚「Co-Authored-By …ã€ã€‚ @@ -1938,14 +1601,8 @@ pulls.auto_merge_canceled_schedule_comment=`å–æ¶ˆäº†åœ¨é€šéŽæ‰€æœ‰æª¢æŸ¥å¾Œè‡ª pulls.delete.title=刪除此åˆä½µè«‹æ±‚? pulls.delete.text=您真的è¦åˆªé™¤æ­¤åˆä½µè«‹æ±‚嗎?(這將會永久移除所有內容。若您還想ä¿ç•™ï¼Œè«‹è€ƒæ…®æ”¹ç‚ºé—œé–‰å®ƒã€‚) -pulls.recently_pushed_new_branches=您在分支 %[1]s 上推é€äº† %[2]s -pulls.upstream_diverging_prompt_base_newer=基底分支 %s 有新變更 -pulls.upstream_diverging_merge=åŒæ­¥ fork -pull.deleted_branch=(已刪除): %s -pull.agit_documentation=查看 AGit 的文件 -comments.edit.already_changed=ç„¡æ³•å„²å­˜ç•™è¨€çš„è®Šæ›´ã€‚çœ‹èµ·ä¾†å…§å®¹å·²è¢«å…¶ä»–ä½¿ç”¨è€…æ›´æ”¹ã€‚è«‹é‡æ–°æ•´ç†é é¢ä¸¦å†æ¬¡å˜—試編輯以é¿å…覆蓋他們的變更 milestones.new=新增里程碑 milestones.closed=æ–¼ %s關閉 @@ -1953,8 +1610,6 @@ milestones.update_ago=已更新 %s milestones.no_due_date=暫無截止日期 milestones.open=開啟 milestones.close=關閉 -milestones.new_subheader=里程碑å¯ç”¨ä¾†çµ„ç¹”å•題和追蹤進度。 -milestones.completeness=%d%% å®Œæˆ milestones.create=建立里程碑 milestones.title=標題 milestones.desc=æè¿° @@ -1971,26 +1626,11 @@ milestones.deletion=刪除里程碑 milestones.deletion_desc=刪除里程碑會從所有相關的å•題移除它。是å¦ç¹¼çºŒï¼Ÿ milestones.deletion_success=里程碑已刪除 milestones.filter_sort.name=å稱 -milestones.filter_sort.earliest_due_data=截止日期由é åˆ°è¿‘ -milestones.filter_sort.latest_due_date=æˆªæ­¢æ—¥æœŸç”±è¿‘åˆ°é  milestones.filter_sort.least_complete=完æˆåº¦ç”±ä½Žåˆ°é«˜ milestones.filter_sort.most_complete=完æˆåº¦ç”±é«˜åˆ°ä½Ž milestones.filter_sort.most_issues=å•題由多到少 milestones.filter_sort.least_issues=å•題由少到多 -signing.will_sign=æ­¤æäº¤å°‡ä½¿ç”¨é‡‘鑰「%sã€ç°½ç½²ã€‚ -signing.wont_sign.error=檢查æäº¤æ˜¯å¦å¯ä»¥ç°½ç½²æ™‚發生錯誤。 -signing.wont_sign.nokey=沒有å¯ç”¨çš„金鑰來簽署此æäº¤ã€‚ -signing.wont_sign.never=æäº¤å¾žä¸ç°½ç½²ã€‚ -signing.wont_sign.always=æäº¤ç¸½æ˜¯ç°½ç½²ã€‚ -signing.wont_sign.pubkey=æäº¤ä¸æœƒè¢«ç°½ç½²ï¼Œå› ç‚ºæ‚¨çš„帳戶沒有關è¯çš„公鑰。 -signing.wont_sign.twofa=您必須啟用雙因素驗證æ‰èƒ½ç°½ç½²æäº¤ã€‚ -signing.wont_sign.parentsigned=æäº¤ä¸æœƒè¢«ç°½ç½²ï¼Œå› ç‚ºçˆ¶æäº¤æœªç°½ç½²ã€‚ -signing.wont_sign.basesigned=åˆä½µä¸æœƒè¢«ç°½ç½²ï¼Œå› ç‚ºåŸºåº•æäº¤æœªç°½ç½²ã€‚ -signing.wont_sign.headsigned=åˆä½µä¸æœƒè¢«ç°½ç½²ï¼Œå› ç‚º head æäº¤æœªç°½ç½²ã€‚ -signing.wont_sign.commitssigned=åˆä½µä¸æœƒè¢«ç°½ç½²ï¼Œå› ç‚ºæ‰€æœ‰ç›¸é—œçš„æäº¤éƒ½æœªç°½ç½²ã€‚ -signing.wont_sign.approved=åˆä½µä¸æœƒè¢«ç°½ç½²ï¼Œå› ç‚º PR 未被核准。 -signing.wont_sign.not_signed_in=你還沒有登入。 ext_wiki=å­˜å–外部 Wiki ext_wiki.desc=連çµå¤–部 Wiki。 @@ -2020,13 +1660,8 @@ wiki.reserved_page=「%sã€æ˜¯ä¿ç•™çš„ Wiki é é¢å稱。 wiki.pages=所有é é¢ wiki.last_updated=最後更新於 %s wiki.page_name_desc=輸入此 Wiki é é¢çš„å稱。一些特殊å稱有:「Homeã€ã€ã€Œ_Sidebarã€ã€ã€Œ_Footerã€ç­‰ã€‚ -wiki.original_git_entry_tooltip=檢視原始 Git æª”æ¡ˆè€Œä¸æ˜¯ä½¿ç”¨å‹å–„連çµã€‚ activity=å‹•æ…‹ -activity.navbar.pulse=脈æ -activity.navbar.code_frequency=程å¼ç¢¼é »çއ -activity.navbar.contributors=è²¢ç»è€… -activity.navbar.recent_commits=最近æäº¤ activity.period.filter_label=期間: activity.period.daily=1 天 activity.period.halfweekly=3 天 @@ -2092,10 +1727,7 @@ activity.git_stats_and_deletions=å’Œ activity.git_stats_deletion_1=刪除 %d 行 activity.git_stats_deletion_n=刪除 %d 行 -contributors.contribution_type.filter_label=è²¢ç»é¡žåž‹ï¼š contributors.contribution_type.commits=æäº¤æ­·å² -contributors.contribution_type.additions=新增 -contributors.contribution_type.deletions=刪除 settings=設定 settings.desc=設定是您å¯ä»¥ç®¡ç†å„²å­˜åº«è¨­å®šçš„地方 @@ -2110,20 +1742,7 @@ settings.hooks=Webhook settings.githooks=Git Hook settings.basic_settings=基本設定 settings.mirror_settings=é¡åƒè¨­å®š -settings.mirror_settings.docs=è¨­å®šæ‚¨çš„å„²å­˜åº«è‡ªå‹•åŒæ­¥å…¶ä»–儲存庫的æäº¤ã€æ¨™ç±¤ã€åˆ†æ”¯ã€‚ -settings.mirror_settings.docs.disabled_pull_mirror.instructions=設定您的專案自動將æäº¤ã€æ¨™ç±¤ã€åˆ†æ”¯æŽ¨é€åˆ°å…¶ä»–儲存庫。您的網站管ç†å“¡å·²åœç”¨äº†æ‹‰å–é¡åƒã€‚ -settings.mirror_settings.docs.disabled_push_mirror.instructions=è¨­å®šæ‚¨çš„å°ˆæ¡ˆè‡ªå‹•å¾žå…¶ä»–å„²å­˜åº«æ‹‰å–æäº¤ã€æ¨™ç±¤ã€åˆ†æ”¯ã€‚ -settings.mirror_settings.docs.disabled_push_mirror.pull_mirror_warning=ç¾åœ¨é€™å€‹åŠŸèƒ½åªèƒ½å¾žã€Œé·ç§»å¤–部儲存庫ã€é€²è¡Œè¨­å®šï¼Œè©³æƒ…è«‹åƒè€ƒ: -settings.mirror_settings.docs.disabled_push_mirror.info=您的網站管ç†å“¡å·²åœç”¨äº†æŽ¨é€é¡åƒã€‚ -settings.mirror_settings.docs.no_new_mirrors=您的儲存庫正在é¡åƒè®Šæ›´åˆ°æˆ–從å¦ä¸€å€‹å„²å­˜åº«ã€‚請注æ„,您目å‰ç„¡æ³•建立任何新的é¡åƒã€‚ -settings.mirror_settings.docs.can_still_use=é›–ç„¶æ‚¨ç„¡æ³•ä¿®æ”¹ç¾æœ‰çš„é¡åƒæˆ–建立新的é¡åƒï¼Œä½†æ‚¨ä»ç„¶å¯ä»¥ä½¿ç”¨ç¾æœ‰çš„é¡åƒã€‚ -settings.mirror_settings.docs.pull_mirror_instructions=設定拉å–é¡åƒè«‹åƒè€ƒï¼š -settings.mirror_settings.docs.more_information_if_disabled=您å¯ä»¥åœ¨é€™è£¡æ‰¾åˆ°æœ‰é—œæŽ¨é€å’Œæ‹‰å–é¡åƒçš„æ›´å¤šè³‡è¨Šï¼š -settings.mirror_settings.docs.doc_link_title=如何é¡åƒå„²å­˜åº«ï¼Ÿ -settings.mirror_settings.docs.doc_link_pull_section=文件中的「從é ç«¯å„²å­˜åº«æ‹‰å–ã€éƒ¨åˆ†ã€‚ -settings.mirror_settings.docs.pulling_remote_title=正在從é ç«¯å„²å­˜åº«æ‹‰å– settings.mirror_settings.mirrored_repository=å·²é¡åƒçš„儲存庫 -settings.mirror_settings.pushed_repository=推é€çš„儲存庫 settings.mirror_settings.direction=æ–¹å‘ settings.mirror_settings.direction.pull=æ‹‰å– settings.mirror_settings.direction.push=æŽ¨é€ @@ -2131,23 +1750,15 @@ settings.mirror_settings.last_update=最近更新時間 settings.mirror_settings.push_mirror.none=未設定推é€é¡åƒ settings.mirror_settings.push_mirror.remote_url=Git é ç«¯å„²å­˜åº« URL settings.mirror_settings.push_mirror.add=新增推é€é¡åƒ -settings.mirror_settings.push_mirror.edit_sync_time=編輯é¡åƒåŒæ­¥é–“éš” settings.sync_mirror=ç«‹å³åŒæ­¥ -settings.pull_mirror_sync_in_progress=ç›®å‰æ­£åœ¨å¾žé ç«¯ %s 拉å–變更。 -settings.push_mirror_sync_in_progress=ç›®å‰æ­£åœ¨æŽ¨é€è®Šæ›´åˆ°é ç«¯ %s。 settings.site=網站 settings.update_settings=更新設定 -settings.update_mirror_settings=æ›´æ–°é¡åƒè¨­å®š -settings.branches.switch_default_branch=切æ›é è¨­åˆ†æ”¯ settings.branches.update_default_branch=æ›´æ–°é è¨­åˆ†æ”¯ settings.branches.add_new_rule=加入新è¦å‰‡ settings.advanced_settings=進階設定 settings.wiki_desc=啟用儲存庫 Wiki settings.use_internal_wiki=使用內建 Wiki -settings.default_wiki_branch_name=é è¨­ Wiki 分支å稱 -settings.default_wiki_everyone_access=登入使用者的é è¨­å­˜å–權é™ï¼š -settings.failed_to_change_default_wiki_branch=更改é è¨­ Wiki 分支失敗。 settings.use_external_wiki=使用外部 Wiki settings.external_wiki_url=外部 Wiki é€£çµ settings.external_wiki_url_error=外部 Wiki ç¶²å€ä¸æ˜¯æœ‰æ•ˆçš„ç¶²å€ã€‚ @@ -2177,10 +1788,6 @@ settings.pulls.default_delete_branch_after_merge=é è¨­åœ¨åˆä½µå¾Œåˆªé™¤åˆä½µ settings.pulls.default_allow_edits_from_maintainers=é è¨­å…許維護者進行編輯 settings.releases_desc=啟用儲存庫版本發佈 settings.packages_desc=啟用儲存庫套件註冊中心 -settings.projects_desc=啟用儲存庫專案 -settings.projects_mode_desc=å°ˆæ¡ˆæ¨¡å¼ (顯示哪種類型的專案) -settings.projects_mode_repo=僅儲存庫專案 -settings.projects_mode_owner=僅使用者或組織專案 settings.projects_mode_all=所有專案 settings.actions_desc=啟用儲存庫 Actions settings.admin_settings=管ç†å“¡è¨­å®š @@ -2207,17 +1814,14 @@ settings.convert_fork_succeed=æ­¤ fork å·²è½‰æ›æˆæ™®é€šå„²å­˜åº«ã€‚ settings.transfer=轉移儲存庫所有權 settings.transfer.rejected=儲存庫轉移被拒絕。 settings.transfer.success=儲存庫已æˆåŠŸè½‰ç§»ã€‚ -settings.transfer.blocked_user=ç„¡æ³•è½‰ç§»å„²å­˜åº«ï¼Œå› ç‚ºæ‚¨è¢«æ–°æ“æœ‰è€…å°éŽ–ã€‚ settings.transfer_abort=å–æ¶ˆè½‰ç§» settings.transfer_abort_invalid=æ‚¨ç„¡æ³•å–æ¶ˆä¸å­˜åœ¨çš„儲存庫轉移。 -settings.transfer_abort_success=æˆåŠŸå–æ¶ˆè½‰ç§»å„²å­˜åº«è‡³ %s。 settings.transfer_desc=å°‡æ­¤å„²å­˜åº«è½‰ç§»çµ¦å…¶ä»–ä½¿ç”¨è€…æˆ–å—æ‚¨ç®¡ç†çš„組織。 settings.transfer_form_title=輸入儲存庫å稱以確èªï¼š settings.transfer_in_progress=ç›®å‰æ­£åœ¨é€²è¡Œè½‰ç§»ã€‚如果您想è¦å°‡æ­¤å„²å­˜åº«è½‰ç§»çµ¦å…¶ä»–ä½¿ç”¨è€…ï¼Œè«‹å–æ¶ˆä»–。 settings.transfer_notices_1=- å¦‚æžœå°‡æ­¤å„²å­˜åº«è½‰ç§»çµ¦å€‹åˆ¥ä½¿ç”¨è€…ï¼Œæ‚¨å°‡æœƒå¤±åŽ»æ­¤å„²å­˜åº«çš„å­˜å–æ¬Šã€‚ settings.transfer_notices_2=- 如果將此儲存庫轉移到您(å…±åŒ)æ“æœ‰çš„çµ„ç¹”ï¼Œæ‚¨å°‡èƒ½ç¹¼çºŒä¿æœ‰æ­¤å„²å­˜åº«çš„å­˜å–æ¬Šã€‚ settings.transfer_notices_3=- å¦‚æžœæ­¤å„²å­˜åº«ç‚ºç§æœ‰å„²å­˜åº«ä¸”將轉移給個別使用者,此動作確ä¿è©²ä½¿ç”¨è€…è‡³å°‘æ“æœ‰è®€å–æ¬Šé™ (å¿…è¦æ™‚將會修改權é™)。 -settings.transfer_notices_4=- 如果此儲存庫屬於組織,並且您將其轉移給å¦ä¸€å€‹çµ„織或個人,您將失去儲存庫å•題與組織專案æ¿ä¹‹é–“的連çµã€‚ settings.transfer_owner=æ–°æ“æœ‰è€… settings.transfer_perform=進行轉移 settings.transfer_started=此儲存庫已被標記為待轉移且正在等待「%sã€çš„ç¢ºèª @@ -2247,14 +1851,12 @@ settings.delete_notices_2=- æ­¤æ“作將永久刪除 %s 儲存 settings.delete_notices_fork_1=- 在此儲存庫刪除後,它的 fork 將會變æˆç¨ç«‹å„²å­˜åº«ã€‚ settings.deletion_success=這個儲存庫已被刪除。 settings.update_settings_success=已更新儲存庫的設定。 -settings.update_settings_no_unit=儲存庫應å…許至少æŸç¨®å½¢å¼çš„互動。 settings.confirm_delete=刪除儲存庫 settings.add_collaborator=增加å”作者 settings.add_collaborator_success=æˆåŠŸå¢žåŠ å”ä½œè€…ï¼ settings.add_collaborator_inactive_user=無法將未啟用的使用者加入為å”作者。 settings.add_collaborator_owner=ç„¡æ³•å°‡æ“æœ‰è€…加入為å”作者。 settings.add_collaborator_duplicate=æ­¤å”作者早已被加入此儲存庫。 -settings.add_collaborator.blocked_user=å”ä½œè€…è¢«å„²å­˜åº«æ“æœ‰è€…å°éŽ–æˆ–å之亦然。 settings.delete_collaborator=移除 settings.collaborator_deletion=移除å”作者 settings.collaborator_deletion_desc=移除å”ä½œè€…å°‡æ‹’çµ•ä»–å­˜å–æ­¤å„²å­˜åº«ã€‚是å¦ç¹¼çºŒï¼Ÿ @@ -2277,14 +1879,12 @@ settings.webhook_deletion_desc=移除 Webhook 將刪除它的設定åŠå‚³é€è¨˜ settings.webhook_deletion_success=Webhook 已移除。 settings.webhook.test_delivery=傳逿¸¬è©¦è³‡æ–™ settings.webhook.test_delivery_desc=使用å‡äº‹ä»¶æ¸¬è©¦æ­¤ Webhook。 -settings.webhook.test_delivery_desc_disabled=è¦ä½¿ç”¨å‡äº‹ä»¶æ¸¬è©¦æ­¤ Webhook,請啟用它。 settings.webhook.request=請求 settings.webhook.response=回應 settings.webhook.headers=標頭 settings.webhook.payload=內容 settings.webhook.body=本體 settings.webhook.replay.description=冿¬¡åŸ·è¡Œæ­¤ Webhook。 -settings.webhook.replay.description_disabled=è¦é‡æ–°åŸ·è¡Œæ­¤ Webhook,請啟用它。 settings.webhook.delivery.success=已將事件加入到傳é€ä½‡åˆ—,å¯èƒ½éœ€è¦ç­‰å¾…å¹¾åˆ†é˜æ‰æœƒå‡ºç¾æ–¼å‚³é€ç´€éŒ„。 settings.githooks_desc=Git Hook 是 Git 本身æä¾›çš„功能。您å¯ä»¥åœ¨ä¸‹æ–¹ç·¨è¼¯ hook 檔案以設定自訂作業。 settings.githook_edit_desc=如果 Hook 未啟動,則會顯示範例文件中的內容。如果想è¦åˆªé™¤æŸå€‹ Hook,則é€å‡ºç©ºç™½å…§å®¹å³å¯ã€‚ @@ -2294,8 +1894,8 @@ settings.update_githook=æ›´æ–° Hook settings.add_webhook_desc=Gitea 會發é€å«æœ‰æŒ‡å®š Content Type çš„ POST 請求到目標 URL。 在 Webhook 指å—閱讀更多內容。 settings.payload_url=目標 URL settings.http_method=HTTP 請求方法 -settings.content_type=POST 內容類型 -settings.secret=密鑰 +settings.content_type=POST Content Type +settings.secret=Secret settings.slack_username=æœå‹™å稱 settings.slack_icon_url=圖示 URL settings.slack_color=é¡è‰² @@ -2317,7 +1917,6 @@ settings.event_wiki_desc=建立ã€é‡æ–°å‘½åã€ç·¨è¼¯ã€åˆªé™¤ Wiki é é¢ã€‚ settings.event_release=版本發布 settings.event_release_desc=åœ¨å„²å­˜åº«ä¸­ç™¼å¸ƒã€æ›´æ–°æˆ–刪除版本。 settings.event_push=æŽ¨é€ -settings.event_force_push=å¼·åˆ¶æŽ¨é€ settings.event_push_desc=推é€åˆ°å„²å­˜åº«ã€‚ settings.event_repository=儲存庫 settings.event_repository_desc=建立或刪除儲存庫。 @@ -2347,14 +1946,9 @@ settings.event_pull_request_review=åˆä½µè«‹æ±‚審核 settings.event_pull_request_review_desc=核准ã€é€€å›žæˆ–æå‡ºå¯©æ ¸ç•™è¨€ã€‚ settings.event_pull_request_sync=åˆä½µè«‹æ±‚åŒæ­¥ settings.event_pull_request_sync_desc=åˆä½µè«‹æ±‚åŒæ­¥ã€‚ -settings.event_pull_request_review_request=åˆä½µè«‹æ±‚審核請求 -settings.event_pull_request_review_request_desc=åˆä½µè«‹æ±‚審核請求或審核請求已移除。 -settings.event_pull_request_approvals=åˆä½µè«‹æ±‚æ ¸å¯ -settings.event_pull_request_merge=åˆä½µè«‹æ±‚åˆä½µ settings.event_package=套件 settings.event_package_desc=套件已在儲存庫中建立或刪除。 settings.branch_filter=åˆ†æ”¯ç¯©é¸ -settings.branch_filter_desc=推é€ã€å»ºç«‹åˆ†æ”¯ã€åˆªé™¤åˆ†æ”¯äº‹ä»¶çš„白å單,請使用 glob æ¯”å°æ¨¡å¼ã€‚如果留白或輸入*,所有分支的事件都會被回報。語法åƒè¦‹ github.com/gobwas/glob。範例:master, {master,release*}。 settings.authorization_header=Authorization 標頭 settings.authorization_header_desc=å­˜åœ¨æ™‚å°‡å°‡åŒ…å«æ­¤ Authorization 標頭在請求中。例: %s。 settings.active=啟用 @@ -2400,72 +1994,26 @@ settings.deploy_key_deletion=刪除部署金鑰 settings.deploy_key_deletion_desc=ç§»é™¤éƒ¨ç½²é‡‘é‘°å°‡æ‹’çµ•å®ƒå­˜å–æ­¤å„²å­˜åº«ã€‚是å¦ç¹¼çºŒï¼Ÿ settings.deploy_key_deletion_success=部署金鑰已移除。 settings.branches=分支 -settings.protected_branch=分支ä¿è­· settings.protected_branch.save_rule=儲存è¦å‰‡ settings.protected_branch.delete_rule=刪除è¦å‰‡ -settings.protected_branch_can_push=å…許推é€ï¼Ÿ -settings.protected_branch_can_push_yes=ä½ å¯ä»¥æŽ¨é€ -settings.protected_branch_can_push_no=ä½ ä¸èƒ½æŽ¨é€ -settings.branch_protection=%s 的分支ä¿è­· settings.protect_this_branch=啟用分支ä¿è­· settings.protect_this_branch_desc=防止刪除分支,並é™åˆ¶ Git 推é€èˆ‡åˆä½µåˆ°åˆ†æ”¯ã€‚ settings.protect_disable_push=åœç”¨æŽ¨é€ settings.protect_disable_push_desc=ä¸å…許推é€åˆ°æ­¤åˆ†æ”¯ã€‚ -settings.protect_disable_force_push=åœç”¨å¼·åˆ¶æŽ¨é€ -settings.protect_disable_force_push_desc=ä¸å…許強制推é€åˆ°æ­¤åˆ†æ”¯ã€‚ settings.protect_enable_push=å•Ÿç”¨æŽ¨é€ settings.protect_enable_push_desc=ä»»ä½•æ“æœ‰å¯«å…¥æ¬Šé™çš„ä½¿ç”¨è€…å°‡å¯æŽ¨é€è‡³è©²åˆ†æ”¯(但ä¸å¯ä½¿ç”¨force push)。 -settings.protect_enable_force_push_all=å•Ÿç”¨å¼·åˆ¶æŽ¨é€ -settings.protect_enable_force_push_all_desc=ä»»ä½•æœ‰æŽ¨é€æ¬Šé™çš„人都å¯ä»¥å¼·åˆ¶æŽ¨é€åˆ°æ­¤åˆ†æ”¯ã€‚ -settings.protect_enable_force_push_allowlist=å…許åå–®é™åˆ¶å¼·åˆ¶æŽ¨é€ -settings.protect_enable_force_push_allowlist_desc=åªæœ‰æŽ¨é€æ¬Šé™çš„å…許å單內的使用者或團隊å¯ä»¥å¼·åˆ¶æŽ¨é€åˆ°æ­¤åˆ†æ”¯ã€‚ settings.protect_enable_merge=啟用åˆä½µ settings.protect_enable_merge_desc=任何有寫入權é™çš„人都å¯å°‡åˆä½µè«‹æ±‚åˆä½µåˆ°æ­¤åˆ†æ”¯ -settings.protect_whitelist_committers=使用白åå–®æŽ§ç®¡æŽ¨é€ -settings.protect_whitelist_committers_desc=僅å…許白å單內的使用者或團隊推é€è‡³è©²åˆ†æ”¯(但ä¸å¯ä½¿ç”¨force push)。 -settings.protect_whitelist_deploy_keys=å°‡æ“æœ‰å¯«å…¥æ¬Šé™çš„部署金鑰加入白å單。 -settings.protect_whitelist_users=å…許推é€çš„使用者: -settings.protect_whitelist_teams=å…許推é€çš„團隊: -settings.protect_force_push_allowlist_users=å…許強制推é€çš„使用者: -settings.protect_force_push_allowlist_teams=å…許強制推é€çš„團隊: -settings.protect_force_push_allowlist_deploy_keys=å…許強制推é€çš„部署金鑰。 -settings.protect_merge_whitelist_committers=啟用åˆä½µç™½åå–® -settings.protect_merge_whitelist_committers_desc=僅å…許白å單內的使用者或團隊將åˆä½µè«‹æ±‚åˆä½µè‡³è©²åˆ†æ”¯ã€‚ -settings.protect_merge_whitelist_users=å…許åˆä½µçš„使用者: -settings.protect_merge_whitelist_teams=å…許åˆä½µçš„團隊: settings.protect_check_status_contexts=啟用狀態檢查 -settings.protect_status_check_patterns=狀態檢查模å¼: -settings.protect_status_check_patterns_desc=輸入模å¼ä»¥æŒ‡å®šå…¶ä»–分支在åˆä½µåˆ°å—æ­¤è¦å‰‡ä¿è­·çš„分支å‰å¿…須通éŽçš„狀態檢查。æ¯è¡ŒæŒ‡å®šä¸€å€‹æ¨¡å¼ï¼Œæ¨¡å¼ä¸å¾—為空白。 -settings.protect_check_status_contexts_desc=åˆä½µå‰å¿…須先通éŽç‹€æ…‹æª¢æŸ¥ã€‚鏿“‡åˆä½µå‰å¿…須通éŽçš„æª¢æŸ¥ã€‚啟用時,必須先將æäº¤æŽ¨é€åˆ°å¦ä¸€å€‹åˆ†æ”¯ï¼Œé€šéŽç‹€æ…‹æª¢æŸ¥å¾Œå†åˆä½µæˆ–直接推é€åˆ°ç¬¦åˆè¦å‰‡çš„åˆ†æ”¯ã€‚å¦‚æžœæœªé¸æ“‡ä»»ä½•項目,最一個æäº¤å¿…å°‡æˆåŠŸé€šéŽç‹€æ…‹æª¢æŸ¥ã€‚ settings.protect_check_status_contexts_list=此儲存庫一週內曾進行éŽç‹€æ…‹æª¢æŸ¥ -settings.protect_status_check_matched=ç¬¦åˆ -settings.protect_invalid_status_check_pattern=狀態檢查模å¼ç„¡æ•ˆ: 「%sã€ã€‚ -settings.protect_no_valid_status_check_patterns=沒有有效的狀態檢查模å¼ã€‚ settings.protect_required_approvals=需è¦çš„æ ¸å¯æ•¸é‡ï¼š -settings.protect_required_approvals_desc=åªæœ‰åœ¨ç²å¾—足夠數é‡çš„æ ¸å¯å¾Œæ‰èƒ½é€²è¡Œåˆä½µã€‚ -settings.protect_approvals_whitelist_enabled=使用白å單控管審核人員與團隊 -settings.protect_approvals_whitelist_enabled_desc=åªæœ‰ç™½å單內的使用者與團隊會被計入需è¦çš„æ ¸å¯æ•¸é‡ã€‚未使用白å單時,將計算任何有寫入權é™ä¹‹äººçš„æ ¸å¯ã€‚ -settings.protect_approvals_whitelist_users=審核者白å單: -settings.protect_approvals_whitelist_teams=審核團隊白å單: settings.dismiss_stale_approvals=æ¨æ£„éŽæ™‚çš„æ ¸å¯ settings.dismiss_stale_approvals_desc=ç•¶æ–°çš„æäº¤æœ‰ä¿®æ”¹åˆ°åˆä½µè«‹æ±‚的內容,並被推é€åˆ°æ­¤åˆ†æ”¯æ™‚ï¼Œå°‡æ¨æ£„舊的核å¯ã€‚ -settings.ignore_stale_approvals=å¿½ç•¥éŽæ™‚çš„æ ¸å¯ -settings.ignore_stale_approvals_desc=ä¸è¨ˆç®—在較舊æäº¤ä¸Šé€²è¡Œçš„æ ¸å¯ï¼ˆéŽæ™‚的審核)作為åˆä½µè«‹æ±‚çš„æ ¸å¯æ•¸é‡ã€‚å¦‚æžœéŽæ™‚çš„å¯©æ ¸å·²ç¶“è¢«æ¨æ£„,則無關緊è¦ã€‚ settings.require_signed_commits=僅接å—經簽署的æäº¤ settings.require_signed_commits_desc=拒絕未經簽署或未經驗證的æäº¤æŽ¨é€åˆ°æ­¤åˆ†æ”¯ã€‚ settings.protect_branch_name_pattern=å—ä¿è­·çš„分支åç¨±æ¨¡å¼ -settings.protect_branch_name_pattern_desc=å—ä¿è­·çš„分支å稱模å¼ã€‚è«‹åƒé–± 文件 以了解模å¼èªžæ³•。範例:main, release/** -settings.protect_patterns=æ¨¡å¼ settings.protect_protected_file_patterns=å—ä¿è­·çš„æª”æ¡ˆæ¨¡å¼ (以分號å€éš”「;ã€): -settings.protect_protected_file_patterns_desc=å³ä¾¿ä½¿ç”¨è€…æœ‰æ¬Šé™æ–°å¢žã€ä¿®æ”¹ã€åˆªé™¤æ­¤åˆ†æ”¯çš„æª”案,ä»ä¸å…許直接修改å—ä¿è­·çš„æª”案。å¯ä»¥ç”¨åŠå½¢åˆ†è™Ÿã€Œ;ã€åˆ†éš”多個模å¼ã€‚è«‹æ–¼ github.com/gobwas/glob æ–‡ä»¶æŸ¥çœ‹æ¨¡å¼æ ¼å¼ã€‚範例: .drone.yml, /docs/**/*.txt。 settings.protect_unprotected_file_patterns=未å—ä¿è­·çš„æª”æ¡ˆæ¨¡å¼ (以分號å€éš”「;ã€): -settings.protect_unprotected_file_patterns_desc=ç•¶ä½¿ç”¨è€…æœ‰å¯«å…¥æ¬Šé™æ™‚,å¯ç¹žé޿ލé€é™åˆ¶ï¼Œç›´æŽ¥ä¿®æ”¹æœªå—ä¿è­·çš„æª”案。å¯ä»¥ç”¨åŠå½¢åˆ†è™Ÿã€Œ;ã€åˆ†éš”多個模å¼ã€‚è«‹æ–¼ github.com/gobwas/glob æ–‡ä»¶æŸ¥çœ‹æ¨¡å¼æ ¼å¼ã€‚範例: .drone.yml, /docs/**/*.txt。 -settings.add_protected_branch=啟用ä¿è­· -settings.delete_protected_branch=åœç”¨ä¿è­· -settings.update_protect_branch_success=已更新「%sã€çš„分支ä¿è­·è¦å‰‡ã€‚ -settings.remove_protected_branch_success=已刪除「%sã€çš„分支ä¿è­·è¦å‰‡ã€‚ -settings.remove_protected_branch_failed=刪除分支ä¿è­·è¦å‰‡ã€Œ%sã€å¤±æ•—。 -settings.protected_branch_deletion=åœç”¨åˆ†æ”¯ä¿è­· settings.protected_branch_deletion_desc=åœç”¨åˆ†æ”¯ä¿è­·å°‡å…許有寫入權é™çš„使用者推é€è‡³è©²åˆ†æ”¯ï¼Œæ˜¯å¦ç¹¼çºŒï¼Ÿ settings.block_rejected_reviews=有退回的審核時阻擋åˆä½µ settings.block_rejected_reviews_desc=如果官方審核人員æå‡ºè®Šæ›´è«‹æ±‚,å³ä½¿æœ‰è¶³å¤ çš„æ ¸å¯ä¹Ÿä¸å…許進行åˆä½µã€‚ @@ -2473,8 +2021,6 @@ settings.block_on_official_review_requests=æœ‰å®˜æ–¹çš„å¯©æ ¸è«‹æ±‚æ™‚é˜»æ“‹åˆ settings.block_on_official_review_requests_desc=如果有官方的審核請求時,å³ä½¿æœ‰è¶³å¤ çš„æ ¸å¯ä¹Ÿä¸å…許進行åˆä½µã€‚ settings.block_outdated_branch=如果åˆä½µè«‹æ±‚å·²ç¶“éŽæ™‚則阻擋åˆä½µ settings.block_outdated_branch_desc=ç•¶ head 分支è½å¾Œæ–¼åŸºç¤Žåˆ†æ”¯æ™‚ä¸å¾—åˆä½µã€‚ -settings.block_admin_merge_override=管ç†å“¡å¿…é ˆéµå®ˆåˆ†æ”¯ä¿è­·è¦å‰‡ -settings.block_admin_merge_override_desc=管ç†å“¡å¿…é ˆéµå®ˆåˆ†æ”¯ä¿è­·è¦å‰‡ï¼Œä¸èƒ½ç¹žéŽå®ƒã€‚ settings.default_branch_desc=è«‹é¸æ“‡ç”¨ä¾†æäº¤ç¨‹å¼ç¢¼å’Œåˆä½µè«‹æ±‚çš„é è¨­åˆ†æ”¯ã€‚ settings.merge_style_desc=åˆä½µæ–¹å¼ settings.default_merge_style_desc=é è¨­åˆä½µæ–¹å¼ @@ -2493,39 +2039,18 @@ settings.tags.protection.allowed.teams=å…許的團隊 settings.tags.protection.allowed.noone=ç„¡ settings.tags.protection.create=ä¿è­·æ¨™ç±¤ settings.tags.protection.none=沒有å—ä¿è­·çš„æ¨™ç±¤ã€‚ -settings.tags.protection.pattern.description=您å¯ä»¥ä½¿ç”¨å–®ä¸€å稱或 glob æ¨¡å¼æˆ–正則表é”å¼ä¾†åŒ¹é…多個標籤。詳情請åƒé–± å—ä¿è­·æ¨™ç±¤æŒ‡å—。 -settings.bot_token=機器人 Token -settings.chat_id=èŠå¤© ID -settings.thread_id=線程 ID -settings.matrix.homeserver_url=主伺æœå™¨ç¶²å€ +settings.bot_token=Bot Token +settings.chat_id=Chat ID +settings.matrix.homeserver_url=Homeserver ç¶²å€ settings.matrix.room_id=èŠå¤©å®¤ ID settings.matrix.message_type=訊æ¯é¡žåž‹ -settings.visibility.private.button=設為ç§äºº -settings.visibility.private.text=å°‡å¯è¦‹æ€§æ›´æ”¹ç‚ºç§äººä¸åƒ…會使儲存庫僅å°å…許的æˆå“¡å¯è¦‹ï¼Œé‚„å¯èƒ½æœƒç§»é™¤å®ƒèˆ‡ forkã€é—œæ³¨è€…和星標之間的關係。 -settings.visibility.private.bullet_title=更改å¯è¦‹æ€§ç‚ºç§äººå°‡ï¼š -settings.visibility.private.bullet_one=使儲存庫僅å°å…許的æˆå“¡å¯è¦‹ã€‚ -settings.visibility.private.bullet_two=å¯èƒ½æœƒç§»é™¤å®ƒèˆ‡ forkã€é—œæ³¨è€… å’Œ 星標 之間的關係。 -settings.visibility.public.button=設為公開 -settings.visibility.public.text=å°‡å¯è¦‹æ€§æ›´æ”¹ç‚ºå…¬é–‹å°‡ä½¿å„²å­˜åº«å°ä»»ä½•人å¯è¦‹ã€‚ -settings.visibility.public.bullet_title=更改å¯è¦‹æ€§ç‚ºå…¬é–‹å°‡ï¼š -settings.visibility.public.bullet_one=使儲存庫å°ä»»ä½•人å¯è¦‹ã€‚ -settings.visibility.success=儲存庫å¯è¦‹æ€§å·²æ›´æ”¹ã€‚ -settings.visibility.error=嘗試更改儲存庫å¯è¦‹æ€§æ™‚發生錯誤。 -settings.visibility.fork_error=無法更改 fork 儲存庫的å¯è¦‹æ€§ã€‚ settings.archive.button=å°å­˜å„²å­˜åº« settings.archive.header=å°å­˜æœ¬å„²å­˜åº« -settings.archive.text=å°å­˜å„²å­˜åº«å°‡ä½¿å…¶å®Œå…¨è®Šç‚ºå”¯è®€ã€‚它將從儀表æ¿ä¸­éš±è—。沒有人(甚至包括您ï¼ï¼‰å°‡èƒ½å¤ é€²è¡Œæ–°çš„æäº¤ï¼Œæˆ–打開任何å•題或åˆä½µè«‹æ±‚。 settings.archive.success=此儲存庫已被å°å­˜ settings.archive.error=嘗試å°å­˜å„²å­˜åº«æ™‚發生錯誤。查看日誌檔以ç²å¾—更多資訊。 settings.archive.error_ismirror=無法å°å­˜é¡åƒå„²å­˜åº«ã€‚ settings.archive.branchsettings_unavailable=å·²å°å­˜çš„儲存庫無法使用分支設定。 settings.archive.tagsettings_unavailable=å·²å°å­˜çš„儲存庫無法使用標籤設定。 -settings.archive.mirrors_unavailable=如果儲存庫已å°å­˜ï¼Œå‰‡ç„¡æ³•使用é¡åƒã€‚ -settings.unarchive.button=å–æ¶ˆå°å­˜å„²å­˜åº« -settings.unarchive.header=å–æ¶ˆå°å­˜æ­¤å„²å­˜åº« -settings.unarchive.text=å–æ¶ˆå°å­˜å„²å­˜åº«å°‡æ¢å¾©å…¶æŽ¥æ”¶æäº¤å’ŒæŽ¨é€çš„èƒ½åŠ›ï¼Œä»¥åŠæ–°å•題和åˆä½µè«‹æ±‚。 -settings.unarchive.success=儲存庫已æˆåŠŸå–æ¶ˆå°å­˜ã€‚ -settings.unarchive.error=å˜—è©¦å–æ¶ˆå°å­˜å„²å­˜åº«æ™‚發生錯誤。查看日誌檔以ç²å¾—更多資訊。 settings.update_avatar_success=已更新儲存庫的大頭貼。 settings.lfs=LFS settings.lfs_filelist=存放在本儲存庫的 LFS 檔案 @@ -2590,9 +2115,8 @@ diff.file_suppressed_line_too_long=檔案差異因為一行或多行太長而無 diff.too_many_files=本差異變更的檔案數é‡éŽå¤šå°Žè‡´éƒ¨åˆ†æª”案未顯示 diff.show_more=顯示更多 diff.load=載入差異 -diff.generated=已產生 -diff.vendored=已供應 -diff.comment.add_line_comment=新增行評論 +diff.generated=generated +diff.vendored=vendored diff.comment.placeholder=留言... diff.comment.add_single_comment=加入單ç¨çš„留言 diff.comment.add_review_comment=新增留言 @@ -2623,7 +2147,6 @@ release.new_release=發布新版本 release.draft=è‰ç¨¿ release.prerelease=é ç™¼å¸ƒç‰ˆæœ¬ release.stable=穩定 -release.latest=最新 release.compare=比較 release.edit=編輯 release.ahead.commits=%d 次æäº¤ @@ -2637,9 +2160,7 @@ release.target=目標分支 release.tag_helper=æ–°å¢žæˆ–é¸æ“‡ç¾æœ‰çš„æ¨™ç±¤ã€‚ release.tag_helper_new=新標籤,將在目標上建立此標籤。 release.tag_helper_existing=ç¾æœ‰çš„æ¨™ç±¤ã€‚ -release.title=版本標題 release.title_empty=標題ä¸å¯ç‚ºç©ºã€‚ -release.message=æè¿°æ­¤ç‰ˆæœ¬ release.prerelease_desc=標記為 Pre-Release release.prerelease_helper=標記此版本ä¸é©åˆç”Ÿç”¢ä½¿ç”¨ã€‚ release.cancel=å–æ¶ˆ @@ -2649,7 +2170,6 @@ release.edit_release=更新發布 release.delete_release=刪除發布 release.delete_tag=刪除標籤 release.deletion=刪除發布 -release.deletion_desc=åˆªé™¤ç‰ˆæœ¬ç™¼å¸ƒåªæœƒå°‡å…¶å¾ž Gitea ä¸­ç§»é™¤ã€‚å®ƒä¸æœƒå½±éŸ¿ Git 標籤ã€å„²å­˜åº«çš„內容或其歷å²ã€‚是å¦ç¹¼çºŒï¼Ÿ release.deletion_success=已刪除此版本發布。 release.deletion_tag_desc=å³å°‡å¾žå„²å­˜åº«ç§»é™¤æ­¤æ¨™ç±¤ã€‚儲存庫內容和歷å²å°‡ä¿æŒä¸è®Šï¼Œæ˜¯å¦ç¹¼çºŒï¼Ÿ release.deletion_tag_success=已刪除此標籤。 @@ -2669,7 +2189,6 @@ branch.already_exists=已存在å為「%sã€çš„分支。 branch.delete_head=刪除 branch.delete=刪除分支「%s〠branch.delete_html=刪除分支 -branch.delete_desc=刪除分支是永久的。雖然被刪除的分支å¯èƒ½æœƒåœ¨å¯¦éš›ç§»é™¤å‰ç¹¼çºŒå­˜åœ¨ä¸€æ®µæ™‚間,但在大多數情æ³ä¸‹ç„¡æ³•撤銷。是å¦ç¹¼çºŒï¼Ÿ branch.deletion_success=已刪除分支「%sã€ã€‚ branch.deletion_failed=刪除分支「%sã€å¤±æ•—。 branch.delete_branch_has_new_commits=因為åˆä½µå¾Œå·²åŠ å…¥äº†æ–°çš„æäº¤ï¼Œã€Œ%sã€åˆ†æ”¯ç„¡æ³•被刪除。 @@ -2678,7 +2197,6 @@ branch.create_from=從「%s〠branch.create_success=已建立分支「%sã€ã€‚ branch.branch_already_exists=此儲存庫已有å為「%sã€çš„分支。 branch.branch_name_conflict=分支å稱「%sã€èˆ‡ç¾æœ‰åˆ†æ”¯ã€Œ%sã€è¡çªã€‚ -branch.tag_collision=無法建立「%sã€åˆ†æ”¯ï¼Œå› ç‚ºæ­¤å„²å­˜åº«ä¸­å·²æœ‰åŒå的標籤。 branch.deleted_by=ç”± %s 刪除 branch.restore_success=已還原分支「%sã€ã€‚ branch.restore_failed=還原分支「%sã€å¤±æ•—。 @@ -2686,13 +2204,10 @@ branch.protected_deletion_failed=分支「%sã€å·²è¢«ä¿è­·ï¼Œä¸èƒ½åˆªé™¤ã€‚ branch.default_deletion_failed=分支「%sã€ç‚ºé è¨­åˆ†æ”¯ï¼Œä¸èƒ½åˆªé™¤ã€‚ branch.restore=還原分支「%s〠branch.download=下載分支「%s〠-branch.rename=釿–°å‘½å分支「%s〠branch.included_desc=此分支是é è¨­åˆ†æ”¯çš„一部分 branch.included=åŒ…å« branch.create_new_branch=從下列分支建立分支: branch.confirm_create_branch=建立分支 -branch.warning_rename_default_branch=æ‚¨æ­£åœ¨é‡æ–°å‘½åé è¨­åˆ†æ”¯ã€‚ -branch.rename_branch_to=釿–°å‘½å「%sã€ç‚º: branch.confirm_rename_branch=釿–°å‘½å分支 branch.create_branch_operation=建立分支 branch.new_branch=建立新分支 @@ -2708,8 +2223,6 @@ tag.create_success=已建立標籤「%sã€ã€‚ topic.manage_topics=管ç†ä¸»é¡Œ topic.done=å®Œæˆ -topic.count_prompt=æ‚¨æœ€å¤šèƒ½é¸æ“‡ 25 個主題 -topic.format_prompt=ä¸»é¡Œå¿…é ˆä»¥å­—æ¯æˆ–數字開頭,å¯ä»¥åŒ…å«ç ´æŠ˜è™Ÿ ('-') 和點 ('.'),最多å¯ä»¥æœ‰ 35 個字元。字æ¯å¿…須是å°å¯«ã€‚ find_file.go_to_file=移至檔案 find_file.no_matching=找ä¸åˆ°ç¬¦åˆçš„æª”案 @@ -2717,16 +2230,8 @@ find_file.no_matching=找ä¸åˆ°ç¬¦åˆçš„æª”案 error.csv.too_large=無法渲染此檔案,因為它太大了。 error.csv.unexpected=無法渲染此檔案,因為它包å«äº†æœªé æœŸçš„字元,於第 %d 行第 %d 列。 error.csv.invalid_field_count=無法渲染此檔案,因為它第 %d è¡Œçš„æ¬„ä½æ•¸é‡æœ‰èª¤ã€‚ -error.broken_git_hook=此儲存庫的 Git hooks 似乎已æå£žã€‚請按照 文件 進行修復,然後推é€ä¸€äº›æäº¤ä»¥åˆ·æ–°ç‹€æ…‹ã€‚ [graphs] -component_loading=正在載入 %s... -component_loading_failed=無法載入 %s -component_loading_info=這å¯èƒ½éœ€è¦ä¸€é»žæ™‚間… -component_failed_to_load=發生æ„外錯誤。 -code_frequency.what=程å¼ç¢¼é »çއ -contributors.what=è²¢ç» -recent_commits.what=最近æäº¤ [org] org_name_holder=組織å稱 @@ -2752,13 +2257,11 @@ team_unit_desc=å…許存å–的儲存庫å€åŸŸ team_unit_disabled=(å·²åœç”¨) form.name_reserved=「%sã€æ˜¯ä¿ç•™çš„組織å稱。 -form.name_pattern_not_allowed=組織å稱ä¸å¯åŒ…å«å­—元「%sã€ã€‚ form.create_org_not_allowed=æ­¤å¸³è™Ÿç¦æ­¢å»ºç«‹çµ„織。 settings=設定 settings.options=組織 settings.full_name=組織全å -settings.email=è¯çµ¡é›»å­éƒµä»¶ settings.website=官方網站 settings.location=æ‰€åœ¨åœ°å€ settings.permission=æ¬Šé™ @@ -2772,7 +2275,6 @@ settings.visibility.private_shortname=ç§æœ‰ settings.update_settings=更新設定 settings.update_setting_success=組織設定已更新。 -settings.change_orgname_prompt=注æ„:更改組織åç¨±å°‡åŒæ™‚更改組織的 URL 並釋放舊å稱。 settings.change_orgname_redirect_prompt=舊的å稱被領用å‰ï¼Œæœƒé‡æ–°å°Žå‘æ–°å稱。 settings.update_avatar_success=已更新組織的大頭貼。 settings.delete=刪除組織 @@ -2840,7 +2342,6 @@ teams.add_nonexistent_repo=您嘗試新增的儲存庫ä¸å­˜åœ¨ï¼Œè«‹å…ˆå»ºç«‹ teams.add_duplicate_users=使用者已經是團隊æˆå“¡äº†ã€‚ teams.repos.none=這個團隊沒有å¯ä»¥å­˜å–的儲存庫。 teams.members.none=這個團隊沒有任何æˆå“¡ã€‚ -teams.members.blocked_user=無法新增使用者,因為它被組織å°éŽ–ã€‚ teams.specific_repositories=指定儲存庫 teams.specific_repositories_helper=æˆå“¡åªèƒ½å­˜å–æ˜Žç¢ºåŠ å…¥æ­¤åœ˜éšŠçš„å„²å­˜åº«ã€‚é¸æ“‡é€™å€‹é¸é …䏿œƒè‡ªå‹•移除é€éŽæ‰€æœ‰å„²å­˜åº«åŠ å…¥çš„å„²å­˜åº«ã€‚ teams.all_repositories=所有儲存庫 @@ -2848,21 +2349,15 @@ teams.all_repositories_helper=åœ˜éšŠæ“æœ‰å¯å­˜å–æ‰€æœ‰å„²å­˜åº«ã€‚é¸æ“‡æ­¤ teams.all_repositories_read_permission_desc=é€™å€‹åœ˜éšŠæ“æœ‰æ‰€æœ‰å„²å­˜åº«çš„è®€å– æ¬Šé™ï¼šæˆå“¡å¯ä»¥æŸ¥çœ‹å’Œ Clone 儲存庫。 teams.all_repositories_write_permission_desc=é€™å€‹åœ˜éšŠæ“æœ‰æ‰€æœ‰å„²å­˜åº«çš„寫入 權é™ï¼šæˆå“¡å¯ä»¥è®€å–和推é€åˆ°å„²å­˜åº«ã€‚ teams.all_repositories_admin_permission_desc=é€™å€‹åœ˜éšŠæ“æœ‰æ‰€æœ‰å„²å­˜åº«çš„管ç†å“¡ 權é™ï¼šæˆå“¡å¯ä»¥è®€å–ã€æŽ¨é€å’Œå¢žåŠ å”作者到儲存庫。 -teams.invite.title=您已被邀請加入組織 %s 中的團隊 %s。 teams.invite.by=邀請人 %s teams.invite.description=請點擊下方按鈕加入團隊。 [admin] -maintenance=ç¶­è­· dashboard=è³‡è¨Šä¸»é  -self_check=自我檢查 -identity_access=èº«ä»½èˆ‡å­˜å– users=使用者帳戶 organizations=組織 -assets=程å¼ç¢¼è³‡ç”¢ repositories=儲存庫 hooks=Webhook -integrations=æ•´åˆ authentication=èªè­‰ä¾†æº emails=使用者電å­ä¿¡ç®± config=組態 @@ -2873,11 +2368,8 @@ monitor=應用監控é¢ç‰ˆ first_page=é¦–é  last_page=æœ«é  total=總計:%d -settings=管ç†å“¡è¨­å®š -dashboard.new_version_hint=ç¾å·²æŽ¨å‡º Gitea %s,您正在執行 %s。詳情請åƒé–±éƒ¨è½æ ¼çš„說明。 dashboard.statistic=æ‘˜è¦ -dashboard.maintenance_operations=ç¶­è­·æ“作 dashboard.system_status=系統狀態 dashboard.operation_name=作業å稱 dashboard.operation_switch=é–‹é—œ @@ -2886,13 +2378,11 @@ dashboard.clean_unbind_oauth=æ¸…ç†æœªç¶å®šçš„ OAuth é€£çµ dashboard.clean_unbind_oauth_success=所有未ç¶å®šçš„ OAuth 連çµå·²åˆªé™¤ã€‚ dashboard.task.started=已開始的任務: %[1]s dashboard.task.process=任務: %[1]s -dashboard.task.cancelled=任務: %[1]s 已喿¶ˆ: %[3]s dashboard.task.error=任務中的錯誤: %[1]s: %[3]s dashboard.task.finished=任務: 已完æˆç”± %[2]s 啟動的 %[1]s dashboard.task.unknown=未知的任務: %[1]s dashboard.cron.started=已開始的 Cron: %[1]s dashboard.cron.process=Cron: %[1]s -dashboard.cron.cancelled=Cron: %[1]s 已喿¶ˆ: %[3]s dashboard.cron.error=Cron 中的錯誤: %s: %[3]s dashboard.cron.finished=Cron: %[1]s å·²å®Œæˆ dashboard.delete_inactive_accounts=刪除所有未啟用帳戶 @@ -2902,8 +2392,6 @@ dashboard.delete_repo_archives.started=刪除所有儲存庫存檔的任務已 dashboard.delete_missing_repos=刪除所有éºå¤± Git 檔案的儲存庫 dashboard.delete_missing_repos.started=刪除所有éºå¤± Git 檔案的儲存庫的任務已啟動。 dashboard.delete_generated_repository_avatars=刪除產生的儲存庫大頭貼 -dashboard.sync_repo_branches=從 Git è³‡æ–™åŒæ­¥éºæ¼çš„分支到資料庫 -dashboard.sync_repo_tags=從 Git è³‡æ–™åŒæ­¥æ¨™ç±¤åˆ°è³‡æ–™åº« dashboard.update_mirrors=æ›´æ–°é¡åƒ dashboard.repo_health_check=å°æ‰€æœ‰å„²å­˜åº«é€²è¡Œå¥åº·æª¢æŸ¥ dashboard.check_repo_stats=檢查所有儲存庫的統計資料 @@ -2918,7 +2406,6 @@ dashboard.reinit_missing_repos=釿–°åˆå§‹åŒ–所有記錄存在但éºå¤±çš„ Git dashboard.sync_external_users=åŒæ­¥å¤–部使用者資料 dashboard.cleanup_hook_task_table=æ¸…ç† hook_task 資料表 dashboard.cleanup_packages=清ç†å·²éŽæœŸçš„套件 -dashboard.cleanup_actions=清ç†éŽæœŸçš„æ“ä½œè³‡æº dashboard.server_uptime=æœå‹™åŸ·è¡Œæ™‚é–“ dashboard.current_goroutine=ç›®å‰çš„ Goroutines æ•¸é‡ dashboard.current_memory_usage=ç›®å‰è¨˜æ†¶é«”ä½¿ç”¨é‡ @@ -2948,19 +2435,9 @@ dashboard.total_gc_time=總 GC æš«åœæ™‚é–“ dashboard.total_gc_pause=總 GC æš«åœæ™‚é–“ dashboard.last_gc_pause=上次 GC æš«åœæ™‚é–“ dashboard.gc_times=GC 執行次數 -dashboard.delete_old_actions=從資料庫刪除所有舊行為 -dashboard.delete_old_actions.started=從資料庫刪除所有舊行為的任務已啟動。 dashboard.update_checker=更新檢查器 dashboard.delete_old_system_notices=從資料庫刪除所有舊系統æç¤º dashboard.gc_lfs=å° LFS meta objects 進行垃圾回收 -dashboard.stop_zombie_tasks=åœæ­¢æ®­å±ä»»å‹™ -dashboard.stop_endless_tasks=åœæ­¢æ°¸ä¸åœæ­¢çš„任務 -dashboard.cancel_abandoned_jobs=å–æ¶ˆå·²æ”¾æ£„的工作 -dashboard.start_schedule_tasks=啟動動作排程任務 -dashboard.sync_branch.started=åˆ†æ”¯åŒæ­¥å·²é–‹å§‹ -dashboard.sync_tag.started=æ¨™ç±¤åŒæ­¥å·²é–‹å§‹ -dashboard.rebuild_issue_indexer=é‡å»ºå•題索引器 -dashboard.sync_repo_licenses=åŒæ­¥å„²å­˜åº«è¨±å¯è­‰ users.user_manage_panel=ä½¿ç”¨è€…å¸³æˆ¶ç®¡ç† users.new_account=建立使用者帳戶 @@ -2969,9 +2446,6 @@ users.full_name=å…¨å users.activated=已啟用 users.admin=管ç†å“¡ users.restricted=å—é™ -users.reserved=ä¿ç•™ -users.bot=機器人 (Bot) -users.remote=é ç«¯ users.2fa=兩步驟驗證 users.repos=儲存庫數 users.created=建立時間 @@ -3018,7 +2492,6 @@ users.list_status_filter.is_prohibit_login=ç¦æ­¢ç™»å…¥ users.list_status_filter.not_prohibit_login=å…許登入 users.list_status_filter.is_2fa_enabled=已啟用兩步驟驗證 users.list_status_filter.not_2fa_enabled=未啟用兩步驟驗證 -users.details=使用者詳細資訊 emails.email_manage_panel=使用者電å­ä¿¡ç®±ç®¡ç† emails.primary=ä¸»è¦ @@ -3031,11 +2504,6 @@ emails.updated=信箱已更新 emails.not_updated=é›»å­ä¿¡ç®±æ›´æ–°å¤±æ•—: %v emails.duplicate_active=此信箱已被其他使用者使用 emails.change_email_header=æ›´æ–°é›»å­ä¿¡ç®±å±¬æ€§ -emails.change_email_text=æ‚¨ç¢ºå®šè¦æ›´æ–°æ­¤é›»å­éƒµä»¶åœ°å€å—Žï¼Ÿ -emails.delete=刪除電å­éƒµä»¶ -emails.delete_desc=您確定è¦åˆªé™¤æ­¤é›»å­éƒµä»¶åœ°å€å—Žï¼Ÿ -emails.deletion_success=é›»å­éƒµä»¶åœ°å€å·²è¢«åˆªé™¤ã€‚ -emails.delete_primary_email_error=您ä¸èƒ½åˆªé™¤ä¸»è¦çš„é›»å­éƒµä»¶åœ°å€ã€‚ orgs.org_manage_panel=çµ„ç¹”ç®¡ç† orgs.name=å稱 @@ -3051,13 +2519,10 @@ repos.name=å稱 repos.private=ç§æœ‰ repos.issues=å•題數 repos.size=å¤§å° -repos.lfs_size=LFS å¤§å° packages.package_manage_panel=å¥—ä»¶ç®¡ç† packages.total_size=總大å°: %s packages.unreferenced_size=未åƒè€ƒå¤§å°: %s -packages.cleanup=清ç†å·²é€¾æœŸçš„資料 -packages.cleanup.success=å·²æˆåŠŸæ¸…ç†éŽæœŸçš„資料 packages.owner=æ“æœ‰è€… packages.creator=建立者 packages.name=å稱 @@ -3068,12 +2533,10 @@ packages.size=å¤§å° packages.published=已發布 defaulthooks=é è¨­ Webhook -defaulthooks.desc=ç•¶æŸäº› Gitea 事件觸發時,Webhook 會自動發出 HTTP POST 請求到伺æœå™¨ã€‚此處定義的 Webhook 是é è¨­å€¼ï¼Œå°‡æœƒè¤‡è£½åˆ°æ‰€æœ‰æ–°å„²å­˜åº«ä¸­ã€‚詳情請åƒé–± webhooks 指å—。 defaulthooks.add_webhook=新增é è¨­ Webhook defaulthooks.update_webhook=æ›´æ–°é è¨­ Webhook systemhooks=系統 Webhook -systemhooks.desc=ç•¶æŸäº› Gitea 事件觸發時,Webhook 會自動發出 HTTP POST 請求到伺æœå™¨ã€‚此處定義的 Webhook 將作用於系統上的所有儲存庫,因此請考慮這å¯èƒ½å°æ•ˆèƒ½ç”¢ç”Ÿçš„影響。詳情請åƒé–± webhooks 指å—。 systemhooks.add_webhook=新增系統 Webhook systemhooks.update_webhook=更新系統 Webhook @@ -3166,20 +2629,8 @@ auths.sspi_default_language=使用者é è¨­èªžè¨€ auths.sspi_default_language_helper=SSPI èªè­‰æ–¹æ³•自動建立之使用者的é è¨­èªžè¨€ï¼Œç•™ç™½ä»¥è‡ªå‹•嵿¸¬ã€‚ auths.tips=幫助æç¤º auths.tips.oauth2.general=OAuth2 èªè­‰ -auths.tips.oauth2.general.tip=註冊新的 OAuth2 èªè­‰æ™‚,回調/é‡å®šå‘ URL 應為: auths.tip.oauth2_provider=OAuth2 æä¾›è€… -auths.tip.bitbucket=註冊新的 OAuth 客戶端並加入權é™ã€ŒAccount - Readã€ã€‚ç¶²å€ï¼šhttps://bitbucket.org/account/user//oauth-consumers/new auths.tip.nextcloud=在您的執行個體中,於é¸å–®ã€Œè¨­å®š -> 安全性 -> OAuth 2.0 客戶端ã€è¨»å†Šæ–°çš„ OAuth 客戶端 -auths.tip.dropbox=建立新的 App。網å€ï¼šhttps://www.dropbox.com/developers/apps -auths.tip.facebook=註冊新的應用程å¼ä¸¦æ–°å¢žç”¢å“「Facebook 登入ã€ã€‚ç¶²å€ï¼šhttps://developers.facebook.com/apps -auths.tip.github=註冊新的 OAuth 應用程å¼ã€‚ç¶²å€ï¼šhttps://github.com/settings/applications/new -auths.tip.gitlab_new=註冊新的應用程å¼ã€‚ç¶²å€ï¼šhttps://discordapp.com/developers/applications/me -auths.tip.google_plus=從 Google API 控制å°å–å¾— OAuth2 用戶端憑證。網å€ï¼šhttps://console.developers.google.com/ -auths.tip.openid_connect=使用 OpenID 連接探索 URL (/.well-known/openid-configuration) 來指定節點 -auths.tip.twitter=建立應用程å¼ä¸¦ç¢ºä¿æœ‰å•Ÿç”¨ã€ŒAllow this application to be used to Sign in with Twitterã€ã€‚ç¶²å€ï¼šhttps://dev.twitter.com/apps -auths.tip.discord=註冊新的應用程å¼ã€‚ç¶²å€ï¼šhttps://discordapp.com/developers/applications/me -auths.tip.gitea=註冊新的 OAuth2 應用程å¼ã€‚指å—å¯åœ¨ %s 找到 -auths.tip.yandex=建立新的應用程å¼ï¼Œå¾žã€ŒYandex.Passport APIã€å€å¡Šé¸æ“‡ã€ŒAccess to email addressã€ã€ã€ŒAccess to user avatarã€å’Œã€ŒAccess to username, first name and surname, genderã€ã€‚ç¶²å€ï¼šhttps://oauth.yandex.com/client/new auths.tip.mastodon=輸入您欲èªè­‰çš„ Mastodon åŸ·è¡Œå€‹é«”çš„è‡ªè¨‚ç¶²å€ (或使用é è¨­å€¼) auths.edit=修改èªè­‰ä¾†æº auths.activated=該èªè­‰ä¾†æºå·²å•Ÿç”¨ @@ -3208,7 +2659,6 @@ config.disable_router_log=關閉路由日誌 config.run_user=以使用者å稱執行 config.run_mode=åŸ·è¡Œæ¨¡å¼ config.git_version=Git 版本 -config.app_data_path=應用程å¼è³‡æ–™è·¯å¾‘ config.repo_root_path=儲存庫目錄 config.lfs_root_path=LFS 根目錄 config.log_file_root_path=日誌路徑 @@ -3283,7 +2733,6 @@ config.mailer_sendmail_timeout=Sendmail 逾時 config.mailer_use_dummy=Dummy config.test_email_placeholder=é›»å­ä¿¡ç®± (例:test@example.com) config.send_test_mail=傳逿¸¬è©¦éƒµä»¶ -config.send_test_mail_submit=å‚³é€ config.test_mail_failed=傳逿¸¬è©¦éƒµä»¶åˆ°ã€Œ%sã€æ™‚失敗: %v config.test_mail_sent=測試郵件已傳é€åˆ°ã€Œ%sã€ã€‚ @@ -3295,10 +2744,6 @@ config.cache_adapter=Cache é©é…器 config.cache_interval=Cache 週期 config.cache_conn=Cache 連接字符串 config.cache_item_ttl=å¿«å–é …ç›® TTL -config.cache_test=æ¸¬è©¦å¿«å– -config.cache_test_failed=測試快å–失敗: %v -config.cache_test_slow=å¿«å–æ¸¬è©¦æˆåŠŸï¼Œä½†å›žæ‡‰é€Ÿåº¦æ…¢: %s -config.cache_test_succeeded=å¿«å–æ¸¬è©¦æˆåŠŸï¼Œå›žæ‡‰æ™‚é–“ç‚º %s config.session_config=Session 組態 config.session_provider=Session æä¾›è€… @@ -3313,7 +2758,6 @@ config.picture_config=圖片和大頭貼組態 config.picture_service=圖片æœå‹™ config.disable_gravatar=åœç”¨ Gravatar config.enable_federated_avatar=啟用 Federated Avatars -config.open_with_editor_app_help=「開啟方å¼ã€ç·¨è¼¯å™¨ç”¨æ–¼å…‹éš†é¸å–®ã€‚如果留空,將使用é è¨­å€¼ã€‚展開以查看é è¨­å€¼ã€‚ config.git_config=Git 組態 config.git_disable_diff_highlight=åœç”¨æ¯”較語法高亮 @@ -3328,15 +2772,12 @@ config.git_pull_timeout=Pull 作業逾時 config.git_gc_timeout=GC 作業逾時 config.log_config=日誌組態 -config.logger_name_fmt=記錄器: %s config.disabled_logger=å·²åœç”¨ config.access_log_mode=å­˜å–æ—¥èªŒæ¨¡å¼ -config.access_log_template=å­˜å–æ—¥èªŒç¯„本 config.xorm_log_sql=記錄 SQL config.set_setting_failed=寫入設定值 %s 失敗 -monitor.stats=統計 monitor.cron=Cron 任務 monitor.name=å稱 @@ -3345,16 +2786,11 @@ monitor.next=下次執行時間 monitor.previous=上次執行時間 monitor.execute_times=執行次數 monitor.process=執行中的處ç†ç¨‹åº -monitor.stacktrace=堆疊追蹤 -monitor.processes_count=%d 個處ç†ç¨‹åº -monitor.download_diagnosis_report=下載診斷報告 monitor.desc=æè¿° monitor.start=開始時間 monitor.execute_time=已執行時間 monitor.last_execution_result=çµæžœ monitor.process.cancel=çµæŸè™•ç†ç¨‹åº -monitor.process.cancel_desc=çµæŸè™•ç†ç¨‹åºå¯èƒ½é€ æˆè³‡æ–™éºå¤± -monitor.process.cancel_notices=çµæŸ: %s? monitor.process.children=å­ç¨‹åº monitor.queues=佇列 @@ -3363,19 +2799,14 @@ monitor.queue.name=å稱 monitor.queue.type=類型 monitor.queue.exemplar=型別 monitor.queue.numberworkers=å·¥ä½œè€…æ•¸é‡ -monitor.queue.activeworkers=æ´»èºå·¥ä½œè€… monitor.queue.maxnumberworkers=æœ€å¤§å·¥ä½œè€…æ•¸é‡ monitor.queue.numberinqueue=ä½‡åˆ—ä¸­çš„æ•¸é‡ -monitor.queue.review_add=審查 / 增加工作者 monitor.queue.settings.title=集å€è¨­å®š -monitor.queue.settings.desc=集倿œƒæ ¹æ“šå·¥ä½œè€…佇列的阻塞情æ³å‹•態增長。 monitor.queue.settings.maxnumberworkers=æœ€å¤§å·¥ä½œè€…æ•¸é‡ monitor.queue.settings.maxnumberworkers.placeholder=ç›®å‰ %[1]d monitor.queue.settings.maxnumberworkers.error=最大工作者數é‡å¿…須是數字 monitor.queue.settings.submit=更新設定 monitor.queue.settings.changed=已更新設定 -monitor.queue.settings.remove_all_items=全部移除 -monitor.queue.settings.remove_all_items_done=佇列中的所有項目已被移除。 notices.system_notice_list=系統æç¤º notices.view_detail_header=查看æç¤ºç´°ç¯€ @@ -3392,14 +2823,6 @@ notices.desc=æè¿° notices.op=æ“作 notices.delete_success=已刪除系統æç¤ºã€‚ -self_check.no_problem_found=尚未發ç¾ä»»ä½•å•題。 -self_check.startup_warnings=啟動警告: -self_check.database_collation_mismatch=é æœŸè³‡æ–™åº«ä½¿ç”¨æŽ’åºè¦å‰‡ï¼š%s -self_check.database_collation_case_insensitive=資料庫正在使用排åºè¦å‰‡ %s,這是一個ä¸å€åˆ†å¤§å°å¯«çš„æŽ’åºè¦å‰‡ã€‚é›–ç„¶ Gitea å¯ä»¥æ­£å¸¸é‹ä½œï¼Œä½†åœ¨æŸäº›ç½•見情æ³ä¸‹å¯èƒ½æœƒå‡ºç¾é æœŸå¤–çš„å•題。 -self_check.database_inconsistent_collation_columns=資料庫正在使用排åºè¦å‰‡ %s,但這些欄ä½ä½¿ç”¨äº†ä¸åŒ¹é…的排åºè¦å‰‡ã€‚這å¯èƒ½æœƒå°Žè‡´ä¸€äº›é æœŸå¤–çš„å•題。 -self_check.database_fix_mysql=å°æ–¼ MySQL/MariaDB 使用者,您å¯ä»¥ä½¿ç”¨ "gitea doctor convert" 命令來修復排åºè¦å‰‡å•題,或者也å¯ä»¥æ‰‹å‹•使用 "ALTER ... COLLATE ..." SQL 語å¥ä¾†ä¿®å¾©å•題。 -self_check.database_fix_mssql=å°æ–¼ MSSQL ä½¿ç”¨è€…ï¼Œç›®å‰æ‚¨åªèƒ½æ‰‹å‹•使用 "ALTER ... COLLATE ..." SQL 語å¥ä¾†ä¿®å¾©å•題。 -self_check.location_origin_mismatch=ç•¶å‰ URL (%[1]s) 與 Gitea 看到的 URL (%[2]s) ä¸åŒ¹é…。如果您使用了åå‘代ç†ï¼Œè«‹ç¢ºä¿ "Host" å’Œ "X-Forwarded-Proto" 標頭設置正確。 [action] create_repo=建立了儲存庫 %s @@ -3427,7 +2850,6 @@ mirror_sync_create=從é¡åƒåŒæ­¥äº†æ–°åƒè€ƒ %[3]s 到 %[3]s 刪除了åƒè€ƒ %[2]s approve_pull_request=`æ ¸å¯äº† %[3]s#%[2]s` reject_pull_request=`æå‡ºäº†ä¿®æ”¹å»ºè­° %[3]s#%[2]s` -publish_release=`發布了 %[3]s çš„ "%[4]s" ` review_dismissed=`å–æ¶ˆäº† %[4]s å° %[3]s#%[2]s 的審核` review_dismissed_reason=原因: create_branch=在 %[4]s 中建立了分支 %[3]s @@ -3456,7 +2878,6 @@ raw_minutes=åˆ†é˜ [dropzone] default_message=拖放檔案或是點擊此處上傳。 -invalid_input_type=您無法上傳此類型的檔案 file_too_big=檔案大å°({{filesize}} MB) è¶…éŽäº†æœ€å¤§å…許大å°({{maxFilesize}} MB) remove_file=移除文件 @@ -3494,10 +2915,8 @@ error.unit_not_allowed=您未被å…è¨±è¨ªå•æ­¤å„²å­˜åº«å€åŸŸ title=套件 desc=管ç†å„²å­˜åº«å¥—件。 empty=ç›®å‰é‚„沒有套件。 -no_metadata=沒有元數據。 empty.documentation=關於套件註冊中心的詳情請åƒé–±èªªæ˜Žæ–‡ä»¶ã€‚ empty.repo=已經上傳了一個套件,但是沒有顯示在這裡嗎?打開套件設定並將其連çµåˆ°é€™å€‹å„²å­˜åº«ã€‚ -registry.documentation=有關 %s 註冊中心的更多資訊,請åƒé–±èªªæ˜Žæ–‡ä»¶ã€‚ filter.type=類型 filter.type.all=所有 filter.no_result=沒有篩é¸çµæžœã€‚ @@ -3529,8 +2948,6 @@ alpine.repository=儲存庫資訊 alpine.repository.branches=分支 alpine.repository.repositories=儲存庫 alpine.repository.architectures=æž¶æ§‹ -arch.registry=在 /etc/pacman.conf 中新增伺æœå™¨åŠç›¸é—œå„²å­˜åº«å’Œæž¶æ§‹: -arch.install=使用 pacman åŒæ­¥å¥—ä»¶: arch.repository=儲存庫資訊 arch.repository.repositories=儲存庫 arch.repository.architectures=æž¶æ§‹ @@ -3556,17 +2973,12 @@ container.layers=æ˜ åƒæª” Layers container.labels=標籤 container.labels.key=éµ container.labels.value=值 -cran.registry=在您的 Rprofile.site 檔設定此註冊中心: cran.install=åŸ·è¡Œä¸‹åˆ—å‘½ä»¤å®‰è£æ­¤å¥—ä»¶: debian.registry=é€éŽä¸‹åˆ—命令設定此註冊中心: -debian.registry.info=å¾žä¸‹åˆ—æ¸…å–®é¸æ“‡$distributionå’Œ$component debian.install=åŸ·è¡Œä¸‹åˆ—å‘½ä»¤å®‰è£æ­¤å¥—ä»¶: debian.repository=儲存庫資訊 -debian.repository.distributions=發行版 -debian.repository.components=元件 debian.repository.architectures=æž¶æ§‹ generic.download=é€éŽä¸‹åˆ—命令下載套件: -go.install=é€éŽä¸‹åˆ—命令安è£å¥—ä»¶: helm.registry=é€éŽä¸‹åˆ—命令設定此註冊中心: helm.install=åŸ·è¡Œä¸‹åˆ—å‘½ä»¤å®‰è£æ­¤å¥—ä»¶: maven.registry=在您專案的 pom.xml 檔設定此註冊中心: @@ -3581,7 +2993,6 @@ npm.install=執行下列命令以使用 npm å®‰è£æ­¤å¥—ä»¶: npm.install2=或將它加到 package.json 檔: npm.dependencies=ç›¸ä¾æ€§ npm.dependencies.development=é–‹ç™¼ç›¸ä¾æ€§ -npm.dependencies.bundle=æ†ç¶ç›¸ä¾æ€§ npm.dependencies.peer=Peer ç›¸ä¾æ€§ npm.dependencies.optional=é¸ç”¨ç›¸ä¾æ€§ npm.details.tag=標籤 @@ -3589,12 +3000,9 @@ pub.install=執行下列命令以使用 Dart å®‰è£æ­¤å¥—ä»¶: pypi.requires=éœ€è¦ Python pypi.install=執行下列命令以使用 pip å®‰è£æ­¤å¥—ä»¶: rpm.registry=é€éŽä¸‹åˆ—命令設定此註冊中心: -rpm.distros.redhat=在基於 RedHat 的發行版上 -rpm.distros.suse=在基於 SUSE 的發行版上 rpm.install=åŸ·è¡Œä¸‹åˆ—å‘½ä»¤å®‰è£æ­¤å¥—ä»¶: rpm.repository=儲存庫資訊 rpm.repository.architectures=æž¶æ§‹ -rpm.repository.multiple_groups=此套件在多個群組中å¯ç”¨ã€‚ rubygems.install=執行下列命令以使用 gem å®‰è£æ­¤å¥—ä»¶: rubygems.install2=或將它加到 Gemfile: rubygems.dependencies.runtime=åŸ·è¡ŒéšŽæ®µç›¸ä¾æ€§ @@ -3618,17 +3026,14 @@ settings.delete.success=已刪除該套件。 settings.delete.error=刪除套件失敗。 owner.settings.cargo.title=Cargo Registry 索引 owner.settings.cargo.initialize=åˆå§‹åŒ–索引 -owner.settings.cargo.initialize.description=使用 Cargo 註冊中心需è¦ä¸€å€‹ç‰¹æ®Šçš„索引 Git 儲存庫。使用此é¸é …將會 (釿–°) 建立儲存庫並自動é…置它。 owner.settings.cargo.initialize.error=åˆå§‹åŒ– Cargo 索引失敗: %v owner.settings.cargo.initialize.success=æˆåŠŸå»ºç«‹äº† Cargo 索引。 owner.settings.cargo.rebuild=é‡å»ºç´¢å¼• -owner.settings.cargo.rebuild.description=如果索引與儲存的 Cargo 套件ä¸åŒæ­¥ï¼Œé‡å»ºç´¢å¼•å¯èƒ½æœƒæœ‰å¹«åŠ©ã€‚ owner.settings.cargo.rebuild.error=é‡å»º Cargo 索引失敗: %v owner.settings.cargo.rebuild.success=æˆåŠŸé‡å»ºäº† Cargo 索引。 owner.settings.cleanuprules.title=ç®¡ç†æ¸…ç†è¦å‰‡ owner.settings.cleanuprules.add=加入清ç†è¦å‰‡ owner.settings.cleanuprules.edit=編輯清ç†è¦å‰‡ -owner.settings.cleanuprules.none=沒有清ç†è¦å‰‡å¯ç”¨ã€‚è«‹åƒé–±èªªæ˜Žæ–‡ä»¶ã€‚ owner.settings.cleanuprules.preview=清ç†è¦å‰‡é è¦½ owner.settings.cleanuprules.preview.overview=已排定è¦ç§»é™¤ %d 個套件。 owner.settings.cleanuprules.preview.none=清ç†è¦å‰‡ä¸ç¬¦åˆä»»ä½•套件。 @@ -3647,7 +3052,6 @@ owner.settings.cleanuprules.success.update=已更新清ç†è¦å‰‡ã€‚ owner.settings.cleanuprules.success.delete=已刪除清ç†è¦å‰‡ã€‚ owner.settings.chef.title=Chef Registry owner.settings.chef.keypair=產生密鑰組 -owner.settings.chef.keypair.description=é©—è­‰ Chef 註冊中心需è¦ä¸€å€‹å¯†é‘°çµ„。如果您之å‰å·²ç”ŸæˆéŽå¯†é‘°çµ„ï¼Œç”Ÿæˆæ–°å¯†é‘°çµ„將會丟棄舊的密鑰組。 [secrets] secrets=Secret @@ -3662,7 +3066,6 @@ deletion=移除 Secret deletion.description=移除 Secret 是永久的且ä¸å¯é‚„原,是å¦ç¹¼çºŒï¼Ÿ deletion.success=已移除此 Secret。 deletion.failed=移除 Secret 失敗。 -management=Secret ç®¡ç† [actions] actions=Actions @@ -3674,7 +3077,6 @@ status.waiting=正在等候 status.running=正在執行 status.success=æˆåŠŸ status.failure=失敗 -status.cancelled=已喿¶ˆ status.skipped=å·²ç•¥éŽ status.blocked=已阻塞 @@ -3691,7 +3093,6 @@ runners.labels=標籤 runners.last_online=最後上線時間 runners.runner_title=Runner runners.task_list=最近在此 Runner 上的任務 -runners.task_list.no_tasks=ç›®å‰é‚„沒有任務。 runners.task_list.run=執行 runners.task_list.status=狀態 runners.task_list.repository=儲存庫 @@ -3712,70 +3113,25 @@ runners.status.idle=é–’ç½® runners.status.active=啟用 runners.status.offline=離線 runners.version=版本 -runners.reset_registration_token=é‡è¨­è¨»å†Š Token runners.reset_registration_token_success=æˆåŠŸé‡è¨­äº† Runner 註冊 Token runs.all_workflows=所有工作æµç¨‹ runs.commit=æäº¤ -runs.scheduled=已排程 -runs.pushed_by=推é€è€… runs.invalid_workflow_helper=工作æµç¨‹è¨­å®šæª”無效。請檢查您的設定檔: %s -runs.no_matching_online_runner_helper=æ²’æœ‰ç¬¦åˆæ¨™ç±¤çš„線上 Runner: %s -runs.no_job_without_needs=工作æµç¨‹å¿…須包å«è‡³å°‘一個沒有ä¾è³´çš„工作。 -runs.no_job=工作æµç¨‹å¿…須包å«è‡³å°‘一個工作 -runs.actor=執行者 runs.status=狀態 -runs.actors_no_select=所有執行者 -runs.status_no_select=所有狀態 -runs.no_results=沒有符åˆçš„çµæžœã€‚ -runs.no_workflows=ç›®å‰é‚„沒有工作æµç¨‹ã€‚ -runs.no_workflows.quick_start=ä¸çŸ¥é“如何開始使用 Gitea Actions?請åƒé–±å¿«é€Ÿå…¥é–€æŒ‡å—。 -runs.no_workflows.documentation=有關 Gitea Actions 的更多資訊,請åƒé–±æ–‡ä»¶ã€‚ runs.no_runs=工作æµç¨‹æ²’有執行éŽã€‚ -runs.empty_commit_message=(空的æäº¤è¨Šæ¯) -runs.expire_log_message=日誌已被清除,因為它們太舊了。 workflow.disable=åœç”¨å·¥ä½œæµç¨‹ workflow.disable_success=å·²æˆåŠŸåœç”¨å·¥ä½œæµç¨‹ã€Œ%sã€ã€‚ workflow.enable=啟用工作æµç¨‹ workflow.enable_success=å·²æˆåŠŸå•Ÿç”¨å·¥ä½œæµç¨‹ã€Œ%sã€ã€‚ -workflow.disabled=工作æµç¨‹å·²åœç”¨ã€‚ -workflow.run=執行工作æµç¨‹ -workflow.not_found=找ä¸åˆ°å·¥ä½œæµç¨‹ã€Œ%sã€ã€‚ -workflow.run_success=工作æµç¨‹ã€Œ%sã€åŸ·è¡ŒæˆåŠŸã€‚ -workflow.from_ref=使用工作æµç¨‹ä¾†è‡ª -workflow.has_workflow_dispatch=此工作æµç¨‹æœ‰ä¸€å€‹ workflow_dispatch 事件觸發器。 need_approval_desc=來自 Frok 儲存庫的åˆä½µè«‹æ±‚éœ€è¦æ ¸å¯æ‰èƒ½åŸ·è¡Œå·¥ä½œæµç¨‹ã€‚ -variables=變數 -variables.management=è®Šæ•¸ç®¡ç† -variables.creation=新增變數 -variables.none=還沒有任何變數。 -variables.deletion=移除變數 -variables.deletion.description=移除變數是永久的且ä¸å¯é‚„原,是å¦ç¹¼çºŒï¼Ÿ -variables.description=變數會被傳é€åˆ°æŸäº› Action 且無法以其他方å¼è®€å–。 -variables.id_not_exist=ID 為 %d 的變數ä¸å­˜åœ¨ã€‚ -variables.edit=編輯變數 -variables.deletion.failed=移除變數失敗。 -variables.deletion.success=已刪除變數。 -variables.creation.failed=新增變數失敗。 -variables.creation.success=已新增變數「%sã€ã€‚ -variables.update.failed=編輯變數失敗。 -variables.update.success=已編輯變數。 - [projects] -deleted.display_name=已刪除的專案 -type-1.display_name=個人專案 -type-2.display_name=儲存庫專案 -type-3.display_name=組織專案 [git.filemode] ; Ordered by git filemode value, ascending. E.g. directory has "040000", normal file has "100644", … -directory=目錄 -normal_file=一般檔案 -executable_file=å¯åŸ·è¡Œæª” symbolic_link=ç¬¦è™Ÿé€£çµ -submodule=å­æ¨¡çµ„ diff --git a/package-lock.json b/package-lock.json index d362e95d3f448..8c59dec079f24 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "@github/relative-time-element": "4.4.4", "@github/text-expander-element": "2.8.0", "@mcaptcha/vanilla-glue": "0.1.0-alpha-3", - "@primer/octicons": "19.14.0", + "@primer/octicons": "19.13.0", "@silverwind/vue3-calendar-heatmap": "2.0.6", "@tiptap/core": "2.11.0", "@tiptap/extension-link": "2.11.0", @@ -28,7 +28,6 @@ "add-asset-webpack-plugin": "3.0.0", "ansi_up": "6.0.2", "asciinema-player": "3.8.1", - "canvas": "3.1.0", "chart.js": "4.4.7", "chartjs-adapter-dayjs-4": "1.0.4", "chartjs-plugin-zoom": "2.2.0", @@ -45,12 +44,12 @@ "idiomorph": "0.3.0", "jquery": "3.7.1", "js-beautify": "1.15.1", - "katex": "0.16.18", + "katex": "0.16.21", "license-checker-webpack-plugin": "0.2.1", "mermaid": "11.4.1", "mini-css-extract-plugin": "2.9.2", "minimatch": "10.0.1", - "monaco-editor": "0.52.2", + "monaco-editor": "0.52.0", "monaco-editor-webpack-plugin": "7.1.0", "pdfobject": "2.3.0", "perfect-debounce": "1.0.0", @@ -59,7 +58,7 @@ "postcss-nesting": "13.0.1", "sortablejs": "1.15.6", "swagger-ui-dist": "5.18.2", - "tailwindcss": "3.4.17", + "tailwindcss": "3.4.16", "throttle-debounce": "5.0.2", "tinycolor2": "1.6.0", "tippy.js": "6.3.7", @@ -72,16 +71,16 @@ "vue-bar-graph": "2.2.0", "vue-chartjs": "5.3.2", "vue-loader": "17.4.2", - "webpack": "5.97.1", + "webpack": "5.97.0", "webpack-cli": "5.1.4", "wrap-ansi": "9.0.0" }, "devDependencies": { "@eslint-community/eslint-plugin-eslint-comments": "4.4.1", - "@playwright/test": "1.49.1", + "@playwright/test": "1.49.0", "@silverwind/vue-tsc": "2.1.13", "@stoplight/spectral-cli": "6.14.2", - "@stylistic/eslint-plugin-js": "2.12.1", + "@stylistic/eslint-plugin-js": "2.11.0", "@stylistic/stylelint-plugin": "3.1.1", "@types/dropzone": "5.7.9", "@types/jquery": "3.5.32", @@ -93,19 +92,19 @@ "@types/throttle-debounce": "5.0.2", "@types/tinycolor2": "1.4.6", "@types/toastify-js": "1.12.3", - "@typescript-eslint/eslint-plugin": "8.18.1", - "@typescript-eslint/parser": "8.18.1", + "@typescript-eslint/eslint-plugin": "8.17.0", + "@typescript-eslint/parser": "8.17.0", "@vitejs/plugin-vue": "5.2.1", "eslint": "8.57.0", "eslint-import-resolver-typescript": "3.7.0", "eslint-plugin-array-func": "4.0.0", "eslint-plugin-github": "5.0.2", - "eslint-plugin-import-x": "4.6.1", + "eslint-plugin-import-x": "4.5.0", "eslint-plugin-no-jquery": "3.1.0", "eslint-plugin-no-use-extend-native": "0.5.0", "eslint-plugin-playwright": "2.1.0", "eslint-plugin-regexp": "2.7.0", - "eslint-plugin-sonarjs": "3.0.1", + "eslint-plugin-sonarjs": "2.0.4", "eslint-plugin-unicorn": "56.0.1", "eslint-plugin-vitest": "0.4.1", "eslint-plugin-vitest-globals": "1.5.0", @@ -114,15 +113,15 @@ "eslint-plugin-wc": "2.2.0", "happy-dom": "15.11.7", "markdownlint-cli": "0.43.0", - "nolyfill": "1.0.43", + "nolyfill": "1.0.42", "postcss-html": "1.7.0", - "stylelint": "16.12.0", + "stylelint": "16.11.0", "stylelint-declaration-block-no-ignored-properties": "2.8.0", "stylelint-declaration-strict-value": "1.10.6", "stylelint-value-no-unknown-custom-properties": "6.0.1", "svgo": "3.3.2", - "type-fest": "4.30.2", - "updates": "16.4.1", + "type-fest": "4.30.0", + "updates": "16.4.0", "vite-string-plugin": "1.3.4", "vitest": "2.1.8" }, @@ -202,6 +201,12 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/code-frame/node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, "node_modules/@babel/compat-data": { "version": "7.26.3", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.3.tgz", @@ -213,22 +218,22 @@ } }, "node_modules/@babel/core": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.0.tgz", - "integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==", + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.2.tgz", + "integrity": "sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==", "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.26.0", - "@babel/generator": "^7.26.0", - "@babel/helper-compilation-targets": "^7.25.9", - "@babel/helper-module-transforms": "^7.26.0", - "@babel/helpers": "^7.26.0", - "@babel/parser": "^7.26.0", - "@babel/template": "^7.25.9", - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.26.0", + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.25.0", + "@babel/helper-compilation-targets": "^7.25.2", + "@babel/helper-module-transforms": "^7.25.2", + "@babel/helpers": "^7.25.0", + "@babel/parser": "^7.25.0", + "@babel/template": "^7.25.0", + "@babel/traverse": "^7.25.2", + "@babel/types": "^7.25.2", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -254,9 +259,9 @@ } }, "node_modules/@babel/eslint-parser": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.25.9.tgz", - "integrity": "sha512-5UXfgpK0j0Xr/xIdgdLEhOFxaDZ0bRPWJJchRpqOSur/3rZoPbqqki5mm0p4NE2cs28krBEiSM2MB7//afRSQQ==", + "version": "7.25.1", + "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.25.1.tgz", + "integrity": "sha512-Y956ghgTT4j7rKesabkh5WeqgSFZVFwaPR0IWFm7KFHFmmJ4afbG49SmfW4S+GyRPx0Dy5jxEWA5t0rpxfElWg==", "dev": true, "license": "MIT", "dependencies": { @@ -339,6 +344,16 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, "node_modules/@babel/helper-compilation-targets/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -702,15 +717,15 @@ } }, "node_modules/@babel/plugin-proposal-decorators": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.25.9.tgz", - "integrity": "sha512-smkNLL/O1ezy9Nhy4CNosc4Va+1wo5w4gzSZeLe6y6dM4mmHfYOCPolXQPHQxonZCF+ZyebxN9vqOolkYrSn5g==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.24.7.tgz", + "integrity": "sha512-RL9GR0pUG5Kc8BUWLNDm2T5OpYwSX15r98I0IkgmRQTXuELq/OynH8xtMTMvTJFjXbMWFVTKtYkTaYQsuAwQlQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/plugin-syntax-decorators": "^7.25.9" + "@babel/helper-create-class-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-decorators": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -732,6 +747,48 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-syntax-decorators": { "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.25.9.tgz", @@ -748,6 +805,32 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-export-namespace-from": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", + "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-syntax-flow": { "version": "7.26.0", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.26.0.tgz", @@ -796,6 +879,32 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-syntax-jsx": { "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.25.9.tgz", @@ -812,6 +921,116 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-syntax-unicode-sets-regex": { "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", @@ -1581,23 +1800,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-regexp-modifiers": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.26.0.tgz", - "integrity": "sha512-vN6saax7lrA2yA/Pak3sCxuD6F5InBjn9IcrIKQPjpsLvuHYLVroTxjdlVRHjjBWxKOqIwpTXDkOssYT4BFdRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, "node_modules/@babel/plugin-transform-reserved-words": { "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.25.9.tgz", @@ -1763,80 +1965,94 @@ } }, "node_modules/@babel/preset-env": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.26.0.tgz", - "integrity": "sha512-H84Fxq0CQJNdPFT2DrfnylZ3cf5K43rGfWK4LJGPpjKHiZlk0/RzwEus3PDDZZg+/Er7lCA03MVacueUuXdzfw==", + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.25.4.tgz", + "integrity": "sha512-W9Gyo+KmcxjGahtt3t9fb14vFRWvPpu5pT6GBlovAK6BTBcxgjfVMSQCfJl4oi35ODrxP6xx2Wr8LNST57Mraw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.26.0", - "@babel/helper-compilation-targets": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-validator-option": "^7.25.9", - "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.25.9", - "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.25.9", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.25.9", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.25.9", - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.25.9", + "@babel/compat-data": "^7.25.4", + "@babel/helper-compilation-targets": "^7.25.2", + "@babel/helper-plugin-utils": "^7.24.8", + "@babel/helper-validator-option": "^7.24.8", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.25.3", + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.25.0", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.25.0", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.24.7", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.25.0", "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", - "@babel/plugin-syntax-import-assertions": "^7.26.0", - "@babel/plugin-syntax-import-attributes": "^7.26.0", + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3", + "@babel/plugin-syntax-import-assertions": "^7.24.7", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", - "@babel/plugin-transform-arrow-functions": "^7.25.9", - "@babel/plugin-transform-async-generator-functions": "^7.25.9", - "@babel/plugin-transform-async-to-generator": "^7.25.9", - "@babel/plugin-transform-block-scoped-functions": "^7.25.9", - "@babel/plugin-transform-block-scoping": "^7.25.9", - "@babel/plugin-transform-class-properties": "^7.25.9", - "@babel/plugin-transform-class-static-block": "^7.26.0", - "@babel/plugin-transform-classes": "^7.25.9", - "@babel/plugin-transform-computed-properties": "^7.25.9", - "@babel/plugin-transform-destructuring": "^7.25.9", - "@babel/plugin-transform-dotall-regex": "^7.25.9", - "@babel/plugin-transform-duplicate-keys": "^7.25.9", - "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.25.9", - "@babel/plugin-transform-dynamic-import": "^7.25.9", - "@babel/plugin-transform-exponentiation-operator": "^7.25.9", - "@babel/plugin-transform-export-namespace-from": "^7.25.9", - "@babel/plugin-transform-for-of": "^7.25.9", - "@babel/plugin-transform-function-name": "^7.25.9", - "@babel/plugin-transform-json-strings": "^7.25.9", - "@babel/plugin-transform-literals": "^7.25.9", - "@babel/plugin-transform-logical-assignment-operators": "^7.25.9", - "@babel/plugin-transform-member-expression-literals": "^7.25.9", - "@babel/plugin-transform-modules-amd": "^7.25.9", - "@babel/plugin-transform-modules-commonjs": "^7.25.9", - "@babel/plugin-transform-modules-systemjs": "^7.25.9", - "@babel/plugin-transform-modules-umd": "^7.25.9", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.25.9", - "@babel/plugin-transform-new-target": "^7.25.9", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.25.9", - "@babel/plugin-transform-numeric-separator": "^7.25.9", - "@babel/plugin-transform-object-rest-spread": "^7.25.9", - "@babel/plugin-transform-object-super": "^7.25.9", - "@babel/plugin-transform-optional-catch-binding": "^7.25.9", - "@babel/plugin-transform-optional-chaining": "^7.25.9", - "@babel/plugin-transform-parameters": "^7.25.9", - "@babel/plugin-transform-private-methods": "^7.25.9", - "@babel/plugin-transform-private-property-in-object": "^7.25.9", - "@babel/plugin-transform-property-literals": "^7.25.9", - "@babel/plugin-transform-regenerator": "^7.25.9", - "@babel/plugin-transform-regexp-modifiers": "^7.26.0", - "@babel/plugin-transform-reserved-words": "^7.25.9", - "@babel/plugin-transform-shorthand-properties": "^7.25.9", - "@babel/plugin-transform-spread": "^7.25.9", - "@babel/plugin-transform-sticky-regex": "^7.25.9", - "@babel/plugin-transform-template-literals": "^7.25.9", - "@babel/plugin-transform-typeof-symbol": "^7.25.9", - "@babel/plugin-transform-unicode-escapes": "^7.25.9", - "@babel/plugin-transform-unicode-property-regex": "^7.25.9", - "@babel/plugin-transform-unicode-regex": "^7.25.9", - "@babel/plugin-transform-unicode-sets-regex": "^7.25.9", + "@babel/plugin-transform-arrow-functions": "^7.24.7", + "@babel/plugin-transform-async-generator-functions": "^7.25.4", + "@babel/plugin-transform-async-to-generator": "^7.24.7", + "@babel/plugin-transform-block-scoped-functions": "^7.24.7", + "@babel/plugin-transform-block-scoping": "^7.25.0", + "@babel/plugin-transform-class-properties": "^7.25.4", + "@babel/plugin-transform-class-static-block": "^7.24.7", + "@babel/plugin-transform-classes": "^7.25.4", + "@babel/plugin-transform-computed-properties": "^7.24.7", + "@babel/plugin-transform-destructuring": "^7.24.8", + "@babel/plugin-transform-dotall-regex": "^7.24.7", + "@babel/plugin-transform-duplicate-keys": "^7.24.7", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.25.0", + "@babel/plugin-transform-dynamic-import": "^7.24.7", + "@babel/plugin-transform-exponentiation-operator": "^7.24.7", + "@babel/plugin-transform-export-namespace-from": "^7.24.7", + "@babel/plugin-transform-for-of": "^7.24.7", + "@babel/plugin-transform-function-name": "^7.25.1", + "@babel/plugin-transform-json-strings": "^7.24.7", + "@babel/plugin-transform-literals": "^7.25.2", + "@babel/plugin-transform-logical-assignment-operators": "^7.24.7", + "@babel/plugin-transform-member-expression-literals": "^7.24.7", + "@babel/plugin-transform-modules-amd": "^7.24.7", + "@babel/plugin-transform-modules-commonjs": "^7.24.8", + "@babel/plugin-transform-modules-systemjs": "^7.25.0", + "@babel/plugin-transform-modules-umd": "^7.24.7", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", + "@babel/plugin-transform-new-target": "^7.24.7", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", + "@babel/plugin-transform-numeric-separator": "^7.24.7", + "@babel/plugin-transform-object-rest-spread": "^7.24.7", + "@babel/plugin-transform-object-super": "^7.24.7", + "@babel/plugin-transform-optional-catch-binding": "^7.24.7", + "@babel/plugin-transform-optional-chaining": "^7.24.8", + "@babel/plugin-transform-parameters": "^7.24.7", + "@babel/plugin-transform-private-methods": "^7.25.4", + "@babel/plugin-transform-private-property-in-object": "^7.24.7", + "@babel/plugin-transform-property-literals": "^7.24.7", + "@babel/plugin-transform-regenerator": "^7.24.7", + "@babel/plugin-transform-reserved-words": "^7.24.7", + "@babel/plugin-transform-shorthand-properties": "^7.24.7", + "@babel/plugin-transform-spread": "^7.24.7", + "@babel/plugin-transform-sticky-regex": "^7.24.7", + "@babel/plugin-transform-template-literals": "^7.24.7", + "@babel/plugin-transform-typeof-symbol": "^7.24.8", + "@babel/plugin-transform-unicode-escapes": "^7.24.7", + "@babel/plugin-transform-unicode-property-regex": "^7.24.7", + "@babel/plugin-transform-unicode-regex": "^7.24.7", + "@babel/plugin-transform-unicode-sets-regex": "^7.25.4", "@babel/preset-modules": "0.1.6-no-external-plugins", "babel-plugin-polyfill-corejs2": "^0.4.10", "babel-plugin-polyfill-corejs3": "^0.10.6", "babel-plugin-polyfill-regenerator": "^0.6.1", - "core-js-compat": "^3.38.1", + "core-js-compat": "^3.37.1", "semver": "^6.3.1" }, "engines": { @@ -1857,15 +2073,15 @@ } }, "node_modules/@babel/preset-flow": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/preset-flow/-/preset-flow-7.25.9.tgz", - "integrity": "sha512-EASHsAhE+SSlEzJ4bzfusnXSHiU+JfAYzj+jbw2vgQKgq5HrUr8qs+vgtiEL5dOH6sEweI+PNt2D7AqrDSHyqQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/preset-flow/-/preset-flow-7.24.7.tgz", + "integrity": "sha512-NL3Lo0NorCU607zU3NwRyJbpaB6E3t0xtd3LfAQKDfkeX4/ggcDXvkmkW42QWT5owUeW/jAe4hn+2qvkV1IbfQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-validator-option": "^7.25.9", - "@babel/plugin-transform-flow-strip-types": "^7.25.9" + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-validator-option": "^7.24.7", + "@babel/plugin-transform-flow-strip-types": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1890,18 +2106,18 @@ } }, "node_modules/@babel/preset-react": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.26.3.tgz", - "integrity": "sha512-Nl03d6T9ky516DGK2YMxrTqvnpUW63TnJMOMonj+Zae0JiPC5BC9xPMSL6L8fiSpA5vP88qfygavVQvnLp+6Cw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.24.7.tgz", + "integrity": "sha512-AAH4lEkpmzFWrGVlHaxJB7RLH21uPQ9+He+eFLWHmF9IuFQVugz8eAsamaW0DXRrTfco5zj1wWtpdcXJUOfsag==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-validator-option": "^7.25.9", - "@babel/plugin-transform-react-display-name": "^7.25.9", - "@babel/plugin-transform-react-jsx": "^7.25.9", - "@babel/plugin-transform-react-jsx-development": "^7.25.9", - "@babel/plugin-transform-react-pure-annotations": "^7.25.9" + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-validator-option": "^7.24.7", + "@babel/plugin-transform-react-display-name": "^7.24.7", + "@babel/plugin-transform-react-jsx": "^7.24.7", + "@babel/plugin-transform-react-jsx-development": "^7.24.7", + "@babel/plugin-transform-react-pure-annotations": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1938,9 +2154,9 @@ } }, "node_modules/@babel/traverse": { - "version": "7.26.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.4.tgz", - "integrity": "sha512-fH+b7Y4p3yqvApJALCPJcwb0/XaOSgtK4pzV6WVjPR5GLFQBRI7pfoX2V2iM48NXvX07NUxxm1Vw98YjqTcU5w==", + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.3.tgz", + "integrity": "sha512-yTmc8J+Sj8yLzwr4PD5Xb/WF3bOYu2C2OoSZPzbuqRm4n98XirsbzaX+GloeO376UnSYIYJ4NCanwV5/ugZkwA==", "dev": true, "license": "MIT", "dependencies": { @@ -2904,31 +3120,18 @@ "license": "MIT" }, "node_modules/@iconify/utils": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-2.2.1.tgz", - "integrity": "sha512-0/7J7hk4PqXmxo5PDBDxmnecw5PxklZJfNjIVG9FM0mEfVrvfudS22rYWsqVk6gR3UJ/mSYS90X4R3znXnqfNA==", + "version": "2.1.33", + "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-2.1.33.tgz", + "integrity": "sha512-jP9h6v/g0BIZx0p7XGJJVtkVnydtbgTgt9mVNcGDYwaa7UhdHdI9dvoq+gKj9sijMSJKxUPEG2JyjsgXjxL7Kw==", "license": "MIT", "dependencies": { - "@antfu/install-pkg": "^0.4.1", + "@antfu/install-pkg": "^0.4.0", "@antfu/utils": "^0.7.10", "@iconify/types": "^2.0.0", - "debug": "^4.4.0", - "globals": "^15.13.0", + "debug": "^4.3.6", "kolorist": "^1.8.0", - "local-pkg": "^0.5.1", - "mlly": "^1.7.3" - } - }, - "node_modules/@iconify/utils/node_modules/globals": { - "version": "15.14.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-15.14.0.tgz", - "integrity": "sha512-OkToC372DtlQeje9/zHIo5CT8lRP/FUgEOKBEhU4e0abL7J7CD24fD9ohiLN5hagG/kWCYj4K5oaxxtj2Z0Dig==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "local-pkg": "^0.5.0", + "mlly": "^1.7.1" } }, "node_modules/@isaacs/cliui": { @@ -3022,9 +3225,9 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", - "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", "license": "MIT", "dependencies": { "@jridgewell/set-array": "^1.2.1", @@ -3300,13 +3503,13 @@ } }, "node_modules/@playwright/test": { - "version": "1.49.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.1.tgz", - "integrity": "sha512-Ky+BVzPz8pL6PQxHqNRW1k3mIyv933LML7HktS8uik0bUXNCdPhoS/kLihiO1tMf/egaJb4IutXd7UywvXEW+g==", + "version": "1.49.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.0.tgz", + "integrity": "sha512-DMulbwQURa8rNIQrf94+jPJQ4FmOVdpE5ZppRNvWVjvhC+6sOeo28r8MgIpQRYouXRtt/FCCXU7zn20jnHR4Qw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright": "1.49.1" + "playwright": "1.49.0" }, "bin": { "playwright": "cli.js" @@ -3326,9 +3529,9 @@ } }, "node_modules/@primer/octicons": { - "version": "19.14.0", - "resolved": "https://registry.npmjs.org/@primer/octicons/-/octicons-19.14.0.tgz", - "integrity": "sha512-9Ovw/xcUFHC/zbsNhr/Hkp1+m9XnNeQvnGHDHrI5vhlf6PRZVzSsdMnesV2xCzQh7jXP3EVRcaeXsUGlsZrfcA==", + "version": "19.13.0", + "resolved": "https://registry.npmjs.org/@primer/octicons/-/octicons-19.13.0.tgz", + "integrity": "sha512-U5g7Bv89At8qFXUy9MQlOUs4iPXR5XR4QPnSZWhmOn2oCgwf4LjdvfC+8OzhUFx2wC8k9TbRRAMVuCkqFLPlfQ==", "license": "MIT", "dependencies": { "object-assign": "^4.1.1" @@ -3388,9 +3591,9 @@ "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.28.1.tgz", - "integrity": "sha512-2aZp8AES04KI2dy3Ss6/MDjXbwBzj+i0GqKtWXgw2/Ma6E4jJvujryO6gJAghIRVz7Vwr9Gtl/8na3nDUKpraQ==", + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.28.0.tgz", + "integrity": "sha512-wLJuPLT6grGZsy34g4N1yRfYeouklTgPhH1gWXCYspenKYD0s3cR99ZevOGw5BexMNywkbV3UkjADisozBmpPQ==", "cpu": [ "arm" ], @@ -3402,9 +3605,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.28.1.tgz", - "integrity": "sha512-EbkK285O+1YMrg57xVA+Dp0tDBRB93/BZKph9XhMjezf6F4TpYjaUSuPt5J0fZXlSag0LmZAsTmdGGqPp4pQFA==", + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.28.0.tgz", + "integrity": "sha512-eiNkznlo0dLmVG/6wf+Ifi/v78G4d4QxRhuUl+s8EWZpDewgk7PX3ZyECUXU0Zq/Ca+8nU8cQpNC4Xgn2gFNDA==", "cpu": [ "arm64" ], @@ -3416,9 +3619,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.28.1.tgz", - "integrity": "sha512-prduvrMKU6NzMq6nxzQw445zXgaDBbMQvmKSJaxpaZ5R1QDM8w+eGxo6Y/jhT/cLoCvnZI42oEqf9KQNYz1fqQ==", + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.28.0.tgz", + "integrity": "sha512-lmKx9yHsppblnLQZOGxdO66gT77bvdBtr/0P+TPOseowE7D9AJoBw8ZDULRasXRWf1Z86/gcOdpBrV6VDUY36Q==", "cpu": [ "arm64" ], @@ -3430,9 +3633,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.28.1.tgz", - "integrity": "sha512-WsvbOunsUk0wccO/TV4o7IKgloJ942hVFK1CLatwv6TJspcCZb9umQkPdvB7FihmdxgaKR5JyxDjWpCOp4uZlQ==", + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.28.0.tgz", + "integrity": "sha512-8hxgfReVs7k9Js1uAIhS6zq3I+wKQETInnWQtgzt8JfGx51R1N6DRVy3F4o0lQwumbErRz52YqwjfvuwRxGv1w==", "cpu": [ "x64" ], @@ -3444,9 +3647,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.28.1.tgz", - "integrity": "sha512-HTDPdY1caUcU4qK23FeeGxCdJF64cKkqajU0iBnTVxS8F7H/7BewvYoG+va1KPSL63kQ1PGNyiwKOfReavzvNA==", + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.28.0.tgz", + "integrity": "sha512-lA1zZB3bFx5oxu9fYud4+g1mt+lYXCoch0M0V/xhqLoGatbzVse0wlSQ1UYOWKpuSu3gyN4qEc0Dxf/DII1bhQ==", "cpu": [ "arm64" ], @@ -3458,9 +3661,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.28.1.tgz", - "integrity": "sha512-m/uYasxkUevcFTeRSM9TeLyPe2QDuqtjkeoTpP9SW0XxUWfcYrGDMkO/m2tTw+4NMAF9P2fU3Mw4ahNvo7QmsQ==", + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.28.0.tgz", + "integrity": "sha512-aI2plavbUDjCQB/sRbeUZWX9qp12GfYkYSJOrdYTL/C5D53bsE2/nBPuoiJKoWp5SN78v2Vr8ZPnB+/VbQ2pFA==", "cpu": [ "x64" ], @@ -3472,9 +3675,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.28.1.tgz", - "integrity": "sha512-QAg11ZIt6mcmzpNE6JZBpKfJaKkqTm1A9+y9O+frdZJEuhQxiugM05gnCWiANHj4RmbgeVJpTdmKRmH/a+0QbA==", + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.28.0.tgz", + "integrity": "sha512-WXveUPKtfqtaNvpf0iOb0M6xC64GzUX/OowbqfiCSXTdi/jLlOmH0Ba94/OkiY2yTGTwteo4/dsHRfh5bDCZ+w==", "cpu": [ "arm" ], @@ -3486,9 +3689,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.28.1.tgz", - "integrity": "sha512-dRP9PEBfolq1dmMcFqbEPSd9VlRuVWEGSmbxVEfiq2cs2jlZAl0YNxFzAQS2OrQmsLBLAATDMb3Z6MFv5vOcXg==", + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.28.0.tgz", + "integrity": "sha512-yLc3O2NtOQR67lI79zsSc7lk31xjwcaocvdD1twL64PK1yNaIqCeWI9L5B4MFPAVGEVjH5k1oWSGuYX1Wutxpg==", "cpu": [ "arm" ], @@ -3500,9 +3703,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.28.1.tgz", - "integrity": "sha512-uGr8khxO+CKT4XU8ZUH1TTEUtlktK6Kgtv0+6bIFSeiSlnGJHG1tSFSjm41uQ9sAO/5ULx9mWOz70jYLyv1QkA==", + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.28.0.tgz", + "integrity": "sha512-+P9G9hjEpHucHRXqesY+3X9hD2wh0iNnJXX/QhS/J5vTdG6VhNYMxJ2rJkQOxRUd17u5mbMLHM7yWGZdAASfcg==", "cpu": [ "arm64" ], @@ -3514,9 +3717,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.28.1.tgz", - "integrity": "sha512-QF54q8MYGAqMLrX2t7tNpi01nvq5RI59UBNx+3+37zoKX5KViPo/gk2QLhsuqok05sSCRluj0D00LzCwBikb0A==", + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.28.0.tgz", + "integrity": "sha512-1xsm2rCKSTpKzi5/ypT5wfc+4bOGa/9yI/eaOLW0oMs7qpC542APWhl4A37AENGZ6St6GBMWhCCMM6tXgTIplw==", "cpu": [ "arm64" ], @@ -3527,24 +3730,10 @@ "linux" ] }, - "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.28.1.tgz", - "integrity": "sha512-vPul4uodvWvLhRco2w0GcyZcdyBfpfDRgNKU+p35AWEbJ/HPs1tOUrkSueVbBS0RQHAf/A+nNtDpvw95PeVKOA==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.28.1.tgz", - "integrity": "sha512-pTnTdBuC2+pt1Rmm2SV7JWRqzhYpEILML4PKODqLz+C7Ou2apEV52h19CR7es+u04KlqplggmN9sqZlekg3R1A==", + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.28.0.tgz", + "integrity": "sha512-zgWxMq8neVQeXL+ouSf6S7DoNeo6EPgi1eeqHXVKQxqPy1B2NvTbaOUWPn/7CfMKL7xvhV0/+fq/Z/J69g1WAQ==", "cpu": [ "ppc64" ], @@ -3556,9 +3745,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.28.1.tgz", - "integrity": "sha512-vWXy1Nfg7TPBSuAncfInmAI/WZDd5vOklyLJDdIRKABcZWojNDY0NJwruY2AcnCLnRJKSaBgf/GiJfauu8cQZA==", + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.28.0.tgz", + "integrity": "sha512-VEdVYacLniRxbRJLNtzwGt5vwS0ycYshofI7cWAfj7Vg5asqj+pt+Q6x4n+AONSZW/kVm+5nklde0qs2EUwU2g==", "cpu": [ "riscv64" ], @@ -3570,9 +3759,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.28.1.tgz", - "integrity": "sha512-/yqC2Y53oZjb0yz8PVuGOQQNOTwxcizudunl/tFs1aLvObTclTwZ0JhXF2XcPT/zuaymemCDSuuUPXJJyqeDOg==", + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.28.0.tgz", + "integrity": "sha512-LQlP5t2hcDJh8HV8RELD9/xlYtEzJkm/aWGsauvdO2ulfl3QYRjqrKW+mGAIWP5kdNCBheqqqYIGElSRCaXfpw==", "cpu": [ "s390x" ], @@ -3584,9 +3773,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.28.1.tgz", - "integrity": "sha512-fzgeABz7rrAlKYB0y2kSEiURrI0691CSL0+KXwKwhxvj92VULEDQLpBYLHpF49MSiPG4sq5CK3qHMnb9tlCjBw==", + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.28.0.tgz", + "integrity": "sha512-Nl4KIzteVEKE9BdAvYoTkW19pa7LR/RBrT6F1dJCV/3pbjwDcaOq+edkP0LXuJ9kflW/xOK414X78r+K84+msw==", "cpu": [ "x64" ], @@ -3598,9 +3787,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.28.1.tgz", - "integrity": "sha512-xQTDVzSGiMlSshpJCtudbWyRfLaNiVPXt1WgdWTwWz9n0U12cI2ZVtWe/Jgwyv/6wjL7b66uu61Vg0POWVfz4g==", + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.28.0.tgz", + "integrity": "sha512-eKpJr4vBDOi4goT75MvW+0dXcNUqisK4jvibY9vDdlgLx+yekxSm55StsHbxUsRxSTt3JEQvlr3cGDkzcSP8bw==", "cpu": [ "x64" ], @@ -3612,9 +3801,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.28.1.tgz", - "integrity": "sha512-wSXmDRVupJstFP7elGMgv+2HqXelQhuNf+IS4V+nUpNVi/GUiBgDmfwD0UGN3pcAnWsgKG3I52wMOBnk1VHr/A==", + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.28.0.tgz", + "integrity": "sha512-Vi+WR62xWGsE/Oj+mD0FNAPY2MEox3cfyG0zLpotZdehPFXwz6lypkGs5y38Jd/NVSbOD02aVad6q6QYF7i8Bg==", "cpu": [ "arm64" ], @@ -3626,9 +3815,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.28.1.tgz", - "integrity": "sha512-ZkyTJ/9vkgrE/Rk9vhMXhf8l9D+eAhbAVbsGsXKy2ohmJaWg0LPQLnIxRdRp/bKyr8tXuPlXhIoGlEB5XpJnGA==", + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.28.0.tgz", + "integrity": "sha512-kN/Vpip8emMLn/eOza+4JwqDZBL6MPNpkdaEsgUtW1NYN3DZvZqSQrbKzJcTL6hd8YNmFTn7XGWMwccOcJBL0A==", "cpu": [ "ia32" ], @@ -3640,9 +3829,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.28.1.tgz", - "integrity": "sha512-ZvK2jBafvttJjoIdKm/Q/Bh7IJ1Ose9IBOwpOXcOvW3ikGTQGmKDgxTC6oCAzW6PynbkKP8+um1du81XJHZ0JA==", + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.28.0.tgz", + "integrity": "sha512-Bvno2/aZT6usSa7lRDL2+hMjVAGjuqaymF1ApZm31JXzniR/hvr14jpU+/z4X6Gt5BPlzosscyJZGUvguXIqeQ==", "cpu": [ "x64" ], @@ -4217,9 +4406,9 @@ } }, "node_modules/@stylistic/eslint-plugin-js": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-js/-/eslint-plugin-js-2.12.1.tgz", - "integrity": "sha512-5ybogtEgWIGCR6dMnaabztbWyVdAPDsf/5XOk6jBonWug875Q9/a6gm9QxnU3rhdyDEnckWKX7dduwYJMOWrVA==", + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-js/-/eslint-plugin-js-2.11.0.tgz", + "integrity": "sha512-btchD0P3iij6cIk5RR5QMdEhtCCV0+L6cNheGhGCd//jaHILZMTi/EOqgEDAf1s4ZoViyExoToM+S2Iwa3U9DA==", "dev": true, "license": "MIT", "dependencies": { @@ -4985,13 +5174,6 @@ "@types/d3-selection": "*" } }, - "node_modules/@types/doctrine": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/@types/doctrine/-/doctrine-0.0.9.tgz", - "integrity": "sha512-eOIHzCUSH7SMfonMG1LsC2f8vxBFtho6NGBznK41R84YzPuvSBzrhEps33IsQiOW9+VL6NQ9DbjQJznk/S4uRA==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/dropzone": { "version": "5.7.9", "resolved": "https://registry.npmjs.org/@types/dropzone/-/dropzone-5.7.9.tgz", @@ -5039,9 +5221,9 @@ "license": "MIT" }, "node_modules/@types/geojson": { - "version": "7946.0.15", - "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.15.tgz", - "integrity": "sha512-9oSxFzDCT2Rj6DfcHF8G++jxBKS7mBqXl5xrRW+Kbvjry6Uduya2iiwqHPhVXpasAVMBYKkEPGgKhd3+/HZ6xA==", + "version": "7946.0.14", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.14.tgz", + "integrity": "sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg==", "license": "MIT" }, "node_modules/@types/hammerjs": { @@ -5126,9 +5308,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.10.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz", - "integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==", + "version": "22.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.1.tgz", + "integrity": "sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ==", "license": "MIT", "dependencies": { "undici-types": "~6.20.0" @@ -5282,17 +5464,17 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.18.1.tgz", - "integrity": "sha512-Ncvsq5CT3Gvh+uJG0Lwlho6suwDfUXH0HztslDf5I+F2wAFAZMRwYLEorumpKLzmO2suAXZ/td1tBg4NZIi9CQ==", + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.17.0.tgz", + "integrity": "sha512-HU1KAdW3Tt8zQkdvNoIijfWDMvdSweFYm4hWh+KwhPstv+sCmWb89hCIP8msFm9N1R/ooh9honpSuvqKWlYy3w==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.18.1", - "@typescript-eslint/type-utils": "8.18.1", - "@typescript-eslint/utils": "8.18.1", - "@typescript-eslint/visitor-keys": "8.18.1", + "@typescript-eslint/scope-manager": "8.17.0", + "@typescript-eslint/type-utils": "8.17.0", + "@typescript-eslint/utils": "8.17.0", + "@typescript-eslint/visitor-keys": "8.17.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -5307,21 +5489,25 @@ }, "peerDependencies": { "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, "node_modules/@typescript-eslint/parser": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.18.1.tgz", - "integrity": "sha512-rBnTWHCdbYM2lh7hjyXqxk70wvon3p2FyaniZuey5TrcGBpfhVp0OxOa6gxr9Q9YhZFKyfbEnxc24ZnVbbUkCA==", + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.17.0.tgz", + "integrity": "sha512-Drp39TXuUlD49F7ilHHCG7TTg8IkA+hxCuULdmzWYICxGXvDXmDmWEjJYZQYgf6l/TFfYNE167m7isnc3xlIEg==", "dev": true, - "license": "MIT", + "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/scope-manager": "8.18.1", - "@typescript-eslint/types": "8.18.1", - "@typescript-eslint/typescript-estree": "8.18.1", - "@typescript-eslint/visitor-keys": "8.18.1", + "@typescript-eslint/scope-manager": "8.17.0", + "@typescript-eslint/types": "8.17.0", + "@typescript-eslint/typescript-estree": "8.17.0", + "@typescript-eslint/visitor-keys": "8.17.0", "debug": "^4.3.4" }, "engines": { @@ -5332,19 +5518,23 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.18.1.tgz", - "integrity": "sha512-HxfHo2b090M5s2+/9Z3gkBhI6xBH8OJCFjH9MhQ+nnoZqxU3wNxkLT+VWXWSFWc3UF3Z+CfPAyqdCTdoXtDPCQ==", + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.17.0.tgz", + "integrity": "sha512-/ewp4XjvnxaREtqsZjF4Mfn078RD/9GmiEAtTeLQ7yFdKnqwTOgRMSvFz4et9U5RiJQ15WTGXPLj89zGusvxBg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.18.1", - "@typescript-eslint/visitor-keys": "8.18.1" + "@typescript-eslint/types": "8.17.0", + "@typescript-eslint/visitor-keys": "8.17.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5355,14 +5545,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.18.1.tgz", - "integrity": "sha512-jAhTdK/Qx2NJPNOTxXpMwlOiSymtR2j283TtPqXkKBdH8OAMmhiUfP0kJjc/qSE51Xrq02Gj9NY7MwK+UxVwHQ==", + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.17.0.tgz", + "integrity": "sha512-q38llWJYPd63rRnJ6wY/ZQqIzPrBCkPdpIsaCfkR3Q4t3p6sb422zougfad4TFW9+ElIFLVDzWGiGAfbb/v2qw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.18.1", - "@typescript-eslint/utils": "8.18.1", + "@typescript-eslint/typescript-estree": "8.17.0", + "@typescript-eslint/utils": "8.17.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -5374,14 +5564,18 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, "node_modules/@typescript-eslint/types": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.18.1.tgz", - "integrity": "sha512-7uoAUsCj66qdNQNpH2G8MyTFlgerum8ubf21s3TSM3XmKXuIn+H2Sifh/ES2nPOPiYSRJWAk0fDkW0APBWcpfw==", + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.17.0.tgz", + "integrity": "sha512-gY2TVzeve3z6crqh2Ic7Cr+CAv6pfb0Egee7J5UAVWCpVvDI/F71wNfolIim4FE6hT15EbpZFVUj9j5i38jYXA==", "dev": true, "license": "MIT", "engines": { @@ -5393,14 +5587,14 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.18.1.tgz", - "integrity": "sha512-z8U21WI5txzl2XYOW7i9hJhxoKKNG1kcU4RzyNvKrdZDmbjkmLBo8bgeiOJmA06kizLI76/CCBAAGlTlEeUfyg==", + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.17.0.tgz", + "integrity": "sha512-JqkOopc1nRKZpX+opvKqnM3XUlM7LpFMD0lYxTqOTKQfCWAmxw45e3qlOCsEqEB2yuacujivudOFpCnqkBDNMw==", "dev": true, - "license": "MIT", + "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/types": "8.18.1", - "@typescript-eslint/visitor-keys": "8.18.1", + "@typescript-eslint/types": "8.17.0", + "@typescript-eslint/visitor-keys": "8.17.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -5415,8 +5609,10 @@ "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, - "peerDependencies": { - "typescript": ">=4.8.4 <5.8.0" + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { @@ -5436,16 +5632,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.18.1.tgz", - "integrity": "sha512-8vikiIj2ebrC4WRdcAdDcmnu9Q/MXXwg+STf40BVfT8exDqBCUPdypvzcUPxEqRGKg9ALagZ0UWcYCtn+4W2iQ==", + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.17.0.tgz", + "integrity": "sha512-bQC8BnEkxqG8HBGKwG9wXlZqg37RKSMY7v/X8VEWD8JG2JuTHuNK0VFvMPMUKQcbk6B+tf05k+4AShAEtCtJ/w==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.18.1", - "@typescript-eslint/types": "8.18.1", - "@typescript-eslint/typescript-estree": "8.18.1" + "@typescript-eslint/scope-manager": "8.17.0", + "@typescript-eslint/types": "8.17.0", + "@typescript-eslint/typescript-estree": "8.17.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5455,18 +5651,22 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.18.1.tgz", - "integrity": "sha512-Vj0WLm5/ZsD013YeUKn+K0y8p1M0jPpxOkKdbD1wB0ns53a5piVY02zjf072TblEweAbcYiFiPoSMF3kp+VhhQ==", + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.17.0.tgz", + "integrity": "sha512-1Hm7THLpO6ww5QU6H/Qp+AusUUl+z/CAm3cNZZ0jQvon9yicgO7Rwd+/WWRpMKLYV6p2UvdbR27c86rzCPpreg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.18.1", + "@typescript-eslint/types": "8.17.0", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -5478,9 +5678,9 @@ } }, "node_modules/@ungap/structured-clone": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.1.tgz", - "integrity": "sha512-fEzPV3hSkSMltkw152tJKNARhOupqbH96MZWyRjNaYZOMIzbrTeQDG+MTc6Mr2pgzFQzFxAfmhGDNP5QK++2ZA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", "dev": true, "license": "ISC" }, @@ -5559,9 +5759,9 @@ } }, "node_modules/@vitest/mocker/node_modules/magic-string": { - "version": "0.30.17", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", - "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "version": "0.30.14", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.14.tgz", + "integrity": "sha512-5c99P1WKTed11ZC0HMJOj6CDIue6F8ySu+bJL+85q1zBEIY8IklrJ1eiKC2NDRh3Ct3FcvmJPyQHb9erXMTJNw==", "dev": true, "license": "MIT", "dependencies": { @@ -5611,9 +5811,9 @@ } }, "node_modules/@vitest/snapshot/node_modules/magic-string": { - "version": "0.30.17", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", - "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "version": "0.30.14", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.14.tgz", + "integrity": "sha512-5c99P1WKTed11ZC0HMJOj6CDIue6F8ySu+bJL+85q1zBEIY8IklrJ1eiKC2NDRh3Ct3FcvmJPyQHb9erXMTJNw==", "dev": true, "license": "MIT", "dependencies": { @@ -5718,9 +5918,9 @@ } }, "node_modules/@vue/compiler-sfc/node_modules/magic-string": { - "version": "0.30.17", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", - "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "version": "0.30.14", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.14.tgz", + "integrity": "sha512-5c99P1WKTed11ZC0HMJOj6CDIue6F8ySu+bJL+85q1zBEIY8IklrJ1eiKC2NDRh3Ct3FcvmJPyQHb9erXMTJNw==", "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" @@ -6283,6 +6483,27 @@ "node": ">=8" } }, + "node_modules/array.prototype.findlast": { + "name": "@nolyfill/array.prototype.findlast", + "version": "1.0.24", + "resolved": "https://registry.npmjs.org/@nolyfill/array.prototype.findlast/-/array.prototype.findlast-1.0.24.tgz", + "integrity": "sha512-yFCyZLs0iNNubzYnBINcOCJAiGtusxiR2F1DnwkOB1HQbWXl/zltkDIWIXO3cJxhQdngDlmM4ysTfyAfoB297g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nolyfill/shared": "1.0.24" + }, + "engines": { + "node": ">=12.4.0" + } + }, + "node_modules/array.prototype.findlast/node_modules/@nolyfill/shared": { + "version": "1.0.24", + "resolved": "https://registry.npmjs.org/@nolyfill/shared/-/shared-1.0.24.tgz", + "integrity": "sha512-TGCpg3k5N7jj9AgU/1xFw9K1g4AC1vEE5ZFkW77oPNNLzprxT17PvFaNr/lr3BkkT5fJ5LNMntaTIq+pyWaeEA==", + "dev": true, + "license": "MIT" + }, "node_modules/array.prototype.findlastindex": { "name": "@nolyfill/array.prototype.findlastindex", "version": "1.0.24", @@ -6332,9 +6553,30 @@ "node": ">=12.4.0" } }, - "node_modules/as-table": { - "version": "1.0.55", - "resolved": "https://registry.npmjs.org/as-table/-/as-table-1.0.55.tgz", + "node_modules/array.prototype.tosorted": { + "name": "@nolyfill/array.prototype.tosorted", + "version": "1.0.24", + "resolved": "https://registry.npmjs.org/@nolyfill/array.prototype.tosorted/-/array.prototype.tosorted-1.0.24.tgz", + "integrity": "sha512-lVo8TVDqaslOaOvEH7iL7glu/WdlX7ZrB+7FZY4BL25hg8TLHvg3e9pxafCp8vAQ96IOL+tdgBdfeoC7qLeQYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nolyfill/shared": "1.0.24" + }, + "engines": { + "node": ">=12.4.0" + } + }, + "node_modules/array.prototype.tosorted/node_modules/@nolyfill/shared": { + "version": "1.0.24", + "resolved": "https://registry.npmjs.org/@nolyfill/shared/-/shared-1.0.24.tgz", + "integrity": "sha512-TGCpg3k5N7jj9AgU/1xFw9K1g4AC1vEE5ZFkW77oPNNLzprxT17PvFaNr/lr3BkkT5fJ5LNMntaTIq+pyWaeEA==", + "dev": true, + "license": "MIT" + }, + "node_modules/as-table": { + "version": "1.0.55", + "resolved": "https://registry.npmjs.org/as-table/-/as-table-1.0.55.tgz", "integrity": "sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==", "dev": true, "license": "MIT", @@ -6534,17 +6776,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "license": "MIT", - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, "node_modules/boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", @@ -6574,9 +6805,9 @@ } }, "node_modules/browserslist": { - "version": "4.24.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.3.tgz", - "integrity": "sha512-1CPmv8iobE2fyRMV97dAcMVegvvWKxmq94hkLiAkUGwKVTyDLw33K+ZxiFrREKmmps4rIw6grcCFCnTMSZ/YiA==", + "version": "4.24.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.2.tgz", + "integrity": "sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==", "funding": [ { "type": "opencollective", @@ -6593,9 +6824,9 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001688", - "electron-to-chromium": "^1.5.73", - "node-releases": "^2.0.19", + "caniuse-lite": "^1.0.30001669", + "electron-to-chromium": "^1.5.41", + "node-releases": "^2.0.18", "update-browserslist-db": "^1.1.1" }, "bin": { @@ -6694,9 +6925,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001690", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001690.tgz", - "integrity": "sha512-5ExiE3qQN6oF8Clf8ifIDcMRCRE/dMGcETG/XGMD8/XiXm6HXQgQTh1yZYLXXpSOsEUlJm1Xr7kGULZTuGtP/w==", + "version": "1.0.30001686", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001686.tgz", + "integrity": "sha512-Y7deg0Aergpa24M3qLC5xjNklnKnhsmSyR/V89dLZ1n0ucJIFNs7PgR2Yfa/Zf6W79SbBicgtGxZr2juHkEUIA==", "funding": [ { "type": "opencollective", @@ -6713,20 +6944,6 @@ ], "license": "CC-BY-4.0" }, - "node_modules/canvas": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/canvas/-/canvas-3.1.0.tgz", - "integrity": "sha512-tTj3CqqukVJ9NgSahykNwtGda7V33VLObwrHfzT0vqJXu7J4d4C/7kQQW3fOEGDfZZoILPut5H00gOjyttPGyg==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "node-addon-api": "^7.0.0", - "prebuild-install": "^7.1.1" - }, - "engines": { - "node": "^18.12.0 || >= 20.9.0" - } - }, "node_modules/chai": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.2.tgz", @@ -6870,12 +7087,6 @@ "node": ">= 6" } }, - "node_modules/chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "license": "ISC" - }, "node_modules/chrome-trace-event": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", @@ -7249,13 +7460,13 @@ } }, "node_modules/css-tree": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz", - "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.0.1.tgz", + "integrity": "sha512-8Fxxv+tGhORlshCdCwnNJytvlvq46sOLSYEx2ZIGurahWvMucSRnyjPA3AmrMq4VPRYbHVpWj5VkiVasrM2H4Q==", "dev": true, "license": "MIT", "dependencies": { - "mdn-data": "2.12.2", + "mdn-data": "2.12.1", "source-map-js": "^1.0.1" }, "engines": { @@ -7865,9 +8076,9 @@ "license": "MIT" }, "node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -7891,21 +8102,6 @@ "node": ">=0.10" } }, - "node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "license": "MIT", - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/deep-eql": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", @@ -7916,10 +8112,25 @@ "node": ">=6" } }, + "node_modules/deep-equal": { + "name": "@nolyfill/deep-equal", + "version": "1.0.29", + "resolved": "https://registry.npmjs.org/@nolyfill/deep-equal/-/deep-equal-1.0.29.tgz", + "integrity": "sha512-EtrJBbOXHhVz8Y1gMYolKgPqh2u96UPqkZMHR0lcjn3y4TC4R7GuN3E4kEhDIpyK3q1+y7HHPHHkt5fGvW1crQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dequal": "2.0.3" + }, + "engines": { + "node": ">=12.4.0" + } + }, "node_modules/deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, "license": "MIT", "engines": { "node": ">=4.0.0" @@ -7951,13 +8162,14 @@ "node": ">= 0.6.0" } }, - "node_modules/detect-libc": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", - "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", - "license": "Apache-2.0", + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=6" } }, "node_modules/didyoumean": { @@ -8052,9 +8264,9 @@ } }, "node_modules/dompurify": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.3.tgz", - "integrity": "sha512-U1U5Hzc2MO0oW3DF+G9qYN0aT7atAou4AgI0XjWz061nyBPbdxkfdhfy5uMgGn6+oLFCfn44ZGbdDqCzVmlOWA==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.2.tgz", + "integrity": "sha512-YMM+erhdZ2nkZ4fTNRTSI94mb7VG7uVF5vj5Zde7tImgnhZE3R6YW/IACGIHb2ux+QkEXMhe591N+5jWOmL4Zw==", "license": "(MPL-2.0 OR Apache-2.0)", "optionalDependencies": { "@types/trusted-types": "^2.0.7" @@ -8147,9 +8359,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.74", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.74.tgz", - "integrity": "sha512-ck3//9RC+6oss/1Bh9tiAVFy5vfSKbRHAFh7Z3/eTRkEqJeWgymloShB17Vg3Z4nmDNp35vAd1BZ6CMW4Wt6Iw==", + "version": "1.5.68", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.68.tgz", + "integrity": "sha512-FgMdJlma0OzUYlbrtZ4AeXjKxKPk6KT8WOP8BjcqxWtlg8qyJQjRzPJzUtUn5GBg1oQ26hFs7HOOHJMYiJRnvQ==", "license": "ISC" }, "node_modules/emoji-regex": { @@ -8167,15 +8379,6 @@ "node": ">= 4" } }, - "node_modules/end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, "node_modules/enhanced-resolve": { "version": "5.17.1", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", @@ -8245,6 +8448,27 @@ "node": ">=12.4.0" } }, + "node_modules/es-iterator-helpers": { + "name": "@nolyfill/es-iterator-helpers", + "version": "1.0.21", + "resolved": "https://registry.npmjs.org/@nolyfill/es-iterator-helpers/-/es-iterator-helpers-1.0.21.tgz", + "integrity": "sha512-i326KeE0nhW4STobcUhkxpXzZUddedCmfh7b/IyXR9kW0CFHiNNT80C3JSEy33mUlhZtk/ezX47nymcFxyBigg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nolyfill/shared": "1.0.21" + }, + "engines": { + "node": ">=12.4.0" + } + }, + "node_modules/es-iterator-helpers/node_modules/@nolyfill/shared": { + "version": "1.0.21", + "resolved": "https://registry.npmjs.org/@nolyfill/shared/-/shared-1.0.21.tgz", + "integrity": "sha512-qDc/NoaFU23E0hhiDPeUrvWzTXIPE+RbvRQtRWSeHHNmCIgYI9HS1jKzNYNJxv4jvZ/1VmM3L6rNVxbj+LBMNA==", + "dev": true, + "license": "MIT" + }, "node_modules/es-module-lexer": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz", @@ -8661,18 +8885,16 @@ } }, "node_modules/eslint-plugin-import-x": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-import-x/-/eslint-plugin-import-x-4.6.1.tgz", - "integrity": "sha512-wluSUifMIb7UfwWXqx7Yx0lE/SGCcGXECLx/9bCmbY2nneLwvAZ4vkd1IXDjPKFvdcdUgr1BaRnaRpx3k2+Pfw==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import-x/-/eslint-plugin-import-x-4.5.0.tgz", + "integrity": "sha512-l0OTfnPF8RwmSXfjT75N8d6ZYLVrVYWpaGlgvVkVqFERCI5SyBfDP7QEMr3kt0zWi2sOa9EQ47clbdFsHkF83Q==", "dev": true, "license": "MIT", "dependencies": { - "@types/doctrine": "^0.0.9", "@typescript-eslint/scope-manager": "^8.1.0", "@typescript-eslint/utils": "^8.1.0", "debug": "^4.3.4", "doctrine": "^3.0.0", - "enhanced-resolve": "^5.17.1", "eslint-import-resolver-node": "^0.3.9", "get-tsconfig": "^4.7.3", "is-glob": "^4.0.3", @@ -8684,27 +8906,874 @@ "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0" + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-import-x/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/eslint-plugin-import/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-import/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-plugin-jsx-a11y": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", + "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "aria-query": "^5.3.2", + "array-includes": "^3.1.8", + "array.prototype.flatmap": "^1.3.2", + "ast-types-flow": "^0.0.8", + "axe-core": "^4.10.0", + "axobject-query": "^4.1.0", + "damerau-levenshtein": "^1.0.8", + "emoji-regex": "^9.2.2", + "hasown": "^2.0.2", + "jsx-ast-utils": "^3.3.5", + "language-tags": "^1.0.9", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "safe-regex-test": "^1.0.3", + "string.prototype.includes": "^2.0.1" + }, + "engines": { + "node": ">=4.0" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-jsx-a11y/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint-plugin-jsx-a11y/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint-plugin-no-jquery": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-no-jquery/-/eslint-plugin-no-jquery-3.1.0.tgz", + "integrity": "sha512-Ze+eRlAbLAoceBqMXI2E9s6o3dC7zE75niP2Sy4D8I/u1TyLegrIpjc4emPN90dH0IA+uXNUmQbzBuCaihxwIQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.0.0" + } + }, + "node_modules/eslint-plugin-no-only-tests": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-no-only-tests/-/eslint-plugin-no-only-tests-3.3.0.tgz", + "integrity": "sha512-brcKcxGnISN2CcVhXJ/kEQlNa0MEfGRtwKtWA16SkqXHKitaKIMrfemJKLKX1YqDU5C/5JY3PvZXd5jEW04e0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=5.0.0" + } + }, + "node_modules/eslint-plugin-no-use-extend-native": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-no-use-extend-native/-/eslint-plugin-no-use-extend-native-0.5.0.tgz", + "integrity": "sha512-dBNjs8hor8rJgeXLH4HTut5eD3RGWf9JUsadIfuL7UosVQ/dnvOKwxEcRrXrFxrMZ8llUVWT+hOimxJABsAUzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-get-set-prop": "^1.0.0", + "is-js-type": "^2.0.0", + "is-obj-prop": "^1.0.0", + "is-proto-prop": "^2.0.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/eslint-plugin-playwright": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-playwright/-/eslint-plugin-playwright-2.1.0.tgz", + "integrity": "sha512-wMbHOehofSB1cBdzz2CLaCYaKNLeTQ0YnOW+7AHa281TJqlpEJUBgTHbRUYOUxiXphfWwOyTPvgr6vvEmArbSA==", + "dev": true, + "license": "MIT", + "workspaces": [ + "examples" + ], + "dependencies": { + "globals": "^13.23.0" + }, + "engines": { + "node": ">=16.6.0" + }, + "peerDependencies": { + "eslint": ">=8.40.0" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.1.tgz", + "integrity": "sha512-gH3iR3g4JfF+yYPaJYkN7jEl9QbweL/YfkoRlNnuIEHEz1vHVlCmWOS+eGGiRuzHQXdJFCOTxRgvju9b8VUmrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.9.1" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": "*", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.36.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.36.1.tgz", + "integrity": "sha512-/qwbqNXZoq+VP30s1d4Nc1C5GTxjJQjk4Jzs4Wq2qzxFM7dSmuG2UkIjg2USMLh3A/aVcUNrK7v0J5U1XEGGwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.2", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.0.19", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.8", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.0", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.11", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", + "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/eslint-plugin-react/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint-plugin-react/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-react/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint-plugin-react/node_modules/resolve": { + "version": "2.0.0-next.5", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/eslint-plugin-react/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-plugin-regexp": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-regexp/-/eslint-plugin-regexp-2.7.0.tgz", + "integrity": "sha512-U8oZI77SBtH8U3ulZ05iu0qEzIizyEDXd+BWHvyVxTOjGwcDcvy/kEpgFG4DYca2ByRLiVPFZ2GeH7j1pdvZTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.11.0", + "comment-parser": "^1.4.0", + "jsdoc-type-pratt-parser": "^4.0.0", + "refa": "^0.12.1", + "regexp-ast-analysis": "^0.7.1", + "scslre": "^0.3.0" + }, + "engines": { + "node": "^18 || >=20" + }, + "peerDependencies": { + "eslint": ">=8.44.0" + } + }, + "node_modules/eslint-plugin-sonarjs": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-sonarjs/-/eslint-plugin-sonarjs-2.0.4.tgz", + "integrity": "sha512-XVVAB/t0WSgHitHNajIcIDmviCO8kB9VSsrjy+4WUEVM3eieY9SDHEtCDaOMTjj6XMtcAr8BFDXCFaP005s+tg==", + "dev": true, + "license": "LGPL-3.0-only", + "dependencies": { + "@babel/core": "7.25.2", + "@babel/eslint-parser": "7.25.1", + "@babel/plugin-proposal-decorators": "7.24.7", + "@babel/preset-env": "7.25.4", + "@babel/preset-flow": "7.24.7", + "@babel/preset-react": "7.24.7", + "@eslint-community/regexpp": "4.11.1", + "@typescript-eslint/eslint-plugin": "7.16.1", + "@typescript-eslint/utils": "7.16.1", + "builtin-modules": "3.3.0", + "bytes": "3.1.2", + "eslint-plugin-import": "2.30.0", + "eslint-plugin-jsx-a11y": "6.10.0", + "eslint-plugin-react": "7.36.1", + "eslint-plugin-react-hooks": "4.6.2", + "eslint-scope": "8.1.0", + "functional-red-black-tree": "1.0.1", + "jsx-ast-utils": "3.3.5", + "minimatch": "10.0.1", + "scslre": "0.3.0", + "semver": "7.6.3", + "typescript": "5.6.2", + "vue-eslint-parser": "9.4.3" + }, + "peerDependencies": { + "eslint": "^8.0.0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-sonarjs/node_modules/@eslint-community/regexpp": { + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.1.tgz", + "integrity": "sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/eslint-plugin-sonarjs/node_modules/@typescript-eslint/eslint-plugin": { + "version": "7.16.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.16.1.tgz", + "integrity": "sha512-SxdPak/5bO0EnGktV05+Hq8oatjAYVY3Zh2bye9pGZy6+jwyR3LG3YKkV4YatlsgqXP28BTeVm9pqwJM96vf2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "7.16.1", + "@typescript-eslint/type-utils": "7.16.1", + "@typescript-eslint/utils": "7.16.1", + "@typescript-eslint/visitor-keys": "7.16.1", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-sonarjs/node_modules/@typescript-eslint/parser": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.18.0.tgz", + "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/typescript-estree": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-sonarjs/node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz", + "integrity": "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-plugin-sonarjs/node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz", + "integrity": "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-plugin-sonarjs/node_modules/@typescript-eslint/scope-manager": { + "version": "7.16.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.16.1.tgz", + "integrity": "sha512-nYpyv6ALte18gbMz323RM+vpFpTjfNdyakbf3nsLvF43uF9KeNC289SUEW3QLZ1xPtyINJ1dIsZOuWuSRIWygw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "7.16.1", + "@typescript-eslint/visitor-keys": "7.16.1" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-plugin-sonarjs/node_modules/@typescript-eslint/scope-manager/node_modules/@typescript-eslint/types": { + "version": "7.16.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.16.1.tgz", + "integrity": "sha512-AQn9XqCzUXd4bAVEsAXM/Izk11Wx2u4H3BAfQVhSfzfDOm/wAON9nP7J5rpkCxts7E5TELmN845xTUCQrD1xIQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-plugin-sonarjs/node_modules/@typescript-eslint/type-utils": { + "version": "7.16.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.16.1.tgz", + "integrity": "sha512-rbu/H2MWXN4SkjIIyWcmYBjlp55VT+1G3duFOIukTNFxr9PI35pLc2ydwAfejCEitCv4uztA07q0QWanOHC7dA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "7.16.1", + "@typescript-eslint/utils": "7.16.1", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-sonarjs/node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/types": { + "version": "7.16.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.16.1.tgz", + "integrity": "sha512-AQn9XqCzUXd4bAVEsAXM/Izk11Wx2u4H3BAfQVhSfzfDOm/wAON9nP7J5rpkCxts7E5TELmN845xTUCQrD1xIQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-plugin-sonarjs/node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": { + "version": "7.16.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.16.1.tgz", + "integrity": "sha512-0vFPk8tMjj6apaAZ1HlwM8w7jbghC8jc1aRNJG5vN8Ym5miyhTQGMqU++kuBFDNKe9NcPeZ6x0zfSzV8xC1UlQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "7.16.1", + "@typescript-eslint/visitor-keys": "7.16.1", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-sonarjs/node_modules/@typescript-eslint/type-utils/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/eslint-plugin-sonarjs/node_modules/@typescript-eslint/types": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.18.0.tgz", + "integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-plugin-sonarjs/node_modules/@typescript-eslint/typescript-estree": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz", + "integrity": "sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-sonarjs/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/visitor-keys": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz", + "integrity": "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-plugin-sonarjs/node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/eslint-plugin-sonarjs/node_modules/@typescript-eslint/utils": { + "version": "7.16.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.16.1.tgz", + "integrity": "sha512-WrFM8nzCowV0he0RlkotGDujx78xudsxnGMBHI88l5J8wEhED6yBwaSLP99ygfrzAjsQvcYQ94quDwI0d7E1fA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "7.16.1", + "@typescript-eslint/types": "7.16.1", + "@typescript-eslint/typescript-estree": "7.16.1" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + } + }, + "node_modules/eslint-plugin-sonarjs/node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/types": { + "version": "7.16.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.16.1.tgz", + "integrity": "sha512-AQn9XqCzUXd4bAVEsAXM/Izk11Wx2u4H3BAfQVhSfzfDOm/wAON9nP7J5rpkCxts7E5TELmN845xTUCQrD1xIQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-plugin-sonarjs/node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree": { + "version": "7.16.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.16.1.tgz", + "integrity": "sha512-0vFPk8tMjj6apaAZ1HlwM8w7jbghC8jc1aRNJG5vN8Ym5miyhTQGMqU++kuBFDNKe9NcPeZ6x0zfSzV8xC1UlQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "7.16.1", + "@typescript-eslint/visitor-keys": "7.16.1", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-sonarjs/node_modules/@typescript-eslint/utils/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/eslint-plugin-sonarjs/node_modules/@typescript-eslint/visitor-keys": { + "version": "7.16.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.16.1.tgz", + "integrity": "sha512-Qlzzx4sE4u3FsHTPQAAQFJFNOuqtuY0LFrZHwQ8IHK705XxBiWOFkfKRWu6niB7hwfgnwIpO4jTC75ozW1PHWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "7.16.1", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-plugin-sonarjs/node_modules/@typescript-eslint/visitor-keys/node_modules/@typescript-eslint/types": { + "version": "7.16.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.16.1.tgz", + "integrity": "sha512-AQn9XqCzUXd4bAVEsAXM/Izk11Wx2u4H3BAfQVhSfzfDOm/wAON9nP7J5rpkCxts7E5TELmN845xTUCQrD1xIQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-plugin-sonarjs/node_modules/aria-query": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", + "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "deep-equal": "^2.0.5" + } + }, + "node_modules/eslint-plugin-sonarjs/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" } }, - "node_modules/eslint-plugin-import-x/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "node_modules/eslint-plugin-sonarjs/node_modules/eslint-plugin-import": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.30.0.tgz", + "integrity": "sha512-/mHNE9jINJfiD2EKkg1BKyPyUk4zdnT54YgbOgfjSakWT5oyX/qQLVNTkehyfpcMxZXMy1zyonZ2v7hZTX43Yw==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "brace-expansion": "^2.0.1" + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.8", + "array.prototype.findlastindex": "^1.2.5", + "array.prototype.flat": "^1.3.2", + "array.prototype.flatmap": "^1.3.2", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.9.0", + "hasown": "^2.0.2", + "is-core-module": "^2.15.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.0", + "semver": "^6.3.1", + "tsconfig-paths": "^3.15.0" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": ">=4" }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" } }, - "node_modules/eslint-plugin-import/node_modules/brace-expansion": { + "node_modules/eslint-plugin-sonarjs/node_modules/eslint-plugin-import/node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", @@ -8715,7 +9784,7 @@ "concat-map": "0.0.1" } }, - "node_modules/eslint-plugin-import/node_modules/debug": { + "node_modules/eslint-plugin-sonarjs/node_modules/eslint-plugin-import/node_modules/debug": { "version": "3.2.7", "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", @@ -8725,20 +9794,7 @@ "ms": "^2.1.1" } }, - "node_modules/eslint-plugin-import/node_modules/doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/eslint-plugin-import/node_modules/minimatch": { + "node_modules/eslint-plugin-sonarjs/node_modules/eslint-plugin-import/node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", @@ -8751,7 +9807,7 @@ "node": "*" } }, - "node_modules/eslint-plugin-import/node_modules/semver": { + "node_modules/eslint-plugin-sonarjs/node_modules/eslint-plugin-import/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", @@ -8761,14 +9817,14 @@ "semver": "bin/semver.js" } }, - "node_modules/eslint-plugin-jsx-a11y": { - "version": "6.10.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", - "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", + "node_modules/eslint-plugin-sonarjs/node_modules/eslint-plugin-jsx-a11y": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.0.tgz", + "integrity": "sha512-ySOHvXX8eSN6zz8Bywacm7CvGNhUtdjvqfQDVe6020TUK34Cywkw7m0KsCCk1Qtm9G1FayfTN1/7mMYnYO2Bhg==", "dev": true, "license": "MIT", "dependencies": { - "aria-query": "^5.3.2", + "aria-query": "~5.1.3", "array-includes": "^3.1.8", "array.prototype.flatmap": "^1.3.2", "ast-types-flow": "^0.0.8", @@ -8776,13 +9832,14 @@ "axobject-query": "^4.1.0", "damerau-levenshtein": "^1.0.8", "emoji-regex": "^9.2.2", + "es-iterator-helpers": "^1.0.19", "hasown": "^2.0.2", "jsx-ast-utils": "^3.3.5", "language-tags": "^1.0.9", "minimatch": "^3.1.2", "object.fromentries": "^2.0.8", "safe-regex-test": "^1.0.3", - "string.prototype.includes": "^2.0.1" + "string.prototype.includes": "^2.0.0" }, "engines": { "node": ">=4.0" @@ -8791,7 +9848,7 @@ "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" } }, - "node_modules/eslint-plugin-jsx-a11y/node_modules/brace-expansion": { + "node_modules/eslint-plugin-sonarjs/node_modules/eslint-plugin-jsx-a11y/node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", @@ -8802,7 +9859,7 @@ "concat-map": "0.0.1" } }, - "node_modules/eslint-plugin-jsx-a11y/node_modules/minimatch": { + "node_modules/eslint-plugin-sonarjs/node_modules/eslint-plugin-jsx-a11y/node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", @@ -8815,155 +9872,48 @@ "node": "*" } }, - "node_modules/eslint-plugin-no-jquery": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-no-jquery/-/eslint-plugin-no-jquery-3.1.0.tgz", - "integrity": "sha512-Ze+eRlAbLAoceBqMXI2E9s6o3dC7zE75niP2Sy4D8I/u1TyLegrIpjc4emPN90dH0IA+uXNUmQbzBuCaihxwIQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "eslint": ">=8.0.0" - } - }, - "node_modules/eslint-plugin-no-only-tests": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-no-only-tests/-/eslint-plugin-no-only-tests-3.3.0.tgz", - "integrity": "sha512-brcKcxGnISN2CcVhXJ/kEQlNa0MEfGRtwKtWA16SkqXHKitaKIMrfemJKLKX1YqDU5C/5JY3PvZXd5jEW04e0Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=5.0.0" - } - }, - "node_modules/eslint-plugin-no-use-extend-native": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-no-use-extend-native/-/eslint-plugin-no-use-extend-native-0.5.0.tgz", - "integrity": "sha512-dBNjs8hor8rJgeXLH4HTut5eD3RGWf9JUsadIfuL7UosVQ/dnvOKwxEcRrXrFxrMZ8llUVWT+hOimxJABsAUzQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-get-set-prop": "^1.0.0", - "is-js-type": "^2.0.0", - "is-obj-prop": "^1.0.0", - "is-proto-prop": "^2.0.0" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/eslint-plugin-playwright": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-playwright/-/eslint-plugin-playwright-2.1.0.tgz", - "integrity": "sha512-wMbHOehofSB1cBdzz2CLaCYaKNLeTQ0YnOW+7AHa281TJqlpEJUBgTHbRUYOUxiXphfWwOyTPvgr6vvEmArbSA==", - "dev": true, - "license": "MIT", - "workspaces": [ - "examples" - ], - "dependencies": { - "globals": "^13.23.0" - }, - "engines": { - "node": ">=16.6.0" - }, - "peerDependencies": { - "eslint": ">=8.40.0" - } - }, - "node_modules/eslint-plugin-prettier": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.1.tgz", - "integrity": "sha512-gH3iR3g4JfF+yYPaJYkN7jEl9QbweL/YfkoRlNnuIEHEz1vHVlCmWOS+eGGiRuzHQXdJFCOTxRgvju9b8VUmrw==", + "node_modules/eslint-plugin-sonarjs/node_modules/eslint-scope": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.1.0.tgz", + "integrity": "sha512-14dSvlhaVhKKsa9Fx1l8A17s7ah7Ef7wCakJ10LYk6+GYmP9yDti2oq2SEwcyndt6knfcZyhyxwY3i9yL78EQw==", "dev": true, - "license": "MIT", + "license": "BSD-2-Clause", "dependencies": { - "prettier-linter-helpers": "^1.0.0", - "synckit": "^0.9.1" + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" }, "engines": { - "node": "^14.18.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://opencollective.com/eslint-plugin-prettier" - }, - "peerDependencies": { - "@types/eslint": ">=8.0.0", - "eslint": ">=8.0.0", - "eslint-config-prettier": "*", - "prettier": ">=3.0.0" - }, - "peerDependenciesMeta": { - "@types/eslint": { - "optional": true - }, - "eslint-config-prettier": { - "optional": true - } + "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint-plugin-regexp": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-regexp/-/eslint-plugin-regexp-2.7.0.tgz", - "integrity": "sha512-U8oZI77SBtH8U3ulZ05iu0qEzIizyEDXd+BWHvyVxTOjGwcDcvy/kEpgFG4DYca2ByRLiVPFZ2GeH7j1pdvZTA==", + "node_modules/eslint-plugin-sonarjs/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.11.0", - "comment-parser": "^1.4.0", - "jsdoc-type-pratt-parser": "^4.0.0", - "refa": "^0.12.1", - "regexp-ast-analysis": "^0.7.1", - "scslre": "^0.3.0" - }, + "license": "Apache-2.0", "engines": { - "node": "^18 || >=20" - }, - "peerDependencies": { - "eslint": ">=8.44.0" - } - }, - "node_modules/eslint-plugin-sonarjs": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-sonarjs/-/eslint-plugin-sonarjs-3.0.1.tgz", - "integrity": "sha512-RT6VgdPqizbMLmTryIc3fB169hRjvDFlqieSZEEswGtApPb4Dn9BndmN9qyfBV/By0hbseIX8zQWKBz5E7lyiQ==", - "dev": true, - "license": "LGPL-3.0-only", - "dependencies": { - "@babel/core": "7.26.0", - "@babel/eslint-parser": "7.25.9", - "@babel/plugin-proposal-decorators": "7.25.9", - "@babel/preset-env": "7.26.0", - "@babel/preset-flow": "7.25.9", - "@babel/preset-react": "7.26.3", - "@eslint-community/regexpp": "4.12.1", - "builtin-modules": "3.3.0", - "bytes": "3.1.2", - "functional-red-black-tree": "1.0.1", - "jsx-ast-utils": "3.3.5", - "minimatch": "9.0.5", - "scslre": "0.3.0", - "semver": "7.6.3", - "typescript": "^5" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, - "peerDependencies": { - "eslint": "^8.0.0 || ^9.0.0" + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint-plugin-sonarjs/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "node_modules/eslint-plugin-sonarjs/node_modules/typescript": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz", + "integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==", "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" }, "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=14.17" } }, "node_modules/eslint-plugin-unicorn": { @@ -9001,9 +9951,9 @@ } }, "node_modules/eslint-plugin-unicorn/node_modules/globals": { - "version": "15.14.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-15.14.0.tgz", - "integrity": "sha512-OkToC372DtlQeje9/zHIo5CT8lRP/FUgEOKBEhU4e0abL7J7CD24fD9ohiLN5hagG/kWCYj4K5oaxxtj2Z0Dig==", + "version": "15.13.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.13.0.tgz", + "integrity": "sha512-49TewVEz0UxZjr1WYYsWpPrhyC/B/pA8Bq0fUmet2n+eR7yn0IvNzNaoBwnK6mdkzcN+se7Ez9zUgULTz2QH4g==", "dev": true, "license": "MIT", "engines": { @@ -9446,15 +10396,6 @@ "node": ">=0.8.x" } }, - "node_modules/expand-template": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", - "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", - "license": "(MIT OR WTFPL)", - "engines": { - "node": ">=6" - } - }, "node_modules/expect-type": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.1.0.tgz", @@ -9668,12 +10609,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "license": "MIT" - }, "node_modules/fs-extra": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", @@ -9781,12 +10716,6 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, - "node_modules/github-from-package": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", - "license": "MIT" - }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -10503,9 +11432,9 @@ } }, "node_modules/jiti": { - "version": "1.21.7", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", - "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "version": "1.21.6", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz", + "integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==", "license": "MIT", "bin": { "jiti": "bin/jiti.js" @@ -10627,9 +11556,10 @@ "license": "MIT" }, "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, "license": "MIT" }, "node_modules/js-types": { @@ -10675,9 +11605,9 @@ } }, "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", "dev": true, "license": "MIT", "bin": { @@ -10797,9 +11727,9 @@ "license": "MIT" }, "node_modules/katex": { - "version": "0.16.18", - "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.18.tgz", - "integrity": "sha512-LRuk0rPdXrecAFwQucYjMiIs0JFefk6N1q/04mlw14aVIVgxq1FO0MA9RiIIGVaKOB5GIP5GH4aBBNraZERmaQ==", + "version": "0.16.21", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.21.tgz", + "integrity": "sha512-XvqR7FgOHtWupfMiigNzmh+MgUVmDGU2kXZm899ZkPfcuoPuFxyHmXsgATDpFZDAXCI8tvinaVcDo8PIIJSo4A==", "funding": [ "https://opencollective.com/katex", "https://github.com/sponsors/katex" @@ -11008,9 +11938,9 @@ } }, "node_modules/linkifyjs": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.2.0.tgz", - "integrity": "sha512-pCj3PrQyATaoTYKHrgWRF3SJwsm61udVh+vuls/Rl6SptiDhgE7ziUIudAedRY9QEfynmM7/RmLEfPUyw1HPCw==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.3.1.tgz", + "integrity": "sha512-DRSlB9DKVW04c4SUdGvKK5FR6be45lTU9M76JnngqPeeGDqPwYc0zdUErtsNVMtxPXgUWV4HbXbnC4sNyBxkYg==", "license": "MIT" }, "node_modules/loader-runner": { @@ -11169,6 +12099,26 @@ "dev": true, "license": "MIT" }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/loose-envify/node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, "node_modules/loupe": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.2.tgz", @@ -11187,13 +12137,13 @@ } }, "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.2.tgz", + "integrity": "sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA==", "dev": true, "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" + "engines": { + "node": "20 || >=22" } }, "node_modules/magic-string": { @@ -11360,9 +12310,9 @@ } }, "node_modules/mdn-data": { - "version": "2.12.2", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", - "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.1.tgz", + "integrity": "sha512-rsfnCbOHjqrhWxwt5/wtSLzpoKTzW7OXdT5lLOIH1OTYhWu9rRJveGq0sKvDZODABH7RX+uoR+DYcpFnq4Tf6Q==", "dev": true, "license": "CC0-1.0" }, @@ -11470,20 +12420,8 @@ "dependencies": { "mime-db": "1.52.0" }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": ">= 0.6" } }, "node_modules/min-indent": { @@ -11535,6 +12473,7 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -11549,12 +12488,6 @@ "node": ">=16 || 14 >=14.17" } }, - "node_modules/mkdirp-classic": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "license": "MIT" - }, "node_modules/mlly": { "version": "1.7.3", "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.3.tgz", @@ -11568,9 +12501,9 @@ } }, "node_modules/monaco-editor": { - "version": "0.52.2", - "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.52.2.tgz", - "integrity": "sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==", + "version": "0.52.0", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.52.0.tgz", + "integrity": "sha512-OeWhNpABLCeTqubfqLMXGsqf6OmPU6pHM85kF3dhy6kq5hnhuVS1p3VrEW/XhWHc71P2tHyS5JFySD8mgs1crw==", "license": "MIT" }, "node_modules/monaco-editor-webpack-plugin": { @@ -11634,12 +12567,6 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/napi-build-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", - "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", - "license": "MIT" - }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -11673,24 +12600,6 @@ "lodash.topath": "^4.5.2" } }, - "node_modules/node-abi": { - "version": "3.75.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.75.0.tgz", - "integrity": "sha512-OhYaY5sDsIka7H7AtijtI9jwGYLyl29eQn/W623DiN/MIv5sUqc4g7BIDThX+gb7di9f6xK02nkp8sdfFWZLTg==", - "license": "MIT", - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/node-addon-api": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", - "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", - "license": "MIT" - }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -11712,9 +12621,9 @@ } }, "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", "license": "MIT" }, "node_modules/node-sarif-builder": { @@ -11732,9 +12641,9 @@ } }, "node_modules/nolyfill": { - "version": "1.0.43", - "resolved": "https://registry.npmjs.org/nolyfill/-/nolyfill-1.0.43.tgz", - "integrity": "sha512-wi8cCDStYl2cmKOWoF5Jv/0PZXkFcRgKpk9rtxWk+7/VDlI6WDBdPM5nUOsphZjibudajsyAh1+QQvxCyLnbMA==", + "version": "1.0.42", + "resolved": "https://registry.npmjs.org/nolyfill/-/nolyfill-1.0.42.tgz", + "integrity": "sha512-OG++zEsBoEbEmsuF/yjh91dLCTxt8tSJdV7ZzufpszpvG69opWsK9MfAnfjntVVQhreEeBNvwnftI/mCZVBh6w==", "dev": true, "license": "MIT", "bin": { @@ -11846,6 +12755,20 @@ "node": ">=12.4.0" } }, + "node_modules/object.entries": { + "name": "@nolyfill/object.entries", + "version": "1.0.28", + "resolved": "https://registry.npmjs.org/@nolyfill/object.entries/-/object.entries-1.0.28.tgz", + "integrity": "sha512-2t4PayP6Sx7Z20HJjcf8XhhPBO8/H31bwMdP0yEdDcxSXeEhl90Ibb9E3XKzSlcsGf43nXyfabHNrnfvdWE4Ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nolyfill/shared": "1.0.28" + }, + "engines": { + "node": ">=12.4.0" + } + }, "node_modules/object.fromentries": { "name": "@nolyfill/object.fromentries", "version": "1.0.28", @@ -12074,16 +12997,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "11.0.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.2.tgz", - "integrity": "sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA==", - "dev": true, - "license": "ISC", - "engines": { - "node": "20 || >=22" - } - }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -12234,13 +13147,13 @@ } }, "node_modules/playwright": { - "version": "1.49.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.1.tgz", - "integrity": "sha512-VYL8zLoNTBxVOrJBbDuRgDWa3i+mfQgDTrL8Ah9QXZ7ax4Dsj0MSq5bYgytRnDVVe+njoKnfsYkH3HzqVj5UZA==", + "version": "1.49.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.0.tgz", + "integrity": "sha512-eKpmys0UFDnfNb3vfsf8Vx2LEOtflgRebl0Im2eQQnYMA4Aqd+Zw8bEOB+7ZKvN76901mRnqdsiOGKxzVTbi7A==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.49.1" + "playwright-core": "1.49.0" }, "bin": { "playwright": "cli.js" @@ -12253,9 +13166,9 @@ } }, "node_modules/playwright-core": { - "version": "1.49.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.1.tgz", - "integrity": "sha512-BzmpVcs4kE2CH15rWfzpjzVGhWERJfmnXmniSyKeRZUs9Ws65m+RGIi7mjJK/euCegfn3i7jvqWeWyHe9y3Vgg==", + "version": "1.49.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.0.tgz", + "integrity": "sha512-R+3KKTQF3npy5GTiKH/T+kdhoJfJojjHESR1YEWhYuEKRVfVaxH3+4+GvXE5xyCngCxhxnykk0Vlah9v8fs3jA==", "dev": true, "license": "Apache-2.0", "bin": { @@ -12345,13 +13258,6 @@ "node": "^12 || >=14" } }, - "node_modules/postcss-html/node_modules/js-tokens": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", - "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", - "dev": true, - "license": "MIT" - }, "node_modules/postcss-import": { "version": "15.1.0", "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", @@ -12467,9 +13373,9 @@ } }, "node_modules/postcss-modules-local-by-default": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz", - "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.1.0.tgz", + "integrity": "sha512-rm0bdSv4jC3BDma3s9H19ZddW0aHX6EoqwDYU2IfZhRN+53QrufTRo2IdkAbRqLx4R2IYbZnbjKKxg4VN5oU9Q==", "license": "MIT", "dependencies": { "icss-utils": "^5.0.0", @@ -12738,32 +13644,6 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "license": "MIT" }, - "node_modules/prebuild-install": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", - "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", - "license": "MIT", - "dependencies": { - "detect-libc": "^2.0.0", - "expand-template": "^2.0.3", - "github-from-package": "0.0.0", - "minimist": "^1.2.3", - "mkdirp-classic": "^0.5.3", - "napi-build-utils": "^2.0.0", - "node-abi": "^3.3.0", - "pump": "^3.0.0", - "rc": "^1.2.7", - "simple-get": "^4.0.0", - "tar-fs": "^2.0.0", - "tunnel-agent": "^0.6.0" - }, - "bin": { - "prebuild-install": "bin.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -12810,10 +13690,22 @@ "dev": true, "license": "Unlicense" }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, "node_modules/prosemirror-changeset": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.2.1.tgz", - "integrity": "sha512-J7msc6wbxB4ekDFj+n9gTW/jav/p53kdlivvuppHsrZXCaQdVgRghoZbSS3kwrRyAstRVQ4/+u5k7YfLgkkQvQ==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.3.1.tgz", + "integrity": "sha512-j0kORIBm8ayJNl3zQvD1TTPHJX3g042et6y/KQhZhnPrruO8exkTgG8X+NRpj7kIyMMEx74Xb3DyMIBtO0IKkQ==", "license": "MIT", "dependencies": { "prosemirror-transform": "^1.0.0" @@ -12829,9 +13721,9 @@ } }, "node_modules/prosemirror-commands": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.6.2.tgz", - "integrity": "sha512-0nDHH++qcf/BuPLYvmqZTUUsPJUCPBUXt0J1ErTcDIS369CTp773itzLGIgIXG4LJXOlwYCr44+Mh4ii6MP1QA==", + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.7.1.tgz", + "integrity": "sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==", "license": "MIT", "dependencies": { "prosemirror-model": "^1.0.0", @@ -12840,9 +13732,9 @@ } }, "node_modules/prosemirror-dropcursor": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.1.tgz", - "integrity": "sha512-M30WJdJZLyXHi3N8vxN6Zh5O8ZBbQCz0gURTfPmTIBNQ5pxrdU7A58QkNqfa98YEjSAL1HUyyU34f6Pm5xBSGw==", + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.2.tgz", + "integrity": "sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw==", "license": "MIT", "dependencies": { "prosemirror-state": "^1.0.0", @@ -12875,9 +13767,9 @@ } }, "node_modules/prosemirror-inputrules": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.4.0.tgz", - "integrity": "sha512-6ygpPRuTJ2lcOXs9JkefieMst63wVJBgHZGl5QOytN7oSZs3Co/BYbc3Yx9zm9H37Bxw8kVzCnDsihsVsL4yEg==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.5.0.tgz", + "integrity": "sha512-K0xJRCmt+uSw7xesnHmcn72yBGTbY45vm8gXI4LZXbx2Z0jwh5aF9xrGQgrVPu0WbyFVFF3E/o9VhJYz6SQWnA==", "license": "MIT", "dependencies": { "prosemirror-state": "^1.0.0", @@ -12885,9 +13777,9 @@ } }, "node_modules/prosemirror-keymap": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.2.tgz", - "integrity": "sha512-EAlXoksqC6Vbocqc0GtzCruZEzYgrn+iiGnNjsJsH4mrnIGex4qbLdWWNza3AW5W36ZRrlBID0eM6bdKH4OStQ==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.3.tgz", + "integrity": "sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==", "license": "MIT", "dependencies": { "prosemirror-state": "^1.0.0", @@ -12895,20 +13787,20 @@ } }, "node_modules/prosemirror-markdown": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/prosemirror-markdown/-/prosemirror-markdown-1.13.1.tgz", - "integrity": "sha512-Sl+oMfMtAjWtlcZoj/5L/Q39MpEnVZ840Xo330WJWUvgyhNmLBLN7MsHn07s53nG/KImevWHSE6fEj4q/GihHw==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/prosemirror-markdown/-/prosemirror-markdown-1.13.2.tgz", + "integrity": "sha512-FPD9rHPdA9fqzNmIIDhhnYQ6WgNoSWX9StUZ8LEKapaXU9i6XgykaHKhp6XMyXlOWetmaFgGDS/nu/w9/vUc5g==", "license": "MIT", "dependencies": { "@types/markdown-it": "^14.0.0", "markdown-it": "^14.0.0", - "prosemirror-model": "^1.20.0" + "prosemirror-model": "^1.25.0" } }, "node_modules/prosemirror-menu": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/prosemirror-menu/-/prosemirror-menu-1.2.4.tgz", - "integrity": "sha512-S/bXlc0ODQup6aiBbWVsX/eM+xJgCTAfMq/nLqaO5ID/am4wS0tTCIkzwytmao7ypEtjj39i7YbJjAgO20mIqA==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/prosemirror-menu/-/prosemirror-menu-1.2.5.tgz", + "integrity": "sha512-qwXzynnpBIeg1D7BAtjOusR+81xCp53j7iWu/IargiRZqRjGIlQuu1f3jFi+ehrHhWMLoyOQTSRx/IWZJqOYtQ==", "license": "MIT", "dependencies": { "crelt": "^1.0.0", @@ -12918,27 +13810,27 @@ } }, "node_modules/prosemirror-model": { - "version": "1.24.1", - "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.24.1.tgz", - "integrity": "sha512-YM053N+vTThzlWJ/AtPtF1j0ebO36nvbmDy4U7qA2XQB8JVaQp1FmB9Jhrps8s+z+uxhhVTny4m20ptUvhk0Mg==", + "version": "1.25.1", + "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.1.tgz", + "integrity": "sha512-AUvbm7qqmpZa5d9fPKMvH1Q5bqYQvAZWOGRvxsB6iFLyycvC9MwNemNVjHVrWgjaoxAfY8XVg7DbvQ/qxvI9Eg==", "license": "MIT", "dependencies": { "orderedmap": "^2.0.0" } }, "node_modules/prosemirror-schema-basic": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/prosemirror-schema-basic/-/prosemirror-schema-basic-1.2.3.tgz", - "integrity": "sha512-h+H0OQwZVqMon1PNn0AG9cTfx513zgIG2DY00eJ00Yvgb3UD+GQ/VlWW5rcaxacpCGT1Yx8nuhwXk4+QbXUfJA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/prosemirror-schema-basic/-/prosemirror-schema-basic-1.2.4.tgz", + "integrity": "sha512-ELxP4TlX3yr2v5rM7Sb70SqStq5NvI15c0j9j/gjsrO5vaw+fnnpovCLEGIcpeGfifkuqJwl4fon6b+KdrODYQ==", "license": "MIT", "dependencies": { - "prosemirror-model": "^1.19.0" + "prosemirror-model": "^1.25.0" } }, "node_modules/prosemirror-schema-list": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.5.0.tgz", - "integrity": "sha512-gg1tAfH1sqpECdhIHOA/aLg2VH3ROKBWQ4m8Qp9mBKrOxQRW61zc+gMCI8nh22gnBzd1t2u1/NPLmO3nAa3ssg==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.5.1.tgz", + "integrity": "sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==", "license": "MIT", "dependencies": { "prosemirror-model": "^1.0.0", @@ -12958,16 +13850,16 @@ } }, "node_modules/prosemirror-tables": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.6.2.tgz", - "integrity": "sha512-97dKocVLrEVTQjZ4GBLdrrMw7Gv3no8H8yMwf5IRM9OoHrzbWpcH5jJxYgNQIRCtdIqwDctT1HdMHrGTiwp1dQ==", + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.7.1.tgz", + "integrity": "sha512-eRQ97Bf+i9Eby99QbyAiyov43iOKgWa7QCGly+lrDt7efZ1v8NWolhXiB43hSDGIXT1UXgbs4KJN3a06FGpr1Q==", "license": "MIT", "dependencies": { "prosemirror-keymap": "^1.2.2", - "prosemirror-model": "^1.24.1", + "prosemirror-model": "^1.25.0", "prosemirror-state": "^1.4.3", - "prosemirror-transform": "^1.10.2", - "prosemirror-view": "^1.37.1" + "prosemirror-transform": "^1.10.3", + "prosemirror-view": "^1.39.1" } }, "node_modules/prosemirror-trailing-node": { @@ -12986,18 +13878,18 @@ } }, "node_modules/prosemirror-transform": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.10.2.tgz", - "integrity": "sha512-2iUq0wv2iRoJO/zj5mv8uDUriOHWzXRnOTVgCzSXnktS/2iQRa3UUQwVlkBlYZFtygw6Nh1+X4mGqoYBINn5KQ==", + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.10.4.tgz", + "integrity": "sha512-pwDy22nAnGqNR1feOQKHxoFkkUtepoFAd3r2hbEDsnf4wp57kKA36hXsB3njA9FtONBEwSDnDeCiJe+ItD+ykw==", "license": "MIT", "dependencies": { "prosemirror-model": "^1.21.0" } }, "node_modules/prosemirror-view": { - "version": "1.37.1", - "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.37.1.tgz", - "integrity": "sha512-MEAnjOdXU1InxEmhjgmEzQAikaS6lF3hD64MveTPpjOGNTl87iRLA1HupC/DEV6YuK7m4Q9DHFNTjwIVtqz5NA==", + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.40.0.tgz", + "integrity": "sha512-2G3svX0Cr1sJjkD/DYWSe3cfV5VPVTBOxI9XQEGWJDFEpsZb/gh4MV29ctv+OJx2RFX4BLt09i+6zaGM/ldkCw==", "license": "MIT", "dependencies": { "prosemirror-model": "^1.20.0", @@ -13021,16 +13913,6 @@ "node": ">=4" } }, - "node_modules/pump": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", - "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -13078,35 +13960,12 @@ "safe-buffer": "^5.1.0" } }, - "node_modules/rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" - } - }, - "node_modules/rc/node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "license": "ISC" - }, - "node_modules/rc/node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true, + "license": "MIT" }, "node_modules/read-cache": { "version": "1.0.0", @@ -13227,20 +14086,6 @@ "node": ">=8" } }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -13356,19 +14201,6 @@ "node": ">=4" } }, - "node_modules/regexpu-core/node_modules/jsesc": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", - "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/regexpu-core/node_modules/regjsparser": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.12.0.tgz", @@ -13440,12 +14272,12 @@ } }, "node_modules/resolve": { - "version": "1.22.9", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.9.tgz", - "integrity": "sha512-QxrmX1DzraFIi9PxdG5VkRfRwIgjwyud+z/iBwfRRrVmHc+P9Q7u2lSSpQ6bjr2gy5lrqIiU9vb6iAeGf2400A==", + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", "license": "MIT", "dependencies": { - "is-core-module": "^2.16.0", + "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, @@ -13653,9 +14485,9 @@ "license": "ISC" }, "node_modules/schema-utils": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.0.tgz", - "integrity": "sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", + "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", "license": "MIT", "dependencies": { "@types/json-schema": "^7.0.9", @@ -13664,7 +14496,7 @@ "ajv-keywords": "^5.1.0" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 12.13.0" }, "funding": { "type": "opencollective", @@ -13780,26 +14612,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/simple-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/simple-eval": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/simple-eval/-/simple-eval-1.0.1.tgz", @@ -13813,31 +14625,6 @@ "node": ">=12" } }, - "node_modules/simple-get": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", - "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "decompress-response": "^6.0.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - } - }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -14052,15 +14839,6 @@ "dev": true, "license": "MIT" }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -14116,6 +14894,34 @@ "node": ">=12.4.0" } }, + "node_modules/string.prototype.matchall": { + "name": "@nolyfill/string.prototype.matchall", + "version": "1.0.28", + "resolved": "https://registry.npmjs.org/@nolyfill/string.prototype.matchall/-/string.prototype.matchall-1.0.28.tgz", + "integrity": "sha512-k74WKi7WmtRV847QWlY1ndg6XU1loeAyO9+NVoXrd7RL5lEjBtovp4CPZkifipBMBrZrZu2WwrQqkGrvLNZYpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nolyfill/shared": "1.0.28" + }, + "engines": { + "node": ">=12.4.0" + } + }, + "node_modules/string.prototype.repeat": { + "name": "@nolyfill/string.prototype.repeat", + "version": "1.0.28", + "resolved": "https://registry.npmjs.org/@nolyfill/string.prototype.repeat/-/string.prototype.repeat-1.0.28.tgz", + "integrity": "sha512-8ww39xe0r4qki8HwAaXTRamO0KpkHHyYoG+PCOFGaBZ8rrlAKcGQcJhu5aB2axauggqsnUfU25j5snEC0aJvYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nolyfill/shared": "1.0.28" + }, + "engines": { + "node": ">=12.4.0" + } + }, "node_modules/string.prototype.trimend": { "name": "@nolyfill/string.prototype.trimend", "version": "1.0.28", @@ -14199,9 +15005,9 @@ "license": "ISC" }, "node_modules/stylelint": { - "version": "16.12.0", - "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.12.0.tgz", - "integrity": "sha512-F8zZ3L/rBpuoBZRvI4JVT20ZanPLXfQLzMOZg1tzPflRVh9mKpOZ8qcSIhh1my3FjAjZWG4T2POwGnmn6a6hbg==", + "version": "16.11.0", + "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.11.0.tgz", + "integrity": "sha512-zrl4IrKmjJQ+h9FoMp69UMCq5SxeHk0URhxUBj4d3ISzo/DplOFBJZc7t7Dr6otB+1bfbbKNLOmCDpzKSlW+Nw==", "dev": true, "funding": [ { @@ -14251,7 +15057,7 @@ "string-width": "^4.2.3", "supports-hyperlinks": "^3.1.0", "svg-tags": "^1.0.0", - "table": "^6.9.0", + "table": "^6.8.2", "write-file-atomic": "^5.0.1" }, "bin": { @@ -14764,9 +15570,9 @@ } }, "node_modules/tailwindcss": { - "version": "3.4.17", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", - "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", + "version": "3.4.16", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.16.tgz", + "integrity": "sha512-TI4Cyx7gDiZ6r44ewaJmt0o6BrMCT5aK5e0rmJ/G9Xq3w7CX/5VXl/zIPEJZFUK5VEqwByyhqNPycPlvcK4ZNw==", "license": "MIT", "dependencies": { "@alloc/quick-lru": "^5.2.0", @@ -14809,38 +15615,10 @@ "node": ">=6" } }, - "node_modules/tar-fs": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz", - "integrity": "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==", - "license": "MIT", - "dependencies": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.1.4" - } - }, - "node_modules/tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "license": "MIT", - "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/terser": { - "version": "5.37.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.37.0.tgz", - "integrity": "sha512-B8wRRkmre4ERucLM/uXx4MOV5cbnOlVAqUst+1+iLKPI0dOgFO28f84ptoQt9HEI537PMzfYa/d+GEPKTRXmYA==", + "version": "5.36.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.36.0.tgz", + "integrity": "sha512-IYV9eNMuFAV4THUspIRXkLakHnV6XO7FEdtKjf/mDyrnqUg9LnlOn6/RwRvM9SZjR4GUq8Nk8zj67FzVARr74w==", "license": "BSD-2-Clause", "dependencies": { "@jridgewell/source-map": "^0.3.3", @@ -14856,16 +15634,16 @@ } }, "node_modules/terser-webpack-plugin": { - "version": "5.3.11", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.11.tgz", - "integrity": "sha512-RVCsMfuD0+cTt3EwX8hSl2Ks56EbFHWmhluwcqoPKtBnfjiT6olaq7PRIRfhyU8nnC2MrnDrBLfrD/RGE+cVXQ==", + "version": "5.3.10", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz", + "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==", "license": "MIT", "dependencies": { - "@jridgewell/trace-mapping": "^0.3.25", + "@jridgewell/trace-mapping": "^0.3.20", "jest-worker": "^27.4.5", - "schema-utils": "^4.3.0", - "serialize-javascript": "^6.0.2", - "terser": "^5.31.1" + "schema-utils": "^3.1.1", + "serialize-javascript": "^6.0.1", + "terser": "^5.26.0" }, "engines": { "node": ">= 10.13.0" @@ -14889,6 +15667,55 @@ } } }, + "node_modules/terser-webpack-plugin/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/terser-webpack-plugin/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "license": "MIT" + }, + "node_modules/terser-webpack-plugin/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/terser/node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", @@ -15081,18 +15908,6 @@ "dev": true, "license": "0BSD" }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } - }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -15107,9 +15922,9 @@ } }, "node_modules/type-fest": { - "version": "4.30.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.30.2.tgz", - "integrity": "sha512-UJShLPYi1aWqCdq9HycOL/gwsuqda1OISdBO3t8RlXQC4QvtuIz4b5FCfe2dQIWEpmlRExKmcTBfP1r9bhY7ig==", + "version": "4.30.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.30.0.tgz", + "integrity": "sha512-G6zXWS1dLj6eagy6sVhOMQiLtJdxQBHIA9Z6HFUNLOlr6MFOgzV8wvmidtPONfPtEUv0uZsy77XJNzTAfwPDaA==", "dev": true, "license": "(MIT OR CC0-1.0)", "engines": { @@ -15133,9 +15948,9 @@ } }, "node_modules/typo-js": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/typo-js/-/typo-js-1.2.5.tgz", - "integrity": "sha512-F45vFWdGX8xahIk/sOp79z2NJs8ETMYsmMChm9D5Hlx3+9j7VnCyQyvij5MOCrNY3NNe8noSyokRjQRfq+Bc7A==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/typo-js/-/typo-js-1.2.4.tgz", + "integrity": "sha512-Oy/k+tFle5NAA3J/yrrYGfvEnPVrDZ8s8/WCwjUE75k331QyKIsFss7byQ/PzBmXLY6h1moRnZbnaxWBe3I3CA==", "license": "BSD-3-Clause" }, "node_modules/uc.micro": { @@ -15247,9 +16062,9 @@ } }, "node_modules/updates": { - "version": "16.4.1", - "resolved": "https://registry.npmjs.org/updates/-/updates-16.4.1.tgz", - "integrity": "sha512-dYsXzAISSiTFk6mg2ZB7IOlhIN5CO4qINxpbN7rrE9ffpuycq82SZ8rvLSc2QpBEO98BBY7wKm3d8SdA94o5TQ==", + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/updates/-/updates-16.4.0.tgz", + "integrity": "sha512-HtkG1MgXbQ5gpqu5eX4qVOwclMHbygiTYjSkFMVDEXuwb5clwkDh75xRb11PzRX9ozVOfcVUHl7lpBNDPquXrw==", "dev": true, "license": "BSD-2-Clause", "bin": { @@ -15444,9 +16259,9 @@ } }, "node_modules/vite/node_modules/rollup": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.28.1.tgz", - "integrity": "sha512-61fXYl/qNVinKmGSTHAZ6Yy8I3YIJC/r2m9feHo6SwVAVcLT5MPwOUFe7EuURA/4m0NR8lXG4BBXuo/IZEsjMg==", + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.28.0.tgz", + "integrity": "sha512-G9GOrmgWHBma4YfCcX8PjH0qhXSdH8B4HDE2o4/jaxj93S4DPCIDoLcXz99eWMji4hB29UFCEd7B2gwGJDR9cQ==", "dev": true, "license": "MIT", "dependencies": { @@ -15460,25 +16275,24 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.28.1", - "@rollup/rollup-android-arm64": "4.28.1", - "@rollup/rollup-darwin-arm64": "4.28.1", - "@rollup/rollup-darwin-x64": "4.28.1", - "@rollup/rollup-freebsd-arm64": "4.28.1", - "@rollup/rollup-freebsd-x64": "4.28.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.28.1", - "@rollup/rollup-linux-arm-musleabihf": "4.28.1", - "@rollup/rollup-linux-arm64-gnu": "4.28.1", - "@rollup/rollup-linux-arm64-musl": "4.28.1", - "@rollup/rollup-linux-loongarch64-gnu": "4.28.1", - "@rollup/rollup-linux-powerpc64le-gnu": "4.28.1", - "@rollup/rollup-linux-riscv64-gnu": "4.28.1", - "@rollup/rollup-linux-s390x-gnu": "4.28.1", - "@rollup/rollup-linux-x64-gnu": "4.28.1", - "@rollup/rollup-linux-x64-musl": "4.28.1", - "@rollup/rollup-win32-arm64-msvc": "4.28.1", - "@rollup/rollup-win32-ia32-msvc": "4.28.1", - "@rollup/rollup-win32-x64-msvc": "4.28.1", + "@rollup/rollup-android-arm-eabi": "4.28.0", + "@rollup/rollup-android-arm64": "4.28.0", + "@rollup/rollup-darwin-arm64": "4.28.0", + "@rollup/rollup-darwin-x64": "4.28.0", + "@rollup/rollup-freebsd-arm64": "4.28.0", + "@rollup/rollup-freebsd-x64": "4.28.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.28.0", + "@rollup/rollup-linux-arm-musleabihf": "4.28.0", + "@rollup/rollup-linux-arm64-gnu": "4.28.0", + "@rollup/rollup-linux-arm64-musl": "4.28.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.28.0", + "@rollup/rollup-linux-riscv64-gnu": "4.28.0", + "@rollup/rollup-linux-s390x-gnu": "4.28.0", + "@rollup/rollup-linux-x64-gnu": "4.28.0", + "@rollup/rollup-linux-x64-musl": "4.28.0", + "@rollup/rollup-win32-arm64-msvc": "4.28.0", + "@rollup/rollup-win32-ia32-msvc": "4.28.0", + "@rollup/rollup-win32-x64-msvc": "4.28.0", "fsevents": "~2.3.2" } }, @@ -15549,9 +16363,9 @@ } }, "node_modules/vitest/node_modules/magic-string": { - "version": "0.30.17", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", - "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "version": "0.30.14", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.14.tgz", + "integrity": "sha512-5c99P1WKTed11ZC0HMJOj6CDIue6F8ySu+bJL+85q1zBEIY8IklrJ1eiKC2NDRh3Ct3FcvmJPyQHb9erXMTJNw==", "dev": true, "license": "MIT", "dependencies": { @@ -15755,9 +16569,9 @@ } }, "node_modules/webpack": { - "version": "5.97.1", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.97.1.tgz", - "integrity": "sha512-EksG6gFY3L1eFMROS/7Wzgrii5mBAFe4rIr3r2BTfo7bcc+DWwFZ4OJ/miOuHJO/A85HwyI4eQ0F6IKXesO7Fg==", + "version": "5.97.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.97.0.tgz", + "integrity": "sha512-CWT8v7ShSfj7tGs4TLRtaOLmOCPWhoKEvp+eA7FVx8Xrjb3XfT0aXdxDItnRZmE8sHcH+a8ayDrJCOjXKxVFfQ==", "license": "MIT", "dependencies": { "@types/eslint-scope": "^3.7.7", diff --git a/package.json b/package.json index 14e8038e7e1d5..85b8c009b3ea2 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "@github/relative-time-element": "4.4.4", "@github/text-expander-element": "2.8.0", "@mcaptcha/vanilla-glue": "0.1.0-alpha-3", - "@primer/octicons": "19.14.0", + "@primer/octicons": "19.13.0", "@silverwind/vue3-calendar-heatmap": "2.0.6", "@tiptap/core": "2.11.0", "@tiptap/extension-link": "2.11.0", @@ -27,7 +27,6 @@ "add-asset-webpack-plugin": "3.0.0", "ansi_up": "6.0.2", "asciinema-player": "3.8.1", - "canvas": "3.1.0", "chart.js": "4.4.7", "chartjs-adapter-dayjs-4": "1.0.4", "chartjs-plugin-zoom": "2.2.0", @@ -43,13 +42,13 @@ "htmx.org": "2.0.4", "idiomorph": "0.3.0", "jquery": "3.7.1", + "katex": "0.16.21", "js-beautify": "1.15.1", - "katex": "0.16.18", "license-checker-webpack-plugin": "0.2.1", "mermaid": "11.4.1", "mini-css-extract-plugin": "2.9.2", "minimatch": "10.0.1", - "monaco-editor": "0.52.2", + "monaco-editor": "0.52.0", "monaco-editor-webpack-plugin": "7.1.0", "pdfobject": "2.3.0", "perfect-debounce": "1.0.0", @@ -58,7 +57,7 @@ "postcss-nesting": "13.0.1", "sortablejs": "1.15.6", "swagger-ui-dist": "5.18.2", - "tailwindcss": "3.4.17", + "tailwindcss": "3.4.16", "throttle-debounce": "5.0.2", "tinycolor2": "1.6.0", "tippy.js": "6.3.7", @@ -71,16 +70,16 @@ "vue-bar-graph": "2.2.0", "vue-chartjs": "5.3.2", "vue-loader": "17.4.2", - "webpack": "5.97.1", + "webpack": "5.97.0", "webpack-cli": "5.1.4", "wrap-ansi": "9.0.0" }, "devDependencies": { "@eslint-community/eslint-plugin-eslint-comments": "4.4.1", - "@playwright/test": "1.49.1", + "@playwright/test": "1.49.0", "@silverwind/vue-tsc": "2.1.13", "@stoplight/spectral-cli": "6.14.2", - "@stylistic/eslint-plugin-js": "2.12.1", + "@stylistic/eslint-plugin-js": "2.11.0", "@stylistic/stylelint-plugin": "3.1.1", "@types/dropzone": "5.7.9", "@types/jquery": "3.5.32", @@ -92,19 +91,19 @@ "@types/throttle-debounce": "5.0.2", "@types/tinycolor2": "1.4.6", "@types/toastify-js": "1.12.3", - "@typescript-eslint/eslint-plugin": "8.18.1", - "@typescript-eslint/parser": "8.18.1", + "@typescript-eslint/eslint-plugin": "8.17.0", + "@typescript-eslint/parser": "8.17.0", "@vitejs/plugin-vue": "5.2.1", "eslint": "8.57.0", "eslint-import-resolver-typescript": "3.7.0", "eslint-plugin-array-func": "4.0.0", "eslint-plugin-github": "5.0.2", - "eslint-plugin-import-x": "4.6.1", + "eslint-plugin-import-x": "4.5.0", "eslint-plugin-no-jquery": "3.1.0", "eslint-plugin-no-use-extend-native": "0.5.0", "eslint-plugin-playwright": "2.1.0", "eslint-plugin-regexp": "2.7.0", - "eslint-plugin-sonarjs": "3.0.1", + "eslint-plugin-sonarjs": "2.0.4", "eslint-plugin-unicorn": "56.0.1", "eslint-plugin-vitest": "0.4.1", "eslint-plugin-vitest-globals": "1.5.0", @@ -113,15 +112,15 @@ "eslint-plugin-wc": "2.2.0", "happy-dom": "15.11.7", "markdownlint-cli": "0.43.0", - "nolyfill": "1.0.43", + "nolyfill": "1.0.42", "postcss-html": "1.7.0", - "stylelint": "16.12.0", + "stylelint": "16.11.0", "stylelint-declaration-block-no-ignored-properties": "2.8.0", "stylelint-declaration-strict-value": "1.10.6", "stylelint-value-no-unknown-custom-properties": "6.0.1", "svgo": "3.3.2", - "type-fest": "4.30.2", - "updates": "16.4.1", + "type-fest": "4.30.0", + "updates": "16.4.0", "vite-string-plugin": "1.3.4", "vitest": "2.1.8" }, diff --git a/routers/api/actions/artifacts.go b/routers/api/actions/artifacts.go index 910edd6d5895d..0a7f92ac40a7b 100644 --- a/routers/api/actions/artifacts.go +++ b/routers/api/actions/artifacts.go @@ -126,10 +126,11 @@ func ArtifactsRoutes(prefix string) *web.Router { func ArtifactContexter() func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { - base := context.NewBaseContext(resp, req) + base, baseCleanUp := context.NewBaseContext(resp, req) + defer baseCleanUp() ctx := &ArtifactContext{Base: base} - ctx.SetContextValue(artifactContextKey, ctx) + ctx.AppendContextValue(artifactContextKey, ctx) // action task call server api with Bearer ACTIONS_RUNTIME_TOKEN // we should verify the ACTIONS_RUNTIME_TOKEN diff --git a/routers/api/actions/artifactsv4.go b/routers/api/actions/artifactsv4.go index 8917a7a8a2320..6dd36888d2429 100644 --- a/routers/api/actions/artifactsv4.go +++ b/routers/api/actions/artifactsv4.go @@ -126,9 +126,12 @@ type artifactV4Routes struct { func ArtifactV4Contexter() func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { - base := context.NewBaseContext(resp, req) + base, baseCleanUp := context.NewBaseContext(resp, req) + defer baseCleanUp() + ctx := &ArtifactContext{Base: base} - ctx.SetContextValue(artifactContextKey, ctx) + ctx.AppendContextValue(artifactContextKey, ctx) + next.ServeHTTP(ctx.Resp, ctx.Req) }) } diff --git a/routers/api/actions/runner/runner.go b/routers/api/actions/runner/runner.go index c55b30f7ebb78..2435807994f90 100644 --- a/routers/api/actions/runner/runner.go +++ b/routers/api/actions/runner/runner.go @@ -69,7 +69,7 @@ func (s *Service) Register( labels := req.Msg.Labels // create new runner - name := util.EllipsisDisplayString(req.Msg.Name, 255) + name, _ := util.SplitStringAtByteN(req.Msg.Name, 255) runner := &actions_model.ActionRunner{ UUID: gouuid.New().String(), Name: name, @@ -156,7 +156,7 @@ func (s *Service) FetchTask( // if the task version in request is not equal to the version in db, // it means there may still be some tasks not be assgined. // try to pick a task for the runner that send the request. - if t, ok, err := pickTask(ctx, runner); err != nil { + if t, ok, err := actions_service.PickTask(ctx, runner); err != nil { log.Error("pick task failed: %v", err) return nil, status.Errorf(codes.Internal, "pick task: %v", err) } else if ok { diff --git a/routers/api/packages/api.go b/routers/api/packages/api.go index 8c06836ff80c2..47ea7137b8250 100644 --- a/routers/api/packages/api.go +++ b/routers/api/packages/api.go @@ -138,10 +138,45 @@ func CommonRoutes() *web.Router { }, reqPackageAccess(perm.AccessModeRead)) r.Group("/arch", func() { r.Methods("HEAD,GET", "/repository.key", arch.GetRepositoryKey) - r.PathGroup("*", func(g *web.RouterPathGroup) { - g.MatchPath("PUT", "/", reqPackageAccess(perm.AccessModeWrite), arch.UploadPackageFile) - g.MatchPath("HEAD,GET", "///", arch.GetPackageOrRepositoryFile) - g.MatchPath("DELETE", "////", reqPackageAccess(perm.AccessModeWrite), arch.DeletePackageVersion) + + r.Methods("HEAD,GET,PUT,DELETE", "*", func(ctx *context.Context) { + path := strings.Trim(ctx.PathParam("*"), "/") + + if ctx.Req.Method == "PUT" { + reqPackageAccess(perm.AccessModeWrite)(ctx) + if ctx.Written() { + return + } + ctx.SetPathParam("repository", path) + arch.UploadPackageFile(ctx) + return + } + + pathFields := strings.Split(path, "/") + pathFieldsLen := len(pathFields) + + if (ctx.Req.Method == "HEAD" || ctx.Req.Method == "GET") && pathFieldsLen >= 2 { + ctx.SetPathParam("repository", strings.Join(pathFields[:pathFieldsLen-2], "/")) + ctx.SetPathParam("architecture", pathFields[pathFieldsLen-2]) + ctx.SetPathParam("filename", pathFields[pathFieldsLen-1]) + arch.GetPackageOrRepositoryFile(ctx) + return + } + + if ctx.Req.Method == "DELETE" && pathFieldsLen >= 3 { + reqPackageAccess(perm.AccessModeWrite)(ctx) + if ctx.Written() { + return + } + ctx.SetPathParam("repository", strings.Join(pathFields[:pathFieldsLen-3], "/")) + ctx.SetPathParam("name", pathFields[pathFieldsLen-3]) + ctx.SetPathParam("version", pathFields[pathFieldsLen-2]) + ctx.SetPathParam("architecture", pathFields[pathFieldsLen-1]) + arch.DeletePackageVersion(ctx) + return + } + + ctx.Status(http.StatusNotFound) }) }, reqPackageAccess(perm.AccessModeRead)) r.Group("/cargo", func() { @@ -430,8 +465,6 @@ func CommonRoutes() *web.Router { r.Post("/api/charts", reqPackageAccess(perm.AccessModeWrite), helm.UploadPackage) }, reqPackageAccess(perm.AccessModeRead)) r.Group("/maven", func() { - // FIXME: this path design is not right. - // It should be `/.../{groupId}/{artifactId}/{version}`, but not `/.../{groupId}-{artifactId}/{version}` r.Put("/*", reqPackageAccess(perm.AccessModeWrite), maven.UploadPackageFile) r.Get("/*", maven.DownloadPackageFile) r.Head("/*", maven.ProvidePackageFileHeader) diff --git a/routers/api/packages/cargo/cargo.go b/routers/api/packages/cargo/cargo.go index 3d8407e6b6000..c1755dc1e84d5 100644 --- a/routers/api/packages/cargo/cargo.go +++ b/routers/api/packages/cargo/cargo.go @@ -51,7 +51,7 @@ func apiError(ctx *context.Context, status int, obj any) { // https://rust-lang.github.io/rfcs/2789-sparse-index.html func RepositoryConfig(ctx *context.Context) { - ctx.JSON(http.StatusOK, cargo_service.BuildConfig(ctx.Package.Owner, setting.Service.RequireSignInView || ctx.Package.Owner.Visibility != structs.VisibleTypePublic)) + ctx.JSON(http.StatusOK, cargo_service.BuildConfig(ctx.Package.Owner, setting.Service.RequireSignInViewStrict || ctx.Package.Owner.Visibility != structs.VisibleTypePublic)) } func EnumeratePackageVersions(ctx *context.Context) { diff --git a/routers/api/packages/container/blob.go b/routers/api/packages/container/blob.go index 4595b9a33deb4..671803788aa34 100644 --- a/routers/api/packages/container/blob.go +++ b/routers/api/packages/container/blob.go @@ -111,12 +111,11 @@ func getOrCreateUploadVersion(ctx context.Context, pi *packages_service.PackageI } var err error if p, err = packages_model.TryInsertPackage(ctx, p); err != nil { - if err == packages_model.ErrDuplicatePackage { - created = false - } else { + if !errors.Is(err, packages_model.ErrDuplicatePackage) { log.Error("Error inserting package: %v", err) return err } + created = false } if created { @@ -135,7 +134,7 @@ func getOrCreateUploadVersion(ctx context.Context, pi *packages_service.PackageI MetadataJSON: "null", } if pv, err = packages_model.GetOrInsertVersion(ctx, pv); err != nil { - if err != packages_model.ErrDuplicatePackageVersion { + if !errors.Is(err, packages_model.ErrDuplicatePackageVersion) { log.Error("Error inserting package: %v", err) return err } @@ -161,7 +160,7 @@ func createFileForBlob(ctx context.Context, pv *packages_model.PackageVersion, p } var err error if pf, err = packages_model.TryInsertFile(ctx, pf); err != nil { - if err == packages_model.ErrDuplicatePackageFile { + if errors.Is(err, packages_model.ErrDuplicatePackageFile) { return nil } log.Error("Error inserting package file: %v", err) diff --git a/routers/api/packages/container/container.go b/routers/api/packages/container/container.go index 3a470ad68523a..6ef1655235617 100644 --- a/routers/api/packages/container/container.go +++ b/routers/api/packages/container/container.go @@ -126,7 +126,7 @@ func apiUnauthorizedError(ctx *context.Context) { // ReqContainerAccess is a middleware which checks the current user valid (real user or ghost if anonymous access is enabled) func ReqContainerAccess(ctx *context.Context) { - if ctx.Doer == nil || (setting.Service.RequireSignInView && ctx.Doer.IsGhost()) { + if ctx.Doer == nil || (setting.Service.RequireSignInViewStrict && ctx.Doer.IsGhost()) { apiUnauthorizedError(ctx) } } @@ -152,7 +152,7 @@ func Authenticate(ctx *context.Context) { u := ctx.Doer packageScope := auth_service.GetAccessScope(ctx.Data) if u == nil { - if setting.Service.RequireSignInView { + if setting.Service.RequireSignInViewStrict { apiUnauthorizedError(ctx) return } @@ -324,7 +324,7 @@ func GetUploadBlob(ctx *context.Context) { upload, err := packages_model.GetBlobUploadByID(ctx, uuid) if err != nil { - if err == packages_model.ErrPackageBlobUploadNotExist { + if errors.Is(err, packages_model.ErrPackageBlobUploadNotExist) { apiErrorDefined(ctx, errBlobUploadUnknown) } else { apiError(ctx, http.StatusInternalServerError, err) @@ -345,7 +345,7 @@ func UploadBlob(ctx *context.Context) { uploader, err := container_service.NewBlobUploader(ctx, ctx.PathParam("uuid")) if err != nil { - if err == packages_model.ErrPackageBlobUploadNotExist { + if errors.Is(err, packages_model.ErrPackageBlobUploadNotExist) { apiErrorDefined(ctx, errBlobUploadUnknown) } else { apiError(ctx, http.StatusInternalServerError, err) @@ -396,7 +396,7 @@ func EndUploadBlob(ctx *context.Context) { uploader, err := container_service.NewBlobUploader(ctx, ctx.PathParam("uuid")) if err != nil { - if err == packages_model.ErrPackageBlobUploadNotExist { + if errors.Is(err, packages_model.ErrPackageBlobUploadNotExist) { apiErrorDefined(ctx, errBlobUploadUnknown) } else { apiError(ctx, http.StatusInternalServerError, err) @@ -465,7 +465,7 @@ func CancelUploadBlob(ctx *context.Context) { _, err := packages_model.GetBlobUploadByID(ctx, uuid) if err != nil { - if err == packages_model.ErrPackageBlobUploadNotExist { + if errors.Is(err, packages_model.ErrPackageBlobUploadNotExist) { apiErrorDefined(ctx, errBlobUploadUnknown) } else { apiError(ctx, http.StatusInternalServerError, err) @@ -501,7 +501,7 @@ func getBlobFromContext(ctx *context.Context) (*packages_model.PackageFileDescri func HeadBlob(ctx *context.Context) { blob, err := getBlobFromContext(ctx) if err != nil { - if err == container_model.ErrContainerBlobNotExist { + if errors.Is(err, container_model.ErrContainerBlobNotExist) { apiErrorDefined(ctx, errBlobUnknown) } else { apiError(ctx, http.StatusInternalServerError, err) @@ -520,7 +520,7 @@ func HeadBlob(ctx *context.Context) { func GetBlob(ctx *context.Context) { blob, err := getBlobFromContext(ctx) if err != nil { - if err == container_model.ErrContainerBlobNotExist { + if errors.Is(err, container_model.ErrContainerBlobNotExist) { apiErrorDefined(ctx, errBlobUnknown) } else { apiError(ctx, http.StatusInternalServerError, err) @@ -639,7 +639,7 @@ func getManifestFromContext(ctx *context.Context) (*packages_model.PackageFileDe func HeadManifest(ctx *context.Context) { manifest, err := getManifestFromContext(ctx) if err != nil { - if err == container_model.ErrContainerBlobNotExist { + if errors.Is(err, container_model.ErrContainerBlobNotExist) { apiErrorDefined(ctx, errManifestUnknown) } else { apiError(ctx, http.StatusInternalServerError, err) @@ -659,7 +659,7 @@ func HeadManifest(ctx *context.Context) { func GetManifest(ctx *context.Context) { manifest, err := getManifestFromContext(ctx) if err != nil { - if err == container_model.ErrContainerBlobNotExist { + if errors.Is(err, container_model.ErrContainerBlobNotExist) { apiErrorDefined(ctx, errManifestUnknown) } else { apiError(ctx, http.StatusInternalServerError, err) @@ -739,7 +739,7 @@ func GetTagList(ctx *context.Context) { image := ctx.PathParam("image") if _, err := packages_model.GetPackageByName(ctx, ctx.Package.Owner.ID, packages_model.TypeContainer, image); err != nil { - if err == packages_model.ErrPackageNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) { apiErrorDefined(ctx, errNameUnknown) } else { apiError(ctx, http.StatusInternalServerError, err) diff --git a/routers/api/packages/container/manifest.go b/routers/api/packages/container/manifest.go index 4a79a58f51f09..ad035cf473e7c 100644 --- a/routers/api/packages/container/manifest.go +++ b/routers/api/packages/container/manifest.go @@ -240,7 +240,7 @@ func processImageManifestIndex(ctx context.Context, mci *manifestCreationInfo, b IsManifest: true, }) if err != nil { - if err == container_model.ErrContainerBlobNotExist { + if errors.Is(err, container_model.ErrContainerBlobNotExist) { return errManifestBlobUnknown } return err @@ -321,12 +321,11 @@ func createPackageAndVersion(ctx context.Context, mci *manifestCreationInfo, met } var err error if p, err = packages_model.TryInsertPackage(ctx, p); err != nil { - if err == packages_model.ErrDuplicatePackage { - created = false - } else { + if !errors.Is(err, packages_model.ErrDuplicatePackage) { log.Error("Error inserting package: %v", err) return nil, err } + created = false } if created { @@ -352,21 +351,23 @@ func createPackageAndVersion(ctx context.Context, mci *manifestCreationInfo, met } var pv *packages_model.PackageVersion if pv, err = packages_model.GetOrInsertVersion(ctx, _pv); err != nil { - if err == packages_model.ErrDuplicatePackageVersion { - if err := packages_service.DeletePackageVersionAndReferences(ctx, pv); err != nil { - return nil, err - } + if !errors.Is(err, packages_model.ErrDuplicatePackageVersion) { + log.Error("Error inserting package: %v", err) + return nil, err + } - // keep download count on overwrite - _pv.DownloadCount = pv.DownloadCount + if err = packages_service.DeletePackageVersionAndReferences(ctx, pv); err != nil { + return nil, err + } + + // keep download count on overwrite + _pv.DownloadCount = pv.DownloadCount - if pv, err = packages_model.GetOrInsertVersion(ctx, _pv); err != nil { + if pv, err = packages_model.GetOrInsertVersion(ctx, _pv); err != nil { + if !errors.Is(err, packages_model.ErrDuplicatePackageVersion) { log.Error("Error inserting package: %v", err) return nil, err } - } else { - log.Error("Error inserting package: %v", err) - return nil, err } } @@ -417,7 +418,7 @@ func createFileFromBlobReference(ctx context.Context, pv, uploadVersion *package } var err error if pf, err = packages_model.TryInsertFile(ctx, pf); err != nil { - if err == packages_model.ErrDuplicatePackageFile { + if errors.Is(err, packages_model.ErrDuplicatePackageFile) { // Skip this blob because the manifest contains the same filesystem layer multiple times. return nil } diff --git a/routers/api/packages/maven/api.go b/routers/api/packages/maven/api.go index 167fe42b56d51..ec6b9cfb0e05b 100644 --- a/routers/api/packages/maven/api.go +++ b/routers/api/packages/maven/api.go @@ -8,7 +8,6 @@ import ( "strings" packages_model "code.gitea.io/gitea/models/packages" - maven_module "code.gitea.io/gitea/modules/packages/maven" ) // MetadataResponse https://maven.apache.org/ref/3.2.5/maven-repository-metadata/repository-metadata.html @@ -22,7 +21,7 @@ type MetadataResponse struct { } // pds is expected to be sorted ascending by CreatedUnix -func createMetadataResponse(pds []*packages_model.PackageDescriptor) *MetadataResponse { +func createMetadataResponse(pds []*packages_model.PackageDescriptor, groupID, artifactID string) *MetadataResponse { var release *packages_model.PackageDescriptor versions := make([]string, 0, len(pds)) @@ -35,11 +34,9 @@ func createMetadataResponse(pds []*packages_model.PackageDescriptor) *MetadataRe latest := pds[len(pds)-1] - metadata := latest.Metadata.(*maven_module.Metadata) - resp := &MetadataResponse{ - GroupID: metadata.GroupID, - ArtifactID: metadata.ArtifactID, + GroupID: groupID, + ArtifactID: artifactID, Latest: latest.Version.Version, Version: versions, } diff --git a/routers/api/packages/maven/maven.go b/routers/api/packages/maven/maven.go index 9474b17bc79d1..9089c2eccfa8f 100644 --- a/routers/api/packages/maven/maven.go +++ b/routers/api/packages/maven/maven.go @@ -13,7 +13,7 @@ import ( "errors" "io" "net/http" - "path/filepath" + "path" "regexp" "sort" "strconv" @@ -25,6 +25,7 @@ import ( "code.gitea.io/gitea/modules/log" packages_module "code.gitea.io/gitea/modules/packages" maven_module "code.gitea.io/gitea/modules/packages/maven" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/routers/api/packages/helper" "code.gitea.io/gitea/services/context" packages_service "code.gitea.io/gitea/services/packages" @@ -44,7 +45,7 @@ const ( var ( errInvalidParameters = errors.New("request parameters are invalid") - illegalCharacters = regexp.MustCompile(`[\\/:"<>|?\*]`) + illegalCharacters = regexp.MustCompile(`[\\/:"<>|?*]`) ) func apiError(ctx *context.Context, status int, obj any) { @@ -83,14 +84,20 @@ func handlePackageFile(ctx *context.Context, serveContent bool) { } func serveMavenMetadata(ctx *context.Context, params parameters) { - // /com/foo/project/maven-metadata.xml[.md5/.sha1/.sha256/.sha512] - - packageName := params.GroupID + "-" + params.ArtifactID - pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeMaven, packageName) + // path pattern: /com/foo/project/maven-metadata.xml[.md5/.sha1/.sha256/.sha512] + // in case there are legacy package names ("GroupID-ArtifactID") we need to check both, new packages always use ":" as separator("GroupID:ArtifactID") + pvsLegacy, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeMaven, params.toInternalPackageNameLegacy()) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeMaven, params.toInternalPackageName()) if err != nil { apiError(ctx, http.StatusInternalServerError, err) return } + pvs = append(pvsLegacy, pvs...) + if len(pvs) == 0 { apiError(ctx, http.StatusNotFound, packages_model.ErrPackageNotExist) return @@ -107,7 +114,7 @@ func serveMavenMetadata(ctx *context.Context, params parameters) { return pds[i].Version.CreatedUnix < pds[j].Version.CreatedUnix }) - xmlMetadata, err := xml.Marshal(createMetadataResponse(pds)) + xmlMetadata, err := xml.Marshal(createMetadataResponse(pds, params.GroupID, params.ArtifactID)) if err != nil { apiError(ctx, http.StatusInternalServerError, err) return @@ -116,10 +123,10 @@ func serveMavenMetadata(ctx *context.Context, params parameters) { latest := pds[len(pds)-1] // http.TimeFormat required a UTC time, refer to https://pkg.go.dev/net/http#TimeFormat - lastModifed := latest.Version.CreatedUnix.AsTime().UTC().Format(http.TimeFormat) - ctx.Resp.Header().Set("Last-Modified", lastModifed) + lastModified := latest.Version.CreatedUnix.AsTime().UTC().Format(http.TimeFormat) + ctx.Resp.Header().Set("Last-Modified", lastModified) - ext := strings.ToLower(filepath.Ext(params.Filename)) + ext := strings.ToLower(path.Ext(params.Filename)) if isChecksumExtension(ext) { var hash []byte switch ext { @@ -147,11 +154,12 @@ func serveMavenMetadata(ctx *context.Context, params parameters) { } func servePackageFile(ctx *context.Context, params parameters, serveContent bool) { - packageName := params.GroupID + "-" + params.ArtifactID - - pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeMaven, packageName, params.Version) + pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeMaven, params.toInternalPackageName(), params.Version) + if errors.Is(err, util.ErrNotExist) { + pv, err = packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeMaven, params.toInternalPackageNameLegacy(), params.Version) + } if err != nil { - if err == packages_model.ErrPackageNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) { apiError(ctx, http.StatusNotFound, err) } else { apiError(ctx, http.StatusInternalServerError, err) @@ -161,14 +169,14 @@ func servePackageFile(ctx *context.Context, params parameters, serveContent bool filename := params.Filename - ext := strings.ToLower(filepath.Ext(filename)) + ext := strings.ToLower(path.Ext(filename)) if isChecksumExtension(ext) { filename = filename[:len(filename)-len(ext)] } pf, err := packages_model.GetFileForVersionByName(ctx, pv.ID, filename, packages_model.EmptyFileKey) if err != nil { - if err == packages_model.ErrPackageFileNotExist { + if errors.Is(err, packages_model.ErrPackageFileNotExist) { apiError(ctx, http.StatusNotFound, err) } else { apiError(ctx, http.StatusInternalServerError, err) @@ -238,15 +246,17 @@ func UploadPackageFile(ctx *context.Context) { return } - log.Trace("Parameters: %+v", params) - // Ignore the package index //maven-metadata.xml if params.IsMeta && params.Version == "" { ctx.Status(http.StatusOK) return } - packageName := params.GroupID + "-" + params.ArtifactID + packageName := params.toInternalPackageName() + if ctx.FormBool("use_legacy_package_name") { + // for testing purpose only + packageName = params.toInternalPackageNameLegacy() + } // for the same package, only one upload at a time releaser, err := globallock.Lock(ctx, mavenPkgNameKey(packageName)) @@ -274,13 +284,26 @@ func UploadPackageFile(ctx *context.Context) { Creator: ctx.Doer, } - ext := filepath.Ext(params.Filename) + // old maven package uses "groupId-artifactId" as package name, so we need to update to the new format "groupId:artifactId" + legacyPackage, err := packages_model.GetPackageByName(ctx, ctx.Package.Owner.ID, packages_model.TypeMaven, params.toInternalPackageNameLegacy()) + if err != nil && !errors.Is(err, packages_model.ErrPackageNotExist) { + apiError(ctx, http.StatusInternalServerError, err) + return + } else if legacyPackage != nil { + err = packages_model.UpdatePackageNameByID(ctx, ctx.Package.Owner.ID, packages_model.TypeMaven, legacyPackage.ID, packageName) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + } + + ext := path.Ext(params.Filename) // Do not upload checksum files but compare the hashes. if isChecksumExtension(ext) { pv, err := packages_model.GetVersionByNameAndVersion(ctx, pvci.Owner.ID, pvci.PackageType, pvci.Name, pvci.Version) if err != nil { - if err == packages_model.ErrPackageNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) { apiError(ctx, http.StatusNotFound, err) return } @@ -289,7 +312,7 @@ func UploadPackageFile(ctx *context.Context) { } pf, err := packages_model.GetFileForVersionByName(ctx, pv.ID, params.Filename[:len(params.Filename)-len(ext)], packages_model.EmptyFileKey) if err != nil { - if err == packages_model.ErrPackageFileNotExist { + if errors.Is(err, packages_model.ErrPackageFileNotExist) { apiError(ctx, http.StatusNotFound, err) return } @@ -343,7 +366,7 @@ func UploadPackageFile(ctx *context.Context) { if pvci.Metadata != nil { pv, err := packages_model.GetVersionByNameAndVersion(ctx, pvci.Owner.ID, pvci.PackageType, pvci.Name, pvci.Version) - if err != nil && err != packages_model.ErrPackageNotExist { + if err != nil && !errors.Is(err, packages_model.ErrPackageNotExist) { apiError(ctx, http.StatusInternalServerError, err) return } @@ -399,9 +422,26 @@ type parameters struct { IsMeta bool } +func (p *parameters) toInternalPackageName() string { + // there cuold be 2 choices: "/" or ":" + // Maven says: "groupId:artifactId:version" in their document: https://maven.apache.org/pom.html#Maven_Coordinates + // but it would be slightly ugly in URL: "/-/packages/maven/group-id%3Aartifact-id" + return p.GroupID + ":" + p.ArtifactID +} + +func (p *parameters) toInternalPackageNameLegacy() string { + return p.GroupID + "-" + p.ArtifactID +} + func extractPathParameters(ctx *context.Context) (parameters, error) { parts := strings.Split(ctx.PathParam("*"), "/") + // formats: + // * /com/group/id/artifactId/maven-metadata.xml[.md5|.sha1|.sha256|.sha512] + // * /com/group/id/artifactId/version-SNAPSHOT/maven-metadata.xml[.md5|.sha1|.sha256|.sha512] + // * /com/group/id/artifactId/version/any-file + // * /com/group/id/artifactId/version-SNAPSHOT/any-file + p := parameters{ Filename: parts[len(parts)-1], } diff --git a/routers/api/packages/npm/api.go b/routers/api/packages/npm/api.go index b4379f3f49702..636680242a480 100644 --- a/routers/api/packages/npm/api.go +++ b/routers/api/packages/npm/api.go @@ -67,6 +67,7 @@ func createPackageMetadataVersion(registryURL string, pd *packages_model.Package BundleDependencies: metadata.BundleDependencies, DevDependencies: metadata.DevelopmentDependencies, PeerDependencies: metadata.PeerDependencies, + PeerDependenciesMeta: metadata.PeerDependenciesMeta, OptionalDependencies: metadata.OptionalDependencies, Readme: metadata.Readme, Bin: metadata.Bin, diff --git a/routers/api/packages/swift/swift.go b/routers/api/packages/swift/swift.go index 4d7fb8b1a6992..9c909d49186f0 100644 --- a/routers/api/packages/swift/swift.go +++ b/routers/api/packages/swift/swift.go @@ -290,7 +290,24 @@ func DownloadManifest(ctx *context.Context) { }) } -// https://github.com/swiftlang/swift-package-manager/blob/main/Documentation/PackageRegistry/Registry.md#endpoint-6 +// formFileOptionalReadCloser returns (nil, nil) if the formKey is not present. +func formFileOptionalReadCloser(ctx *context.Context, formKey string) (io.ReadCloser, error) { + multipartFile, _, err := ctx.Req.FormFile(formKey) + if err != nil && !errors.Is(err, http.ErrMissingFile) { + return nil, err + } + if multipartFile != nil { + return multipartFile, nil + } + + content := ctx.Req.FormValue(formKey) + if content == "" { + return nil, nil + } + return io.NopCloser(strings.NewReader(ctx.Req.FormValue(formKey))), nil +} + +// UploadPackageFile refers to https://github.com/swiftlang/swift-package-manager/blob/main/Documentation/PackageRegistry/Registry.md#endpoint-6 func UploadPackageFile(ctx *context.Context) { packageScope := ctx.PathParam("scope") packageName := ctx.PathParam("name") @@ -304,9 +321,9 @@ func UploadPackageFile(ctx *context.Context) { packageVersion := v.Core().String() - file, _, err := ctx.Req.FormFile("source-archive") - if err != nil { - apiError(ctx, http.StatusBadRequest, err) + file, err := formFileOptionalReadCloser(ctx, "source-archive") + if file == nil || err != nil { + apiError(ctx, http.StatusBadRequest, "unable to read source-archive file") return } defer file.Close() @@ -318,10 +335,13 @@ func UploadPackageFile(ctx *context.Context) { } defer buf.Close() - var mr io.Reader - metadata := ctx.Req.FormValue("metadata") - if metadata != "" { - mr = strings.NewReader(metadata) + mr, err := formFileOptionalReadCloser(ctx, "metadata") + if err != nil { + apiError(ctx, http.StatusBadRequest, "unable to read metadata file") + return + } + if mr != nil { + defer mr.Close() } pck, err := swift_module.ParsePackage(buf, buf.Size(), mr) diff --git a/routers/api/v1/admin/adopt.go b/routers/api/v1/admin/adopt.go index 55ea8c6758cf6..613d123494223 100644 --- a/routers/api/v1/admin/adopt.go +++ b/routers/api/v1/admin/adopt.go @@ -80,8 +80,8 @@ func AdoptRepository(ctx *context.APIContext) { // "$ref": "#/responses/notFound" // "403": // "$ref": "#/responses/forbidden" - ownerName := ctx.PathParam("username") - repoName := ctx.PathParam("reponame") + ownerName := ctx.PathParam(":username") + repoName := ctx.PathParam(":reponame") ctxUser, err := user_model.GetUserByName(ctx, ownerName) if err != nil { @@ -142,8 +142,8 @@ func DeleteUnadoptedRepository(ctx *context.APIContext) { // "$ref": "#/responses/empty" // "403": // "$ref": "#/responses/forbidden" - ownerName := ctx.PathParam("username") - repoName := ctx.PathParam("reponame") + ownerName := ctx.PathParam(":username") + repoName := ctx.PathParam(":reponame") ctxUser, err := user_model.GetUserByName(ctx, ownerName) if err != nil { diff --git a/routers/api/v1/admin/cron.go b/routers/api/v1/admin/cron.go index 962e0077763ff..fba9d33f25f62 100644 --- a/routers/api/v1/admin/cron.go +++ b/routers/api/v1/admin/cron.go @@ -74,7 +74,7 @@ func PostCronTask(ctx *context.APIContext) { // "$ref": "#/responses/empty" // "404": // "$ref": "#/responses/notFound" - task := cron.GetTask(ctx.PathParam("task")) + task := cron.GetTask(ctx.PathParam(":task")) if task == nil { ctx.NotFound() return diff --git a/routers/api/v1/admin/email.go b/routers/api/v1/admin/email.go index 3de94d6868e60..6fe418249ba17 100644 --- a/routers/api/v1/admin/email.go +++ b/routers/api/v1/admin/email.go @@ -38,7 +38,7 @@ func GetAllEmails(ctx *context.APIContext) { listOptions := utils.GetListOptions(ctx) emails, maxResults, err := user_model.SearchEmails(ctx, &user_model.SearchEmailOptions{ - Keyword: ctx.PathParam("email"), + Keyword: ctx.PathParam(":email"), ListOptions: listOptions, }) if err != nil { @@ -82,6 +82,6 @@ func SearchEmail(ctx *context.APIContext) { // "403": // "$ref": "#/responses/forbidden" - ctx.SetPathParam("email", ctx.FormTrim("q")) + ctx.SetPathParam(":email", ctx.FormTrim("q")) GetAllEmails(ctx) } diff --git a/routers/api/v1/admin/hooks.go b/routers/api/v1/admin/hooks.go index 6b4689047b5fe..db481fbf594bc 100644 --- a/routers/api/v1/admin/hooks.go +++ b/routers/api/v1/admin/hooks.go @@ -73,7 +73,7 @@ func GetHook(ctx *context.APIContext) { // "200": // "$ref": "#/responses/Hook" - hookID := ctx.PathParamInt64("id") + hookID := ctx.PathParamInt64(":id") hook, err := webhook.GetSystemOrDefaultWebhook(ctx, hookID) if err != nil { if errors.Is(err, util.ErrNotExist) { @@ -142,7 +142,7 @@ func EditHook(ctx *context.APIContext) { form := web.GetForm(ctx).(*api.EditHookOption) // TODO in body params - hookID := ctx.PathParamInt64("id") + hookID := ctx.PathParamInt64(":id") utils.EditSystemHook(ctx, form, hookID) } @@ -164,7 +164,7 @@ func DeleteHook(ctx *context.APIContext) { // "204": // "$ref": "#/responses/empty" - hookID := ctx.PathParamInt64("id") + hookID := ctx.PathParamInt64(":id") if err := webhook.DeleteDefaultSystemWebhook(ctx, hookID); err != nil { if errors.Is(err, util.ErrNotExist) { ctx.NotFound() diff --git a/routers/api/v1/admin/user.go b/routers/api/v1/admin/user.go index 21cb2f9ccdfdb..b0f40084da38b 100644 --- a/routers/api/v1/admin/user.go +++ b/routers/api/v1/admin/user.go @@ -9,12 +9,10 @@ import ( "fmt" "net/http" + "code.gitea.io/gitea/models" asymkey_model "code.gitea.io/gitea/models/asymkey" "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/db" - org_model "code.gitea.io/gitea/models/organization" - packages_model "code.gitea.io/gitea/models/packages" - repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/auth/password" "code.gitea.io/gitea/modules/log" @@ -249,7 +247,7 @@ func EditUser(ctx *context.APIContext) { } if err := user_service.UpdateUser(ctx, ctx.ContextUser, opts); err != nil { - if user_model.IsErrDeleteLastAdminUser(err) { + if models.IsErrDeleteLastAdminUser(err) { ctx.Error(http.StatusBadRequest, "LastAdmin", err) } else { ctx.Error(http.StatusInternalServerError, "UpdateUser", err) @@ -301,10 +299,10 @@ func DeleteUser(ctx *context.APIContext) { } if err := user_service.DeleteUser(ctx, ctx.ContextUser, ctx.FormBool("purge")); err != nil { - if repo_model.IsErrUserOwnRepos(err) || - org_model.IsErrUserHasOrgs(err) || - packages_model.IsErrUserOwnPackages(err) || - user_model.IsErrDeleteLastAdminUser(err) { + if models.IsErrUserOwnRepos(err) || + models.IsErrUserHasOrgs(err) || + models.IsErrUserOwnPackages(err) || + models.IsErrDeleteLastAdminUser(err) { ctx.Error(http.StatusUnprocessableEntity, "", err) } else { ctx.Error(http.StatusInternalServerError, "DeleteUser", err) @@ -375,7 +373,7 @@ func DeleteUserPublicKey(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - if err := asymkey_service.DeletePublicKey(ctx, ctx.ContextUser, ctx.PathParamInt64("id")); err != nil { + if err := asymkey_service.DeletePublicKey(ctx, ctx.ContextUser, ctx.PathParamInt64(":id")); err != nil { if asymkey_model.IsErrKeyNotExist(err) { ctx.NotFound() } else if asymkey_model.IsErrKeyAccessDenied(err) { diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 2f943d306cc5c..f937a475b361a 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -356,7 +356,7 @@ func reqToken() func(ctx *context.APIContext) { func reqExploreSignIn() func(ctx *context.APIContext) { return func(ctx *context.APIContext) { - if (setting.Service.RequireSignInView || setting.Service.Explore.RequireSigninView) && !ctx.IsSigned { + if (setting.Service.RequireSignInViewStrict || setting.Service.Explore.RequireSigninView) && !ctx.IsSigned { ctx.Error(http.StatusUnauthorized, "reqExploreSignIn", "you must be signed in to search for users") } } @@ -596,12 +596,12 @@ func orgAssignment(args ...bool) func(ctx *context.APIContext) { var err error if assignOrg { - ctx.Org.Organization, err = organization.GetOrgByName(ctx, ctx.PathParam("org")) + ctx.Org.Organization, err = organization.GetOrgByName(ctx, ctx.PathParam(":org")) if err != nil { if organization.IsErrOrgNotExist(err) { - redirectUserID, err := user_model.LookupUserRedirect(ctx, ctx.PathParam("org")) + redirectUserID, err := user_model.LookupUserRedirect(ctx, ctx.PathParam(":org")) if err == nil { - context.RedirectToUser(ctx.Base, ctx.PathParam("org"), redirectUserID) + context.RedirectToUser(ctx.Base, ctx.PathParam(":org"), redirectUserID) } else if user_model.IsErrUserRedirectNotExist(err) { ctx.NotFound("GetOrgByName", err) } else { @@ -616,7 +616,7 @@ func orgAssignment(args ...bool) func(ctx *context.APIContext) { } if assignTeam { - ctx.Org.Team, err = organization.GetTeamByID(ctx, ctx.PathParamInt64("teamid")) + ctx.Org.Team, err = organization.GetTeamByID(ctx, ctx.PathParamInt64(":teamid")) if err != nil { if organization.IsErrTeamNotExist(err) { ctx.NotFound() @@ -874,7 +874,7 @@ func Routes() *web.Router { m.Use(apiAuth(buildAuthGroup())) m.Use(verifyAuthWithOptions(&common.VerifyOptions{ - SignInRequired: setting.Service.RequireSignInView, + SignInRequired: setting.Service.RequireSignInViewStrict, })) addActionsRoutes := func( @@ -1190,6 +1190,7 @@ func Routes() *web.Router { m.Get("/archive/*", reqRepoReader(unit.TypeCode), repo.GetArchive) m.Combo("/forks").Get(repo.ListForks). Post(reqToken(), reqRepoReader(unit.TypeCode), bind(api.CreateForkOption{}), repo.CreateFork) + m.Post("/merge-upstream", reqToken(), mustNotBeArchived, reqRepoWriter(unit.TypeCode), bind(api.MergeUpstreamRequest{}), repo.MergeUpstream) m.Group("/branches", func() { m.Get("", repo.ListBranches) m.Get("/*", repo.GetBranch) diff --git a/routers/api/v1/notify/threads.go b/routers/api/v1/notify/threads.go index 58a38cfd18c1f..0761e684a3f9c 100644 --- a/routers/api/v1/notify/threads.go +++ b/routers/api/v1/notify/threads.go @@ -101,7 +101,7 @@ func ReadThread(ctx *context.APIContext) { } func getThread(ctx *context.APIContext) *activities_model.Notification { - n, err := activities_model.GetNotificationByID(ctx, ctx.PathParamInt64("id")) + n, err := activities_model.GetNotificationByID(ctx, ctx.PathParamInt64(":id")) if err != nil { if db.IsErrNotExist(err) { ctx.Error(http.StatusNotFound, "GetNotificationByID", err) diff --git a/routers/api/v1/org/action.go b/routers/api/v1/org/action.go index 199ee7d7773b9..05919c523439b 100644 --- a/routers/api/v1/org/action.go +++ b/routers/api/v1/org/action.go @@ -450,7 +450,11 @@ func (Action) UpdateVariable(ctx *context.APIContext) { if opt.Name == "" { opt.Name = ctx.PathParam("variablename") } - if _, err := actions_service.UpdateVariable(ctx, v.ID, opt.Name, opt.Value); err != nil { + + v.Name = opt.Name + v.Data = opt.Value + + if _, err := actions_service.UpdateVariableNameData(ctx, v); err != nil { if errors.Is(err, util.ErrInvalidArgument) { ctx.Error(http.StatusBadRequest, "UpdateVariable", err) } else { diff --git a/routers/api/v1/org/label.go b/routers/api/v1/org/label.go index 2a9bd92e878d8..24ee4ed642302 100644 --- a/routers/api/v1/org/label.go +++ b/routers/api/v1/org/label.go @@ -139,7 +139,7 @@ func GetLabel(ctx *context.APIContext) { label *issues_model.Label err error ) - strID := ctx.PathParam("id") + strID := ctx.PathParam(":id") if intID, err2 := strconv.ParseInt(strID, 10, 64); err2 != nil { label, err = issues_model.GetLabelInOrgByName(ctx, ctx.Org.Organization.ID, strID) } else { @@ -190,7 +190,7 @@ func EditLabel(ctx *context.APIContext) { // "422": // "$ref": "#/responses/validationError" form := web.GetForm(ctx).(*api.EditLabelOption) - l, err := issues_model.GetLabelInOrgByID(ctx, ctx.Org.Organization.ID, ctx.PathParamInt64("id")) + l, err := issues_model.GetLabelInOrgByID(ctx, ctx.Org.Organization.ID, ctx.PathParamInt64(":id")) if err != nil { if issues_model.IsErrOrgLabelNotExist(err) { ctx.NotFound() @@ -249,7 +249,7 @@ func DeleteLabel(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - if err := issues_model.DeleteLabel(ctx, ctx.Org.Organization.ID, ctx.PathParamInt64("id")); err != nil { + if err := issues_model.DeleteLabel(ctx, ctx.Org.Organization.ID, ctx.PathParamInt64(":id")); err != nil { ctx.Error(http.StatusInternalServerError, "DeleteLabel", err) return } diff --git a/routers/api/v1/org/member.go b/routers/api/v1/org/member.go index 23c7da3d968d4..294d33014d134 100644 --- a/routers/api/v1/org/member.go +++ b/routers/api/v1/org/member.go @@ -143,7 +143,7 @@ func IsMember(ctx *context.APIContext) { // "404": // description: user is not a member - userToCheck := user.GetContextUserByPathParam(ctx) + userToCheck := user.GetUserByParams(ctx) if ctx.Written() { return } @@ -194,7 +194,7 @@ func IsPublicMember(ctx *context.APIContext) { // "404": // description: user is not a public member - userToCheck := user.GetContextUserByPathParam(ctx) + userToCheck := user.GetUserByParams(ctx) if ctx.Written() { return } @@ -236,7 +236,7 @@ func PublicizeMember(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - userToPublicize := user.GetContextUserByPathParam(ctx) + userToPublicize := user.GetUserByParams(ctx) if ctx.Written() { return } @@ -278,7 +278,7 @@ func ConcealMember(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - userToConceal := user.GetContextUserByPathParam(ctx) + userToConceal := user.GetUserByParams(ctx) if ctx.Written() { return } @@ -318,7 +318,7 @@ func DeleteMember(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - member := user.GetContextUserByPathParam(ctx) + member := user.GetUserByParams(ctx) if ctx.Written() { return } diff --git a/routers/api/v1/org/org.go b/routers/api/v1/org/org.go index d65f922434417..3fb653bcb6d0c 100644 --- a/routers/api/v1/org/org.go +++ b/routers/api/v1/org/org.go @@ -131,7 +131,7 @@ func GetUserOrgsPermissions(ctx *context.APIContext) { // "$ref": "#/responses/notFound" var o *user_model.User - if o = user.GetUserByPathParam(ctx, "org"); o == nil { + if o = user.GetUserByParamsName(ctx, ":org"); o == nil { return } diff --git a/routers/api/v1/org/team.go b/routers/api/v1/org/team.go index 7f44f6ed95db9..bc50960b61b11 100644 --- a/routers/api/v1/org/team.go +++ b/routers/api/v1/org/team.go @@ -449,7 +449,7 @@ func GetTeamMember(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - u := user.GetContextUserByPathParam(ctx) + u := user.GetUserByParams(ctx) if ctx.Written() { return } @@ -492,7 +492,7 @@ func AddTeamMember(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - u := user.GetContextUserByPathParam(ctx) + u := user.GetUserByParams(ctx) if ctx.Written() { return } @@ -532,7 +532,7 @@ func RemoveTeamMember(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - u := user.GetContextUserByPathParam(ctx) + u := user.GetUserByParams(ctx) if ctx.Written() { return } @@ -573,19 +573,19 @@ func GetTeamRepos(ctx *context.APIContext) { // "$ref": "#/responses/notFound" team := ctx.Org.Team - teamRepos, err := repo_model.GetTeamRepositories(ctx, &repo_model.SearchTeamRepoOptions{ + teamRepos, err := organization.GetTeamRepositories(ctx, &organization.SearchTeamRepoOptions{ ListOptions: utils.GetListOptions(ctx), TeamID: team.ID, }) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetTeamRepositories", err) + ctx.Error(http.StatusInternalServerError, "GetTeamRepos", err) return } repos := make([]*api.Repository, len(teamRepos)) for i, repo := range teamRepos { permission, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err) + ctx.Error(http.StatusInternalServerError, "GetTeamRepos", err) return } repos[i] = convert.ToRepo(ctx, repo, permission) @@ -645,7 +645,7 @@ func GetTeamRepo(ctx *context.APIContext) { // getRepositoryByParams get repository by a team's organization ID and repo name func getRepositoryByParams(ctx *context.APIContext) *repo_model.Repository { - repo, err := repo_model.GetRepositoryByName(ctx, ctx.Org.Team.OrgID, ctx.PathParam("reponame")) + repo, err := repo_model.GetRepositoryByName(ctx, ctx.Org.Team.OrgID, ctx.PathParam(":reponame")) if err != nil { if repo_model.IsErrRepoNotExist(err) { ctx.NotFound() diff --git a/routers/api/v1/repo/action.go b/routers/api/v1/repo/action.go index d27e8d2427b39..8f464c8dbd9c3 100644 --- a/routers/api/v1/repo/action.go +++ b/routers/api/v1/repo/action.go @@ -414,7 +414,11 @@ func (Action) UpdateVariable(ctx *context.APIContext) { if opt.Name == "" { opt.Name = ctx.PathParam("variablename") } - if _, err := actions_service.UpdateVariable(ctx, v.ID, opt.Name, opt.Value); err != nil { + + v.Name = opt.Name + v.Data = opt.Value + + if _, err := actions_service.UpdateVariableNameData(ctx, v); err != nil { if errors.Is(err, util.ErrInvalidArgument) { ctx.Error(http.StatusBadRequest, "UpdateVariable", err) } else { diff --git a/routers/api/v1/repo/branch.go b/routers/api/v1/repo/branch.go index 67dfda39a8138..da0391150bd73 100644 --- a/routers/api/v1/repo/branch.go +++ b/routers/api/v1/repo/branch.go @@ -9,6 +9,7 @@ import ( "fmt" "net/http" + "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/db" git_model "code.gitea.io/gitea/models/git" "code.gitea.io/gitea/models/organization" @@ -18,12 +19,12 @@ import ( "code.gitea.io/gitea/modules/optional" repo_module "code.gitea.io/gitea/modules/repository" api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/routers/api/v1/utils" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/convert" pull_service "code.gitea.io/gitea/services/pull" - release_service "code.gitea.io/gitea/services/release" repo_service "code.gitea.io/gitea/services/repository" ) @@ -247,7 +248,7 @@ func CreateBranch(ctx *context.APIContext) { if err != nil { if git_model.IsErrBranchNotExist(err) { ctx.Error(http.StatusNotFound, "", "The old branch does not exist") - } else if release_service.IsErrTagAlreadyExists(err) { + } else if models.IsErrTagAlreadyExists(err) { ctx.Error(http.StatusConflict, "", "The branch with the same tag already exists.") } else if git_model.IsErrBranchAlreadyExists(err) || git.IsErrPushOutOfDate(err) { ctx.Error(http.StatusConflict, "", "The branch already exists.") @@ -487,7 +488,7 @@ func GetBranchProtection(ctx *context.APIContext) { // "$ref": "#/responses/notFound" repo := ctx.Repo.Repository - bpName := ctx.PathParam("name") + bpName := ctx.PathParam(":name") bp, err := git_model.GetProtectedBranchRuleByName(ctx, repo.ID, bpName) if err != nil { ctx.Error(http.StatusInternalServerError, "GetProtectedBranchByID", err) @@ -729,11 +730,15 @@ func CreateBranchProtection(ctx *context.APIContext) { } else { if !isPlainRule { if ctx.Repo.GitRepo == nil { - ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx, ctx.Repo.Repository) + ctx.Repo.GitRepo, err = gitrepo.OpenRepository(ctx, ctx.Repo.Repository) if err != nil { ctx.Error(http.StatusInternalServerError, "OpenRepository", err) return } + defer func() { + ctx.Repo.GitRepo.Close() + ctx.Repo.GitRepo = nil + }() } // FIXME: since we only need to recheck files protected rules, we could improve this matchedBranches, err := git_model.FindAllMatchedBranches(ctx, ctx.Repo.Repository.ID, ruleName) @@ -805,7 +810,7 @@ func EditBranchProtection(ctx *context.APIContext) { // "$ref": "#/responses/repoArchivedError" form := web.GetForm(ctx).(*api.EditBranchProtectionOption) repo := ctx.Repo.Repository - bpName := ctx.PathParam("name") + bpName := ctx.PathParam(":name") protectBranch, err := git_model.GetProtectedBranchRuleByName(ctx, repo.ID, bpName) if err != nil { ctx.Error(http.StatusInternalServerError, "GetProtectedBranchByID", err) @@ -1057,11 +1062,15 @@ func EditBranchProtection(ctx *context.APIContext) { } else { if !isPlainRule { if ctx.Repo.GitRepo == nil { - ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx, ctx.Repo.Repository) + ctx.Repo.GitRepo, err = gitrepo.OpenRepository(ctx, ctx.Repo.Repository) if err != nil { ctx.Error(http.StatusInternalServerError, "OpenRepository", err) return } + defer func() { + ctx.Repo.GitRepo.Close() + ctx.Repo.GitRepo = nil + }() } // FIXME: since we only need to recheck files protected rules, we could improve this @@ -1124,7 +1133,7 @@ func DeleteBranchProtection(ctx *context.APIContext) { // "$ref": "#/responses/notFound" repo := ctx.Repo.Repository - bpName := ctx.PathParam("name") + bpName := ctx.PathParam(":name") bp, err := git_model.GetProtectedBranchRuleByName(ctx, repo.ID, bpName) if err != nil { ctx.Error(http.StatusInternalServerError, "GetProtectedBranchByID", err) @@ -1186,3 +1195,47 @@ func UpdateBranchProtectionPriories(ctx *context.APIContext) { ctx.Status(http.StatusNoContent) } + +func MergeUpstream(ctx *context.APIContext) { + // swagger:operation POST /repos/{owner}/{repo}/merge-upstream repository repoMergeUpstream + // --- + // summary: Merge a branch from upstream + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // - name: body + // in: body + // schema: + // "$ref": "#/definitions/MergeUpstreamRequest" + // responses: + // "200": + // "$ref": "#/responses/MergeUpstreamResponse" + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + form := web.GetForm(ctx).(*api.MergeUpstreamRequest) + mergeStyle, err := repo_service.MergeUpstream(ctx, ctx.Doer, ctx.Repo.Repository, form.Branch) + if err != nil { + if errors.Is(err, util.ErrInvalidArgument) { + ctx.Error(http.StatusBadRequest, "MergeUpstream", err) + return + } else if errors.Is(err, util.ErrNotExist) { + ctx.Error(http.StatusNotFound, "MergeUpstream", err) + return + } + ctx.Error(http.StatusInternalServerError, "MergeUpstream", err) + return + } + ctx.JSON(http.StatusOK, &api.MergeUpstreamResponse{MergeStyle: mergeStyle}) +} diff --git a/routers/api/v1/repo/collaborators.go b/routers/api/v1/repo/collaborators.go index da3ee54e691de..0bbf5a1ea49e8 100644 --- a/routers/api/v1/repo/collaborators.go +++ b/routers/api/v1/repo/collaborators.go @@ -103,7 +103,7 @@ func IsCollaborator(ctx *context.APIContext) { // "422": // "$ref": "#/responses/validationError" - user, err := user_model.GetUserByName(ctx, ctx.PathParam("collaborator")) + user, err := user_model.GetUserByName(ctx, ctx.PathParam(":collaborator")) if err != nil { if user_model.IsErrUserNotExist(err) { ctx.Error(http.StatusUnprocessableEntity, "", err) @@ -163,7 +163,7 @@ func AddOrUpdateCollaborator(ctx *context.APIContext) { form := web.GetForm(ctx).(*api.AddCollaboratorOption) - collaborator, err := user_model.GetUserByName(ctx, ctx.PathParam("collaborator")) + collaborator, err := user_model.GetUserByName(ctx, ctx.PathParam(":collaborator")) if err != nil { if user_model.IsErrUserNotExist(err) { ctx.Error(http.StatusUnprocessableEntity, "", err) @@ -226,7 +226,7 @@ func DeleteCollaborator(ctx *context.APIContext) { // "422": // "$ref": "#/responses/validationError" - collaborator, err := user_model.GetUserByName(ctx, ctx.PathParam("collaborator")) + collaborator, err := user_model.GetUserByName(ctx, ctx.PathParam(":collaborator")) if err != nil { if user_model.IsErrUserNotExist(err) { ctx.Error(http.StatusUnprocessableEntity, "", err) @@ -274,12 +274,12 @@ func GetRepoPermissions(ctx *context.APIContext) { // "403": // "$ref": "#/responses/forbidden" - if !ctx.Doer.IsAdmin && ctx.Doer.LoginName != ctx.PathParam("collaborator") && !ctx.IsUserRepoAdmin() { + if !ctx.Doer.IsAdmin && ctx.Doer.LoginName != ctx.PathParam(":collaborator") && !ctx.IsUserRepoAdmin() { ctx.Error(http.StatusForbidden, "User", "Only admins can query all permissions, repo admins can query all repo permissions, collaborators can query only their own") return } - collaborator, err := user_model.GetUserByName(ctx, ctx.PathParam("collaborator")) + collaborator, err := user_model.GetUserByName(ctx, ctx.PathParam(":collaborator")) if err != nil { if user_model.IsErrUserNotExist(err) { ctx.Error(http.StatusNotFound, "GetUserByName", err) diff --git a/routers/api/v1/repo/commits.go b/routers/api/v1/repo/commits.go index 3b144d0c43765..788c75fab2ffe 100644 --- a/routers/api/v1/repo/commits.go +++ b/routers/api/v1/repo/commits.go @@ -63,7 +63,7 @@ func GetSingleCommit(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - sha := ctx.PathParam("sha") + sha := ctx.PathParam(":sha") if !git.IsValidRefPattern(sha) { ctx.Error(http.StatusUnprocessableEntity, "no valid ref or sha", fmt.Sprintf("no valid ref or sha: %s", sha)) return @@ -312,8 +312,8 @@ func DownloadCommitDiffOrPatch(ctx *context.APIContext) { // "$ref": "#/responses/string" // "404": // "$ref": "#/responses/notFound" - sha := ctx.PathParam("sha") - diffType := git.RawDiffType(ctx.PathParam("diffType")) + sha := ctx.PathParam(":sha") + diffType := git.RawDiffType(ctx.PathParam(":diffType")) if err := git.GetRawDiff(ctx.Repo.GitRepo, sha, diffType, ctx.Resp); err != nil { if git.IsErrNotExist(err) { diff --git a/routers/api/v1/repo/compare.go b/routers/api/v1/repo/compare.go index 87b890cb62b33..1678bc033c639 100644 --- a/routers/api/v1/repo/compare.go +++ b/routers/api/v1/repo/compare.go @@ -44,12 +44,13 @@ func CompareDiff(ctx *context.APIContext) { // "$ref": "#/responses/notFound" if ctx.Repo.GitRepo == nil { - var err error - ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx, ctx.Repo.Repository) + gitRepo, err := gitrepo.OpenRepository(ctx, ctx.Repo.Repository) if err != nil { ctx.Error(http.StatusInternalServerError, "OpenRepository", err) return } + ctx.Repo.GitRepo = gitRepo + defer gitRepo.Close() } infoPath := ctx.PathParam("*") diff --git a/routers/api/v1/repo/download.go b/routers/api/v1/repo/download.go index eb967772edfa7..3620c1465fe9c 100644 --- a/routers/api/v1/repo/download.go +++ b/routers/api/v1/repo/download.go @@ -28,12 +28,13 @@ func DownloadArchive(ctx *context.APIContext) { } if ctx.Repo.GitRepo == nil { - var err error - ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx, ctx.Repo.Repository) + gitRepo, err := gitrepo.OpenRepository(ctx, ctx.Repo.Repository) if err != nil { ctx.Error(http.StatusInternalServerError, "OpenRepository", err) return } + ctx.Repo.GitRepo = gitRepo + defer gitRepo.Close() } r, err := archiver_service.NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, ctx.PathParam("*"), tp) diff --git a/routers/api/v1/repo/file.go b/routers/api/v1/repo/file.go index 6591b9a752faa..4aed2e5e92c9b 100644 --- a/routers/api/v1/repo/file.go +++ b/routers/api/v1/repo/file.go @@ -11,10 +11,10 @@ import ( "fmt" "io" "net/http" - "path" "strings" "time" + "code.gitea.io/gitea/models" git_model "code.gitea.io/gitea/models/git" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" @@ -29,7 +29,6 @@ import ( "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/routers/common" "code.gitea.io/gitea/services/context" - pull_service "code.gitea.io/gitea/services/pull" archiver_service "code.gitea.io/gitea/services/repository/archiver" files_service "code.gitea.io/gitea/services/repository/files" ) @@ -242,19 +241,14 @@ func getBlobForEntry(ctx *context.APIContext) (blob *git.Blob, entry *git.TreeEn return nil, nil, nil } - info, _, err := git.Entries([]*git.TreeEntry{entry}).GetCommitsInfo(ctx, ctx.Repo.Commit, path.Dir("/" + ctx.Repo.TreePath)[1:]) + latestCommit, err := ctx.Repo.GitRepo.GetTreePathLatestCommit(ctx.Repo.Commit.ID.String(), ctx.Repo.TreePath) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetCommitsInfo", err) + ctx.Error(http.StatusInternalServerError, "GetTreePathLatestCommit", err) return nil, nil, nil } + when := &latestCommit.Committer.When - if len(info) == 1 { - // Not Modified - lastModified = &info[0].Commit.Committer.When - } - blob = entry.Blob() - - return blob, entry, lastModified + return entry.Blob(), entry, when } // GetArchive get archive of a repository @@ -287,12 +281,13 @@ func GetArchive(ctx *context.APIContext) { // "$ref": "#/responses/notFound" if ctx.Repo.GitRepo == nil { - var err error - ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx, ctx.Repo.Repository) + gitRepo, err := gitrepo.OpenRepository(ctx, ctx.Repo.Repository) if err != nil { ctx.Error(http.StatusInternalServerError, "OpenRepository", err) return } + ctx.Repo.GitRepo = gitRepo + defer gitRepo.Close() } archiveDownload(ctx) @@ -735,12 +730,12 @@ func UpdateFile(ctx *context.APIContext) { } func handleCreateOrUpdateFileError(ctx *context.APIContext, err error) { - if files_service.IsErrUserCannotCommit(err) || pull_service.IsErrFilePathProtected(err) { + if models.IsErrUserCannotCommit(err) || models.IsErrFilePathProtected(err) { ctx.Error(http.StatusForbidden, "Access", err) return } - if git_model.IsErrBranchAlreadyExists(err) || files_service.IsErrFilenameInvalid(err) || pull_service.IsErrSHADoesNotMatch(err) || - files_service.IsErrFilePathInvalid(err) || files_service.IsErrRepoFileAlreadyExists(err) { + if git_model.IsErrBranchAlreadyExists(err) || models.IsErrFilenameInvalid(err) || models.IsErrSHADoesNotMatch(err) || + models.IsErrFilePathInvalid(err) || models.IsErrRepoFileAlreadyExists(err) { ctx.Error(http.StatusUnprocessableEntity, "Invalid", err) return } @@ -886,17 +881,17 @@ func DeleteFile(ctx *context.APIContext) { } if filesResponse, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, opts); err != nil { - if git.IsErrBranchNotExist(err) || files_service.IsErrRepoFileDoesNotExist(err) || git.IsErrNotExist(err) { + if git.IsErrBranchNotExist(err) || models.IsErrRepoFileDoesNotExist(err) || git.IsErrNotExist(err) { ctx.Error(http.StatusNotFound, "DeleteFile", err) return } else if git_model.IsErrBranchAlreadyExists(err) || - files_service.IsErrFilenameInvalid(err) || - pull_service.IsErrSHADoesNotMatch(err) || - files_service.IsErrCommitIDDoesNotMatch(err) || - files_service.IsErrSHAOrCommitIDNotProvided(err) { + models.IsErrFilenameInvalid(err) || + models.IsErrSHADoesNotMatch(err) || + models.IsErrCommitIDDoesNotMatch(err) || + models.IsErrSHAOrCommitIDNotProvided(err) { ctx.Error(http.StatusBadRequest, "DeleteFile", err) return - } else if files_service.IsErrUserCannotCommit(err) { + } else if models.IsErrUserCannotCommit(err) { ctx.Error(http.StatusForbidden, "DeleteFile", err) return } diff --git a/routers/api/v1/repo/fork.go b/routers/api/v1/repo/fork.go index 14a1a8d1c4a3d..f96c432b92988 100644 --- a/routers/api/v1/repo/fork.go +++ b/routers/api/v1/repo/fork.go @@ -132,13 +132,15 @@ func CreateFork(ctx *context.APIContext) { } return } - isMember, err := org.IsOrgMember(ctx, ctx.Doer.ID) - if err != nil { - ctx.Error(http.StatusInternalServerError, "IsOrgMember", err) - return - } else if !isMember { - ctx.Error(http.StatusForbidden, "isMemberNot", fmt.Sprintf("User is no Member of Organisation '%s'", org.Name)) - return + if !ctx.Doer.IsAdmin { + isMember, err := org.IsOrgMember(ctx, ctx.Doer.ID) + if err != nil { + ctx.Error(http.StatusInternalServerError, "IsOrgMember", err) + return + } else if !isMember { + ctx.Error(http.StatusForbidden, "isMemberNot", fmt.Sprintf("User is no Member of Organisation '%s'", org.Name)) + return + } } forker = org.AsUser() } diff --git a/routers/api/v1/repo/git_hook.go b/routers/api/v1/repo/git_hook.go index 9b66b69068abb..0887a90096f5a 100644 --- a/routers/api/v1/repo/git_hook.go +++ b/routers/api/v1/repo/git_hook.go @@ -79,7 +79,7 @@ func GetGitHook(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - hookID := ctx.PathParam("id") + hookID := ctx.PathParam(":id") hook, err := ctx.Repo.GitRepo.GetHook(hookID) if err != nil { if err == git.ErrNotValidHook { @@ -126,7 +126,7 @@ func EditGitHook(ctx *context.APIContext) { // "$ref": "#/responses/notFound" form := web.GetForm(ctx).(*api.EditGitHookOption) - hookID := ctx.PathParam("id") + hookID := ctx.PathParam(":id") hook, err := ctx.Repo.GitRepo.GetHook(hookID) if err != nil { if err == git.ErrNotValidHook { @@ -175,7 +175,7 @@ func DeleteGitHook(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - hookID := ctx.PathParam("id") + hookID := ctx.PathParam(":id") hook, err := ctx.Repo.GitRepo.GetHook(hookID) if err != nil { if err == git.ErrNotValidHook { diff --git a/routers/api/v1/repo/hook.go b/routers/api/v1/repo/hook.go index 03143c8f99e2e..9ef57da1b98fd 100644 --- a/routers/api/v1/repo/hook.go +++ b/routers/api/v1/repo/hook.go @@ -109,7 +109,7 @@ func GetHook(ctx *context.APIContext) { // "$ref": "#/responses/notFound" repo := ctx.Repo - hookID := ctx.PathParamInt64("id") + hookID := ctx.PathParamInt64(":id") hook, err := utils.GetRepoHook(ctx, repo.Repository.ID, hookID) if err != nil { return @@ -168,7 +168,7 @@ func TestHook(ctx *context.APIContext) { ref = r } - hookID := ctx.PathParamInt64("id") + hookID := ctx.PathParamInt64(":id") hook, err := utils.GetRepoHook(ctx, ctx.Repo.Repository.ID, hookID) if err != nil { return @@ -263,7 +263,7 @@ func EditHook(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" form := web.GetForm(ctx).(*api.EditHookOption) - hookID := ctx.PathParamInt64("id") + hookID := ctx.PathParamInt64(":id") utils.EditRepoHook(ctx, form, hookID) } @@ -296,7 +296,7 @@ func DeleteHook(ctx *context.APIContext) { // "$ref": "#/responses/empty" // "404": // "$ref": "#/responses/notFound" - if err := webhook.DeleteWebhookByRepoID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("id")); err != nil { + if err := webhook.DeleteWebhookByRepoID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":id")); err != nil { if webhook.IsErrWebhookNotExist(err) { ctx.NotFound() } else { diff --git a/routers/api/v1/repo/hook_test.go b/routers/api/v1/repo/hook_test.go index c659a16f5441b..c2f3a972ef6fc 100644 --- a/routers/api/v1/repo/hook_test.go +++ b/routers/api/v1/repo/hook_test.go @@ -18,7 +18,7 @@ func TestTestHook(t *testing.T) { unittest.PrepareTestEnv(t) ctx, _ := contexttest.MockAPIContext(t, "user2/repo1/wiki/_pages") - ctx.SetPathParam("id", "1") + ctx.SetPathParam(":id", "1") contexttest.LoadRepo(t, ctx, 1) contexttest.LoadRepoCommit(t, ctx) contexttest.LoadUser(t, ctx, 2) diff --git a/routers/api/v1/repo/issue.go b/routers/api/v1/repo/issue.go index 86dbcee5f7de6..da949a56fd2a1 100644 --- a/routers/api/v1/repo/issue.go +++ b/routers/api/v1/repo/issue.go @@ -613,7 +613,7 @@ func GetIssue(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - issue, err := issues_model.GetIssueWithAttrsByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) + issue, err := issues_model.GetIssueWithAttrsByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) if err != nil { if issues_model.IsErrIssueNotExist(err) { ctx.NotFound() @@ -733,7 +733,7 @@ func CreateIssue(ctx *context.APIContext) { } if form.Closed { - if err := issue_service.CloseIssue(ctx, issue, ctx.Doer, ""); err != nil { + if err := issue_service.ChangeStatus(ctx, issue, ctx.Doer, "", true); err != nil { if issues_model.IsErrDependenciesLeft(err) { ctx.Error(http.StatusPreconditionFailed, "DependenciesLeft", "cannot close this issue because it still has open dependencies") return @@ -793,7 +793,7 @@ func EditIssue(ctx *context.APIContext) { // "$ref": "#/responses/error" form := web.GetForm(ctx).(*api.EditIssueOption) - issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) + issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) if err != nil { if issues_model.IsErrIssueNotExist(err) { ctx.NotFound() @@ -895,6 +895,15 @@ func EditIssue(ctx *context.APIContext) { issue.MilestoneID != *form.Milestone { oldMilestoneID := issue.MilestoneID issue.MilestoneID = *form.Milestone + if issue.MilestoneID > 0 { + issue.Milestone, err = issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, *form.Milestone) + if err != nil { + ctx.Error(http.StatusInternalServerError, "GetMilestoneByRepoID", err) + return + } + } else { + issue.Milestone = nil + } if err = issue_service.ChangeMilestoneAssign(ctx, issue, ctx.Doer, oldMilestoneID); err != nil { ctx.Error(http.StatusInternalServerError, "ChangeMilestoneAssign", err) return @@ -912,11 +921,27 @@ func EditIssue(ctx *context.APIContext) { } } - state := api.StateType(*form.State) - closeOrReopenIssue(ctx, issue, state) - if ctx.Written() { + var isClosed bool + switch state := api.StateType(*form.State); state { + case api.StateOpen: + isClosed = false + case api.StateClosed: + isClosed = true + default: + ctx.Error(http.StatusPreconditionFailed, "UnknownIssueStateError", fmt.Sprintf("unknown state: %s", state)) return } + + if issue.IsClosed != isClosed { + if err := issue_service.ChangeStatus(ctx, issue, ctx.Doer, "", isClosed); err != nil { + if issues_model.IsErrDependenciesLeft(err) { + ctx.Error(http.StatusPreconditionFailed, "DependenciesLeft", "cannot close this issue because it still has open dependencies") + return + } + ctx.Error(http.StatusInternalServerError, "ChangeStatus", err) + return + } + } } // Refetch from database to assign some automatic values @@ -960,7 +985,7 @@ func DeleteIssue(ctx *context.APIContext) { // "$ref": "#/responses/forbidden" // "404": // "$ref": "#/responses/notFound" - issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) + issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) if err != nil { if issues_model.IsErrIssueNotExist(err) { ctx.NotFound(err) @@ -1016,7 +1041,7 @@ func UpdateIssueDeadline(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" form := web.GetForm(ctx).(*api.EditDeadlineOption) - issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) + issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) if err != nil { if issues_model.IsErrIssueNotExist(err) { ctx.NotFound() @@ -1039,26 +1064,3 @@ func UpdateIssueDeadline(ctx *context.APIContext) { ctx.JSON(http.StatusCreated, api.IssueDeadline{Deadline: deadlineUnix.AsTimePtr()}) } - -func closeOrReopenIssue(ctx *context.APIContext, issue *issues_model.Issue, state api.StateType) { - if state != api.StateOpen && state != api.StateClosed { - ctx.Error(http.StatusPreconditionFailed, "UnknownIssueStateError", fmt.Sprintf("unknown state: %s", state)) - return - } - - if state == api.StateClosed && !issue.IsClosed { - if err := issue_service.CloseIssue(ctx, issue, ctx.Doer, ""); err != nil { - if issues_model.IsErrDependenciesLeft(err) { - ctx.Error(http.StatusPreconditionFailed, "DependenciesLeft", "cannot close this issue or pull request because it still has open dependencies") - return - } - ctx.Error(http.StatusInternalServerError, "CloseIssue", err) - return - } - } else if state == api.StateOpen && issue.IsClosed { - if err := issue_service.ReopenIssue(ctx, issue, ctx.Doer, ""); err != nil { - ctx.Error(http.StatusInternalServerError, "ReopenIssue", err) - return - } - } -} diff --git a/routers/api/v1/repo/issue_comment.go b/routers/api/v1/repo/issue_comment.go index 96a61a527e93b..f9b5aa816bc96 100644 --- a/routers/api/v1/repo/issue_comment.go +++ b/routers/api/v1/repo/issue_comment.go @@ -68,7 +68,7 @@ func ListIssueComments(ctx *context.APIContext) { ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err) return } - issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) + issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) if err != nil { ctx.Error(http.StatusInternalServerError, "GetRawIssueByIndex", err) return @@ -172,7 +172,7 @@ func ListIssueCommentsAndTimeline(ctx *context.APIContext) { ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err) return } - issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) + issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) if err != nil { ctx.Error(http.StatusInternalServerError, "GetRawIssueByIndex", err) return @@ -380,7 +380,7 @@ func CreateIssueComment(ctx *context.APIContext) { // "$ref": "#/responses/repoArchivedError" form := web.GetForm(ctx).(*api.CreateIssueCommentOption) - issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) + issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) if err != nil { ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err) return @@ -445,7 +445,7 @@ func GetIssueComment(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - comment, err := issues_model.GetCommentByID(ctx, ctx.PathParamInt64("id")) + comment, err := issues_model.GetCommentByID(ctx, ctx.PathParamInt64(":id")) if err != nil { if issues_model.IsErrCommentNotExist(err) { ctx.NotFound(err) @@ -579,7 +579,7 @@ func EditIssueCommentDeprecated(ctx *context.APIContext) { } func editIssueComment(ctx *context.APIContext, form api.EditIssueCommentOption) { - comment, err := issues_model.GetCommentByID(ctx, ctx.PathParamInt64("id")) + comment, err := issues_model.GetCommentByID(ctx, ctx.PathParamInt64(":id")) if err != nil { if issues_model.IsErrCommentNotExist(err) { ctx.NotFound(err) @@ -696,7 +696,7 @@ func DeleteIssueCommentDeprecated(ctx *context.APIContext) { } func deleteIssueComment(ctx *context.APIContext) { - comment, err := issues_model.GetCommentByID(ctx, ctx.PathParamInt64("id")) + comment, err := issues_model.GetCommentByID(ctx, ctx.PathParamInt64(":id")) if err != nil { if issues_model.IsErrCommentNotExist(err) { ctx.NotFound(err) diff --git a/routers/api/v1/repo/issue_dependency.go b/routers/api/v1/repo/issue_dependency.go index 19dcf999b804b..ae7502c661b51 100644 --- a/routers/api/v1/repo/issue_dependency.go +++ b/routers/api/v1/repo/issue_dependency.go @@ -61,7 +61,7 @@ func GetIssueDependencies(ctx *context.APIContext) { return } - issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) + issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) if err != nil { if issues_model.IsErrIssueNotExist(err) { ctx.NotFound("IsErrIssueNotExist", err) @@ -499,7 +499,7 @@ func RemoveIssueBlocking(ctx *context.APIContext) { } func getParamsIssue(ctx *context.APIContext) *issues_model.Issue { - issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) + issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) if err != nil { if issues_model.IsErrIssueNotExist(err) { ctx.NotFound("IsErrIssueNotExist", err) diff --git a/routers/api/v1/repo/issue_label.go b/routers/api/v1/repo/issue_label.go index ee1a842bc619d..6507864a3c53f 100644 --- a/routers/api/v1/repo/issue_label.go +++ b/routers/api/v1/repo/issue_label.go @@ -47,7 +47,7 @@ func ListIssueLabels(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) + issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) if err != nil { if issues_model.IsErrIssueNotExist(err) { ctx.NotFound() @@ -163,7 +163,7 @@ func DeleteIssueLabel(ctx *context.APIContext) { // "422": // "$ref": "#/responses/validationError" - issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) + issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) if err != nil { if issues_model.IsErrIssueNotExist(err) { ctx.NotFound() @@ -178,7 +178,7 @@ func DeleteIssueLabel(ctx *context.APIContext) { return } - label, err := issues_model.GetLabelByID(ctx, ctx.PathParamInt64("id")) + label, err := issues_model.GetLabelByID(ctx, ctx.PathParamInt64(":id")) if err != nil { if issues_model.IsErrLabelNotExist(err) { ctx.Error(http.StatusUnprocessableEntity, "", err) @@ -285,7 +285,7 @@ func ClearIssueLabels(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) + issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) if err != nil { if issues_model.IsErrIssueNotExist(err) { ctx.NotFound() @@ -309,7 +309,7 @@ func ClearIssueLabels(ctx *context.APIContext) { } func prepareForReplaceOrAdd(ctx *context.APIContext, form api.IssueLabelsOption) (*issues_model.Issue, []*issues_model.Label, error) { - issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) + issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) if err != nil { if issues_model.IsErrIssueNotExist(err) { ctx.NotFound() diff --git a/routers/api/v1/repo/issue_pin.go b/routers/api/v1/repo/issue_pin.go index 388d4a3e99baa..0ef9033291591 100644 --- a/routers/api/v1/repo/issue_pin.go +++ b/routers/api/v1/repo/issue_pin.go @@ -41,7 +41,7 @@ func PinIssue(ctx *context.APIContext) { // "$ref": "#/responses/forbidden" // "404": // "$ref": "#/responses/notFound" - issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) + issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) if err != nil { if issues_model.IsErrIssueNotExist(err) { ctx.NotFound() @@ -98,7 +98,7 @@ func UnpinIssue(ctx *context.APIContext) { // "$ref": "#/responses/forbidden" // "404": // "$ref": "#/responses/notFound" - issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) + issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) if err != nil { if issues_model.IsErrIssueNotExist(err) { ctx.NotFound() @@ -159,7 +159,7 @@ func MoveIssuePin(ctx *context.APIContext) { // "$ref": "#/responses/forbidden" // "404": // "$ref": "#/responses/notFound" - issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) + issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) if err != nil { if issues_model.IsErrIssueNotExist(err) { ctx.NotFound() @@ -169,7 +169,7 @@ func MoveIssuePin(ctx *context.APIContext) { return } - err = issue.MovePin(ctx, int(ctx.PathParamInt64("position"))) + err = issue.MovePin(ctx, int(ctx.PathParamInt64(":position"))) if err != nil { ctx.Error(http.StatusInternalServerError, "MovePin", err) return diff --git a/routers/api/v1/repo/issue_reaction.go b/routers/api/v1/repo/issue_reaction.go index ead86a717fae2..8d43cd518bcc3 100644 --- a/routers/api/v1/repo/issue_reaction.go +++ b/routers/api/v1/repo/issue_reaction.go @@ -51,7 +51,7 @@ func GetIssueCommentReactions(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - comment, err := issues_model.GetCommentByID(ctx, ctx.PathParamInt64("id")) + comment, err := issues_model.GetCommentByID(ctx, ctx.PathParamInt64(":id")) if err != nil { if issues_model.IsErrCommentNotExist(err) { ctx.NotFound(err) @@ -188,7 +188,7 @@ func DeleteIssueCommentReaction(ctx *context.APIContext) { } func changeIssueCommentReaction(ctx *context.APIContext, form api.EditReactionOption, isCreateType bool) { - comment, err := issues_model.GetCommentByID(ctx, ctx.PathParamInt64("id")) + comment, err := issues_model.GetCommentByID(ctx, ctx.PathParamInt64(":id")) if err != nil { if issues_model.IsErrCommentNotExist(err) { ctx.NotFound(err) @@ -295,7 +295,7 @@ func GetIssueReactions(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - issue, err := issues_model.GetIssueWithAttrsByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) + issue, err := issues_model.GetIssueWithAttrsByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) if err != nil { if issues_model.IsErrIssueNotExist(err) { ctx.NotFound() @@ -419,7 +419,7 @@ func DeleteIssueReaction(ctx *context.APIContext) { } func changeIssueReaction(ctx *context.APIContext, form api.EditReactionOption, isCreateType bool) { - issue, err := issues_model.GetIssueWithAttrsByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) + issue, err := issues_model.GetIssueWithAttrsByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) if err != nil { if issues_model.IsErrIssueNotExist(err) { ctx.NotFound() diff --git a/routers/api/v1/repo/issue_stopwatch.go b/routers/api/v1/repo/issue_stopwatch.go index e7fba6d0ed183..4605ae211030b 100644 --- a/routers/api/v1/repo/issue_stopwatch.go +++ b/routers/api/v1/repo/issue_stopwatch.go @@ -161,7 +161,7 @@ func DeleteIssueStopwatch(ctx *context.APIContext) { } func prepareIssueStopwatch(ctx *context.APIContext, shouldExist bool) (*issues_model.Issue, error) { - issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) + issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) if err != nil { if issues_model.IsErrIssueNotExist(err) { ctx.NotFound() diff --git a/routers/api/v1/repo/issue_subscription.go b/routers/api/v1/repo/issue_subscription.go index 4fb80b1ec4f35..e51baad0b6c5a 100644 --- a/routers/api/v1/repo/issue_subscription.go +++ b/routers/api/v1/repo/issue_subscription.go @@ -104,7 +104,7 @@ func DelIssueSubscription(ctx *context.APIContext) { } func setIssueSubscription(ctx *context.APIContext, watch bool) { - issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) + issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) if err != nil { if issues_model.IsErrIssueNotExist(err) { ctx.NotFound() @@ -115,7 +115,7 @@ func setIssueSubscription(ctx *context.APIContext, watch bool) { return } - user, err := user_model.GetUserByName(ctx, ctx.PathParam("user")) + user, err := user_model.GetUserByName(ctx, ctx.PathParam(":user")) if err != nil { if user_model.IsErrUserNotExist(err) { ctx.NotFound() @@ -185,7 +185,7 @@ func CheckIssueSubscription(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) + issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) if err != nil { if issues_model.IsErrIssueNotExist(err) { ctx.NotFound() @@ -251,7 +251,7 @@ func GetIssueSubscribers(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) + issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) if err != nil { if issues_model.IsErrIssueNotExist(err) { ctx.NotFound() diff --git a/routers/api/v1/repo/issue_tracked_time.go b/routers/api/v1/repo/issue_tracked_time.go index 57961b0660548..8d5e9fdad4113 100644 --- a/routers/api/v1/repo/issue_tracked_time.go +++ b/routers/api/v1/repo/issue_tracked_time.go @@ -75,7 +75,7 @@ func ListTrackedTimes(ctx *context.APIContext) { ctx.NotFound("Timetracker is disabled") return } - issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) + issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) if err != nil { if issues_model.IsErrIssueNotExist(err) { ctx.NotFound(err) @@ -181,7 +181,7 @@ func AddTime(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" form := web.GetForm(ctx).(*api.AddTimeOption) - issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) + issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) if err != nil { if issues_model.IsErrIssueNotExist(err) { ctx.NotFound(err) @@ -264,7 +264,7 @@ func ResetIssueTime(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) + issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) if err != nil { if issues_model.IsErrIssueNotExist(err) { ctx.NotFound(err) @@ -337,7 +337,7 @@ func DeleteTime(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) + issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) if err != nil { if issues_model.IsErrIssueNotExist(err) { ctx.NotFound(err) @@ -356,7 +356,7 @@ func DeleteTime(ctx *context.APIContext) { return } - time, err := issues_model.GetTrackedTimeByID(ctx, ctx.PathParamInt64("id")) + time, err := issues_model.GetTrackedTimeByID(ctx, ctx.PathParamInt64(":id")) if err != nil { if db.IsErrNotExist(err) { ctx.NotFound(err) @@ -422,7 +422,7 @@ func ListTrackedTimesByUser(ctx *context.APIContext) { ctx.Error(http.StatusBadRequest, "", "time tracking disabled") return } - user, err := user_model.GetUserByName(ctx, ctx.PathParam("timetrackingusername")) + user, err := user_model.GetUserByName(ctx, ctx.PathParam(":timetrackingusername")) if err != nil { if user_model.IsErrUserNotExist(err) { ctx.NotFound(err) diff --git a/routers/api/v1/repo/key.go b/routers/api/v1/repo/key.go index 23cc9226287d2..e5115980eb0a9 100644 --- a/routers/api/v1/repo/key.go +++ b/routers/api/v1/repo/key.go @@ -143,7 +143,7 @@ func GetDeployKey(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - key, err := asymkey_model.GetDeployKeyByID(ctx, ctx.PathParamInt64("id")) + key, err := asymkey_model.GetDeployKeyByID(ctx, ctx.PathParamInt64(":id")) if err != nil { if asymkey_model.IsErrDeployKeyNotExist(err) { ctx.NotFound() @@ -279,7 +279,7 @@ func DeleteDeploykey(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - if err := asymkey_service.DeleteDeployKey(ctx, ctx.Repo.Repository, ctx.PathParamInt64("id")); err != nil { + if err := asymkey_service.DeleteDeployKey(ctx, ctx.Doer, ctx.PathParamInt64(":id")); err != nil { if asymkey_model.IsErrKeyAccessDenied(err) { ctx.Error(http.StatusForbidden, "", "You do not have access to this key") } else { diff --git a/routers/api/v1/repo/label.go b/routers/api/v1/repo/label.go index 1ece2521e0f1f..c2c43db6a4a04 100644 --- a/routers/api/v1/repo/label.go +++ b/routers/api/v1/repo/label.go @@ -99,7 +99,7 @@ func GetLabel(ctx *context.APIContext) { l *issues_model.Label err error ) - strID := ctx.PathParam("id") + strID := ctx.PathParam(":id") if intID, err2 := strconv.ParseInt(strID, 10, 64); err2 != nil { l, err = issues_model.GetLabelInRepoByName(ctx, ctx.Repo.Repository.ID, strID) } else { @@ -212,7 +212,7 @@ func EditLabel(ctx *context.APIContext) { // "$ref": "#/responses/validationError" form := web.GetForm(ctx).(*api.EditLabelOption) - l, err := issues_model.GetLabelInRepoByID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("id")) + l, err := issues_model.GetLabelInRepoByID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":id")) if err != nil { if issues_model.IsErrRepoLabelNotExist(err) { ctx.NotFound() @@ -276,7 +276,7 @@ func DeleteLabel(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - if err := issues_model.DeleteLabel(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("id")); err != nil { + if err := issues_model.DeleteLabel(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":id")); err != nil { ctx.Error(http.StatusInternalServerError, "DeleteLabel", err) return } diff --git a/routers/api/v1/repo/migrate.go b/routers/api/v1/repo/migrate.go index 452825c0a3ef1..dcbd8f3dd50aa 100644 --- a/routers/api/v1/repo/migrate.go +++ b/routers/api/v1/repo/migrate.go @@ -10,13 +10,13 @@ import ( "net/http" "strings" + "code.gitea.io/gitea/models" "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" 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/graceful" "code.gitea.io/gitea/modules/lfs" "code.gitea.io/gitea/modules/log" @@ -27,6 +27,7 @@ import ( "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/convert" + "code.gitea.io/gitea/services/forms" "code.gitea.io/gitea/services/migrations" notify_service "code.gitea.io/gitea/services/notify" repo_service "code.gitea.io/gitea/services/repository" @@ -103,7 +104,7 @@ func Migrate(ctx *context.APIContext) { } } - remoteAddr, err := git.ParseRemoteAddr(form.CloneAddr, form.AuthUsername, form.AuthPassword) + remoteAddr, err := forms.ParseRemoteAddr(form.CloneAddr, form.AuthUsername, form.AuthPassword) if err == nil { err = migrations.IsMigrateURLAllowed(remoteAddr, ctx.Doer) } @@ -236,7 +237,7 @@ func handleMigrateError(ctx *context.APIContext, repoOwner *user_model.User, err ctx.Error(http.StatusUnprocessableEntity, "", fmt.Sprintf("The username '%s' contains invalid characters.", err.(db.ErrNameCharsNotAllowed).Name)) case db.IsErrNamePatternNotAllowed(err): ctx.Error(http.StatusUnprocessableEntity, "", fmt.Sprintf("The pattern '%s' is not allowed in a username.", err.(db.ErrNamePatternNotAllowed).Pattern)) - case git.IsErrInvalidCloneAddr(err): + case models.IsErrInvalidCloneAddr(err): ctx.Error(http.StatusUnprocessableEntity, "", err) case base.IsErrNotSupported(err): ctx.Error(http.StatusUnprocessableEntity, "", err) @@ -255,8 +256,8 @@ func handleMigrateError(ctx *context.APIContext, repoOwner *user_model.User, err } func handleRemoteAddrError(ctx *context.APIContext, err error) { - if git.IsErrInvalidCloneAddr(err) { - addrErr := err.(*git.ErrInvalidCloneAddr) + if models.IsErrInvalidCloneAddr(err) { + addrErr := err.(*models.ErrInvalidCloneAddr) switch { case addrErr.IsURLError: ctx.Error(http.StatusUnprocessableEntity, "", err) diff --git a/routers/api/v1/repo/milestone.go b/routers/api/v1/repo/milestone.go index 8d7516491ed33..78907c85a5847 100644 --- a/routers/api/v1/repo/milestone.go +++ b/routers/api/v1/repo/milestone.go @@ -280,7 +280,7 @@ func DeleteMilestone(ctx *context.APIContext) { // getMilestoneByIDOrName get milestone by ID and if not available by name func getMilestoneByIDOrName(ctx *context.APIContext) *issues_model.Milestone { - mile := ctx.PathParam("id") + mile := ctx.PathParam(":id") mileID, _ := strconv.ParseInt(mile, 0, 64) if mileID != 0 { diff --git a/routers/api/v1/repo/mirror.go b/routers/api/v1/repo/mirror.go index c911f6830cb95..310b82881eac0 100644 --- a/routers/api/v1/repo/mirror.go +++ b/routers/api/v1/repo/mirror.go @@ -9,10 +9,10 @@ import ( "net/http" "time" + "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" - "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" @@ -20,6 +20,7 @@ import ( "code.gitea.io/gitea/routers/api/v1/utils" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/convert" + "code.gitea.io/gitea/services/forms" "code.gitea.io/gitea/services/migrations" mirror_service "code.gitea.io/gitea/services/mirror" ) @@ -223,7 +224,7 @@ func GetPushMirrorByName(ctx *context.APIContext) { return } - mirrorName := ctx.PathParam("name") + mirrorName := ctx.PathParam(":name") // Get push mirror of a specific repo by remoteName pushMirror, exist, err := db.Get[repo_model.PushMirror](ctx, repo_model.PushMirrorOptions{ RepoID: ctx.Repo.Repository.ID, @@ -324,7 +325,7 @@ func DeletePushMirrorByRemoteName(ctx *context.APIContext) { return } - remoteName := ctx.PathParam("name") + remoteName := ctx.PathParam(":name") // Delete push mirror on repo by name. err := repo_model.DeletePushMirrors(ctx, repo_model.PushMirrorOptions{RepoID: ctx.Repo.Repository.ID, RemoteName: remoteName}) if err != nil { @@ -343,7 +344,7 @@ func CreatePushMirror(ctx *context.APIContext, mirrorOption *api.CreatePushMirro return } - address, err := git.ParseRemoteAddr(mirrorOption.RemoteAddress, mirrorOption.RemoteUsername, mirrorOption.RemotePassword) + address, err := forms.ParseRemoteAddr(mirrorOption.RemoteAddress, mirrorOption.RemoteUsername, mirrorOption.RemotePassword) if err == nil { err = migrations.IsMigrateURLAllowed(address, ctx.ContextUser) } @@ -396,8 +397,8 @@ func CreatePushMirror(ctx *context.APIContext, mirrorOption *api.CreatePushMirro } func HandleRemoteAddressError(ctx *context.APIContext, err error) { - if git.IsErrInvalidCloneAddr(err) { - addrErr := err.(*git.ErrInvalidCloneAddr) + if models.IsErrInvalidCloneAddr(err) { + addrErr := err.(*models.ErrInvalidCloneAddr) switch { case addrErr.IsProtocolInvalid: ctx.Error(http.StatusBadRequest, "CreatePushMirror", "Invalid mirror protocol") diff --git a/routers/api/v1/repo/notes.go b/routers/api/v1/repo/notes.go index 8fec844cc44ec..8689d25e157a2 100644 --- a/routers/api/v1/repo/notes.go +++ b/routers/api/v1/repo/notes.go @@ -52,7 +52,7 @@ func GetNote(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - sha := ctx.PathParam("sha") + sha := ctx.PathParam(":sha") if !git.IsValidRefPattern(sha) { ctx.Error(http.StatusUnprocessableEntity, "no valid ref or sha", fmt.Sprintf("no valid ref or sha: %s", sha)) return diff --git a/routers/api/v1/repo/patch.go b/routers/api/v1/repo/patch.go index 5e24dcf891294..0e0601b7d93e7 100644 --- a/routers/api/v1/repo/patch.go +++ b/routers/api/v1/repo/patch.go @@ -7,13 +7,13 @@ import ( "net/http" "time" + "code.gitea.io/gitea/models" git_model "code.gitea.io/gitea/models/git" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/modules/git" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/services/context" - pull_service "code.gitea.io/gitea/services/pull" "code.gitea.io/gitea/services/repository/files" ) @@ -92,12 +92,12 @@ func ApplyDiffPatch(ctx *context.APIContext) { fileResponse, err := files.ApplyDiffPatch(ctx, ctx.Repo.Repository, ctx.Doer, opts) if err != nil { - if files.IsErrUserCannotCommit(err) || pull_service.IsErrFilePathProtected(err) { + if models.IsErrUserCannotCommit(err) || models.IsErrFilePathProtected(err) { ctx.Error(http.StatusForbidden, "Access", err) return } - if git_model.IsErrBranchAlreadyExists(err) || files.IsErrFilenameInvalid(err) || pull_service.IsErrSHADoesNotMatch(err) || - files.IsErrFilePathInvalid(err) || files.IsErrRepoFileAlreadyExists(err) { + if git_model.IsErrBranchAlreadyExists(err) || models.IsErrFilenameInvalid(err) || models.IsErrSHADoesNotMatch(err) || + models.IsErrFilePathInvalid(err) || models.IsErrRepoFileAlreadyExists(err) { ctx.Error(http.StatusUnprocessableEntity, "Invalid", err) return } diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go index 06b2b60d9ba94..f8913cbb2fd35 100644 --- a/routers/api/v1/repo/pull.go +++ b/routers/api/v1/repo/pull.go @@ -12,6 +12,7 @@ import ( "strings" "time" + "code.gitea.io/gitea/models" activities_model "code.gitea.io/gitea/models/activities" git_model "code.gitea.io/gitea/models/git" issues_model "code.gitea.io/gitea/models/issues" @@ -179,7 +180,7 @@ func GetPullRequest(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) + pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) if err != nil { if issues_model.IsErrPullRequestNotExist(err) { ctx.NotFound() @@ -264,7 +265,7 @@ func GetPullRequestByBaseHead(ctx *context.APIContext) { headBranch = head } - pr, err := issues_model.GetPullRequestByBaseHeadInfo(ctx, ctx.Repo.Repository.ID, headRepoID, ctx.PathParam("base"), headBranch) + pr, err := issues_model.GetPullRequestByBaseHeadInfo(ctx, ctx.Repo.Repository.ID, headRepoID, ctx.PathParam(":base"), headBranch) if err != nil { if issues_model.IsErrPullRequestNotExist(err) { ctx.NotFound() @@ -324,7 +325,7 @@ func DownloadPullDiffOrPatch(ctx *context.APIContext) { // "$ref": "#/responses/string" // "404": // "$ref": "#/responses/notFound" - pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) + pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) if err != nil { if issues_model.IsErrPullRequestNotExist(err) { ctx.NotFound() @@ -334,7 +335,7 @@ func DownloadPullDiffOrPatch(ctx *context.APIContext) { return } var patch bool - if ctx.PathParam("diffType") == "diff" { + if ctx.PathParam(":diffType") == "diff" { patch = false } else { patch = true @@ -603,7 +604,7 @@ func EditPullRequest(ctx *context.APIContext) { // "$ref": "#/responses/validationError" form := web.GetForm(ctx).(*api.EditPullRequestOption) - pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) + pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) if err != nil { if issues_model.IsErrPullRequestNotExist(err) { ctx.NotFound() @@ -693,6 +694,11 @@ func EditPullRequest(ctx *context.APIContext) { issue.MilestoneID != form.Milestone { oldMilestoneID := issue.MilestoneID issue.MilestoneID = form.Milestone + issue.Milestone, err = issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, form.Milestone) + if err != nil { + ctx.Error(http.StatusInternalServerError, "GetMilestoneByRepoID", err) + return + } if err = issue_service.ChangeMilestoneAssign(ctx, issue, ctx.Doer, oldMilestoneID); err != nil { ctx.Error(http.StatusInternalServerError, "ChangeMilestoneAssign", err) return @@ -728,11 +734,27 @@ func EditPullRequest(ctx *context.APIContext) { return } - state := api.StateType(*form.State) - closeOrReopenIssue(ctx, issue, state) - if ctx.Written() { + var isClosed bool + switch state := api.StateType(*form.State); state { + case api.StateOpen: + isClosed = false + case api.StateClosed: + isClosed = true + default: + ctx.Error(http.StatusPreconditionFailed, "UnknownPRStateError", fmt.Sprintf("unknown state: %s", state)) return } + + if issue.IsClosed != isClosed { + if err := issue_service.ChangeStatus(ctx, issue, ctx.Doer, "", isClosed); err != nil { + if issues_model.IsErrDependenciesLeft(err) { + ctx.Error(http.StatusPreconditionFailed, "DependenciesLeft", "cannot close this pull request because it still has open dependencies") + return + } + ctx.Error(http.StatusInternalServerError, "ChangeStatus", err) + return + } + } } // change pull target branch @@ -748,7 +770,7 @@ func EditPullRequest(ctx *context.APIContext) { } else if issues_model.IsErrIssueIsClosed(err) { ctx.Error(http.StatusUnprocessableEntity, "IsErrIssueIsClosed", err) return - } else if pull_service.IsErrPullRequestHasMerged(err) { + } else if models.IsErrPullRequestHasMerged(err) { ctx.Error(http.StatusConflict, "IsErrPullRequestHasMerged", err) return } @@ -815,7 +837,7 @@ func IsPullRequestMerged(ctx *context.APIContext) { // "404": // description: pull request has not been merged - pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) + pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) if err != nil { if issues_model.IsErrPullRequestNotExist(err) { ctx.NotFound() @@ -873,7 +895,7 @@ func MergePullRequest(ctx *context.APIContext) { form := web.GetForm(ctx).(*forms.MergePullRequestForm) - pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) + pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) if err != nil { if issues_model.IsErrPullRequestNotExist(err) { ctx.NotFound("GetPullRequestByIndex", err) @@ -924,7 +946,7 @@ func MergePullRequest(ctx *context.APIContext) { ctx.Error(http.StatusMethodNotAllowed, "PR is a work in progress", "Work in progress PRs cannot be merged") } else if errors.Is(err, pull_service.ErrNotMergeableState) { ctx.Error(http.StatusMethodNotAllowed, "PR not in mergeable state", "Please try again later") - } else if pull_service.IsErrDisallowedToMerge(err) { + } else if models.IsErrDisallowedToMerge(err) { ctx.Error(http.StatusMethodNotAllowed, "PR is not ready to be merged", err) } else if asymkey_service.IsErrWontSign(err) { ctx.Error(http.StatusMethodNotAllowed, fmt.Sprintf("Protected branch %s requires signed commits but this merge would not be signed", pr.BaseBranch), err) @@ -937,7 +959,7 @@ func MergePullRequest(ctx *context.APIContext) { // handle manually-merged mark if manuallyMerged { if err := pull_service.MergedManually(ctx, pr, ctx.Doer, ctx.Repo.GitRepo, form.MergeCommitID); err != nil { - if pull_service.IsErrInvalidMergeStyle(err) { + if models.IsErrInvalidMergeStyle(err) { ctx.Error(http.StatusMethodNotAllowed, "Invalid merge style", fmt.Errorf("%s is not allowed an allowed merge style for this repository", repo_model.MergeStyle(form.Do))) return } @@ -987,20 +1009,20 @@ func MergePullRequest(ctx *context.APIContext) { } if err := pull_service.Merge(ctx, pr, ctx.Doer, ctx.Repo.GitRepo, repo_model.MergeStyle(form.Do), form.HeadCommitID, message, false); err != nil { - if pull_service.IsErrInvalidMergeStyle(err) { + if models.IsErrInvalidMergeStyle(err) { ctx.Error(http.StatusMethodNotAllowed, "Invalid merge style", fmt.Errorf("%s is not allowed an allowed merge style for this repository", repo_model.MergeStyle(form.Do))) - } else if pull_service.IsErrMergeConflicts(err) { - conflictError := err.(pull_service.ErrMergeConflicts) + } else if models.IsErrMergeConflicts(err) { + conflictError := err.(models.ErrMergeConflicts) ctx.JSON(http.StatusConflict, conflictError) - } else if pull_service.IsErrRebaseConflicts(err) { - conflictError := err.(pull_service.ErrRebaseConflicts) + } else if models.IsErrRebaseConflicts(err) { + conflictError := err.(models.ErrRebaseConflicts) ctx.JSON(http.StatusConflict, conflictError) - } else if pull_service.IsErrMergeUnrelatedHistories(err) { - conflictError := err.(pull_service.ErrMergeUnrelatedHistories) + } else if models.IsErrMergeUnrelatedHistories(err) { + conflictError := err.(models.ErrMergeUnrelatedHistories) ctx.JSON(http.StatusConflict, conflictError) } else if git.IsErrPushOutOfDate(err) { ctx.Error(http.StatusConflict, "Merge", "merge push out of date") - } else if pull_service.IsErrSHADoesNotMatch(err) { + } else if models.IsErrSHADoesNotMatch(err) { ctx.Error(http.StatusConflict, "Merge", "head out of date") } else if git.IsErrPushRejected(err) { errPushRej := err.(*git.ErrPushRejected) @@ -1240,7 +1262,7 @@ func UpdatePullRequest(ctx *context.APIContext) { // "422": // "$ref": "#/responses/validationError" - pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) + pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) if err != nil { if issues_model.IsErrPullRequestNotExist(err) { ctx.NotFound() @@ -1291,10 +1313,10 @@ func UpdatePullRequest(ctx *context.APIContext) { message := fmt.Sprintf("Merge branch '%s' into %s", pr.BaseBranch, pr.HeadBranch) if err = pull_service.Update(ctx, pr, ctx.Doer, message, rebase, repo_model.MergeStrategyDefault, repo_model.MergeStrategyOptionNone); err != nil { - if pull_service.IsErrMergeConflicts(err) { + if models.IsErrMergeConflicts(err) { ctx.Error(http.StatusConflict, "Update", "merge failed because of conflict") return - } else if pull_service.IsErrRebaseConflicts(err) { + } else if models.IsErrRebaseConflicts(err) { ctx.Error(http.StatusConflict, "Update", "rebase failed because of conflict") return } @@ -1339,7 +1361,7 @@ func CancelScheduledAutoMerge(ctx *context.APIContext) { // "423": // "$ref": "#/responses/repoArchivedError" - pullIndex := ctx.PathParamInt64("index") + pullIndex := ctx.PathParamInt64(":index") pull, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, pullIndex) if err != nil { if issues_model.IsErrPullRequestNotExist(err) { @@ -1425,7 +1447,7 @@ func GetPullRequestCommits(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) + pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) if err != nil { if issues_model.IsErrPullRequestNotExist(err) { ctx.NotFound() @@ -1548,7 +1570,7 @@ func GetPullRequestFiles(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) + pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) if err != nil { if issues_model.IsErrPullRequestNotExist(err) { ctx.NotFound() @@ -1621,7 +1643,9 @@ func GetPullRequestFiles(ctx *context.APIContext) { apiFiles := make([]*api.ChangedFile, 0, limit) for i := start; i < start+limit; i++ { - apiFiles = append(apiFiles, convert.ToChangedFile(diff.Files[i], pr.HeadRepo, endCommitID)) + // refs/pull/1/head stores the HEAD commit ID, allowing all related commits to be found in the base repository. + // The head repository might have been deleted, so we should not rely on it here. + apiFiles = append(apiFiles, convert.ToChangedFile(diff.Files[i], pr.BaseRepo, endCommitID)) } ctx.SetLinkHeader(totalNumberOfFiles, listOptions.PageSize) diff --git a/routers/api/v1/repo/pull_review.go b/routers/api/v1/repo/pull_review.go index 6d7a326370c08..def860eee8fc7 100644 --- a/routers/api/v1/repo/pull_review.go +++ b/routers/api/v1/repo/pull_review.go @@ -61,7 +61,7 @@ func ListPullReviews(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) + pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) if err != nil { if issues_model.IsErrPullRequestNotExist(err) { ctx.NotFound("GetPullRequestByIndex", err) @@ -306,7 +306,7 @@ func CreatePullReview(ctx *context.APIContext) { // "$ref": "#/responses/validationError" opts := web.GetForm(ctx).(*api.CreatePullReviewOptions) - pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) + pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) if err != nil { if issues_model.IsErrPullRequestNotExist(err) { ctx.NotFound("GetPullRequestByIndex", err) @@ -533,7 +533,7 @@ func preparePullReviewType(ctx *context.APIContext, pr *issues_model.PullRequest // prepareSingleReview return review, related pull and false or nil, nil and true if an error happen func prepareSingleReview(ctx *context.APIContext) (*issues_model.Review, *issues_model.PullRequest, bool) { - pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) + pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) if err != nil { if issues_model.IsErrPullRequestNotExist(err) { ctx.NotFound("GetPullRequestByIndex", err) @@ -543,7 +543,7 @@ func prepareSingleReview(ctx *context.APIContext) (*issues_model.Review, *issues return nil, nil, true } - review, err := issues_model.GetReviewByID(ctx, ctx.PathParamInt64("id")) + review, err := issues_model.GetReviewByID(ctx, ctx.PathParamInt64(":id")) if err != nil { if issues_model.IsErrReviewNotExist(err) { ctx.NotFound("GetReviewByID", err) @@ -698,7 +698,7 @@ func parseReviewersByNames(ctx *context.APIContext, reviewerNames, teamReviewerN } func apiReviewRequest(ctx *context.APIContext, opts api.PullReviewRequestOptions, isAdd bool) { - pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) + pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) if err != nil { if issues_model.IsErrPullRequestNotExist(err) { ctx.NotFound("GetPullRequestByIndex", err) diff --git a/routers/api/v1/repo/release.go b/routers/api/v1/repo/release.go index 076f00f1d1429..478bdbd797a8e 100644 --- a/routers/api/v1/repo/release.go +++ b/routers/api/v1/repo/release.go @@ -7,6 +7,7 @@ import ( "fmt" "net/http" + "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/perm" repo_model "code.gitea.io/gitea/models/repo" @@ -50,7 +51,7 @@ func GetRelease(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - id := ctx.PathParamInt64("id") + id := ctx.PathParamInt64(":id") release, err := repo_model.GetReleaseForRepoByID(ctx, ctx.Repo.Repository.ID, id) if err != nil && !repo_model.IsErrReleaseNotExist(err) { ctx.Error(http.StatusInternalServerError, "GetReleaseForRepoByID", err) @@ -249,7 +250,7 @@ func CreateRelease(ctx *context.APIContext) { if err := release_service.CreateRelease(ctx.Repo.GitRepo, rel, nil, ""); err != nil { if repo_model.IsErrReleaseAlreadyExist(err) { ctx.Error(http.StatusConflict, "ReleaseAlreadyExist", err) - } else if release_service.IsErrProtectedTagName(err) { + } else if models.IsErrProtectedTagName(err) { ctx.Error(http.StatusUnprocessableEntity, "ProtectedTagName", err) } else if git.IsErrNotExist(err) { ctx.Error(http.StatusNotFound, "ErrNotExist", fmt.Errorf("target \"%v\" not found: %w", rel.Target, err)) @@ -319,7 +320,7 @@ func EditRelease(ctx *context.APIContext) { // "$ref": "#/responses/notFound" form := web.GetForm(ctx).(*api.EditReleaseOption) - id := ctx.PathParamInt64("id") + id := ctx.PathParamInt64(":id") rel, err := repo_model.GetReleaseForRepoByID(ctx, ctx.Repo.Repository.ID, id) if err != nil && !repo_model.IsErrReleaseNotExist(err) { ctx.Error(http.StatusInternalServerError, "GetReleaseForRepoByID", err) @@ -396,7 +397,7 @@ func DeleteRelease(ctx *context.APIContext) { // "422": // "$ref": "#/responses/validationError" - id := ctx.PathParamInt64("id") + id := ctx.PathParamInt64(":id") rel, err := repo_model.GetReleaseForRepoByID(ctx, ctx.Repo.Repository.ID, id) if err != nil && !repo_model.IsErrReleaseNotExist(err) { ctx.Error(http.StatusInternalServerError, "GetReleaseForRepoByID", err) @@ -407,7 +408,7 @@ func DeleteRelease(ctx *context.APIContext) { return } if err := release_service.DeleteReleaseByID(ctx, ctx.Repo.Repository, rel, ctx.Doer, false); err != nil { - if release_service.IsErrProtectedTagName(err) { + if models.IsErrProtectedTagName(err) { ctx.Error(http.StatusUnprocessableEntity, "delTag", "user not allowed to delete protected tag") return } diff --git a/routers/api/v1/repo/release_attachment.go b/routers/api/v1/repo/release_attachment.go index 54ca1fc8431d1..ed6cc8e1eaf3b 100644 --- a/routers/api/v1/repo/release_attachment.go +++ b/routers/api/v1/repo/release_attachment.go @@ -72,12 +72,12 @@ func GetReleaseAttachment(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - releaseID := ctx.PathParamInt64("id") + releaseID := ctx.PathParamInt64(":id") if !checkReleaseMatchRepo(ctx, releaseID) { return } - attachID := ctx.PathParamInt64("attachment_id") + attachID := ctx.PathParamInt64(":attachment_id") attach, err := repo_model.GetAttachmentByID(ctx, attachID) if err != nil { if repo_model.IsErrAttachmentNotExist(err) { @@ -126,7 +126,7 @@ func ListReleaseAttachments(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - releaseID := ctx.PathParamInt64("id") + releaseID := ctx.PathParamInt64(":id") release, err := repo_model.GetReleaseByID(ctx, releaseID) if err != nil { if repo_model.IsErrReleaseNotExist(err) { @@ -199,7 +199,7 @@ func CreateReleaseAttachment(ctx *context.APIContext) { } // Check if release exists an load release - releaseID := ctx.PathParamInt64("id") + releaseID := ctx.PathParamInt64(":id") if !checkReleaseMatchRepo(ctx, releaseID) { return } @@ -299,12 +299,12 @@ func EditReleaseAttachment(ctx *context.APIContext) { form := web.GetForm(ctx).(*api.EditAttachmentOptions) // Check if release exists an load release - releaseID := ctx.PathParamInt64("id") + releaseID := ctx.PathParamInt64(":id") if !checkReleaseMatchRepo(ctx, releaseID) { return } - attachID := ctx.PathParamInt64("attachment_id") + attachID := ctx.PathParamInt64(":attachment_id") attach, err := repo_model.GetAttachmentByID(ctx, attachID) if err != nil { if repo_model.IsErrAttachmentNotExist(err) { @@ -372,12 +372,12 @@ func DeleteReleaseAttachment(ctx *context.APIContext) { // "$ref": "#/responses/notFound" // Check if release exists an load release - releaseID := ctx.PathParamInt64("id") + releaseID := ctx.PathParamInt64(":id") if !checkReleaseMatchRepo(ctx, releaseID) { return } - attachID := ctx.PathParamInt64("attachment_id") + attachID := ctx.PathParamInt64(":attachment_id") attach, err := repo_model.GetAttachmentByID(ctx, attachID) if err != nil { if repo_model.IsErrAttachmentNotExist(err) { diff --git a/routers/api/v1/repo/release_tags.go b/routers/api/v1/repo/release_tags.go index 7380c5231c55c..6df47af8d98e5 100644 --- a/routers/api/v1/repo/release_tags.go +++ b/routers/api/v1/repo/release_tags.go @@ -6,10 +6,11 @@ package repo import ( "net/http" + "code.gitea.io/gitea/models" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/convert" - release_service "code.gitea.io/gitea/services/release" + releaseservice "code.gitea.io/gitea/services/release" ) // GetReleaseByTag get a single release of a repository by tag name @@ -41,7 +42,7 @@ func GetReleaseByTag(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - tag := ctx.PathParam("tag") + tag := ctx.PathParam(":tag") release, err := repo_model.GetRelease(ctx, ctx.Repo.Repository.ID, tag) if err != nil { @@ -94,7 +95,7 @@ func DeleteReleaseByTag(ctx *context.APIContext) { // "422": // "$ref": "#/responses/validationError" - tag := ctx.PathParam("tag") + tag := ctx.PathParam(":tag") release, err := repo_model.GetRelease(ctx, ctx.Repo.Repository.ID, tag) if err != nil { @@ -111,8 +112,8 @@ func DeleteReleaseByTag(ctx *context.APIContext) { return } - if err = release_service.DeleteReleaseByID(ctx, ctx.Repo.Repository, release, ctx.Doer, false); err != nil { - if release_service.IsErrProtectedTagName(err) { + if err = releaseservice.DeleteReleaseByID(ctx, ctx.Repo.Repository, release, ctx.Doer, false); err != nil { + if models.IsErrProtectedTagName(err) { ctx.Error(http.StatusUnprocessableEntity, "delTag", "user not allowed to delete protected tag") return } diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go index a192e241b7be0..c1bfa5bcbbff9 100644 --- a/routers/api/v1/repo/repo.go +++ b/routers/api/v1/repo/repo.go @@ -12,7 +12,6 @@ import ( "strings" "time" - actions_model "code.gitea.io/gitea/models/actions" activities_model "code.gitea.io/gitea/models/activities" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/organization" @@ -495,7 +494,7 @@ func CreateOrgRepo(ctx *context.APIContext) { // "403": // "$ref": "#/responses/forbidden" opt := web.GetForm(ctx).(*api.CreateRepoOption) - org, err := organization.GetOrgByName(ctx, ctx.PathParam("org")) + org, err := organization.GetOrgByName(ctx, ctx.PathParam(":org")) if err != nil { if organization.IsErrOrgNotExist(err) { ctx.Error(http.StatusUnprocessableEntity, "", err) @@ -575,7 +574,7 @@ func GetByID(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - repo, err := repo_model.GetRepositoryByID(ctx, ctx.PathParamInt64("id")) + repo, err := repo_model.GetRepositoryByID(ctx, ctx.PathParamInt64(":id")) if err != nil { if repo_model.IsErrRepoNotExist(err) { ctx.NotFound() @@ -726,11 +725,12 @@ func updateBasicProperties(ctx *context.APIContext, opts api.EditRepoOption) err if ctx.Repo.GitRepo == nil && !repo.IsEmpty { var err error - ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx, repo) + ctx.Repo.GitRepo, err = gitrepo.OpenRepository(ctx, repo) if err != nil { ctx.Error(http.StatusInternalServerError, "Unable to OpenRepository", err) return err } + defer ctx.Repo.GitRepo.Close() } // Default branch only updated if changed and exist or the repository is empty @@ -1049,7 +1049,7 @@ func updateRepoArchivedState(ctx *context.APIContext, opts api.EditRepoOption) e ctx.Error(http.StatusInternalServerError, "ArchiveRepoState", err) return err } - if err := actions_model.CleanRepoScheduleTasks(ctx, repo); err != nil { + if err := actions_service.CleanRepoScheduleTasks(ctx, repo); err != nil { log.Error("CleanRepoScheduleTasks for archived repo %s/%s: %v", ctx.Repo.Owner.Name, repo.Name, err) } log.Trace("Repository was archived: %s/%s", ctx.Repo.Owner.Name, repo.Name) diff --git a/routers/api/v1/repo/tag.go b/routers/api/v1/repo/tag.go index 8447a8f1f223e..a72df78666e40 100644 --- a/routers/api/v1/repo/tag.go +++ b/routers/api/v1/repo/tag.go @@ -9,6 +9,7 @@ import ( "net/http" "strings" + "code.gitea.io/gitea/models" git_model "code.gitea.io/gitea/models/git" "code.gitea.io/gitea/models/organization" repo_model "code.gitea.io/gitea/models/repo" @@ -18,7 +19,7 @@ import ( "code.gitea.io/gitea/routers/api/v1/utils" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/convert" - release_service "code.gitea.io/gitea/services/release" + releaseservice "code.gitea.io/gitea/services/release" ) // ListTags list all the tags of a repository @@ -204,12 +205,12 @@ func CreateTag(ctx *context.APIContext) { return } - if err := release_service.CreateNewTag(ctx, ctx.Doer, ctx.Repo.Repository, commit.ID.String(), form.TagName, form.Message); err != nil { - if release_service.IsErrTagAlreadyExists(err) { + if err := releaseservice.CreateNewTag(ctx, ctx.Doer, ctx.Repo.Repository, commit.ID.String(), form.TagName, form.Message); err != nil { + if models.IsErrTagAlreadyExists(err) { ctx.Error(http.StatusConflict, "tag exist", err) return } - if release_service.IsErrProtectedTagName(err) { + if models.IsErrProtectedTagName(err) { ctx.Error(http.StatusUnprocessableEntity, "CreateNewTag", "user not allowed to create protected tag") return } @@ -279,8 +280,8 @@ func DeleteTag(ctx *context.APIContext) { return } - if err = release_service.DeleteReleaseByID(ctx, ctx.Repo.Repository, tag, ctx.Doer, true); err != nil { - if release_service.IsErrProtectedTagName(err) { + if err = releaseservice.DeleteReleaseByID(ctx, ctx.Repo.Repository, tag, ctx.Doer, true); err != nil { + if models.IsErrProtectedTagName(err) { ctx.Error(http.StatusUnprocessableEntity, "delTag", "user not allowed to delete protected tag") return } @@ -357,7 +358,7 @@ func GetTagProtection(ctx *context.APIContext) { // "$ref": "#/responses/notFound" repo := ctx.Repo.Repository - id := ctx.PathParamInt64("id") + id := ctx.PathParamInt64(":id") pt, err := git_model.GetProtectedTagByID(ctx, id) if err != nil { ctx.Error(http.StatusInternalServerError, "GetProtectedTagByID", err) @@ -521,7 +522,7 @@ func EditTagProtection(ctx *context.APIContext) { repo := ctx.Repo.Repository form := web.GetForm(ctx).(*api.EditTagProtectionOption) - id := ctx.PathParamInt64("id") + id := ctx.PathParamInt64(":id") pt, err := git_model.GetProtectedTagByID(ctx, id) if err != nil { ctx.Error(http.StatusInternalServerError, "GetProtectedTagByID", err) @@ -616,7 +617,7 @@ func DeleteTagProtection(ctx *context.APIContext) { // "$ref": "#/responses/notFound" repo := ctx.Repo.Repository - id := ctx.PathParamInt64("id") + id := ctx.PathParamInt64(":id") pt, err := git_model.GetProtectedTagByID(ctx, id) if err != nil { ctx.Error(http.StatusInternalServerError, "GetProtectedTagByID", err) diff --git a/routers/api/v1/repo/teams.go b/routers/api/v1/repo/teams.go index e5a2d5c320b03..82ecaf30201c7 100644 --- a/routers/api/v1/repo/teams.go +++ b/routers/api/v1/repo/teams.go @@ -42,7 +42,7 @@ func ListTeams(ctx *context.APIContext) { return } - teams, err := organization.GetRepoTeams(ctx, ctx.Repo.Repository.OwnerID, ctx.Repo.Repository.ID) + teams, err := organization.GetRepoTeams(ctx, ctx.Repo.Repository) if err != nil { ctx.InternalServerError(err) return @@ -221,7 +221,7 @@ func changeRepoTeam(ctx *context.APIContext, add bool) { } func getTeamByParam(ctx *context.APIContext) *organization.Team { - team, err := organization.GetTeam(ctx, ctx.Repo.Owner.ID, ctx.PathParam("team")) + team, err := organization.GetTeam(ctx, ctx.Repo.Owner.ID, ctx.PathParam(":team")) if err != nil { if organization.IsErrTeamNotExist(err) { ctx.Error(http.StatusNotFound, "TeamNotExit", err) diff --git a/routers/api/v1/repo/topic.go b/routers/api/v1/repo/topic.go index a1a15e7f46d7d..6b9eedf6e0ea5 100644 --- a/routers/api/v1/repo/topic.go +++ b/routers/api/v1/repo/topic.go @@ -162,7 +162,7 @@ func AddTopic(ctx *context.APIContext) { // "422": // "$ref": "#/responses/invalidTopicsError" - topicName := strings.TrimSpace(strings.ToLower(ctx.PathParam("topic"))) + topicName := strings.TrimSpace(strings.ToLower(ctx.PathParam(":topic"))) if !repo_model.ValidateTopic(topicName) { ctx.JSON(http.StatusUnprocessableEntity, map[string]any{ @@ -229,7 +229,7 @@ func DeleteTopic(ctx *context.APIContext) { // "422": // "$ref": "#/responses/invalidTopicsError" - topicName := strings.TrimSpace(strings.ToLower(ctx.PathParam("topic"))) + topicName := strings.TrimSpace(strings.ToLower(ctx.PathParam(":topic"))) if !repo_model.ValidateTopic(topicName) { ctx.JSON(http.StatusUnprocessableEntity, map[string]any{ diff --git a/routers/api/v1/repo/transfer.go b/routers/api/v1/repo/transfer.go index b2090cac41cad..776b336761ff8 100644 --- a/routers/api/v1/repo/transfer.go +++ b/routers/api/v1/repo/transfer.go @@ -8,6 +8,7 @@ import ( "fmt" "net/http" + "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/perm" access_model "code.gitea.io/gitea/models/perm/access" @@ -100,14 +101,14 @@ func Transfer(ctx *context.APIContext) { } if ctx.Repo.GitRepo != nil { - _ = ctx.Repo.GitRepo.Close() + ctx.Repo.GitRepo.Close() ctx.Repo.GitRepo = nil } oldFullname := ctx.Repo.Repository.FullName() if err := repo_service.StartRepositoryTransfer(ctx, ctx.Doer, newOwner, ctx.Repo.Repository, teams); err != nil { - if repo_model.IsErrRepoTransferInProgress(err) { + if models.IsErrRepoTransferInProgress(err) { ctx.Error(http.StatusConflict, "StartRepositoryTransfer", err) return } @@ -212,9 +213,9 @@ func RejectTransfer(ctx *context.APIContext) { } func acceptOrRejectRepoTransfer(ctx *context.APIContext, accept bool) error { - repoTransfer, err := repo_model.GetPendingRepositoryTransfer(ctx, ctx.Repo.Repository) + repoTransfer, err := models.GetPendingRepositoryTransfer(ctx, ctx.Repo.Repository) if err != nil { - if repo_model.IsErrNoPendingTransfer(err) { + if models.IsErrNoPendingTransfer(err) { ctx.NotFound() return nil } diff --git a/routers/api/v1/repo/tree.go b/routers/api/v1/repo/tree.go index 768e5d41c1e37..efb247c19e2fe 100644 --- a/routers/api/v1/repo/tree.go +++ b/routers/api/v1/repo/tree.go @@ -56,7 +56,7 @@ func GetTree(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - sha := ctx.PathParam("sha") + sha := ctx.PathParam(":sha") if len(sha) == 0 { ctx.Error(http.StatusBadRequest, "", "sha not provided") return diff --git a/routers/api/v1/repo/wiki.go b/routers/api/v1/repo/wiki.go index 352d8f48fc082..53dc948632ecf 100644 --- a/routers/api/v1/repo/wiki.go +++ b/routers/api/v1/repo/wiki.go @@ -136,7 +136,7 @@ func EditWikiPage(ctx *context.APIContext) { form := web.GetForm(ctx).(*api.CreateWikiPageOptions) - oldWikiName := wiki_service.WebPathFromRequest(ctx.PathParamRaw("pageName")) + oldWikiName := wiki_service.WebPathFromRequest(ctx.PathParamRaw(":pageName")) newWikiName := wiki_service.UserTitleToWebPath("", form.Title) if len(newWikiName) == 0 { @@ -193,7 +193,7 @@ func getWikiPage(ctx *context.APIContext, wikiName wiki_service.WebPath) *api.Wi } // get commit count - wiki revisions - commitsCount, _ := wikiRepo.FileCommitsCount("master", pageFilename) + commitsCount, _ := wikiRepo.FileCommitsCount(ctx.Repo.Repository.DefaultWikiBranch, pageFilename) // Get last change information. lastCommit, err := wikiRepo.GetCommitByPath(pageFilename) @@ -242,7 +242,7 @@ func DeleteWikiPage(ctx *context.APIContext) { // "423": // "$ref": "#/responses/repoArchivedError" - wikiName := wiki_service.WebPathFromRequest(ctx.PathParamRaw("pageName")) + wikiName := wiki_service.WebPathFromRequest(ctx.PathParamRaw(":pageName")) if err := wiki_service.DeleteWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, wikiName); err != nil { if err.Error() == "file does not exist" { @@ -370,7 +370,7 @@ func GetWikiPage(ctx *context.APIContext) { // "$ref": "#/responses/notFound" // get requested pagename - pageName := wiki_service.WebPathFromRequest(ctx.PathParamRaw("pageName")) + pageName := wiki_service.WebPathFromRequest(ctx.PathParamRaw(":pageName")) wikiPage := getWikiPage(ctx, pageName) if !ctx.Written() { @@ -420,7 +420,7 @@ func ListPageRevisions(ctx *context.APIContext) { } // get requested pagename - pageName := wiki_service.WebPathFromRequest(ctx.PathParamRaw("pageName")) + pageName := wiki_service.WebPathFromRequest(ctx.PathParamRaw(":pageName")) if len(pageName) == 0 { pageName = "Home" } @@ -432,7 +432,7 @@ func ListPageRevisions(ctx *context.APIContext) { } // get commit count - wiki revisions - commitsCount, _ := wikiRepo.FileCommitsCount("master", pageFilename) + commitsCount, _ := wikiRepo.FileCommitsCount(ctx.Repo.Repository.DefaultWikiBranch, pageFilename) page := ctx.FormInt("page") if page <= 1 { @@ -442,7 +442,7 @@ func ListPageRevisions(ctx *context.APIContext) { // get Commit Count commitsHistory, err := wikiRepo.CommitsByFileAndRange( git.CommitsByFileAndRangeOptions{ - Revision: "master", + Revision: ctx.Repo.Repository.DefaultWikiBranch, File: pageFilename, Page: page, }) @@ -486,7 +486,7 @@ func findWikiRepoCommit(ctx *context.APIContext) (*git.Repository, *git.Commit) return nil, nil } - commit, err := wikiRepo.GetBranchCommit("master") + commit, err := wikiRepo.GetBranchCommit(ctx.Repo.Repository.DefaultWikiBranch) if err != nil { if git.IsErrNotExist(err) { ctx.NotFound(err) diff --git a/routers/api/v1/swagger/repo.go b/routers/api/v1/swagger/repo.go index b9d2a0217cd4f..f754c80a5b3d5 100644 --- a/routers/api/v1/swagger/repo.go +++ b/routers/api/v1/swagger/repo.go @@ -448,3 +448,15 @@ type swaggerCompare struct { // in:body Body api.Compare `json:"body"` } + +// swagger:response MergeUpstreamRequest +type swaggerMergeUpstreamRequest struct { + // in:body + Body api.MergeUpstreamRequest `json:"body"` +} + +// swagger:response MergeUpstreamResponse +type swaggerMergeUpstreamResponse struct { + // in:body + Body api.MergeUpstreamResponse `json:"body"` +} diff --git a/routers/api/v1/user/action.go b/routers/api/v1/user/action.go index 22707196f4cca..baa4b3b81ec74 100644 --- a/routers/api/v1/user/action.go +++ b/routers/api/v1/user/action.go @@ -212,7 +212,11 @@ func UpdateVariable(ctx *context.APIContext) { if opt.Name == "" { opt.Name = ctx.PathParam("variablename") } - if _, err := actions_service.UpdateVariable(ctx, v.ID, opt.Name, opt.Value); err != nil { + + v.Name = opt.Name + v.Data = opt.Value + + if _, err := actions_service.UpdateVariableNameData(ctx, v); err != nil { if errors.Is(err, util.ErrInvalidArgument) { ctx.Error(http.StatusBadRequest, "UpdateVariable", err) } else { diff --git a/routers/api/v1/user/app.go b/routers/api/v1/user/app.go index bfbc2ba6220ac..9583bb548c7c5 100644 --- a/routers/api/v1/user/app.go +++ b/routers/api/v1/user/app.go @@ -165,7 +165,7 @@ func DeleteAccessToken(ctx *context.APIContext) { // "422": // "$ref": "#/responses/error" - token := ctx.PathParam("id") + token := ctx.PathParam(":id") tokenID, _ := strconv.ParseInt(token, 0, 64) if tokenID == 0 { @@ -306,7 +306,7 @@ func DeleteOauth2Application(ctx *context.APIContext) { // "$ref": "#/responses/empty" // "404": // "$ref": "#/responses/notFound" - appID := ctx.PathParamInt64("id") + appID := ctx.PathParamInt64(":id") if err := auth_model.DeleteOAuth2Application(ctx, appID, ctx.Doer.ID); err != nil { if auth_model.IsErrOAuthApplicationNotFound(err) { ctx.NotFound() @@ -338,7 +338,7 @@ func GetOauth2Application(ctx *context.APIContext) { // "$ref": "#/responses/OAuth2Application" // "404": // "$ref": "#/responses/notFound" - appID := ctx.PathParamInt64("id") + appID := ctx.PathParamInt64(":id") app, err := auth_model.GetOAuth2ApplicationByID(ctx, appID) if err != nil { if auth_model.IsErrOauthClientIDInvalid(err) || auth_model.IsErrOAuthApplicationNotFound(err) { @@ -382,7 +382,7 @@ func UpdateOauth2Application(ctx *context.APIContext) { // "$ref": "#/responses/OAuth2Application" // "404": // "$ref": "#/responses/notFound" - appID := ctx.PathParamInt64("id") + appID := ctx.PathParamInt64(":id") data := web.GetForm(ctx).(*api.CreateOAuth2ApplicationOptions) diff --git a/routers/api/v1/user/follower.go b/routers/api/v1/user/follower.go index 8f46808f9ec8c..6abb70de194d4 100644 --- a/routers/api/v1/user/follower.go +++ b/routers/api/v1/user/follower.go @@ -201,7 +201,7 @@ func CheckFollowing(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - target := GetUserByPathParam(ctx, "target") // FIXME: it is not right to call this function, it should load the "target" directly + target := GetUserByParamsName(ctx, ":target") if ctx.Written() { return } diff --git a/routers/api/v1/user/gpg_key.go b/routers/api/v1/user/gpg_key.go index ef667a188329c..ba5c0fdc45411 100644 --- a/routers/api/v1/user/gpg_key.go +++ b/routers/api/v1/user/gpg_key.go @@ -116,7 +116,7 @@ func GetGPGKey(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - key, err := asymkey_model.GetGPGKeyForUserByID(ctx, ctx.Doer.ID, ctx.PathParamInt64("id")) + key, err := asymkey_model.GetGPGKeyForUserByID(ctx, ctx.Doer.ID, ctx.PathParamInt64(":id")) if err != nil { if asymkey_model.IsErrGPGKeyNotExist(err) { ctx.NotFound() @@ -280,7 +280,7 @@ func DeleteGPGKey(ctx *context.APIContext) { return } - if err := asymkey_model.DeleteGPGKey(ctx, ctx.Doer, ctx.PathParamInt64("id")); err != nil { + if err := asymkey_model.DeleteGPGKey(ctx, ctx.Doer, ctx.PathParamInt64(":id")); err != nil { if asymkey_model.IsErrGPGKeyAccessDenied(err) { ctx.Error(http.StatusForbidden, "", "You do not have access to this key") } else { diff --git a/routers/api/v1/user/helper.go b/routers/api/v1/user/helper.go index 9a6f305700d46..23a526cd676a5 100644 --- a/routers/api/v1/user/helper.go +++ b/routers/api/v1/user/helper.go @@ -10,9 +10,8 @@ import ( "code.gitea.io/gitea/services/context" ) -// GetUserByPathParam get user by the path param name -// it will redirect to the user's new name if the user's name has been changed -func GetUserByPathParam(ctx *context.APIContext, name string) *user_model.User { +// GetUserByParamsName get user by name +func GetUserByParamsName(ctx *context.APIContext, name string) *user_model.User { username := ctx.PathParam(name) user, err := user_model.GetUserByName(ctx, username) if err != nil { @@ -30,7 +29,7 @@ func GetUserByPathParam(ctx *context.APIContext, name string) *user_model.User { return user } -// GetContextUserByPathParam returns user whose name is presented in URL (path param "username"). -func GetContextUserByPathParam(ctx *context.APIContext) *user_model.User { - return GetUserByPathParam(ctx, "username") +// GetUserByParams returns user whose name is presented in URL (":username"). +func GetUserByParams(ctx *context.APIContext) *user_model.User { + return GetUserByParamsName(ctx, ":username") } diff --git a/routers/api/v1/user/key.go b/routers/api/v1/user/key.go index 5a9125b4f30a5..e4278c2ec0ef6 100644 --- a/routers/api/v1/user/key.go +++ b/routers/api/v1/user/key.go @@ -179,7 +179,7 @@ func GetPublicKey(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - key, err := asymkey_model.GetPublicKeyByID(ctx, ctx.PathParamInt64("id")) + key, err := asymkey_model.GetPublicKeyByID(ctx, ctx.PathParamInt64(":id")) if err != nil { if asymkey_model.IsErrKeyNotExist(err) { ctx.NotFound() @@ -274,7 +274,7 @@ func DeletePublicKey(ctx *context.APIContext) { return } - id := ctx.PathParamInt64("id") + id := ctx.PathParamInt64(":id") externallyManaged, err := asymkey_model.PublicKeyIsExternallyManaged(ctx, id) if err != nil { if asymkey_model.IsErrKeyNotExist(err) { diff --git a/routers/api/v1/user/user.go b/routers/api/v1/user/user.go index 43dabe1b60c1e..e668326861e1a 100644 --- a/routers/api/v1/user/user.go +++ b/routers/api/v1/user/user.go @@ -121,7 +121,7 @@ func GetInfo(ctx *context.APIContext) { if !user_model.IsUserVisibleToViewer(ctx, ctx.ContextUser, ctx.Doer) { // fake ErrUserNotExist error message to not leak information about existence - ctx.NotFound("GetUserByName", user_model.ErrUserNotExist{Name: ctx.PathParam("username")}) + ctx.NotFound("GetUserByName", user_model.ErrUserNotExist{Name: ctx.PathParam(":username")}) return } ctx.JSON(http.StatusOK, convert.ToUser(ctx, ctx.ContextUser, ctx.Doer)) diff --git a/routers/api/v1/utils/hook.go b/routers/api/v1/utils/hook.go index 4328878e19613..8e91c2e0efcc7 100644 --- a/routers/api/v1/utils/hook.go +++ b/routers/api/v1/utils/hook.go @@ -205,6 +205,8 @@ func addHook(ctx *context.APIContext, form *api.CreateHookOption, ownerID, repoI Wiki: util.SliceContainsString(form.Events, string(webhook_module.HookEventWiki), true), Repository: util.SliceContainsString(form.Events, string(webhook_module.HookEventRepository), true), Release: util.SliceContainsString(form.Events, string(webhook_module.HookEventRelease), true), + Package: util.SliceContainsString(form.Events, string(webhook_module.HookEventPackage), true), + Status: util.SliceContainsString(form.Events, string(webhook_module.HookEventStatus), true), }, BranchFilter: form.BranchFilter, }, diff --git a/routers/common/blockexpensive.go b/routers/common/blockexpensive.go new file mode 100644 index 0000000000000..ba11ff4cf93cb --- /dev/null +++ b/routers/common/blockexpensive.go @@ -0,0 +1,92 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package common + +import ( + "context" + "net/http" + "strings" + + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/web/middleware" + + "github.com/go-chi/chi/v5" +) + +func BlockExpensive() func(next http.Handler) http.Handler { + if !setting.Service.BlockAnonymousAccessExpensive { + return nil + } + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + ret := determineRequestPriority(req.Context()) + if !ret.SignedIn { + if ret.Expensive || ret.LongPolling { + http.Redirect(w, req, setting.AppSubURL+"/user/login", http.StatusSeeOther) + return + } + } + next.ServeHTTP(w, req) + }) + } +} + +func isRoutePathExpensive(routePattern string) bool { + if strings.HasPrefix(routePattern, "/user/") || strings.HasPrefix(routePattern, "/login/") { + return false + } + + expensivePaths := []string{ + // code related + "/{username}/{reponame}/archive/", + "/{username}/{reponame}/blame/", + "/{username}/{reponame}/commit/", + "/{username}/{reponame}/commits/", + "/{username}/{reponame}/graph", + "/{username}/{reponame}/media/", + "/{username}/{reponame}/raw/", + "/{username}/{reponame}/src/", + + // issue & PR related (no trailing slash) + "/{username}/{reponame}/issues", + "/{username}/{reponame}/{type:issues}", + "/{username}/{reponame}/pulls", + "/{username}/{reponame}/{type:pulls}", + "/{username}/{reponame}/{type:issues|pulls}", // for 1.23 only + + // wiki + "/{username}/{reponame}/wiki/", + + // activity + "/{username}/{reponame}/activity/", + } + for _, path := range expensivePaths { + if strings.HasPrefix(routePattern, path) { + return true + } + } + return false +} + +func isRoutePathForLongPolling(routePattern string) bool { + return routePattern == "/user/events" +} + +func determineRequestPriority(ctx context.Context) (ret struct { + SignedIn bool + Expensive bool + LongPolling bool +}, +) { + dataStore := middleware.GetContextData(ctx) + chiRoutePath := chi.RouteContext(ctx).RoutePattern() + if _, ok := dataStore[middleware.ContextDataKeySignedUser].(*user_model.User); ok { + ret.SignedIn = true + } else { + ret.Expensive = isRoutePathExpensive(chiRoutePath) + ret.LongPolling = isRoutePathForLongPolling(chiRoutePath) + } + return ret +} diff --git a/routers/common/blockexpensive_test.go b/routers/common/blockexpensive_test.go new file mode 100644 index 0000000000000..db5c0db7ddaa6 --- /dev/null +++ b/routers/common/blockexpensive_test.go @@ -0,0 +1,30 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package common + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestBlockExpensive(t *testing.T) { + cases := []struct { + expensive bool + routePath string + }{ + {false, "/user/xxx"}, + {false, "/login/xxx"}, + {true, "/{username}/{reponame}/archive/xxx"}, + {true, "/{username}/{reponame}/graph"}, + {true, "/{username}/{reponame}/src/xxx"}, + {true, "/{username}/{reponame}/wiki/xxx"}, + {true, "/{username}/{reponame}/activity/xxx"}, + } + for _, c := range cases { + assert.Equal(t, c.expensive, isRoutePathExpensive(c.routePath), "routePath: %s", c.routePath) + } + + assert.True(t, isRoutePathForLongPolling("/user/events")) +} diff --git a/routers/common/codesearch.go b/routers/common/codesearch.go new file mode 100644 index 0000000000000..7cd01068b0a8f --- /dev/null +++ b/routers/common/codesearch.go @@ -0,0 +1,39 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package common + +import ( + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/services/context" +) + +func PrepareCodeSearch(ctx *context.Context) (ret struct { + Keyword string + Language string + IsFuzzy bool +}, +) { + ret.Language = ctx.FormTrim("l") + ret.Keyword = ctx.FormTrim("q") + + fuzzyDefault := setting.Indexer.RepoIndexerEnabled + fuzzyAllow := true + if setting.Indexer.RepoType == "bleve" && setting.Indexer.TypeBleveMaxFuzzniess == 0 { + fuzzyDefault = false + fuzzyAllow = false + } + isFuzzy := ctx.FormOptionalBool("fuzzy").ValueOrDefault(fuzzyDefault) + if isFuzzy && !fuzzyAllow { + ctx.Flash.Info("Fuzzy search is disabled by default due to performance reasons", true) + isFuzzy = false + } + + ctx.Data["IsBleveFuzzyDisabled"] = true + ctx.Data["Keyword"] = ret.Keyword + ctx.Data["Language"] = ret.Language + ctx.Data["IsFuzzy"] = isFuzzy + + ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled + return ret +} diff --git a/routers/common/errpage.go b/routers/common/errpage.go index c0b16dbdde3cf..402ca44c12d21 100644 --- a/routers/common/errpage.go +++ b/routers/common/errpage.go @@ -8,6 +8,7 @@ import ( "net/http" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/httpcache" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" @@ -17,7 +18,7 @@ import ( "code.gitea.io/gitea/services/context" ) -const tplStatus500 templates.TplName = "status/500" +const tplStatus500 base.TplName = "status/500" // RenderPanicErrorPage renders a 500 page, and it never panics func RenderPanicErrorPage(w http.ResponseWriter, req *http.Request, err any) { @@ -46,7 +47,7 @@ func RenderPanicErrorPage(w http.ResponseWriter, req *http.Request, err any) { ctxData["ErrorMsg"] = "PANIC: " + combinedErr } - err = templates.HTMLRenderer().HTML(w, http.StatusInternalServerError, tplStatus500, ctxData, tmplCtx) + err = templates.HTMLRenderer().HTML(w, http.StatusInternalServerError, string(tplStatus500), ctxData, tmplCtx) if err != nil { log.Error("Error occurs again when rendering error page: %v", err) w.WriteHeader(http.StatusInternalServerError) diff --git a/routers/common/errpage_test.go b/routers/common/errpage_test.go index dfea55f510b4a..4fd63ba49e7e6 100644 --- a/routers/common/errpage_test.go +++ b/routers/common/errpage_test.go @@ -12,8 +12,8 @@ import ( "testing" "code.gitea.io/gitea/models/unittest" - "code.gitea.io/gitea/modules/reqctx" "code.gitea.io/gitea/modules/test" + "code.gitea.io/gitea/modules/web/middleware" "github.com/stretchr/testify/assert" ) @@ -21,7 +21,7 @@ import ( func TestRenderPanicErrorPage(t *testing.T) { w := httptest.NewRecorder() req := &http.Request{URL: &url.URL{}} - req = req.WithContext(reqctx.NewRequestContextForTest(context.Background())) + req = req.WithContext(middleware.WithContextData(context.Background())) RenderPanicErrorPage(w, req, errors.New("fake panic error (for test only)")) respContent := w.Body.String() assert.Contains(t, respContent, `class="page-content status-page-500"`) diff --git a/routers/common/middleware.go b/routers/common/middleware.go index 12b0c67b01823..51e42d87a0b9e 100644 --- a/routers/common/middleware.go +++ b/routers/common/middleware.go @@ -4,14 +4,16 @@ package common import ( + go_context "context" "fmt" "net/http" "strings" "code.gitea.io/gitea/modules/cache" "code.gitea.io/gitea/modules/httplib" - "code.gitea.io/gitea/modules/reqctx" + "code.gitea.io/gitea/modules/process" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/web/middleware" "code.gitea.io/gitea/modules/web/routing" "code.gitea.io/gitea/services/context" @@ -22,76 +24,65 @@ import ( // ProtocolMiddlewares returns HTTP protocol related middlewares, and it provides a global panic recovery func ProtocolMiddlewares() (handlers []any) { - // the order is important - handlers = append(handlers, ChiRoutePathHandler()) // make sure chi has correct paths - handlers = append(handlers, RequestContextHandler()) // prepare the context and panic recovery - - if setting.ReverseProxyLimit > 0 && len(setting.ReverseProxyTrustedProxies) > 0 { - handlers = append(handlers, ForwardedHeadersHandler(setting.ReverseProxyLimit, setting.ReverseProxyTrustedProxies)) - } - - if setting.IsRouteLogEnabled() { - handlers = append(handlers, routing.NewLoggerHandler()) - } - - if setting.IsAccessLogEnabled() { - handlers = append(handlers, context.AccessLogger()) - } - - return handlers -} - -func RequestContextHandler() func(h http.Handler) http.Handler { - return func(next http.Handler) http.Handler { + // make sure chi uses EscapedPath(RawPath) as RoutePath, then "%2f" could be handled correctly + handlers = append(handlers, func(next http.Handler) http.Handler { return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { - profDesc := fmt.Sprintf("%s: %s", req.Method, req.RequestURI) - ctx, finished := reqctx.NewRequestContext(req.Context(), profDesc) - defer finished() + ctx := chi.RouteContext(req.Context()) + if req.URL.RawPath == "" { + ctx.RoutePath = req.URL.EscapedPath() + } else { + ctx.RoutePath = req.URL.RawPath + } + next.ServeHTTP(resp, req) + }) + }) + // prepare the ContextData and panic recovery + handlers = append(handlers, func(next http.Handler) http.Handler { + return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { defer func() { if err := recover(); err != nil { RenderPanicErrorPage(resp, req, err) // it should never panic } }() - - ds := reqctx.GetRequestDataStore(ctx) - req = req.WithContext(cache.WithCacheContext(ctx)) - ds.SetContextValue(httplib.RequestContextKey, req) - ds.AddCleanUp(func() { - if req.MultipartForm != nil { - _ = req.MultipartForm.RemoveAll() // remove the temp files buffered to tmp directory - } - }) - next.ServeHTTP(context.WrapResponseWriter(resp), req) + req = req.WithContext(middleware.WithContextData(req.Context())) + req = req.WithContext(go_context.WithValue(req.Context(), httplib.RequestContextKey, req)) + next.ServeHTTP(resp, req) }) - } -} + }) -func ChiRoutePathHandler() func(h http.Handler) http.Handler { - // make sure chi uses EscapedPath(RawPath) as RoutePath, then "%2f" could be handled correctly - return func(next http.Handler) http.Handler { + // wrap the request and response, use the process context and add it to the process manager + handlers = append(handlers, func(next http.Handler) http.Handler { return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { - ctx := chi.RouteContext(req.Context()) - if req.URL.RawPath == "" { - ctx.RoutePath = req.URL.EscapedPath() + ctx, _, finished := process.GetManager().AddTypedContext(req.Context(), fmt.Sprintf("%s: %s", req.Method, req.RequestURI), process.RequestProcessType, true) + defer finished() + next.ServeHTTP(context.WrapResponseWriter(resp), req.WithContext(cache.WithCacheContext(ctx))) + }) + }) + + if setting.ReverseProxyLimit > 0 { + opt := proxy.NewForwardedHeadersOptions(). + WithForwardLimit(setting.ReverseProxyLimit). + ClearTrustedProxies() + for _, n := range setting.ReverseProxyTrustedProxies { + if !strings.Contains(n, "/") { + opt.AddTrustedProxy(n) } else { - ctx.RoutePath = req.URL.RawPath + opt.AddTrustedNetwork(n) } - next.ServeHTTP(resp, req) - }) + } + handlers = append(handlers, proxy.ForwardedHeaders(opt)) } -} -func ForwardedHeadersHandler(limit int, trustedProxies []string) func(h http.Handler) http.Handler { - opt := proxy.NewForwardedHeadersOptions().WithForwardLimit(limit).ClearTrustedProxies() - for _, n := range trustedProxies { - if !strings.Contains(n, "/") { - opt.AddTrustedProxy(n) - } else { - opt.AddTrustedNetwork(n) - } + if setting.IsRouteLogEnabled() { + handlers = append(handlers, routing.NewLoggerHandler()) } - return proxy.ForwardedHeaders(opt) + + if setting.IsAccessLogEnabled() { + handlers = append(handlers, context.AccessLogger()) + } + + return handlers } func Sessioner() func(next http.Handler) http.Handler { diff --git a/routers/init.go b/routers/init.go index e7aa765bf03c9..98ce1bc4c9858 100644 --- a/routers/init.go +++ b/routers/init.go @@ -133,7 +133,7 @@ func InitWebInstalled(ctx context.Context) { highlight.NewContext() external.RegisterRenderers() - markup.Init(markup_service.FormalRenderHelperFuncs()) + markup.Init(markup_service.ProcessorHelper()) if setting.EnableSQLite3 { log.Info("SQLite3 support is enabled") diff --git a/routers/install/install.go b/routers/install/install.go index 8a1d57aa0b3bb..73ce97a334551 100644 --- a/routers/install/install.go +++ b/routers/install/install.go @@ -21,11 +21,11 @@ import ( system_model "code.gitea.io/gitea/models/system" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/auth/password/hash" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/generate" "code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/optional" - "code.gitea.io/gitea/modules/reqctx" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/timeutil" @@ -43,8 +43,8 @@ import ( const ( // tplInstall template for installation page - tplInstall templates.TplName = "install" - tplPostInstall templates.TplName = "post-install" + tplInstall base.TplName = "install" + tplPostInstall base.TplName = "post-install" ) // getSupportedDbTypeNames returns a slice for supported database types and names. The slice is used to keep the order @@ -62,11 +62,15 @@ func Contexter() func(next http.Handler) http.Handler { envConfigKeys := setting.CollectEnvConfigKeys() return func(next http.Handler) http.Handler { return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { - base := context.NewBaseContext(resp, req) + base, baseCleanUp := context.NewBaseContext(resp, req) + defer baseCleanUp() + ctx := context.NewWebContext(base, rnd, session.GetSession(req)) - ctx.SetContextValue(context.WebContextKey, ctx) + ctx.AppendContextValue(context.WebContextKey, ctx) ctx.Data.MergeFrom(middleware.CommonTemplateContextData()) - ctx.Data.MergeFrom(reqctx.ContextData{ + ctx.Data.MergeFrom(middleware.ContextData{ + "Context": ctx, // TODO: use "ctx" in template and remove this + "locale": ctx.Locale, "Title": ctx.Locale.Tr("install.install"), "PageIsInstall": true, "DbTypeNames": dbTypeNames, @@ -152,7 +156,7 @@ func Install(ctx *context.Context) { form.DisableRegistration = setting.Service.DisableRegistration form.AllowOnlyExternalRegistration = setting.Service.AllowOnlyExternalRegistration form.EnableCaptcha = setting.Service.EnableCaptcha - form.RequireSignInView = setting.Service.RequireSignInView + form.RequireSignInView = setting.Service.RequireSignInViewStrict form.DefaultKeepEmailPrivate = setting.Service.DefaultKeepEmailPrivate form.DefaultAllowCreateOrganization = setting.Service.DefaultAllowCreateOrganization form.DefaultEnableTimetracking = setting.Service.DefaultEnableTimetracking diff --git a/routers/private/default_branch.go b/routers/private/default_branch.go index c375d70dc6f2b..8f6e9084df79e 100644 --- a/routers/private/default_branch.go +++ b/routers/private/default_branch.go @@ -16,9 +16,9 @@ import ( // SetDefaultBranch updates the default branch func SetDefaultBranch(ctx *gitea_context.PrivateContext) { - ownerName := ctx.PathParam("owner") - repoName := ctx.PathParam("repo") - branch := ctx.PathParam("branch") + ownerName := ctx.PathParam(":owner") + repoName := ctx.PathParam(":repo") + branch := ctx.PathParam(":branch") ctx.Repo.Repository.DefaultBranch = branch if err := gitrepo.SetDefaultBranch(ctx, ctx.Repo.Repository, ctx.Repo.Repository.DefaultBranch); err != nil { diff --git a/routers/private/hook_post_receive.go b/routers/private/hook_post_receive.go index 32c28287395e2..8d12b7a953663 100644 --- a/routers/private/hook_post_receive.go +++ b/routers/private/hook_post_receive.go @@ -40,8 +40,8 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) { // b) our update function will likely change the repository in the db so we will need to refresh it // c) we don't always need the repo - ownerName := ctx.PathParam("owner") - repoName := ctx.PathParam("repo") + ownerName := ctx.PathParam(":owner") + repoName := ctx.PathParam(":repo") // defer getting the repository at this point - as we should only retrieve it if we're going to call update var ( diff --git a/routers/private/hook_pre_receive.go b/routers/private/hook_pre_receive.go index eb7bb2b480a5d..73fe9b886cff8 100644 --- a/routers/private/hook_pre_receive.go +++ b/routers/private/hook_pre_receive.go @@ -8,6 +8,7 @@ import ( "net/http" "os" + "code.gitea.io/gitea/models" asymkey_model "code.gitea.io/gitea/models/asymkey" git_model "code.gitea.io/gitea/models/git" issues_model "code.gitea.io/gitea/models/issues" @@ -236,7 +237,7 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID string, r if len(globs) > 0 { _, err := pull_service.CheckFileProtection(gitRepo, branchName, oldCommitID, newCommitID, globs, 1, ctx.env) if err != nil { - if !pull_service.IsErrFilePathProtected(err) { + if !models.IsErrFilePathProtected(err) { log.Error("Unable to check file protection for commits from %s to %s in %-v: %v", oldCommitID, newCommitID, repo, err) ctx.JSON(http.StatusInternalServerError, private.Response{ Err: fmt.Sprintf("Unable to check file protection for commits from %s to %s: %v", oldCommitID, newCommitID, err), @@ -245,7 +246,7 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID string, r } changedProtectedfiles = true - protectedFilePath = err.(pull_service.ErrFilePathProtected).Path + protectedFilePath = err.(models.ErrFilePathProtected).Path } } @@ -373,7 +374,7 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID string, r // Check all status checks and reviews are ok if err := pull_service.CheckPullBranchProtections(ctx, pr, true); err != nil { - if pull_service.IsErrDisallowedToMerge(err) { + if models.IsErrDisallowedToMerge(err) { log.Warn("Forbidden: User %d is not allowed push to protected branch %s in %-v and pr #%d is not ready to be merged: %s", ctx.opts.UserID, branchName, repo, pr.Index, err.Error()) ctx.JSON(http.StatusForbidden, private.Response{ UserMsg: fmt.Sprintf("Not allowed to push to protected branch %s and pr #%d is not ready to be merged: %s", branchName, ctx.opts.PullRequestID, err.Error()), diff --git a/routers/private/internal.go b/routers/private/internal.go index a78c76f8970f9..1fb72f13d9cc1 100644 --- a/routers/private/internal.go +++ b/routers/private/internal.go @@ -63,8 +63,8 @@ func Routes() *web.Router { r.Post("/ssh/{id}/update/{repoid}", UpdatePublicKeyInRepo) r.Post("/ssh/log", bind(private.SSHLogOption{}), SSHLog) r.Post("/hook/pre-receive/{owner}/{repo}", RepoAssignment, bind(private.HookOptions{}), HookPreReceive) - r.Post("/hook/post-receive/{owner}/{repo}", context.OverrideContext(), bind(private.HookOptions{}), HookPostReceive) - r.Post("/hook/proc-receive/{owner}/{repo}", context.OverrideContext(), RepoAssignment, bind(private.HookOptions{}), HookProcReceive) + r.Post("/hook/post-receive/{owner}/{repo}", context.OverrideContext, bind(private.HookOptions{}), HookPostReceive) + r.Post("/hook/proc-receive/{owner}/{repo}", context.OverrideContext, RepoAssignment, bind(private.HookOptions{}), HookProcReceive) r.Post("/hook/set-default-branch/{owner}/{repo}/{branch}", RepoAssignment, SetDefaultBranch) r.Get("/serv/none/{keyid}", ServNoCommand) r.Get("/serv/command/{keyid}/{owner}/{repo}", ServCommand) @@ -88,7 +88,7 @@ func Routes() *web.Router { // Fortunately, the LFS handlers are able to handle requests without a complete web context common.AddOwnerRepoGitLFSRoutes(r, func(ctx *context.PrivateContext) { webContext := &context.Context{Base: ctx.Base} - ctx.SetContextValue(context.WebContextKey, webContext) + ctx.AppendContextValue(context.WebContextKey, webContext) }) }) diff --git a/routers/private/internal_repo.go b/routers/private/internal_repo.go index 8a53e1ed23723..aad0a3fb1aa83 100644 --- a/routers/private/internal_repo.go +++ b/routers/private/internal_repo.go @@ -4,6 +4,7 @@ package private import ( + "context" "fmt" "net/http" @@ -16,29 +17,40 @@ import ( // This file contains common functions relating to setting the Repository for the internal routes -// RepoAssignment assigns the repository and git repository to the private context -func RepoAssignment(ctx *gitea_context.PrivateContext) { - ownerName := ctx.PathParam("owner") - repoName := ctx.PathParam("repo") +// RepoAssignment assigns the repository and gitrepository to the private context +func RepoAssignment(ctx *gitea_context.PrivateContext) context.CancelFunc { + ownerName := ctx.PathParam(":owner") + repoName := ctx.PathParam(":repo") repo := loadRepository(ctx, ownerName, repoName) if ctx.Written() { // Error handled in loadRepository - return + return nil } - gitRepo, err := gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx, repo) + gitRepo, err := gitrepo.OpenRepository(ctx, repo) if err != nil { log.Error("Failed to open repository: %s/%s Error: %v", ownerName, repoName, err) ctx.JSON(http.StatusInternalServerError, private.Response{ Err: fmt.Sprintf("Failed to open repository: %s/%s Error: %v", ownerName, repoName, err), }) - return + return nil } + ctx.Repo = &gitea_context.Repository{ Repository: repo, GitRepo: gitRepo, } + + // We opened it, we should close it + cancel := func() { + // If it's been set to nil then assume someone else has closed it. + if ctx.Repo.GitRepo != nil { + ctx.Repo.GitRepo.Close() + } + } + + return cancel } func loadRepository(ctx *gitea_context.PrivateContext, ownerName, repoName string) *repo_model.Repository { diff --git a/routers/private/key.go b/routers/private/key.go index 9fd0a16c074f6..063db76520fca 100644 --- a/routers/private/key.go +++ b/routers/private/key.go @@ -14,8 +14,8 @@ import ( // UpdatePublicKeyInRepo update public key and deploy key updates func UpdatePublicKeyInRepo(ctx *context.PrivateContext) { - keyID := ctx.PathParamInt64("id") - repoID := ctx.PathParamInt64("repoid") + keyID := ctx.PathParamInt64(":id") + repoID := ctx.PathParamInt64(":repoid") if err := asymkey_model.UpdatePublicKeyUpdated(ctx, keyID); err != nil { ctx.JSON(http.StatusInternalServerError, private.Response{ Err: err.Error(), diff --git a/routers/private/serv.go b/routers/private/serv.go index ecff3b7a531c1..c6153ebb17ed2 100644 --- a/routers/private/serv.go +++ b/routers/private/serv.go @@ -25,7 +25,7 @@ import ( // ServNoCommand returns information about the provided keyid func ServNoCommand(ctx *context.PrivateContext) { - keyID := ctx.PathParamInt64("keyid") + keyID := ctx.PathParamInt64(":keyid") if keyID <= 0 { ctx.JSON(http.StatusBadRequest, private.Response{ UserMsg: fmt.Sprintf("Bad key id: %d", keyID), @@ -77,10 +77,11 @@ func ServNoCommand(ctx *context.PrivateContext) { // ServCommand returns information about the provided keyid func ServCommand(ctx *context.PrivateContext) { - keyID := ctx.PathParamInt64("keyid") - ownerName := ctx.PathParam("owner") - repoName := ctx.PathParam("repo") + keyID := ctx.PathParamInt64(":keyid") + ownerName := ctx.PathParam(":owner") + repoName := ctx.PathParam(":repo") mode := perm.AccessMode(ctx.FormInt("mode")) + verb := ctx.FormString("verb") // Set the basic parts of the results to return results := private.ServCommandResults{ @@ -286,7 +287,7 @@ func ServCommand(ctx *context.PrivateContext) { repo.IsPrivate || owner.Visibility.IsPrivate() || (user != nil && user.IsRestricted) || // user will be nil if the key is a deploykey - setting.Service.RequireSignInView) { + setting.Service.RequireSignInViewStrict) { if key.Type == asymkey_model.KeyTypeDeploy { if deployKey.Mode < mode { ctx.JSON(http.StatusUnauthorized, private.Response{ @@ -295,8 +296,11 @@ func ServCommand(ctx *context.PrivateContext) { return } } else { - // Because of the special ref "refs/for" we will need to delay write permission check - if git.DefaultFeatures().SupportProcReceive && unitType == unit.TypeCode { + // Because of the special ref "refs/for" (AGit) we will need to delay write permission check, + // AGit flow needs to write its own ref when the doer has "reader" permission (allowing to create PR). + // The real permission check is done in HookPreReceive (routers/private/hook_pre_receive.go). + // Here it should relax the permission check for "git push (git-receive-pack)", but not for others like LFS operations. + if git.DefaultFeatures().SupportProcReceive && unitType == unit.TypeCode && verb == "git-receive-pack" { mode = perm.AccessModeRead } diff --git a/routers/web/admin/admin.go b/routers/web/admin/admin.go index 3902a1efb1474..37c54b5362f3b 100644 --- a/routers/web/admin/admin.go +++ b/routers/web/admin/admin.go @@ -21,7 +21,6 @@ import ( "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/updatechecker" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/services/context" @@ -32,14 +31,14 @@ import ( ) const ( - tplDashboard templates.TplName = "admin/dashboard" - tplSystemStatus templates.TplName = "admin/system_status" - tplSelfCheck templates.TplName = "admin/self_check" - tplCron templates.TplName = "admin/cron" - tplQueue templates.TplName = "admin/queue" - tplStacktrace templates.TplName = "admin/stacktrace" - tplQueueManage templates.TplName = "admin/queue_manage" - tplStats templates.TplName = "admin/stats" + tplDashboard base.TplName = "admin/dashboard" + tplSystemStatus base.TplName = "admin/system_status" + tplSelfCheck base.TplName = "admin/self_check" + tplCron base.TplName = "admin/cron" + tplQueue base.TplName = "admin/queue" + tplStacktrace base.TplName = "admin/stacktrace" + tplQueueManage base.TplName = "admin/queue_manage" + tplStats base.TplName = "admin/stats" ) var sysStatus struct { diff --git a/routers/web/admin/applications.go b/routers/web/admin/applications.go index aec6349f21467..9b48f21eca9f8 100644 --- a/routers/web/admin/applications.go +++ b/routers/web/admin/applications.go @@ -9,15 +9,15 @@ import ( "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/templates" user_setting "code.gitea.io/gitea/routers/web/user/setting" "code.gitea.io/gitea/services/context" ) var ( - tplSettingsApplications templates.TplName = "admin/applications/list" - tplSettingsOauth2ApplicationEdit templates.TplName = "admin/applications/oauth2_edit" + tplSettingsApplications base.TplName = "admin/applications/list" + tplSettingsOauth2ApplicationEdit base.TplName = "admin/applications/oauth2_edit" ) func newOAuth2CommonHandlers() *user_setting.OAuth2CommonHandlers { diff --git a/routers/web/admin/auths.go b/routers/web/admin/auths.go index 249347e8352f3..60e2b7c86fcf0 100644 --- a/routers/web/admin/auths.go +++ b/routers/web/admin/auths.go @@ -15,9 +15,9 @@ import ( "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/modules/auth/pam" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" auth_service "code.gitea.io/gitea/services/auth" @@ -33,9 +33,9 @@ import ( ) const ( - tplAuths templates.TplName = "admin/auth/list" - tplAuthNew templates.TplName = "admin/auth/new" - tplAuthEdit templates.TplName = "admin/auth/edit" + tplAuths base.TplName = "admin/auth/list" + tplAuthNew base.TplName = "admin/auth/new" + tplAuthEdit base.TplName = "admin/auth/edit" ) var ( @@ -337,7 +337,7 @@ func EditAuthSource(ctx *context.Context) { oauth2providers := oauth2.GetSupportedOAuth2Providers() ctx.Data["OAuth2Providers"] = oauth2providers - source, err := auth.GetSourceByID(ctx, ctx.PathParamInt64("authid")) + source, err := auth.GetSourceByID(ctx, ctx.PathParamInt64(":authid")) if err != nil { ctx.ServerError("auth.GetSourceByID", err) return @@ -371,7 +371,7 @@ func EditAuthSourcePost(ctx *context.Context) { oauth2providers := oauth2.GetSupportedOAuth2Providers() ctx.Data["OAuth2Providers"] = oauth2providers - source, err := auth.GetSourceByID(ctx, ctx.PathParamInt64("authid")) + source, err := auth.GetSourceByID(ctx, ctx.PathParamInt64(":authid")) if err != nil { ctx.ServerError("auth.GetSourceByID", err) return @@ -442,7 +442,7 @@ func EditAuthSourcePost(ctx *context.Context) { // DeleteAuthSource response for deleting an auth source func DeleteAuthSource(ctx *context.Context) { - source, err := auth.GetSourceByID(ctx, ctx.PathParamInt64("authid")) + source, err := auth.GetSourceByID(ctx, ctx.PathParamInt64(":authid")) if err != nil { ctx.ServerError("auth.GetSourceByID", err) return @@ -454,7 +454,7 @@ func DeleteAuthSource(ctx *context.Context) { } else { ctx.Flash.Error(fmt.Sprintf("auth_service.DeleteSource: %v", err)) } - ctx.JSONRedirect(setting.AppSubURL + "/-/admin/auths/" + url.PathEscape(ctx.PathParam("authid"))) + ctx.JSONRedirect(setting.AppSubURL + "/-/admin/auths/" + url.PathEscape(ctx.PathParam(":authid"))) return } log.Trace("Authentication deleted by admin(%s): %d", ctx.Doer.Name, source.ID) diff --git a/routers/web/admin/config.go b/routers/web/admin/config.go index 520f14e89fa2e..d067250a5b6b0 100644 --- a/routers/web/admin/config.go +++ b/routers/web/admin/config.go @@ -11,13 +11,13 @@ import ( "strings" system_model "code.gitea.io/gitea/models/system" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/cache" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting/config" - "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/mailer" @@ -26,8 +26,8 @@ import ( ) const ( - tplConfig templates.TplName = "admin/config" - tplConfigSettings templates.TplName = "admin/config_settings" + tplConfig base.TplName = "admin/config" + tplConfigSettings base.TplName = "admin/config_settings" ) // SendTestMail send test mail to confirm mail service is OK diff --git a/routers/web/admin/emails.go b/routers/web/admin/emails.go index e925de893722a..e9c97d8b8f914 100644 --- a/routers/web/admin/emails.go +++ b/routers/web/admin/emails.go @@ -10,16 +10,16 @@ import ( "code.gitea.io/gitea/models/db" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/user" ) const ( - tplEmails templates.TplName = "admin/emails/list" + tplEmails base.TplName = "admin/emails/list" ) // Emails show all emails diff --git a/routers/web/admin/hooks.go b/routers/web/admin/hooks.go index 34dc0fc9b00d1..91ca6e3fa7bf7 100644 --- a/routers/web/admin/hooks.go +++ b/routers/web/admin/hooks.go @@ -7,15 +7,15 @@ import ( "net/http" "code.gitea.io/gitea/models/webhook" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/services/context" ) const ( // tplAdminHooks template path to render hook settings - tplAdminHooks templates.TplName = "admin/hooks" + tplAdminHooks base.TplName = "admin/hooks" ) // DefaultOrSystemWebhooks renders both admin default and system webhook list pages diff --git a/routers/web/admin/notice.go b/routers/web/admin/notice.go index 21a8ab0d17642..5f7432e62907a 100644 --- a/routers/web/admin/notice.go +++ b/routers/web/admin/notice.go @@ -10,14 +10,14 @@ import ( "code.gitea.io/gitea/models/db" system_model "code.gitea.io/gitea/models/system" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/services/context" ) const ( - tplNotices templates.TplName = "admin/notice" + tplNotices base.TplName = "admin/notice" ) // Notices show notices for admin diff --git a/routers/web/admin/orgs.go b/routers/web/admin/orgs.go index 35e61efa173d8..cea28f82203b2 100644 --- a/routers/web/admin/orgs.go +++ b/routers/web/admin/orgs.go @@ -7,15 +7,15 @@ package admin import ( "code.gitea.io/gitea/models/db" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/structs" - "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/routers/web/explore" "code.gitea.io/gitea/services/context" ) const ( - tplOrgs templates.TplName = "admin/org/list" + tplOrgs base.TplName = "admin/org/list" ) // Organizations show all the organizations diff --git a/routers/web/admin/packages.go b/routers/web/admin/packages.go index da345f2f89b06..2b9edc622d6cd 100644 --- a/routers/web/admin/packages.go +++ b/routers/web/admin/packages.go @@ -10,16 +10,16 @@ import ( "code.gitea.io/gitea/models/db" packages_model "code.gitea.io/gitea/models/packages" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/services/context" packages_service "code.gitea.io/gitea/services/packages" packages_cleanup_service "code.gitea.io/gitea/services/packages/cleanup" ) const ( - tplPackagesList templates.TplName = "admin/packages/list" + tplPackagesList base.TplName = "admin/packages/list" ) // Packages shows all packages diff --git a/routers/web/admin/repos.go b/routers/web/admin/repos.go index 27d39dcf4bef3..75e5ee5d86fc3 100644 --- a/routers/web/admin/repos.go +++ b/routers/web/admin/repos.go @@ -12,9 +12,9 @@ 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/base" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/routers/web/explore" "code.gitea.io/gitea/services/context" @@ -22,8 +22,8 @@ import ( ) const ( - tplRepos templates.TplName = "admin/repo/list" - tplUnadoptedRepos templates.TplName = "admin/repo/unadopted" + tplRepos base.TplName = "admin/repo/list" + tplUnadoptedRepos base.TplName = "admin/repo/unadopted" ) // Repos show all the repositories diff --git a/routers/web/admin/users.go b/routers/web/admin/users.go index fdd18b2f9de48..a6b0b5c78bb13 100644 --- a/routers/web/admin/users.go +++ b/routers/web/admin/users.go @@ -11,17 +11,17 @@ import ( "strconv" "strings" + "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/db" org_model "code.gitea.io/gitea/models/organization" - packages_model "code.gitea.io/gitea/models/packages" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/auth/password" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/routers/web/explore" @@ -33,10 +33,10 @@ import ( ) const ( - tplUsers templates.TplName = "admin/user/list" - tplUserNew templates.TplName = "admin/user/new" - tplUserView templates.TplName = "admin/user/view" - tplUserEdit templates.TplName = "admin/user/edit" + tplUsers base.TplName = "admin/user/list" + tplUserNew base.TplName = "admin/user/new" + tplUserView base.TplName = "admin/user/view" + tplUserEdit base.TplName = "admin/user/edit" ) // UserSearchDefaultAdminSort is the default sort type for admin view @@ -219,7 +219,7 @@ func NewUserPost(ctx *context.Context) { } func prepareUserInfo(ctx *context.Context) *user_model.User { - u, err := user_model.GetUserByID(ctx, ctx.PathParamInt64("userid")) + u, err := user_model.GetUserByID(ctx, ctx.PathParamInt64(":userid")) if err != nil { if user_model.IsErrUserNotExist(err) { ctx.Redirect(setting.AppSubURL + "/-/admin/users") @@ -446,7 +446,7 @@ func EditUserPost(ctx *context.Context) { } if err := user_service.UpdateUser(ctx, u, opts); err != nil { - if user_model.IsErrDeleteLastAdminUser(err) { + if models.IsErrDeleteLastAdminUser(err) { ctx.RenderWithErr(ctx.Tr("auth.last_admin"), tplUserEdit, &form) } else { ctx.ServerError("UpdateUser", err) @@ -481,12 +481,12 @@ func EditUserPost(ctx *context.Context) { } ctx.Flash.Success(ctx.Tr("admin.users.update_profile_success")) - ctx.Redirect(setting.AppSubURL + "/-/admin/users/" + url.PathEscape(ctx.PathParam("userid"))) + ctx.Redirect(setting.AppSubURL + "/-/admin/users/" + url.PathEscape(ctx.PathParam(":userid"))) } // DeleteUser response for deleting a user func DeleteUser(ctx *context.Context) { - u, err := user_model.GetUserByID(ctx, ctx.PathParamInt64("userid")) + u, err := user_model.GetUserByID(ctx, ctx.PathParamInt64(":userid")) if err != nil { ctx.ServerError("GetUserByID", err) return @@ -495,24 +495,24 @@ func DeleteUser(ctx *context.Context) { // admin should not delete themself if u.ID == ctx.Doer.ID { ctx.Flash.Error(ctx.Tr("admin.users.cannot_delete_self")) - ctx.Redirect(setting.AppSubURL + "/-/admin/users/" + url.PathEscape(ctx.PathParam("userid"))) + ctx.Redirect(setting.AppSubURL + "/-/admin/users/" + url.PathEscape(ctx.PathParam(":userid"))) return } if err = user_service.DeleteUser(ctx, u, ctx.FormBool("purge")); err != nil { switch { - case repo_model.IsErrUserOwnRepos(err): + case models.IsErrUserOwnRepos(err): ctx.Flash.Error(ctx.Tr("admin.users.still_own_repo")) - ctx.Redirect(setting.AppSubURL + "/-/admin/users/" + url.PathEscape(ctx.PathParam("userid"))) - case org_model.IsErrUserHasOrgs(err): + ctx.Redirect(setting.AppSubURL + "/-/admin/users/" + url.PathEscape(ctx.PathParam(":userid"))) + case models.IsErrUserHasOrgs(err): ctx.Flash.Error(ctx.Tr("admin.users.still_has_org")) - ctx.Redirect(setting.AppSubURL + "/-/admin/users/" + url.PathEscape(ctx.PathParam("userid"))) - case packages_model.IsErrUserOwnPackages(err): + ctx.Redirect(setting.AppSubURL + "/-/admin/users/" + url.PathEscape(ctx.PathParam(":userid"))) + case models.IsErrUserOwnPackages(err): ctx.Flash.Error(ctx.Tr("admin.users.still_own_packages")) - ctx.Redirect(setting.AppSubURL + "/-/admin/users/" + url.PathEscape(ctx.PathParam("userid"))) - case user_model.IsErrDeleteLastAdminUser(err): + ctx.Redirect(setting.AppSubURL + "/-/admin/users/" + url.PathEscape(ctx.PathParam(":userid"))) + case models.IsErrDeleteLastAdminUser(err): ctx.Flash.Error(ctx.Tr("auth.last_admin")) - ctx.Redirect(setting.AppSubURL + "/-/admin/users/" + url.PathEscape(ctx.PathParam("userid"))) + ctx.Redirect(setting.AppSubURL + "/-/admin/users/" + url.PathEscape(ctx.PathParam(":userid"))) default: ctx.ServerError("DeleteUser", err) } diff --git a/routers/web/auth/2fa.go b/routers/web/auth/2fa.go index fe363fe90ad93..f93177bf96b8b 100644 --- a/routers/web/auth/2fa.go +++ b/routers/web/auth/2fa.go @@ -9,8 +9,8 @@ import ( "code.gitea.io/gitea/models/auth" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/externalaccount" @@ -18,8 +18,8 @@ import ( ) var ( - tplTwofa templates.TplName = "user/auth/twofa" - tplTwofaScratch templates.TplName = "user/auth/twofa_scratch" + tplTwofa base.TplName = "user/auth/twofa" + tplTwofaScratch base.TplName = "user/auth/twofa_scratch" ) // TwoFactor shows the user a two-factor authentication page. diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go index 28268e31336c6..ee70889a53af1 100644 --- a/routers/web/auth/auth.go +++ b/routers/web/auth/auth.go @@ -15,13 +15,13 @@ import ( "code.gitea.io/gitea/models/db" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/auth/password" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/eventsource" "code.gitea.io/gitea/modules/httplib" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/session" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" @@ -38,10 +38,10 @@ import ( ) const ( - tplSignIn templates.TplName = "user/auth/signin" // for sign in page - tplSignUp templates.TplName = "user/auth/signup" // for sign up page - TplActivate templates.TplName = "user/auth/activate" // for activate user - TplActivatePrompt templates.TplName = "user/auth/activate_prompt" // for showing a message for user activation + tplSignIn base.TplName = "user/auth/signin" // for sign in page + tplSignUp base.TplName = "user/auth/signup" // for sign up page + TplActivate base.TplName = "user/auth/activate" // for activate user + TplActivatePrompt base.TplName = "user/auth/activate_prompt" // for showing a message for user activation ) // autoSignIn reads cookie and try to auto-login. @@ -169,6 +169,7 @@ func prepareSignInPageData(ctx *context.Context) { ctx.Data["PageIsLogin"] = true ctx.Data["EnableSSPI"] = auth.IsSSPIEnabled(ctx) ctx.Data["EnablePasswordSignInForm"] = setting.Service.EnablePasswordSignInForm + ctx.Data["EnablePasskeyAuth"] = setting.Service.EnablePasskeyAuth if setting.Service.EnableCaptcha && setting.Service.RequireCaptchaForLogin { context.SetCaptchaData(ctx) @@ -413,8 +414,6 @@ func SignUp(ctx *context.Context) { ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/sign_up" - ctx.Data["Email"] = ctx.FormString("user_email") - oauth2Providers, err := oauth2.GetOAuth2Providers(ctx, optional.Some(true)) if err != nil { ctx.ServerError("UserSignUp", err) @@ -519,7 +518,7 @@ func SignUpPost(ctx *context.Context) { // createAndHandleCreatedUser calls createUserInContext and // then handleUserCreated. -func createAndHandleCreatedUser(ctx *context.Context, tpl templates.TplName, form any, u *user_model.User, overwrites *user_model.CreateUserOverwriteOptions, gothUser *goth.User, allowLink bool) bool { +func createAndHandleCreatedUser(ctx *context.Context, tpl base.TplName, form any, u *user_model.User, overwrites *user_model.CreateUserOverwriteOptions, gothUser *goth.User, allowLink bool) bool { if !createUserInContext(ctx, tpl, form, u, overwrites, gothUser, allowLink) { return false } @@ -528,7 +527,7 @@ func createAndHandleCreatedUser(ctx *context.Context, tpl templates.TplName, for // createUserInContext creates a user and handles errors within a given context. // Optionally a template can be specified. -func createUserInContext(ctx *context.Context, tpl templates.TplName, form any, u *user_model.User, overwrites *user_model.CreateUserOverwriteOptions, gothUser *goth.User, allowLink bool) (ok bool) { +func createUserInContext(ctx *context.Context, tpl base.TplName, form any, u *user_model.User, overwrites *user_model.CreateUserOverwriteOptions, gothUser *goth.User, allowLink bool) (ok bool) { meta := &user_model.Meta{ InitialIP: ctx.RemoteAddr(), InitialUserAgent: ctx.Req.UserAgent(), diff --git a/routers/web/auth/linkaccount.go b/routers/web/auth/linkaccount.go index 147d8d380244c..a476536f36f44 100644 --- a/routers/web/auth/linkaccount.go +++ b/routers/web/auth/linkaccount.go @@ -11,9 +11,9 @@ import ( "code.gitea.io/gitea/models/auth" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" auth_service "code.gitea.io/gitea/services/auth" @@ -25,10 +25,11 @@ import ( "github.com/markbates/goth" ) -var tplLinkAccount templates.TplName = "user/auth/link_account" +var tplLinkAccount base.TplName = "user/auth/link_account" // LinkAccount shows the page where the user can decide to login or create a new account func LinkAccount(ctx *context.Context) { + // FIXME: these common template variables should be prepared in one common function, but not just copy-paste again and again. ctx.Data["DisablePassword"] = !setting.Service.RequireExternalRegistrationPassword || setting.Service.AllowOnlyExternalRegistration ctx.Data["Title"] = ctx.Tr("link_account") ctx.Data["LinkAccountMode"] = true @@ -43,13 +44,20 @@ func LinkAccount(ctx *context.Context) { ctx.Data["CfTurnstileSitekey"] = setting.Service.CfTurnstileSitekey ctx.Data["DisableRegistration"] = setting.Service.DisableRegistration ctx.Data["AllowOnlyInternalRegistration"] = setting.Service.AllowOnlyInternalRegistration + ctx.Data["EnablePasswordSignInForm"] = setting.Service.EnablePasswordSignInForm ctx.Data["ShowRegistrationButton"] = false + ctx.Data["EnablePasskeyAuth"] = setting.Service.EnablePasskeyAuth // use this to set the right link into the signIn and signUp templates in the link_account template ctx.Data["SignInLink"] = setting.AppSubURL + "/user/link_account_signin" ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/link_account_signup" gothUser, ok := ctx.Session.Get("linkAccountGothUser").(goth.User) + + // If you'd like to quickly debug the "link account" page layout, just uncomment the blow line + // Don't worry, when the below line exists, the lint won't pass: ineffectual assignment to gothUser (ineffassign) + // gothUser, ok = goth.User{Email: "invalid-email", Name: "."}, true // intentionally use invalid data to avoid pass the registration check + if !ok { // no account in session, so just redirect to the login page, then the user could restart the process ctx.Redirect(setting.AppSubURL + "/user/login") @@ -92,7 +100,7 @@ func LinkAccount(ctx *context.Context) { ctx.HTML(http.StatusOK, tplLinkAccount) } -func handleSignInError(ctx *context.Context, userName string, ptrForm any, tmpl templates.TplName, invoker string, err error) { +func handleSignInError(ctx *context.Context, userName string, ptrForm any, tmpl base.TplName, invoker string, err error) { if errors.Is(err, util.ErrNotExist) { ctx.RenderWithErr(ctx.Tr("form.username_password_incorrect"), tmpl, ptrForm) } else if errors.Is(err, util.ErrInvalidArgument) { @@ -135,7 +143,10 @@ func LinkAccountPostSignIn(ctx *context.Context) { ctx.Data["McaptchaURL"] = setting.Service.McaptchaURL ctx.Data["CfTurnstileSitekey"] = setting.Service.CfTurnstileSitekey ctx.Data["DisableRegistration"] = setting.Service.DisableRegistration + ctx.Data["AllowOnlyInternalRegistration"] = setting.Service.AllowOnlyInternalRegistration + ctx.Data["EnablePasswordSignInForm"] = setting.Service.EnablePasswordSignInForm ctx.Data["ShowRegistrationButton"] = false + ctx.Data["EnablePasskeyAuth"] = setting.Service.EnablePasskeyAuth // use this to set the right link into the signIn and signUp templates in the link_account template ctx.Data["SignInLink"] = setting.AppSubURL + "/user/link_account_signin" @@ -223,7 +234,10 @@ func LinkAccountPostRegister(ctx *context.Context) { ctx.Data["McaptchaURL"] = setting.Service.McaptchaURL ctx.Data["CfTurnstileSitekey"] = setting.Service.CfTurnstileSitekey ctx.Data["DisableRegistration"] = setting.Service.DisableRegistration + ctx.Data["AllowOnlyInternalRegistration"] = setting.Service.AllowOnlyInternalRegistration + ctx.Data["EnablePasswordSignInForm"] = setting.Service.EnablePasswordSignInForm ctx.Data["ShowRegistrationButton"] = false + ctx.Data["EnablePasskeyAuth"] = setting.Service.EnablePasskeyAuth // use this to set the right link into the signIn and signUp templates in the link_account template ctx.Data["SignInLink"] = setting.AppSubURL + "/user/link_account_signin" diff --git a/routers/web/auth/oauth.go b/routers/web/auth/oauth.go index 7a9721cf56ffb..75f94de0edad7 100644 --- a/routers/web/auth/oauth.go +++ b/routers/web/auth/oauth.go @@ -15,11 +15,11 @@ import ( "code.gitea.io/gitea/models/auth" user_model "code.gitea.io/gitea/models/user" auth_module "code.gitea.io/gitea/modules/auth" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/web/middleware" source_service "code.gitea.io/gitea/services/auth/source" "code.gitea.io/gitea/services/auth/source/oauth2" @@ -34,7 +34,7 @@ import ( // SignInOAuth handles the OAuth2 login buttons func SignInOAuth(ctx *context.Context) { - provider := ctx.PathParam("provider") + provider := ctx.PathParam(":provider") authSource, err := auth.GetActiveOAuth2SourceByName(ctx, provider) if err != nil { @@ -73,7 +73,7 @@ func SignInOAuth(ctx *context.Context) { // SignInOAuthCallback handles the callback from the given provider func SignInOAuthCallback(ctx *context.Context) { - provider := ctx.PathParam("provider") + provider := ctx.PathParam(":provider") if ctx.Req.FormValue("error") != "" { var errorKeyValues []string @@ -194,7 +194,7 @@ func SignInOAuthCallback(ctx *context.Context) { u.IsAdmin = isAdmin.ValueOrDefault(false) u.IsRestricted = isRestricted.ValueOrDefault(false) - if !createAndHandleCreatedUser(ctx, templates.TplName(""), nil, u, overwriteDefault, &gothUser, setting.OAuth2Client.AccountLinking != setting.OAuth2AccountLinkingDisabled) { + if !createAndHandleCreatedUser(ctx, base.TplName(""), nil, u, overwriteDefault, &gothUser, setting.OAuth2Client.AccountLinking != setting.OAuth2AccountLinkingDisabled) { // error already handled return } diff --git a/routers/web/auth/oauth2_provider.go b/routers/web/auth/oauth2_provider.go index 6262ad8a6db44..5dbb529e0aad0 100644 --- a/routers/web/auth/oauth2_provider.go +++ b/routers/web/auth/oauth2_provider.go @@ -18,7 +18,6 @@ import ( "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/web" auth_service "code.gitea.io/gitea/services/auth" "code.gitea.io/gitea/services/context" @@ -30,8 +29,8 @@ import ( ) const ( - tplGrantAccess templates.TplName = "user/auth/grant" - tplGrantError templates.TplName = "user/auth/grant_error" + tplGrantAccess base.TplName = "user/auth/grant" + tplGrantError base.TplName = "user/auth/grant_error" ) // TODO move error and responses to SDK or models @@ -249,7 +248,7 @@ func AuthorizeOAuth(ctx *context.Context) { }, form.RedirectURI) return } - if err := ctx.Session.Set("CodeChallengeMethod", form.CodeChallenge); err != nil { + if err := ctx.Session.Set("CodeChallenge", form.CodeChallenge); err != nil { handleAuthorizeError(ctx, AuthorizeError{ ErrorCode: ErrorCodeServerError, ErrorDescription: "cannot set code challenge", diff --git a/routers/web/auth/openid.go b/routers/web/auth/openid.go index 41d37ecb8b300..83268faacb6ad 100644 --- a/routers/web/auth/openid.go +++ b/routers/web/auth/openid.go @@ -10,9 +10,9 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/auth/openid" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/services/auth" @@ -21,9 +21,9 @@ import ( ) const ( - tplSignInOpenID templates.TplName = "user/auth/signin_openid" - tplConnectOID templates.TplName = "user/auth/signup_openid_connect" - tplSignUpOID templates.TplName = "user/auth/signup_openid_register" + tplSignInOpenID base.TplName = "user/auth/signin_openid" + tplConnectOID base.TplName = "user/auth/signup_openid_connect" + tplSignUpOID base.TplName = "user/auth/signup_openid_register" ) // SignInOpenID render sign in page diff --git a/routers/web/auth/password.go b/routers/web/auth/password.go index 614e086f773b0..5ec9468ecd8c7 100644 --- a/routers/web/auth/password.go +++ b/routers/web/auth/password.go @@ -11,10 +11,10 @@ import ( "code.gitea.io/gitea/models/auth" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/auth/password" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web/middleware" @@ -26,9 +26,9 @@ import ( var ( // tplMustChangePassword template for updating a user's password - tplMustChangePassword templates.TplName = "user/auth/change_passwd" - tplForgotPassword templates.TplName = "user/auth/forgot_passwd" - tplResetPassword templates.TplName = "user/auth/reset_passwd" + tplMustChangePassword base.TplName = "user/auth/change_passwd" + tplForgotPassword base.TplName = "user/auth/forgot_passwd" + tplResetPassword base.TplName = "user/auth/reset_passwd" ) // ForgotPasswd render the forget password page diff --git a/routers/web/auth/webauthn.go b/routers/web/auth/webauthn.go index 69031adeaa671..d03bcaaf03d20 100644 --- a/routers/web/auth/webauthn.go +++ b/routers/web/auth/webauthn.go @@ -11,9 +11,9 @@ import ( "code.gitea.io/gitea/models/auth" user_model "code.gitea.io/gitea/models/user" wa "code.gitea.io/gitea/modules/auth/webauthn" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/externalaccount" @@ -21,7 +21,7 @@ import ( "github.com/go-webauthn/webauthn/webauthn" ) -var tplWebAuthn templates.TplName = "user/auth/webauthn" +var tplWebAuthn base.TplName = "user/auth/webauthn" // WebAuthn shows the WebAuthn login page func WebAuthn(ctx *context.Context) { @@ -50,6 +50,11 @@ func WebAuthn(ctx *context.Context) { // WebAuthnPasskeyAssertion submits a WebAuthn challenge for the passkey login to the browser func WebAuthnPasskeyAssertion(ctx *context.Context) { + if !setting.Service.EnablePasskeyAuth { + ctx.Error(http.StatusForbidden) + return + } + assertion, sessionData, err := wa.WebAuthn.BeginDiscoverableLogin() if err != nil { ctx.ServerError("webauthn.BeginDiscoverableLogin", err) @@ -66,6 +71,11 @@ func WebAuthnPasskeyAssertion(ctx *context.Context) { // WebAuthnPasskeyLogin handles the WebAuthn login process using a Passkey func WebAuthnPasskeyLogin(ctx *context.Context) { + if !setting.Service.EnablePasskeyAuth { + ctx.Error(http.StatusForbidden) + return + } + sessionData, okData := ctx.Session.Get("webauthnPasskeyAssertion").(*webauthn.SessionData) if !okData || sessionData == nil { ctx.ServerError("ctx.Session.Get", errors.New("not in WebAuthn session")) diff --git a/routers/web/devtest/devtest.go b/routers/web/devtest/devtest.go index 1ea1398173e0c..0068c9fe88771 100644 --- a/routers/web/devtest/devtest.go +++ b/routers/web/devtest/devtest.go @@ -9,10 +9,7 @@ import ( "strings" "time" - "code.gitea.io/gitea/models/asymkey" - "code.gitea.io/gitea/models/db" - user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/services/context" ) @@ -45,85 +42,16 @@ func FetchActionTest(ctx *context.Context) { ctx.JSONRedirect("") } -func prepareMockData(ctx *context.Context) { - if ctx.Req.URL.Path == "/devtest/gitea-ui" { - now := time.Now() - ctx.Data["TimeNow"] = now - ctx.Data["TimePast5s"] = now.Add(-5 * time.Second) - ctx.Data["TimeFuture5s"] = now.Add(5 * time.Second) - ctx.Data["TimePast2m"] = now.Add(-2 * time.Minute) - ctx.Data["TimeFuture2m"] = now.Add(2 * time.Minute) - ctx.Data["TimePast1y"] = now.Add(-1 * 366 * 86400 * time.Second) - ctx.Data["TimeFuture1y"] = now.Add(1 * 366 * 86400 * time.Second) - } - - if ctx.Req.URL.Path == "/devtest/commit-sign-badge" { - var commits []*asymkey.SignCommit - mockUsers, _ := db.Find[user_model.User](ctx, user_model.SearchUserOptions{ListOptions: db.ListOptions{PageSize: 1}}) - mockUser := mockUsers[0] - commits = append(commits, &asymkey.SignCommit{ - Verification: &asymkey.CommitVerification{}, - UserCommit: &user_model.UserCommit{ - Commit: &git.Commit{ID: git.Sha1ObjectFormat.EmptyObjectID()}, - }, - }) - commits = append(commits, &asymkey.SignCommit{ - Verification: &asymkey.CommitVerification{ - Verified: true, - Reason: "name / key-id", - SigningUser: mockUser, - SigningKey: &asymkey.GPGKey{KeyID: "12345678"}, - TrustStatus: "trusted", - }, - UserCommit: &user_model.UserCommit{ - User: mockUser, - Commit: &git.Commit{ID: git.Sha1ObjectFormat.EmptyObjectID()}, - }, - }) - commits = append(commits, &asymkey.SignCommit{ - Verification: &asymkey.CommitVerification{ - Verified: true, - Reason: "name / key-id", - SigningUser: mockUser, - SigningSSHKey: &asymkey.PublicKey{Fingerprint: "aa:bb:cc:dd:ee"}, - TrustStatus: "untrusted", - }, - UserCommit: &user_model.UserCommit{ - User: mockUser, - Commit: &git.Commit{ID: git.Sha1ObjectFormat.EmptyObjectID()}, - }, - }) - commits = append(commits, &asymkey.SignCommit{ - Verification: &asymkey.CommitVerification{ - Verified: true, - Reason: "name / key-id", - SigningUser: mockUser, - SigningSSHKey: &asymkey.PublicKey{Fingerprint: "aa:bb:cc:dd:ee"}, - TrustStatus: "other(unmatch)", - }, - UserCommit: &user_model.UserCommit{ - User: mockUser, - Commit: &git.Commit{ID: git.Sha1ObjectFormat.EmptyObjectID()}, - }, - }) - commits = append(commits, &asymkey.SignCommit{ - Verification: &asymkey.CommitVerification{ - Warning: true, - Reason: "gpg.error", - SigningEmail: "test@example.com", - }, - UserCommit: &user_model.UserCommit{ - User: mockUser, - Commit: &git.Commit{ID: git.Sha1ObjectFormat.EmptyObjectID()}, - }, - }) - - ctx.Data["MockCommits"] = commits - } -} - func Tmpl(ctx *context.Context) { - prepareMockData(ctx) + now := time.Now() + ctx.Data["TimeNow"] = now + ctx.Data["TimePast5s"] = now.Add(-5 * time.Second) + ctx.Data["TimeFuture5s"] = now.Add(5 * time.Second) + ctx.Data["TimePast2m"] = now.Add(-2 * time.Minute) + ctx.Data["TimeFuture2m"] = now.Add(2 * time.Minute) + ctx.Data["TimePast1y"] = now.Add(-1 * 366 * 86400 * time.Second) + ctx.Data["TimeFuture1y"] = now.Add(1 * 366 * 86400 * time.Second) + if ctx.Req.Method == "POST" { _ = ctx.Req.ParseForm() ctx.Flash.Info("form: "+ctx.Req.Method+" "+ctx.Req.RequestURI+"
"+ @@ -133,5 +61,6 @@ func Tmpl(ctx *context.Context) { ) time.Sleep(2 * time.Second) } - ctx.HTML(http.StatusOK, templates.TplName("devtest"+path.Clean("/"+ctx.PathParam("sub")))) + + ctx.HTML(http.StatusOK, base.TplName("devtest"+path.Clean("/"+ctx.PathParam("sub")))) } diff --git a/routers/web/devtest/mock_actions.go b/routers/web/devtest/mock_actions.go index f29b8e40465a1..46e302d634a98 100644 --- a/routers/web/devtest/mock_actions.go +++ b/routers/web/devtest/mock_actions.go @@ -31,11 +31,7 @@ func generateMockStepsLog(logCur actions.LogCursor) (stepsLog []*actions.ViewSte "##[endgroup]", } cur := logCur.Cursor // usually the cursor is the "file offset", but here we abuse it as "line number" to make the mock easier, intentionally - mockCount := util.Iif(logCur.Step == 0, 3, 1) - if logCur.Step == 1 && logCur.Cursor == 0 { - mockCount = 30 // for the first batch, return as many as possible to test the auto-expand and auto-scroll - } - for i := 0; i < mockCount; i++ { + for i := 0; i < util.Iif(logCur.Step == 0, 3, 1); i++ { logStr := mockedLogs[int(cur)%len(mockedLogs)] cur++ logStr = strings.ReplaceAll(logStr, "{step}", fmt.Sprintf("%d", logCur.Step)) @@ -60,21 +56,6 @@ func MockActionsRunsJobs(ctx *context.Context) { resp.State.Run.Status = actions_model.StatusRunning.String() resp.State.Run.CanCancel = true resp.State.Run.CanDeleteArtifact = true - resp.State.Run.WorkflowID = "workflow-id" - resp.State.Run.WorkflowLink = "./workflow-link" - resp.State.Run.Commit = actions.ViewCommit{ - ShortSha: "ccccdddd", - Link: "./commit-link", - Pusher: actions.ViewUser{ - DisplayName: "pusher user", - Link: "./pusher-link", - }, - Branch: actions.ViewBranch{ - Name: "commit-branch", - Link: "./branch-link", - IsDeleted: false, - }, - } resp.Artifacts = append(resp.Artifacts, &actions.ArtifactsViewItem{ Name: "artifact-a", Size: 100 * 1024, diff --git a/routers/web/explore/code.go b/routers/web/explore/code.go index 4df89253b4ca6..3fca36c9abaaf 100644 --- a/routers/web/explore/code.go +++ b/routers/web/explore/code.go @@ -8,15 +8,16 @@ import ( "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/modules/base" code_indexer "code.gitea.io/gitea/modules/indexer/code" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/templates" + "code.gitea.io/gitea/routers/common" "code.gitea.io/gitea/services/context" ) const ( // tplExploreCode explore code page template - tplExploreCode templates.TplName = "explore/code" + tplExploreCode base.TplName = "explore/code" ) // Code render explore code page @@ -32,18 +33,10 @@ func Code(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("explore") ctx.Data["PageIsExplore"] = true ctx.Data["PageIsExploreCode"] = true - - language := ctx.FormTrim("l") - keyword := ctx.FormTrim("q") - - isFuzzy := ctx.FormOptionalBool("fuzzy").ValueOrDefault(true) - - ctx.Data["Keyword"] = keyword - ctx.Data["Language"] = language - ctx.Data["IsFuzzy"] = isFuzzy ctx.Data["PageIsViewCode"] = true - if keyword == "" { + prepareSearch := common.PrepareCodeSearch(ctx) + if prepareSearch.Keyword == "" { ctx.HTML(http.StatusOK, tplExploreCode) return } @@ -80,9 +73,9 @@ func Code(ctx *context.Context) { if (len(repoIDs) > 0) || isAdmin { total, searchResults, searchResultLanguages, err = code_indexer.PerformSearch(ctx, &code_indexer.SearchOptions{ RepoIDs: repoIDs, - Keyword: keyword, - IsKeywordFuzzy: isFuzzy, - Language: language, + Keyword: prepareSearch.Keyword, + IsKeywordFuzzy: prepareSearch.IsFuzzy, + Language: prepareSearch.Language, Paginator: &db.ListOptions{ Page: page, PageSize: setting.UI.RepoSearchPagingNum, @@ -138,7 +131,7 @@ func Code(ctx *context.Context) { pager := context.NewPagination(total, setting.UI.RepoSearchPagingNum, page, 5) pager.SetDefaultParams(ctx) - pager.AddParamString("l", language) + pager.AddParamString("l", prepareSearch.Language) ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, tplExploreCode) diff --git a/routers/web/explore/repo.go b/routers/web/explore/repo.go index c421aea715109..5b6f612e72201 100644 --- a/routers/web/explore/repo.go +++ b/routers/web/explore/repo.go @@ -9,17 +9,17 @@ import ( "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/sitemap" - "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/services/context" ) const ( // tplExploreRepos explore repositories page template - tplExploreRepos templates.TplName = "explore/repos" - relevantReposOnlyParam string = "only_show_relevant" + tplExploreRepos base.TplName = "explore/repos" + relevantReposOnlyParam string = "only_show_relevant" ) // RepoSearchOptions when calling search repositories @@ -29,7 +29,7 @@ type RepoSearchOptions struct { Restricted bool PageSize int OnlyShowRelevant bool - TplName templates.TplName + TplName base.TplName } // RenderRepoSearch render repositories search page diff --git a/routers/web/explore/user.go b/routers/web/explore/user.go index ef103af8cf5ef..c009982d420ce 100644 --- a/routers/web/explore/user.go +++ b/routers/web/explore/user.go @@ -9,20 +9,20 @@ import ( "code.gitea.io/gitea/models/db" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/sitemap" "code.gitea.io/gitea/modules/structs" - "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/services/context" ) const ( // tplExploreUsers explore users page template - tplExploreUsers templates.TplName = "explore/users" + tplExploreUsers base.TplName = "explore/users" ) var nullByte = []byte{0x00} @@ -32,7 +32,7 @@ func isKeywordValid(keyword string) bool { } // RenderUserSearch render user search page -func RenderUserSearch(ctx *context.Context, opts *user_model.SearchUserOptions, tplName templates.TplName) { +func RenderUserSearch(ctx *context.Context, opts *user_model.SearchUserOptions, tplName base.TplName) { // Sitemap index for sitemap paths opts.Page = int(ctx.PathParamInt64("idx")) isSitemap := ctx.PathParam("idx") != "" diff --git a/routers/web/feed/branch.go b/routers/web/feed/branch.go index 80ce2ad198bcd..a8a001e0cd297 100644 --- a/routers/web/feed/branch.go +++ b/routers/web/feed/branch.go @@ -43,6 +43,7 @@ func ShowBranchFeed(ctx *context.Context, repo *repo.Repository, formatType stri }, Description: commit.Message(), Content: commit.Message(), + Created: commit.Committer.When, }) } diff --git a/routers/web/feed/file.go b/routers/web/feed/file.go index 1ab768ff27fcb..48f87c7c62d43 100644 --- a/routers/web/feed/file.go +++ b/routers/web/feed/file.go @@ -55,6 +55,7 @@ func ShowFileFeed(ctx *context.Context, repo *repo.Repository, formatType string }, Description: commit.Message(), Content: commit.Message(), + Created: commit.Committer.When, }) } diff --git a/routers/web/feed/render.go b/routers/web/feed/render.go index 462ebb97b5a89..f975fc7cb2f2f 100644 --- a/routers/web/feed/render.go +++ b/routers/web/feed/render.go @@ -9,7 +9,7 @@ import ( // RenderBranchFeed render format for branch or file func RenderBranchFeed(ctx *context.Context) { - _, _, showFeedType := GetFeedType(ctx.PathParam("reponame"), ctx.Req) + _, _, showFeedType := GetFeedType(ctx.PathParam(":reponame"), ctx.Req) if ctx.Repo.TreePath == "" { ShowBranchFeed(ctx, ctx.Repo.Repository, showFeedType) } else { diff --git a/routers/web/githttp.go b/routers/web/githttp.go index 5831e6f52366d..06de811f16e11 100644 --- a/routers/web/githttp.go +++ b/routers/web/githttp.go @@ -4,26 +4,12 @@ package web import ( - "net/http" - - "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/routers/web/repo" "code.gitea.io/gitea/services/context" ) func addOwnerRepoGitHTTPRouters(m *web.Router) { - reqGitSignIn := func(ctx *context.Context) { - if !setting.Service.RequireSignInView { - return - } - // rely on the results of Contexter - if !ctx.IsSigned { - // TODO: support digit auth - which would be Authorization header with digit - ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm="Gitea"`) - ctx.Error(http.StatusUnauthorized) - } - } m.Group("/{username}/{reponame}", func() { m.Methods("POST,OPTIONS", "/git-upload-pack", repo.ServiceUploadPack) m.Methods("POST,OPTIONS", "/git-receive-pack", repo.ServiceReceivePack) @@ -36,5 +22,5 @@ func addOwnerRepoGitHTTPRouters(m *web.Router) { m.Methods("GET,OPTIONS", "/objects/{head:[0-9a-f]{2}}/{hash:[0-9a-f]{38,62}}", repo.GetLooseObject) m.Methods("GET,OPTIONS", "/objects/pack/pack-{file:[0-9a-f]{40,64}}.pack", repo.GetPackFile) m.Methods("GET,OPTIONS", "/objects/pack/pack-{file:[0-9a-f]{40,64}}.idx", repo.GetIdxFile) - }, optSignInIgnoreCsrf, reqGitSignIn, repo.HTTPGitEnabledHandler, repo.CorsHandler(), context.UserAssignmentWeb()) + }, optSignInIgnoreCsrf, repo.HTTPGitEnabledHandler, repo.CorsHandler(), context.UserAssignmentWeb()) } diff --git a/routers/web/home.go b/routers/web/home.go index 9ad495d54f73c..d4be0931e850d 100644 --- a/routers/web/home.go +++ b/routers/web/home.go @@ -11,12 +11,12 @@ 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/base" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/sitemap" "code.gitea.io/gitea/modules/structs" - "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/web/middleware" "code.gitea.io/gitea/routers/web/auth" "code.gitea.io/gitea/routers/web/user" @@ -25,7 +25,7 @@ import ( const ( // tplHome home page template - tplHome templates.TplName = "home" + tplHome base.TplName = "home" ) // Home render home page diff --git a/routers/web/misc/swagger.go b/routers/web/misc/swagger.go index 1ca347551c1e5..5fddfa8885f82 100644 --- a/routers/web/misc/swagger.go +++ b/routers/web/misc/swagger.go @@ -6,12 +6,12 @@ package misc import ( "net/http" - "code.gitea.io/gitea/modules/templates" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/services/context" ) // tplSwagger swagger page template -const tplSwagger templates.TplName = "swagger/ui" +const tplSwagger base.TplName = "swagger/ui" // Swagger render swagger-ui page with v1 json func Swagger(ctx *context.Context) { diff --git a/routers/web/org/block.go b/routers/web/org/block.go index aeb4bd51a8c95..d40458e25066f 100644 --- a/routers/web/org/block.go +++ b/routers/web/org/block.go @@ -6,13 +6,13 @@ package org import ( "net/http" - "code.gitea.io/gitea/modules/templates" + "code.gitea.io/gitea/modules/base" shared_user "code.gitea.io/gitea/routers/web/shared/user" "code.gitea.io/gitea/services/context" ) const ( - tplSettingsBlockedUsers templates.TplName = "org/settings/blocked_users" + tplSettingsBlockedUsers base.TplName = "org/settings/blocked_users" ) func BlockedUsers(ctx *context.Context) { diff --git a/routers/web/org/home.go b/routers/web/org/home.go index bdc43acc300db..f02c08ae7691a 100644 --- a/routers/web/org/home.go +++ b/routers/web/org/home.go @@ -13,29 +13,29 @@ import ( "code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/renderhelper" repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/markup/markdown" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/util" shared_user "code.gitea.io/gitea/routers/web/shared/user" "code.gitea.io/gitea/services/context" ) const ( - tplOrgHome templates.TplName = "org/home" + tplOrgHome base.TplName = "org/home" ) // Home show organization home page func Home(ctx *context.Context) { - uname := ctx.PathParam("username") + uname := ctx.PathParam(":username") if strings.HasSuffix(uname, ".keys") || strings.HasSuffix(uname, ".gpg") { ctx.NotFound("", nil) return } - ctx.SetPathParam("org", uname) + ctx.SetPathParam(":org", uname) context.HandleOrgAssignment(ctx) if ctx.Written() { return diff --git a/routers/web/org/members.go b/routers/web/org/members.go index 5a134caecb0a2..7af087c4df38d 100644 --- a/routers/web/org/members.go +++ b/routers/web/org/members.go @@ -9,9 +9,9 @@ import ( "code.gitea.io/gitea/models/organization" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/templates" shared_user "code.gitea.io/gitea/routers/web/shared/user" "code.gitea.io/gitea/services/context" org_service "code.gitea.io/gitea/services/org" @@ -19,7 +19,7 @@ import ( const ( // tplMembers template for organization members page - tplMembers templates.TplName = "org/member/members" + tplMembers base.TplName = "org/member/members" ) // Members render organization users page @@ -90,7 +90,7 @@ func MembersAction(ctx *context.Context) { org := ctx.Org.Organization - switch ctx.PathParam("action") { + switch ctx.PathParam(":action") { case "private": if ctx.Doer.ID != member.ID && !ctx.Org.IsOwner { ctx.Error(http.StatusNotFound) @@ -131,7 +131,7 @@ func MembersAction(ctx *context.Context) { } if err != nil { - log.Error("Action(%s): %v", ctx.PathParam("action"), err) + log.Error("Action(%s): %v", ctx.PathParam(":action"), err) ctx.JSON(http.StatusOK, map[string]any{ "ok": false, "err": err.Error(), @@ -140,7 +140,7 @@ func MembersAction(ctx *context.Context) { } redirect := ctx.Org.OrgLink + "/members" - if ctx.PathParam("action") == "leave" { + if ctx.PathParam(":action") == "leave" { redirect = setting.AppSubURL + "/" } diff --git a/routers/web/org/org.go b/routers/web/org/org.go index 856a6057643b3..f94dd16eaef96 100644 --- a/routers/web/org/org.go +++ b/routers/web/org/org.go @@ -11,9 +11,9 @@ import ( "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/organization" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/forms" @@ -21,7 +21,7 @@ import ( const ( // tplCreateOrg template path for create organization - tplCreateOrg templates.TplName = "org/create" + tplCreateOrg base.TplName = "org/create" ) // Create render the page for create organization diff --git a/routers/web/org/projects.go b/routers/web/org/projects.go index efcc8fadc8e8d..8eeb67a1acbf9 100644 --- a/routers/web/org/projects.go +++ b/routers/web/org/projects.go @@ -15,6 +15,7 @@ import ( project_model "code.gitea.io/gitea/models/project" attachment_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" @@ -28,9 +29,9 @@ import ( ) const ( - tplProjects templates.TplName = "org/projects/list" - tplProjectsNew templates.TplName = "org/projects/new" - tplProjectsView templates.TplName = "org/projects/view" + tplProjects base.TplName = "org/projects/list" + tplProjectsNew base.TplName = "org/projects/new" + tplProjectsView base.TplName = "org/projects/view" ) // MustEnableProjects check if projects are enabled in settings @@ -77,6 +78,11 @@ func Projects(ctx *context.Context) { return } + if err := project_service.LoadIssueNumbersForProjects(ctx, projects, ctx.Doer); err != nil { + ctx.ServerError("LoadIssueNumbersForProjects", err) + return + } + opTotal, err := db.Count[project_model.Project](ctx, project_model.SearchOptions{ OwnerID: ctx.ContextUser.ID, IsClosed: optional.Some(!isShowClosed), @@ -196,7 +202,7 @@ func NewProjectPost(ctx *context.Context) { // ChangeProjectStatus updates the status of a project between "open" and "close" func ChangeProjectStatus(ctx *context.Context) { var toClose bool - switch ctx.PathParam("action") { + switch ctx.PathParam(":action") { case "open": toClose = false case "close": @@ -205,18 +211,18 @@ func ChangeProjectStatus(ctx *context.Context) { ctx.JSONRedirect(ctx.ContextUser.HomeLink() + "/-/projects") return } - id := ctx.PathParamInt64("id") + id := ctx.PathParamInt64(":id") if err := project_model.ChangeProjectStatusByRepoIDAndID(ctx, 0, id, toClose); err != nil { ctx.NotFoundOrServerError("ChangeProjectStatusByRepoIDAndID", project_model.IsErrProjectNotExist, err) return } - ctx.JSONRedirect(fmt.Sprintf("%s/-/projects/%d", ctx.ContextUser.HomeLink(), id)) + ctx.JSONRedirect(project_model.ProjectLinkForOrg(ctx.ContextUser, id)) } // DeleteProject delete a project func DeleteProject(ctx *context.Context) { - p, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64("id")) + p, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64(":id")) if err != nil { ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err) return @@ -245,7 +251,7 @@ func RenderEditProject(ctx *context.Context) { shared_user.RenderUserHeader(ctx) - p, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64("id")) + p, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64(":id")) if err != nil { ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err) return @@ -261,7 +267,7 @@ func RenderEditProject(ctx *context.Context) { ctx.Data["redirect"] = ctx.FormString("redirect") ctx.Data["HomeLink"] = ctx.ContextUser.HomeLink() ctx.Data["card_type"] = p.CardType - ctx.Data["CancelLink"] = fmt.Sprintf("%s/-/projects/%d", ctx.ContextUser.HomeLink(), p.ID) + ctx.Data["CancelLink"] = project_model.ProjectLinkForOrg(ctx.ContextUser, p.ID) ctx.HTML(http.StatusOK, tplProjectsNew) } @@ -269,13 +275,13 @@ func RenderEditProject(ctx *context.Context) { // EditProjectPost response for editing a project func EditProjectPost(ctx *context.Context) { form := web.GetForm(ctx).(*forms.CreateProjectForm) - projectID := ctx.PathParamInt64("id") + projectID := ctx.PathParamInt64(":id") ctx.Data["Title"] = ctx.Tr("repo.projects.edit") ctx.Data["PageIsEditProjects"] = true ctx.Data["PageIsViewProjects"] = true ctx.Data["CanWriteProjects"] = canWriteProjects(ctx) ctx.Data["CardTypes"] = project_model.GetCardConfig() - ctx.Data["CancelLink"] = fmt.Sprintf("%s/-/projects/%d", ctx.ContextUser.HomeLink(), projectID) + ctx.Data["CancelLink"] = project_model.ProjectLinkForOrg(ctx.ContextUser, projectID) shared_user.RenderUserHeader(ctx) @@ -318,7 +324,7 @@ func EditProjectPost(ctx *context.Context) { // ViewProject renders the project with board view for a project func ViewProject(ctx *context.Context) { - project, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64("id")) + project, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64(":id")) if err != nil { ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err) return @@ -327,6 +333,10 @@ func ViewProject(ctx *context.Context) { ctx.NotFound("", nil) return } + if err := project.LoadOwner(ctx); err != nil { + ctx.ServerError("LoadOwner", err) + return + } columns, err := project.GetColumns(ctx) if err != nil { @@ -340,14 +350,21 @@ func ViewProject(ctx *context.Context) { } assigneeID := ctx.FormInt64("assignee") // TODO: use "optional" but not 0 in the future - issuesMap, err := issues_model.LoadIssuesFromColumnList(ctx, columns, &issues_model.IssuesOptions{ + opts := issues_model.IssuesOptions{ LabelIDs: labelIDs, AssigneeID: optional.Some(assigneeID), - }) + Owner: project.Owner, + Doer: ctx.Doer, + } + + issuesMap, err := project_service.LoadIssuesFromProject(ctx, project, &opts) if err != nil { ctx.ServerError("LoadIssuesOfColumns", err) return } + for _, column := range columns { + column.NumIssues = int64(len(issuesMap[column.ID])) + } if project.CardType != project_model.CardTypeTextOnly { issuesAttachmentMap := make(map[int64][]*attachment_model.Attachment) @@ -447,18 +464,18 @@ func DeleteProjectColumn(ctx *context.Context) { return } - project, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64("id")) + project, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64(":id")) if err != nil { ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err) return } - pb, err := project_model.GetColumn(ctx, ctx.PathParamInt64("columnID")) + pb, err := project_model.GetColumn(ctx, ctx.PathParamInt64(":columnID")) if err != nil { ctx.ServerError("GetProjectColumn", err) return } - if pb.ProjectID != ctx.PathParamInt64("id") { + if pb.ProjectID != ctx.PathParamInt64(":id") { ctx.JSON(http.StatusUnprocessableEntity, map[string]string{ "message": fmt.Sprintf("ProjectColumn[%d] is not in Project[%d] as expected", pb.ID, project.ID), }) @@ -472,7 +489,7 @@ func DeleteProjectColumn(ctx *context.Context) { return } - if err := project_model.DeleteColumnByID(ctx, ctx.PathParamInt64("columnID")); err != nil { + if err := project_model.DeleteColumnByID(ctx, ctx.PathParamInt64(":columnID")); err != nil { ctx.ServerError("DeleteProjectColumnByID", err) return } @@ -484,7 +501,7 @@ func DeleteProjectColumn(ctx *context.Context) { func AddColumnToProjectPost(ctx *context.Context) { form := web.GetForm(ctx).(*forms.EditProjectColumnForm) - project, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64("id")) + project, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64(":id")) if err != nil { ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err) return @@ -512,18 +529,18 @@ func CheckProjectColumnChangePermissions(ctx *context.Context) (*project_model.P return nil, nil } - project, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64("id")) + project, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64(":id")) if err != nil { ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err) return nil, nil } - column, err := project_model.GetColumn(ctx, ctx.PathParamInt64("columnID")) + column, err := project_model.GetColumn(ctx, ctx.PathParamInt64(":columnID")) if err != nil { ctx.ServerError("GetProjectColumn", err) return nil, nil } - if column.ProjectID != ctx.PathParamInt64("id") { + if column.ProjectID != ctx.PathParamInt64(":id") { ctx.JSON(http.StatusUnprocessableEntity, map[string]string{ "message": fmt.Sprintf("ProjectColumn[%d] is not in Project[%d] as expected", column.ID, project.ID), }) @@ -587,7 +604,7 @@ func MoveIssues(ctx *context.Context) { return } - project, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64("id")) + project, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64(":id")) if err != nil { ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err) return @@ -597,7 +614,7 @@ func MoveIssues(ctx *context.Context) { return } - column, err := project_model.GetColumn(ctx, ctx.PathParamInt64("columnID")) + column, err := project_model.GetColumn(ctx, ctx.PathParamInt64(":columnID")) if err != nil { ctx.NotFoundOrServerError("GetProjectColumn", project_model.IsErrProjectColumnNotExist, err) return diff --git a/routers/web/org/projects_test.go b/routers/web/org/projects_test.go index c3a769e621e9e..c52cb7ed4ce98 100644 --- a/routers/web/org/projects_test.go +++ b/routers/web/org/projects_test.go @@ -18,8 +18,8 @@ func TestCheckProjectColumnChangePermissions(t *testing.T) { ctx, _ := contexttest.MockContext(t, "user2/-/projects/4/4") contexttest.LoadUser(t, ctx, 2) ctx.ContextUser = ctx.Doer // user2 - ctx.SetPathParam("id", "4") - ctx.SetPathParam("columnID", "4") + ctx.SetPathParam(":id", "4") + ctx.SetPathParam(":columnID", "4") project, column := org.CheckProjectColumnChangePermissions(ctx) assert.NotNil(t, project) diff --git a/routers/web/org/setting.go b/routers/web/org/setting.go index cb1c4213c96a6..494ada4323a66 100644 --- a/routers/web/org/setting.go +++ b/routers/web/org/setting.go @@ -8,16 +8,16 @@ import ( "net/http" "net/url" + "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/db" - packages_model "code.gitea.io/gitea/models/packages" 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/base" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/optional" repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/web" shared_user "code.gitea.io/gitea/routers/web/shared/user" user_setting "code.gitea.io/gitea/routers/web/user/setting" @@ -30,13 +30,13 @@ import ( const ( // tplSettingsOptions template path for render settings - tplSettingsOptions templates.TplName = "org/settings/options" + tplSettingsOptions base.TplName = "org/settings/options" // tplSettingsDelete template path for render delete repository - tplSettingsDelete templates.TplName = "org/settings/delete" + tplSettingsDelete base.TplName = "org/settings/delete" // tplSettingsHooks template path for render hook settings - tplSettingsHooks templates.TplName = "org/settings/hooks" + tplSettingsHooks base.TplName = "org/settings/hooks" // tplSettingsLabels template path for render labels settings - tplSettingsLabels templates.TplName = "org/settings/labels" + tplSettingsLabels base.TplName = "org/settings/labels" ) // Settings render the main settings page @@ -178,10 +178,10 @@ func SettingsDelete(ctx *context.Context) { } if err := org_service.DeleteOrganization(ctx, ctx.Org.Organization, false); err != nil { - if repo_model.IsErrUserOwnRepos(err) { + if models.IsErrUserOwnRepos(err) { ctx.Flash.Error(ctx.Tr("form.org_still_own_repo")) ctx.Redirect(ctx.Org.OrgLink + "/settings/delete") - } else if packages_model.IsErrUserOwnPackages(err) { + } else if models.IsErrUserOwnPackages(err) { ctx.Flash.Error(ctx.Tr("form.org_still_own_packages")) ctx.Redirect(ctx.Org.OrgLink + "/settings/delete") } else { diff --git a/routers/web/org/setting_oauth2.go b/routers/web/org/setting_oauth2.go index c93058477ef5c..7f855795d3639 100644 --- a/routers/web/org/setting_oauth2.go +++ b/routers/web/org/setting_oauth2.go @@ -9,16 +9,16 @@ import ( "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/templates" shared_user "code.gitea.io/gitea/routers/web/shared/user" user_setting "code.gitea.io/gitea/routers/web/user/setting" "code.gitea.io/gitea/services/context" ) const ( - tplSettingsApplications templates.TplName = "org/settings/applications" - tplSettingsOAuthApplicationEdit templates.TplName = "org/settings/applications_oauth2_edit" + tplSettingsApplications base.TplName = "org/settings/applications" + tplSettingsOAuthApplicationEdit base.TplName = "org/settings/applications_oauth2_edit" ) func newOAuth2CommonHandlers(org *context.Organization) *user_setting.OAuth2CommonHandlers { diff --git a/routers/web/org/setting_packages.go b/routers/web/org/setting_packages.go index 0912a9e0fde9b..af9836e42c2b3 100644 --- a/routers/web/org/setting_packages.go +++ b/routers/web/org/setting_packages.go @@ -7,17 +7,17 @@ import ( "fmt" "net/http" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/templates" shared "code.gitea.io/gitea/routers/web/shared/packages" shared_user "code.gitea.io/gitea/routers/web/shared/user" "code.gitea.io/gitea/services/context" ) const ( - tplSettingsPackages templates.TplName = "org/settings/packages" - tplSettingsPackagesRuleEdit templates.TplName = "org/settings/packages_cleanup_rules_edit" - tplSettingsPackagesRulePreview templates.TplName = "org/settings/packages_cleanup_rules_preview" + tplSettingsPackages base.TplName = "org/settings/packages" + tplSettingsPackagesRuleEdit base.TplName = "org/settings/packages_cleanup_rules_edit" + tplSettingsPackagesRulePreview base.TplName = "org/settings/packages_cleanup_rules_preview" ) func Packages(ctx *context.Context) { diff --git a/routers/web/org/teams.go b/routers/web/org/teams.go index 0137f2cc961f2..bd78832103251 100644 --- a/routers/web/org/teams.go +++ b/routers/web/org/teams.go @@ -19,9 +19,9 @@ import ( repo_model "code.gitea.io/gitea/models/repo" unit_model "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/web" shared_user "code.gitea.io/gitea/routers/web/shared/user" "code.gitea.io/gitea/services/context" @@ -33,15 +33,15 @@ import ( const ( // tplTeams template path for teams list page - tplTeams templates.TplName = "org/team/teams" + tplTeams base.TplName = "org/team/teams" // tplTeamNew template path for create new team page - tplTeamNew templates.TplName = "org/team/new" + tplTeamNew base.TplName = "org/team/new" // tplTeamMembers template path for showing team members page - tplTeamMembers templates.TplName = "org/team/members" + tplTeamMembers base.TplName = "org/team/members" // tplTeamRepositories template path for showing team repositories page - tplTeamRepositories templates.TplName = "org/team/repositories" + tplTeamRepositories base.TplName = "org/team/repositories" // tplTeamInvite template path for team invites page - tplTeamInvite templates.TplName = "org/team/invite" + tplTeamInvite base.TplName = "org/team/invite" ) // Teams render teams list page @@ -71,7 +71,7 @@ func Teams(ctx *context.Context) { func TeamsAction(ctx *context.Context) { page := ctx.FormString("page") var err error - switch ctx.PathParam("action") { + switch ctx.PathParam(":action") { case "join": if !ctx.Org.IsOwner { ctx.Error(http.StatusNotFound) @@ -84,7 +84,7 @@ func TeamsAction(ctx *context.Context) { if org_model.IsErrLastOrgOwner(err) { ctx.Flash.Error(ctx.Tr("form.last_org_owner")) } else { - log.Error("Action(%s): %v", ctx.PathParam("action"), err) + log.Error("Action(%s): %v", ctx.PathParam(":action"), err) ctx.JSON(http.StatusOK, map[string]any{ "ok": false, "err": err.Error(), @@ -111,7 +111,7 @@ func TeamsAction(ctx *context.Context) { if org_model.IsErrLastOrgOwner(err) { ctx.Flash.Error(ctx.Tr("form.last_org_owner")) } else { - log.Error("Action(%s): %v", ctx.PathParam("action"), err) + log.Error("Action(%s): %v", ctx.PathParam(":action"), err) ctx.JSON(http.StatusOK, map[string]any{ "ok": false, "err": err.Error(), @@ -178,7 +178,7 @@ func TeamsAction(ctx *context.Context) { } if err := org_model.RemoveInviteByID(ctx, iid, ctx.Org.Team.ID); err != nil { - log.Error("Action(%s): %v", ctx.PathParam("action"), err) + log.Error("Action(%s): %v", ctx.PathParam(":action"), err) ctx.ServerError("RemoveInviteByID", err) return } @@ -192,7 +192,7 @@ func TeamsAction(ctx *context.Context) { } else if errors.Is(err, user_model.ErrBlockedUser) { ctx.Flash.Error(ctx.Tr("org.teams.members.blocked_user")) } else { - log.Error("Action(%s): %v", ctx.PathParam("action"), err) + log.Error("Action(%s): %v", ctx.PathParam(":action"), err) ctx.JSON(http.StatusOK, map[string]any{ "ok": false, "err": err.Error(), @@ -233,7 +233,7 @@ func TeamsRepoAction(ctx *context.Context) { } var err error - action := ctx.PathParam("action") + action := ctx.PathParam(":action") switch action { case "add": repoName := path.Base(ctx.FormString("repo_name")) @@ -258,7 +258,7 @@ func TeamsRepoAction(ctx *context.Context) { } if err != nil { - log.Error("Action(%s): '%s' %v", ctx.PathParam("action"), ctx.Org.Team.Name, err) + log.Error("Action(%s): '%s' %v", ctx.PathParam(":action"), ctx.Org.Team.Name, err) ctx.ServerError("TeamsRepoAction", err) return } @@ -410,15 +410,11 @@ func TeamRepositories(ctx *context.Context) { return } - repos, err := repo_model.GetTeamRepositories(ctx, &repo_model.SearchTeamRepoOptions{ - TeamID: ctx.Org.Team.ID, - }) - if err != nil { - ctx.ServerError("GetTeamRepositories", err) + if err := ctx.Org.Team.LoadRepositories(ctx); err != nil { + ctx.ServerError("GetRepositories", err) return } ctx.Data["Units"] = unit_model.Units - ctx.Data["TeamRepos"] = repos ctx.HTML(http.StatusOK, tplTeamRepositories) } diff --git a/routers/web/org/view_view.go b/routers/web/org/view_view.go index 7a0ad889e4de4..23c64f3ae1d90 100644 --- a/routers/web/org/view_view.go +++ b/routers/web/org/view_view.go @@ -1,14 +1,17 @@ +// Copyright 2025 The Bindersnap Authors. All rights reserved. +// SPDX-License-Identifier: LicenseRef-License + package org import ( "net/http" - "code.gitea.io/gitea/modules/templates" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/services/context" ) const ( - tplOrgViewHome templates.TplName = "org/view/home" + tplOrgViewHome base.TplName = "org/view/home" ) // View render repository view page diff --git a/routers/web/repo/actions/actions.go b/routers/web/repo/actions/actions.go index 099593bff08c7..46b7461883f18 100644 --- a/routers/web/repo/actions/actions.go +++ b/routers/web/repo/actions/actions.go @@ -6,6 +6,7 @@ package actions import ( "bytes" stdCtx "context" + "errors" "fmt" "net/http" "slices" @@ -17,12 +18,12 @@ import ( repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/modules/actions" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/util" shared_user "code.gitea.io/gitea/routers/web/shared/user" "code.gitea.io/gitea/services/context" @@ -33,8 +34,8 @@ import ( ) const ( - tplListActions templates.TplName = "repo/actions/list" - tplViewActions templates.TplName = "repo/actions/view" + tplListActions base.TplName = "repo/actions/list" + tplViewActions base.TplName = "repo/actions/view" ) type Workflow struct { @@ -77,7 +78,11 @@ func List(ctx *context.Context) { return } else if !empty { commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch) - if err != nil { + if errors.Is(err, util.ErrNotExist) { + ctx.Data["NotFoundPrompt"] = ctx.Tr("repo.branch.default_branch_not_exist", ctx.Repo.Repository.DefaultBranch) + ctx.NotFound("GetBranchCommit", err) + return + } else if err != nil { ctx.ServerError("GetBranchCommit", err) return } diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go index ba17fa427d1a8..1e829caa8c823 100644 --- a/routers/web/repo/actions/view.go +++ b/routers/web/repo/actions/view.go @@ -855,7 +855,7 @@ func Run(ctx *context_module.Context) { inputs := make(map[string]any) if workflowDispatch := workflow.WorkflowDispatchConfig(); workflowDispatch != nil { for name, config := range workflowDispatch.Inputs { - value := ctx.Req.PostForm.Get(name) + value := ctx.Req.PostFormValue(name) if config.Type == "boolean" { // https://www.w3.org/TR/html401/interact/forms.html // https://stackoverflow.com/questions/11424037/do-checkbox-inputs-only-post-data-if-theyre-checked @@ -903,7 +903,7 @@ func Run(ctx *context_module.Context) { } // cancel running jobs of the same workflow - if err := actions_model.CancelPreviousJobs( + if err := actions_service.CancelPreviousJobs( ctx, run.RepoID, run.Ref, diff --git a/routers/web/repo/activity.go b/routers/web/repo/activity.go index 1d809ad8e9d98..65fd379406aba 100644 --- a/routers/web/repo/activity.go +++ b/routers/web/repo/activity.go @@ -8,13 +8,14 @@ import ( "time" activities_model "code.gitea.io/gitea/models/activities" + "code.gitea.io/gitea/models/git" "code.gitea.io/gitea/models/unit" - "code.gitea.io/gitea/modules/templates" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/services/context" ) const ( - tplActivity templates.TplName = "repo/activity" + tplActivity base.TplName = "repo/activity" ) // Activity render the page to show repository latest changes @@ -52,12 +53,26 @@ func Activity(ctx *context.Context) { ctx.Data["DateUntil"] = timeUntil ctx.Data["PeriodText"] = ctx.Tr("repo.activity.period." + ctx.Data["Period"].(string)) + canReadCode := ctx.Repo.CanRead(unit.TypeCode) + if canReadCode { + // GetActivityStats needs to read the default branch to get some information + branchExist, _ := git.IsBranchExist(ctx, ctx.Repo.Repository.ID, ctx.Repo.Repository.DefaultBranch) + if !branchExist { + ctx.Data["NotFoundPrompt"] = ctx.Tr("repo.branch.default_branch_not_exist", ctx.Repo.Repository.DefaultBranch) + ctx.NotFound("", nil) + return + } + } + var err error - if ctx.Data["Activity"], err = activities_model.GetActivityStats(ctx, ctx.Repo.Repository, timeFrom, + // TODO: refactor these arguments to a struct + ctx.Data["Activity"], err = activities_model.GetActivityStats(ctx, ctx.Repo.Repository, timeFrom, ctx.Repo.CanRead(unit.TypeReleases), ctx.Repo.CanRead(unit.TypeIssues), ctx.Repo.CanRead(unit.TypePullRequests), - ctx.Repo.CanRead(unit.TypeCode)); err != nil { + canReadCode, + ) + if err != nil { ctx.ServerError("GetActivityStats", err) return } diff --git a/routers/web/repo/attachment.go b/routers/web/repo/attachment.go index f8e51521be525..04f480f611013 100644 --- a/routers/web/repo/attachment.go +++ b/routers/web/repo/attachment.go @@ -154,5 +154,5 @@ func ServeAttachment(ctx *context.Context, uuid string) { // GetAttachment serve attachments func GetAttachment(ctx *context.Context) { - ServeAttachment(ctx, ctx.PathParam("uuid")) + ServeAttachment(ctx, ctx.PathParam(":uuid")) } diff --git a/routers/web/repo/branch.go b/routers/web/repo/branch.go index 72fd958e28b35..c918cd7a72fe2 100644 --- a/routers/web/repo/branch.go +++ b/routers/web/repo/branch.go @@ -11,27 +11,27 @@ import ( "net/url" "strings" + "code.gitea.io/gitea/models" git_model "code.gitea.io/gitea/models/git" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/optional" repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/routers/utils" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/forms" - pull_service "code.gitea.io/gitea/services/pull" release_service "code.gitea.io/gitea/services/release" repo_service "code.gitea.io/gitea/services/repository" ) const ( - tplBranch templates.TplName = "repo/branch/list" + tplBranch base.TplName = "repo/branch/list" ) // Branches render repository branch page @@ -203,14 +203,14 @@ func CreateBranch(ctx *context.Context) { err = repo_service.CreateNewBranchFromCommit(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.GitRepo, ctx.Repo.CommitID, form.NewBranchName) } if err != nil { - if release_service.IsErrProtectedTagName(err) { + if models.IsErrProtectedTagName(err) { ctx.Flash.Error(ctx.Tr("repo.release.tag_name_protected")) ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()) return } - if release_service.IsErrTagAlreadyExists(err) { - e := err.(release_service.ErrTagAlreadyExists) + if models.IsErrTagAlreadyExists(err) { + e := err.(models.ErrTagAlreadyExists) ctx.Flash.Error(ctx.Tr("repo.branch.tag_collision", e.TagName)) ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()) return @@ -267,7 +267,7 @@ func MergeUpstream(ctx *context.Context) { if errors.Is(err, util.ErrNotExist) { ctx.JSONError(ctx.Tr("error.not_found")) return - } else if pull_service.IsErrMergeConflicts(err) { + } else if models.IsErrMergeConflicts(err) { ctx.JSONError(ctx.Tr("repo.pulls.merge_conflict")) return } diff --git a/routers/web/repo/cherry_pick.go b/routers/web/repo/cherry_pick.go index 35f158df5258d..61aff78d49810 100644 --- a/routers/web/repo/cherry_pick.go +++ b/routers/web/repo/cherry_pick.go @@ -8,11 +8,12 @@ import ( "errors" "strings" + "code.gitea.io/gitea/models" git_model "code.gitea.io/gitea/models/git" "code.gitea.io/gitea/models/unit" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/services/context" @@ -20,12 +21,12 @@ import ( "code.gitea.io/gitea/services/repository/files" ) -var tplCherryPick templates.TplName = "repo/editor/cherry_pick" +var tplCherryPick base.TplName = "repo/editor/cherry_pick" // CherryPick handles cherrypick GETs func CherryPick(ctx *context.Context) { - ctx.Data["SHA"] = ctx.PathParam("sha") - cherryPickCommit, err := ctx.Repo.GitRepo.GetCommit(ctx.PathParam("sha")) + ctx.Data["SHA"] = ctx.PathParam(":sha") + cherryPickCommit, err := ctx.Repo.GitRepo.GetCommit(ctx.PathParam(":sha")) if err != nil { if git.IsErrNotExist(err) { ctx.NotFound("Missing Commit", err) @@ -37,7 +38,7 @@ func CherryPick(ctx *context.Context) { if ctx.FormString("cherry-pick-type") == "revert" { ctx.Data["CherryPickType"] = "revert" - ctx.Data["commit_summary"] = "revert " + ctx.PathParam("sha") + ctx.Data["commit_summary"] = "revert " + ctx.PathParam(":sha") ctx.Data["commit_message"] = "revert " + cherryPickCommit.Message() } else { ctx.Data["CherryPickType"] = "cherry-pick" @@ -66,7 +67,7 @@ func CherryPick(ctx *context.Context) { func CherryPickPost(ctx *context.Context) { form := web.GetForm(ctx).(*forms.CherryPickForm) - sha := ctx.PathParam("sha") + sha := ctx.PathParam(":sha") ctx.Data["SHA"] = sha if form.Revert { ctx.Data["CherryPickType"] = "revert" @@ -130,7 +131,7 @@ func CherryPickPost(ctx *context.Context) { ctx.Data["Err_NewBranchName"] = true ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplCherryPick, &form) return - } else if files.IsErrCommitIDDoesNotMatch(err) { + } else if models.IsErrCommitIDDoesNotMatch(err) { ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+form.LastCommit+"..."+ctx.Repo.CommitID), tplPatchFile, &form) return } @@ -140,7 +141,7 @@ func CherryPickPost(ctx *context.Context) { if form.Revert { if err := git.GetReverseRawDiff(ctx, ctx.Repo.Repository.RepoPath(), sha, buf); err != nil { if git.IsErrNotExist(err) { - ctx.NotFound("GetRawDiff", errors.New("commit "+ctx.PathParam("sha")+" does not exist.")) + ctx.NotFound("GetRawDiff", errors.New("commit "+ctx.PathParam(":sha")+" does not exist.")) return } ctx.ServerError("GetRawDiff", err) @@ -149,7 +150,7 @@ func CherryPickPost(ctx *context.Context) { } else { if err := git.GetRawDiff(ctx.Repo.GitRepo, sha, git.RawDiffType("patch"), buf); err != nil { if git.IsErrNotExist(err) { - ctx.NotFound("GetRawDiff", errors.New("commit "+ctx.PathParam("sha")+" does not exist.")) + ctx.NotFound("GetRawDiff", errors.New("commit "+ctx.PathParam(":sha")+" does not exist.")) return } ctx.ServerError("GetRawDiff", err) @@ -167,7 +168,7 @@ func CherryPickPost(ctx *context.Context) { ctx.Data["Err_NewBranchName"] = true ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplCherryPick, &form) return - } else if files.IsErrCommitIDDoesNotMatch(err) { + } else if models.IsErrCommitIDDoesNotMatch(err) { ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+form.LastCommit+"..."+ctx.Repo.CommitID), tplPatchFile, &form) return } diff --git a/routers/web/repo/code_frequency.go b/routers/web/repo/code_frequency.go index 6572adce74687..e62965ec70231 100644 --- a/routers/web/repo/code_frequency.go +++ b/routers/web/repo/code_frequency.go @@ -7,13 +7,13 @@ import ( "errors" "net/http" - "code.gitea.io/gitea/modules/templates" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/services/context" contributors_service "code.gitea.io/gitea/services/repository" ) const ( - tplCodeFrequency templates.TplName = "repo/activity" + tplCodeFrequency base.TplName = "repo/activity" ) // CodeFrequency renders the page to show repository code frequency @@ -34,7 +34,7 @@ func CodeFrequencyData(ctx *context.Context) { ctx.Status(http.StatusAccepted) return } - ctx.ServerError("GetCodeFrequencyData", err) + ctx.ServerError("GetContributorStats", err) } else { ctx.JSON(http.StatusOK, contributorStats["total"].Weeks) } diff --git a/routers/web/repo/commit.go b/routers/web/repo/commit.go index 93578c2bb711e..119121c870188 100644 --- a/routers/web/repo/commit.go +++ b/routers/web/repo/commit.go @@ -27,7 +27,6 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/gitdiff" @@ -35,10 +34,10 @@ import ( ) const ( - tplCommits templates.TplName = "repo/commits" - tplGraph templates.TplName = "repo/graph" - tplGraphDiv templates.TplName = "repo/graph/div" - tplCommitPage templates.TplName = "repo/commit_page" + tplCommits base.TplName = "repo/commits" + tplGraph base.TplName = "repo/graph" + tplGraphDiv base.TplName = "repo/graph/div" + tplCommitPage base.TplName = "repo/commit_page" ) // RefCommits render commits page @@ -282,7 +281,7 @@ func Diff(ctx *context.Context) { userName := ctx.Repo.Owner.Name repoName := ctx.Repo.Repository.Name - commitID := ctx.PathParam("sha") + commitID := ctx.PathParam(":sha") var ( gitRepo *git.Repository err error @@ -441,13 +440,13 @@ func RawDiff(ctx *context.Context) { } if err := git.GetRawDiff( gitRepo, - ctx.PathParam("sha"), - git.RawDiffType(ctx.PathParam("ext")), + ctx.PathParam(":sha"), + git.RawDiffType(ctx.PathParam(":ext")), ctx.Resp, ); err != nil { if git.IsErrNotExist(err) { ctx.NotFound("GetRawDiff", - errors.New("commit "+ctx.PathParam("sha")+" does not exist.")) + errors.New("commit "+ctx.PathParam(":sha")+" does not exist.")) return } ctx.ServerError("GetRawDiff", err) diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go index 7386dca7e643c..1b5faf01f45be 100644 --- a/routers/web/repo/compare.go +++ b/routers/web/repo/compare.go @@ -33,7 +33,6 @@ import ( "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" - "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/typesniffer" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/routers/common" @@ -43,9 +42,9 @@ import ( ) const ( - tplCompare templates.TplName = "repo/diff/compare" - tplBlobExcerpt templates.TplName = "repo/diff/blob_excerpt" - tplDiffBox templates.TplName = "repo/diff/box" + tplCompare base.TplName = "repo/diff/compare" + tplBlobExcerpt base.TplName = "repo/diff/blob_excerpt" + tplDiffBox base.TplName = "repo/diff/box" ) // setCompareContext sets context data. @@ -414,7 +413,6 @@ func ParseCompareInfo(ctx *context.Context) *common.CompareInfo { ctx.ServerError("OpenRepository", err) return nil } - defer ci.HeadGitRepo.Close() } else { ctx.NotFound("ParseCompareInfo", nil) return nil @@ -685,7 +683,7 @@ func PrepareCompareDiff( } if len(title) > 255 { var trailer string - title, trailer = util.EllipsisDisplayStringX(title, 255) + title, trailer = util.SplitStringAtByteN(title, 255) if len(trailer) > 0 { if ctx.Data["content"] != nil { ctx.Data["content"] = fmt.Sprintf("%s\n\n%s", trailer, ctx.Data["content"]) @@ -730,7 +728,7 @@ func getBranchesAndTagsForRepo(ctx gocontext.Context, repo *repo_model.Repositor func CompareDiff(ctx *context.Context) { ci := ParseCompareInfo(ctx) defer func() { - if ci != nil && ci.HeadGitRepo != nil { + if !ctx.Repo.PullRequest.SameRepo && ci != nil && ci.HeadGitRepo != nil { ci.HeadGitRepo.Close() } }() @@ -885,7 +883,7 @@ func ExcerptBlob(ctx *context.Context) { direction := ctx.FormString("direction") filePath := ctx.FormString("path") gitRepo := ctx.Repo.GitRepo - if ctx.Data["PageIsWiki"] == true { + if ctx.FormBool("wiki") { var err error gitRepo, err = gitrepo.OpenWikiRepository(ctx, ctx.Repo.Repository) if err != nil { diff --git a/routers/web/repo/contributors.go b/routers/web/repo/contributors.go index e9c09199556b3..762fbf93795f3 100644 --- a/routers/web/repo/contributors.go +++ b/routers/web/repo/contributors.go @@ -7,13 +7,13 @@ import ( "errors" "net/http" - "code.gitea.io/gitea/modules/templates" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/services/context" contributors_service "code.gitea.io/gitea/services/repository" ) const ( - tplContributors templates.TplName = "repo/activity" + tplContributors base.TplName = "repo/activity" ) // Contributors render the page to show repository contributors graph diff --git a/routers/web/repo/download.go b/routers/web/repo/download.go index 1ed907b2f97c2..cb1163c70b7ed 100644 --- a/routers/web/repo/download.go +++ b/routers/web/repo/download.go @@ -5,7 +5,6 @@ package repo import ( - "path" "time" git_model "code.gitea.io/gitea/models/git" @@ -82,7 +81,7 @@ func ServeBlobOrLFS(ctx *context.Context, blob *git.Blob, lastModified *time.Tim return common.ServeBlob(ctx.Base, ctx.Repo.TreePath, blob, lastModified) } -func getBlobForEntry(ctx *context.Context) (blob *git.Blob, lastModified *time.Time) { +func getBlobForEntry(ctx *context.Context) (*git.Blob, *time.Time) { entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath) if err != nil { if git.IsErrNotExist(err) { @@ -98,19 +97,14 @@ func getBlobForEntry(ctx *context.Context) (blob *git.Blob, lastModified *time.T return nil, nil } - info, _, err := git.Entries([]*git.TreeEntry{entry}).GetCommitsInfo(ctx, ctx.Repo.Commit, path.Dir("/" + ctx.Repo.TreePath)[1:]) + latestCommit, err := ctx.Repo.GitRepo.GetTreePathLatestCommit(ctx.Repo.Commit.ID.String(), ctx.Repo.TreePath) if err != nil { - ctx.ServerError("GetCommitsInfo", err) + ctx.ServerError("GetTreePathLatestCommit", err) return nil, nil } + lastModified := &latestCommit.Committer.When - if len(info) == 1 { - // Not Modified - lastModified = &info[0].Commit.Committer.When - } - blob = entry.Blob() - - return blob, lastModified + return entry.Blob(), lastModified } // SingleDownload download a file by repos path diff --git a/routers/web/repo/editor.go b/routers/web/repo/editor.go index 561c4508c022c..b37d7f7efaf61 100644 --- a/routers/web/repo/editor.go +++ b/routers/web/repo/editor.go @@ -12,9 +12,11 @@ import ( "path/filepath" "strings" + "code.gitea.io/gitea/models" git_model "code.gitea.io/gitea/models/git" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/charset" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/json" @@ -22,7 +24,6 @@ import ( "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/pandoc" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/typesniffer" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" @@ -34,10 +35,10 @@ import ( ) const ( - tplEditFile templates.TplName = "repo/editor/edit" - tplEditDiffPreview templates.TplName = "repo/editor/diff_preview" - tplDeleteFile templates.TplName = "repo/editor/delete" - tplUploadFile templates.TplName = "repo/editor/upload" + tplEditFile base.TplName = "repo/editor/edit" + tplEditDiffPreview base.TplName = "repo/editor/diff_preview" + tplDeleteFile base.TplName = "repo/editor/delete" + tplUploadFile base.TplName = "repo/editor/upload" frmCommitChoiceDirect string = "direct" frmCommitChoiceNewBranch string = "commit-to-new-branch" @@ -308,12 +309,12 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b } else if git_model.IsErrLFSFileLocked(err) { ctx.Data["Err_TreePath"] = true ctx.RenderWithErr(ctx.Tr("repo.editor.upload_file_is_locked", err.(git_model.ErrLFSFileLocked).Path, err.(git_model.ErrLFSFileLocked).UserName), tplEditFile, &form) - } else if files_service.IsErrFilenameInvalid(err) { + } else if models.IsErrFilenameInvalid(err) { ctx.Data["Err_TreePath"] = true ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_invalid", form.TreePath), tplEditFile, &form) - } else if files_service.IsErrFilePathInvalid(err) { + } else if models.IsErrFilePathInvalid(err) { ctx.Data["Err_TreePath"] = true - if fileErr, ok := err.(files_service.ErrFilePathInvalid); ok { + if fileErr, ok := err.(models.ErrFilePathInvalid); ok { switch fileErr.Type { case git.EntryModeSymlink: ctx.RenderWithErr(ctx.Tr("repo.editor.file_is_a_symlink", fileErr.Path), tplEditFile, &form) @@ -327,7 +328,7 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b } else { ctx.Error(http.StatusInternalServerError, err.Error()) } - } else if files_service.IsErrRepoFileAlreadyExists(err) { + } else if models.IsErrRepoFileAlreadyExists(err) { ctx.Data["Err_TreePath"] = true ctx.RenderWithErr(ctx.Tr("repo.editor.file_already_exists", form.TreePath), tplEditFile, &form) } else if git.IsErrBranchNotExist(err) { @@ -345,7 +346,7 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b } else { ctx.Error(http.StatusInternalServerError, err.Error()) } - } else if files_service.IsErrCommitIDDoesNotMatch(err) { + } else if models.IsErrCommitIDDoesNotMatch(err) { ctx.RenderWithErr(ctx.Tr("repo.editor.commit_id_not_matching"), tplEditFile, &form) } else if git.IsErrPushOutOfDate(err) { ctx.RenderWithErr(ctx.Tr("repo.editor.push_out_of_date"), tplEditFile, &form) @@ -513,14 +514,14 @@ func DeleteFilePost(ctx *context.Context) { Signoff: form.Signoff, }); err != nil { // This is where we handle all the errors thrown by repofiles.DeleteRepoFile - if git.IsErrNotExist(err) || files_service.IsErrRepoFileDoesNotExist(err) { + if git.IsErrNotExist(err) || models.IsErrRepoFileDoesNotExist(err) { ctx.RenderWithErr(ctx.Tr("repo.editor.file_deleting_no_longer_exists", ctx.Repo.TreePath), tplDeleteFile, &form) - } else if files_service.IsErrFilenameInvalid(err) { + } else if models.IsErrFilenameInvalid(err) { ctx.Data["Err_TreePath"] = true ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_invalid", ctx.Repo.TreePath), tplDeleteFile, &form) - } else if files_service.IsErrFilePathInvalid(err) { + } else if models.IsErrFilePathInvalid(err) { ctx.Data["Err_TreePath"] = true - if fileErr, ok := err.(files_service.ErrFilePathInvalid); ok { + if fileErr, ok := err.(models.ErrFilePathInvalid); ok { switch fileErr.Type { case git.EntryModeSymlink: ctx.RenderWithErr(ctx.Tr("repo.editor.file_is_a_symlink", fileErr.Path), tplDeleteFile, &form) @@ -548,7 +549,7 @@ func DeleteFilePost(ctx *context.Context) { } else { ctx.Error(http.StatusInternalServerError, err.Error()) } - } else if files_service.IsErrCommitIDDoesNotMatch(err) || git.IsErrPushOutOfDate(err) { + } else if models.IsErrCommitIDDoesNotMatch(err) || git.IsErrPushOutOfDate(err) { ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_deleting", ctx.Repo.RepoLink+"/compare/"+util.PathEscapeSegments(form.LastCommit)+"..."+util.PathEscapeSegments(ctx.Repo.CommitID)), tplDeleteFile, &form) } else if git.IsErrPushRejected(err) { errPushRej := err.(*git.ErrPushRejected) @@ -722,12 +723,12 @@ func UploadFilePost(ctx *context.Context) { if git_model.IsErrLFSFileLocked(err) { ctx.Data["Err_TreePath"] = true ctx.RenderWithErr(ctx.Tr("repo.editor.upload_file_is_locked", err.(git_model.ErrLFSFileLocked).Path, err.(git_model.ErrLFSFileLocked).UserName), tplUploadFile, &form) - } else if files_service.IsErrFilenameInvalid(err) { + } else if models.IsErrFilenameInvalid(err) { ctx.Data["Err_TreePath"] = true ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_invalid", form.TreePath), tplUploadFile, &form) - } else if files_service.IsErrFilePathInvalid(err) { + } else if models.IsErrFilePathInvalid(err) { ctx.Data["Err_TreePath"] = true - fileErr := err.(files_service.ErrFilePathInvalid) + fileErr := err.(models.ErrFilePathInvalid) switch fileErr.Type { case git.EntryModeSymlink: ctx.RenderWithErr(ctx.Tr("repo.editor.file_is_a_symlink", fileErr.Path), tplUploadFile, &form) @@ -738,7 +739,7 @@ func UploadFilePost(ctx *context.Context) { default: ctx.Error(http.StatusInternalServerError, err.Error()) } - } else if files_service.IsErrRepoFileAlreadyExists(err) { + } else if models.IsErrRepoFileAlreadyExists(err) { ctx.Data["Err_TreePath"] = true ctx.RenderWithErr(ctx.Tr("repo.editor.file_already_exists", form.TreePath), tplUploadFile, &form) } else if git.IsErrBranchNotExist(err) { @@ -827,7 +828,9 @@ func UploadFileToServer(ctx *context.Context) { if nameExt == docxExt { snapBuf := new(bytes.Buffer) // Do we need to init pandoc? - file.Seek(0, io.SeekStart) + if _, err := file.Seek(0, io.SeekStart); err != nil { + log.Error("Failed to reset seek on pandoc output. %s", err) + } err = pandoc.ConvertDocxToSnap(ctx, file, snapBuf) file.Close() if err != nil { diff --git a/routers/web/repo/editor_test.go b/routers/web/repo/editor_test.go index 566db3169358d..68d69408ac883 100644 --- a/routers/web/repo/editor_test.go +++ b/routers/web/repo/editor_test.go @@ -43,7 +43,7 @@ func TestCleanUploadName(t *testing.T) { func TestGetUniquePatchBranchName(t *testing.T) { unittest.PrepareTestEnv(t) ctx, _ := contexttest.MockContext(t, "user2/repo1") - ctx.SetPathParam("id", "1") + ctx.SetPathParam(":id", "1") contexttest.LoadRepo(t, ctx, 1) contexttest.LoadRepoCommit(t, ctx) contexttest.LoadUser(t, ctx, 2) @@ -58,7 +58,7 @@ func TestGetUniquePatchBranchName(t *testing.T) { func TestGetClosestParentWithFiles(t *testing.T) { unittest.PrepareTestEnv(t) ctx, _ := contexttest.MockContext(t, "user2/repo1") - ctx.SetPathParam("id", "1") + ctx.SetPathParam(":id", "1") contexttest.LoadRepo(t, ctx, 1) contexttest.LoadRepoCommit(t, ctx) contexttest.LoadUser(t, ctx, 2) diff --git a/routers/web/repo/find.go b/routers/web/repo/find.go index 3a3a7610e772b..2c44552f9c432 100644 --- a/routers/web/repo/find.go +++ b/routers/web/repo/find.go @@ -6,13 +6,13 @@ package repo import ( "net/http" - "code.gitea.io/gitea/modules/templates" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/services/context" ) const ( - tplFindFiles templates.TplName = "repo/find/files" + tplFindFiles base.TplName = "repo/find/files" ) // FindFiles render the page to find repository files diff --git a/routers/web/repo/fork.go b/routers/web/repo/fork.go index 786b5d7e43500..86af70561767c 100644 --- a/routers/web/repo/fork.go +++ b/routers/web/repo/fork.go @@ -13,12 +13,12 @@ import ( "code.gitea.io/gitea/models/organization" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/structs" - "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/forms" @@ -26,7 +26,7 @@ import ( ) const ( - tplFork templates.TplName = "repo/pulls/fork" + tplFork base.TplName = "repo/pulls/fork" ) func getForkRepository(ctx *context.Context) *repo_model.Repository { diff --git a/routers/web/repo/githttp.go b/routers/web/repo/githttp.go index 6b2a7fd07667e..2c2f59b7be698 100644 --- a/routers/web/repo/githttp.go +++ b/routers/web/repo/githttp.go @@ -57,8 +57,8 @@ func CorsHandler() func(next http.Handler) http.Handler { // httpBase implementation git smart HTTP protocol func httpBase(ctx *context.Context) *serviceHandler { - username := ctx.PathParam("username") - reponame := strings.TrimSuffix(ctx.PathParam("reponame"), ".git") + username := ctx.PathParam(":username") + reponame := strings.TrimSuffix(ctx.PathParam(":reponame"), ".git") if ctx.FormString("go-get") == "1" { context.EarlyResponseForGoGetMeta(ctx) @@ -127,7 +127,7 @@ func httpBase(ctx *context.Context) *serviceHandler { // Only public pull don't need auth. isPublicPull := repoExist && !repo.IsPrivate && isPull var ( - askAuth = !isPublicPull || setting.Service.RequireSignInView + askAuth = !isPublicPull || setting.Service.RequireSignInViewStrict environ []string ) @@ -458,6 +458,7 @@ func serviceRPC(ctx *context.Context, h *serviceHandler, service string) { var stderr bytes.Buffer cmd.AddArguments("--stateless-rpc").AddDynamicArguments(h.getRepoDir()) + cmd.SetDescription(fmt.Sprintf("%s %s %s [repo_path: %s]", git.GitExecutable, service, "--stateless-rpc", h.getRepoDir())) if err := cmd.Run(&git.RunOpts{ Dir: h.getRepoDir(), Env: append(os.Environ(), h.environ...), diff --git a/routers/web/repo/hub.go b/routers/web/repo/hub.go index 0b396df8fe0a8..8400017a5bfe5 100644 --- a/routers/web/repo/hub.go +++ b/routers/web/repo/hub.go @@ -1,3 +1,6 @@ +// Copyright 2025 The Bindersnap Authors. All rights reserved. +// SPDX-License-Identifier: LicenseRef-License + package repo import ( @@ -5,16 +8,16 @@ import ( "strings" repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/log" repo_module "code.gitea.io/gitea/modules/repository" - "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/services/context" ) const ( - tplRepoHub templates.TplName = "repo/hub/home" - tplRepoHubEmpty templates.TplName = "repo/hub/empty" + tplRepoHub base.TplName = "repo/hub/home" + tplRepoHubEmpty base.TplName = "repo/hub/empty" ) // Hub render repository customer view page diff --git a/routers/web/repo/hub_search.go b/routers/web/repo/hub_search.go index 2c171ea83e3c9..ca21d64dc255a 100644 --- a/routers/web/repo/hub_search.go +++ b/routers/web/repo/hub_search.go @@ -1,3 +1,6 @@ +// Copyright 2025 The Bindersnap Authors. All rights reserved. +// SPDX-License-Identifier: LicenseRef-License + package repo import ( @@ -5,15 +8,15 @@ import ( "strings" "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/git" code_indexer "code.gitea.io/gitea/modules/indexer/code" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/services/context" ) const ( - tplRepoHubSearch templates.TplName = "repo/hub/search" + tplRepoHubSearch base.TplName = "repo/hub/search" ) // Hub search render repository hub search page diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index a3a4e73d7b007..93f57b6dc5739 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -21,6 +21,7 @@ import ( repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/markup/markdown" "code.gitea.io/gitea/modules/optional" @@ -36,14 +37,14 @@ import ( ) const ( - tplAttachment templates.TplName = "repo/issue/view_content/attachments" + tplAttachment base.TplName = "repo/issue/view_content/attachments" - tplIssues templates.TplName = "repo/issue/list" - tplIssueNew templates.TplName = "repo/issue/new" - tplIssueChoose templates.TplName = "repo/issue/choose" - tplIssueView templates.TplName = "repo/issue/view" + tplIssues base.TplName = "repo/issue/list" + tplIssueNew base.TplName = "repo/issue/new" + tplIssueChoose base.TplName = "repo/issue/choose" + tplIssueView base.TplName = "repo/issue/view" - tplReactions templates.TplName = "repo/issue/view_content/reactions" + tplReactions base.TplName = "repo/issue/view_content/reactions" issueTemplateKey = "IssueTemplate" issueTemplateTitleKey = "IssueTemplateTitle" @@ -181,7 +182,7 @@ func retrieveProjectsInternal(ctx *context.Context, repo *repo_model.Repository) // GetActionIssue will return the issue which is used in the context. func GetActionIssue(ctx *context.Context) *issues_model.Issue { - issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) + issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) if err != nil { ctx.NotFoundOrServerError("GetIssueByIndex", issues_model.IsErrIssueNotExist, err) return nil @@ -246,7 +247,7 @@ func getActionIssues(ctx *context.Context) issues_model.IssueList { // GetIssueInfo get an issue of a repository func GetIssueInfo(ctx *context.Context) { - issue, err := issues_model.GetIssueWithAttrsByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) + issue, err := issues_model.GetIssueWithAttrsByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) if err != nil { if issues_model.IsErrIssueNotExist(err) { ctx.Error(http.StatusNotFound) @@ -379,7 +380,7 @@ func UpdateIssueContent(ctx *context.Context) { // UpdateIssueDeadline updates an issue deadline func UpdateIssueDeadline(ctx *context.Context) { - issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) + issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) if err != nil { if issues_model.IsErrIssueNotExist(err) { ctx.NotFound("GetIssueByIndex", err) @@ -417,6 +418,16 @@ func UpdateIssueMilestone(ctx *context.Context) { continue } issue.MilestoneID = milestoneID + if milestoneID > 0 { + var err error + issue.Milestone, err = issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, milestoneID) + if err != nil { + ctx.ServerError("GetMilestoneByRepoID", err) + return + } + } else { + issue.Milestone = nil + } if err := issue_service.ChangeMilestoneAssign(ctx, issue, ctx.Doer, oldMilestoneID); err != nil { ctx.ServerError("ChangeMilestoneAssign", err) return @@ -506,7 +517,7 @@ func ChangeIssueReaction(ctx *context.Context) { return } - switch ctx.PathParam("action") { + switch ctx.PathParam(":action") { case "react": reaction, err := issue_service.CreateIssueReaction(ctx, ctx.Doer, issue, form.Content) if err != nil { @@ -540,7 +551,7 @@ func ChangeIssueReaction(ctx *context.Context) { log.Trace("Reaction for issue removed: %d/%d", ctx.Repo.Repository.ID, issue.ID) default: - ctx.NotFound(fmt.Sprintf("Unknown action %s", ctx.PathParam("action")), nil) + ctx.NotFound(fmt.Sprintf("Unknown action %s", ctx.PathParam(":action")), nil) return } diff --git a/routers/web/repo/issue_comment.go b/routers/web/repo/issue_comment.go index 8564c613de04d..6b7b29d9d71bc 100644 --- a/routers/web/repo/issue_comment.go +++ b/routers/web/repo/issue_comment.go @@ -154,28 +154,25 @@ func NewComment(ctx *context.Context) { if pr != nil { ctx.Flash.Info(ctx.Tr("repo.pulls.open_unmerged_pull_exists", pr.Index)) } else { - if form.Status == "close" && !issue.IsClosed { - if err := issue_service.CloseIssue(ctx, issue, ctx.Doer, ""); err != nil { - log.Error("CloseIssue: %v", err) - if issues_model.IsErrDependenciesLeft(err) { - if issue.IsPull { - ctx.JSONError(ctx.Tr("repo.issues.dependency.pr_close_blocked")) - } else { - ctx.JSONError(ctx.Tr("repo.issues.dependency.issue_close_blocked")) - } - return - } - } else { - if err := stopTimerIfAvailable(ctx, ctx.Doer, issue); err != nil { - ctx.ServerError("stopTimerIfAvailable", err) - return + isClosed := form.Status == "close" + if err := issue_service.ChangeStatus(ctx, issue, ctx.Doer, "", isClosed); err != nil { + log.Error("ChangeStatus: %v", err) + + if issues_model.IsErrDependenciesLeft(err) { + if issue.IsPull { + ctx.JSONError(ctx.Tr("repo.issues.dependency.pr_close_blocked")) + } else { + ctx.JSONError(ctx.Tr("repo.issues.dependency.issue_close_blocked")) } - log.Trace("Issue [%d] status changed to closed: %v", issue.ID, issue.IsClosed) + return } - } else if form.Status == "reopen" && issue.IsClosed { - if err := issue_service.ReopenIssue(ctx, issue, ctx.Doer, ""); err != nil { - log.Error("ReopenIssue: %v", err) + } else { + if err := stopTimerIfAvailable(ctx, ctx.Doer, issue); err != nil { + ctx.ServerError("CreateOrStopIssueStopwatch", err) + return } + + log.Trace("Issue [%d] status changed to closed: %v", issue.ID, issue.IsClosed) } } } @@ -212,7 +209,7 @@ func NewComment(ctx *context.Context) { // UpdateCommentContent change comment of issue's content func UpdateCommentContent(ctx *context.Context) { - comment, err := issues_model.GetCommentByID(ctx, ctx.PathParamInt64("id")) + comment, err := issues_model.GetCommentByID(ctx, ctx.PathParamInt64(":id")) if err != nil { ctx.NotFoundOrServerError("GetCommentByID", issues_model.IsErrCommentNotExist, err) return @@ -290,7 +287,7 @@ func UpdateCommentContent(ctx *context.Context) { // DeleteComment delete comment of issue func DeleteComment(ctx *context.Context) { - comment, err := issues_model.GetCommentByID(ctx, ctx.PathParamInt64("id")) + comment, err := issues_model.GetCommentByID(ctx, ctx.PathParamInt64(":id")) if err != nil { ctx.NotFoundOrServerError("GetCommentByID", issues_model.IsErrCommentNotExist, err) return @@ -325,7 +322,7 @@ func DeleteComment(ctx *context.Context) { // ChangeCommentReaction create a reaction for comment func ChangeCommentReaction(ctx *context.Context) { form := web.GetForm(ctx).(*forms.ReactionForm) - comment, err := issues_model.GetCommentByID(ctx, ctx.PathParamInt64("id")) + comment, err := issues_model.GetCommentByID(ctx, ctx.PathParamInt64(":id")) if err != nil { ctx.NotFoundOrServerError("GetCommentByID", issues_model.IsErrCommentNotExist, err) return @@ -369,7 +366,7 @@ func ChangeCommentReaction(ctx *context.Context) { return } - switch ctx.PathParam("action") { + switch ctx.PathParam(":action") { case "react": reaction, err := issue_service.CreateCommentReaction(ctx, ctx.Doer, comment, form.Content) if err != nil { @@ -403,7 +400,7 @@ func ChangeCommentReaction(ctx *context.Context) { log.Trace("Reaction for comment removed: %d/%d/%d", ctx.Repo.Repository.ID, comment.Issue.ID, comment.ID) default: - ctx.NotFound(fmt.Sprintf("Unknown action %s", ctx.PathParam("action")), nil) + ctx.NotFound(fmt.Sprintf("Unknown action %s", ctx.PathParam(":action")), nil) return } @@ -430,7 +427,7 @@ func ChangeCommentReaction(ctx *context.Context) { // GetCommentAttachments returns attachments for the comment func GetCommentAttachments(ctx *context.Context) { - comment, err := issues_model.GetCommentByID(ctx, ctx.PathParamInt64("id")) + comment, err := issues_model.GetCommentByID(ctx, ctx.PathParamInt64(":id")) if err != nil { ctx.NotFoundOrServerError("GetCommentByID", issues_model.IsErrCommentNotExist, err) return diff --git a/routers/web/repo/issue_dependency.go b/routers/web/repo/issue_dependency.go index f1d133edb0064..0f6787386d99a 100644 --- a/routers/web/repo/issue_dependency.go +++ b/routers/web/repo/issue_dependency.go @@ -109,7 +109,7 @@ func RemoveDependency(ctx *context.Context) { } // Dependency Type - depTypeStr := ctx.Req.PostForm.Get("dependencyType") + depTypeStr := ctx.Req.PostFormValue("dependencyType") var depType issues_model.DependencyType diff --git a/routers/web/repo/issue_label.go b/routers/web/repo/issue_label.go index 5ef6b09faa0d8..4874baaa543ca 100644 --- a/routers/web/repo/issue_label.go +++ b/routers/web/repo/issue_label.go @@ -9,10 +9,10 @@ import ( "code.gitea.io/gitea/models/db" issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/organization" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/label" "code.gitea.io/gitea/modules/log" repo_module "code.gitea.io/gitea/modules/repository" - "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/forms" @@ -20,7 +20,7 @@ import ( ) const ( - tplLabels templates.TplName = "repo/issue/labels" + tplLabels base.TplName = "repo/issue/labels" ) // Labels render issue's labels page diff --git a/routers/web/repo/issue_label_test.go b/routers/web/repo/issue_label_test.go index 8a613e2c7e53c..c86a03da51a28 100644 --- a/routers/web/repo/issue_label_test.go +++ b/routers/web/repo/issue_label_test.go @@ -162,11 +162,10 @@ func TestUpdateIssueLabel_Toggle(t *testing.T) { UpdateIssueLabel(ctx) assert.EqualValues(t, http.StatusOK, ctx.Resp.Status()) for _, issueID := range testCase.IssueIDs { - if testCase.ExpectedAdd { - unittest.AssertExistsAndLoadBean(t, &issues_model.IssueLabel{IssueID: issueID, LabelID: testCase.LabelID}) - } else { - unittest.AssertNotExistsBean(t, &issues_model.IssueLabel{IssueID: issueID, LabelID: testCase.LabelID}) - } + unittest.AssertExistsIf(t, testCase.ExpectedAdd, &issues_model.IssueLabel{ + IssueID: issueID, + LabelID: testCase.LabelID, + }) } unittest.CheckConsistencyFor(t, &issues_model.Label{}) } diff --git a/routers/web/repo/issue_list.go b/routers/web/repo/issue_list.go index 2f615a100e3d0..ff98bf8ec8ecf 100644 --- a/routers/web/repo/issue_list.go +++ b/routers/web/repo/issue_list.go @@ -418,11 +418,14 @@ func UpdateIssueStatus(ctx *context.Context) { return } - action := ctx.FormString("action") - if action != "open" && action != "close" { + var isClosed bool + switch action := ctx.FormString("action"); action { + case "open": + isClosed = false + case "close": + isClosed = true + default: log.Warn("Unrecognized action: %s", action) - ctx.JSONOK() - return } if _, err := issues.LoadRepositories(ctx); err != nil { @@ -438,20 +441,15 @@ func UpdateIssueStatus(ctx *context.Context) { if issue.IsPull && issue.PullRequest.HasMerged { continue } - if action == "close" && !issue.IsClosed { - if err := issue_service.CloseIssue(ctx, issue, ctx.Doer, ""); err != nil { + if issue.IsClosed != isClosed { + if err := issue_service.ChangeStatus(ctx, issue, ctx.Doer, "", isClosed); err != nil { if issues_model.IsErrDependenciesLeft(err) { ctx.JSON(http.StatusPreconditionFailed, map[string]any{ "error": ctx.Tr("repo.issues.dependency.issue_batch_close_blocked", issue.Index), }) return } - ctx.ServerError("CloseIssue", err) - return - } - } else if action == "open" && issue.IsClosed { - if err := issue_service.ReopenIssue(ctx, issue, ctx.Doer, ""); err != nil { - ctx.ServerError("ReopenIssue", err) + ctx.ServerError("ChangeStatus", err) return } } @@ -750,7 +748,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt // Issues render issues page func Issues(ctx *context.Context) { - isPullList := ctx.PathParam("type") == "pulls" + isPullList := ctx.PathParam(":type") == "pulls" if isPullList { MustAllowPulls(ctx) if ctx.Written() { diff --git a/routers/web/repo/issue_new.go b/routers/web/repo/issue_new.go index 9a941ce85714b..9115fc1771e1d 100644 --- a/routers/web/repo/issue_new.go +++ b/routers/web/repo/issue_new.go @@ -276,13 +276,16 @@ func ValidateRepoMetasForNewIssue(ctx *context.Context, form forms.CreateIssueFo } pageMetaData.ProjectsData.SelectedProjectID = form.ProjectID + // prepare assignees candidateAssignees := toSet(pageMetaData.AssigneesData.CandidateAssignees, func(user *user_model.User) int64 { return user.ID }) inputAssigneeIDs, _ := base.StringsToInt64s(strings.Split(form.AssigneeIDs, ",")) - if len(inputAssigneeIDs) > 0 && !candidateAssignees.Contains(inputAssigneeIDs...) { - ctx.NotFound("", nil) - return ret + var assigneeIDStrings []string + for _, inputAssigneeID := range inputAssigneeIDs { + if candidateAssignees.Contains(inputAssigneeID) { + assigneeIDStrings = append(assigneeIDStrings, strconv.FormatInt(inputAssigneeID, 10)) + } } - pageMetaData.AssigneesData.SelectedAssigneeIDs = form.AssigneeIDs + pageMetaData.AssigneesData.SelectedAssigneeIDs = strings.Join(assigneeIDStrings, ",") // Check if the passed reviewers (user/team) actually exist var reviewers []*user_model.User @@ -396,8 +399,15 @@ func NewIssuePost(ctx *context.Context) { log.Trace("Issue created: %d/%d", repo.ID, issue.ID) if ctx.FormString("redirect_after_creation") == "project" && projectID > 0 { - ctx.JSONRedirect(ctx.Repo.RepoLink + "/projects/" + strconv.FormatInt(projectID, 10)) - } else { - ctx.JSONRedirect(issue.Link()) + project, err := project_model.GetProjectByID(ctx, projectID) + if err == nil { + if project.Type == project_model.TypeOrganization { + ctx.JSONRedirect(project_model.ProjectLinkForOrg(ctx.Repo.Owner, project.ID)) + } else { + ctx.JSONRedirect(project_model.ProjectLinkForRepo(repo, project.ID)) + } + return + } } + ctx.JSONRedirect(issue.Link()) } diff --git a/routers/web/repo/issue_page_meta.go b/routers/web/repo/issue_page_meta.go index b536b04d7c603..93cc38bffa1cf 100644 --- a/routers/web/repo/issue_page_meta.go +++ b/routers/web/repo/issue_page_meta.go @@ -79,27 +79,35 @@ func retrieveRepoIssueMetaData(ctx *context.Context, repo *repo_model.Repository return data } - data.CanModifyIssueOrPull = ctx.Repo.CanWriteIssuesOrPulls(isPull) && !ctx.Repo.Repository.IsArchived - if !data.CanModifyIssueOrPull { + // it sets "Branches" template data, + // it is used to render the "edit PR target branches" dropdown, and the "branch selector" in the issue's sidebar. + PrepareBranchList(ctx) + if ctx.Written() { return data } - data.retrieveAssigneesDataForIssueWriter(ctx) + // it sets the "Assignees" template data, and the data is also used to "mention" users. + data.retrieveAssigneesData(ctx) if ctx.Written() { return data } - data.retrieveMilestonesDataForIssueWriter(ctx) - if ctx.Written() { + // TODO: the issue/pull permissions are quite complex and unclear + // A reader could create an issue/PR with setting some meta (eg: assignees from issue template, reviewers, target branch) + // A reader(creator) could update some meta (eg: target branch), but can't change assignees anymore. + // For non-creator users, only writers could update some meta (eg: assignees, milestone, project) + // Need to clarify the logic and add some tests in the future + data.CanModifyIssueOrPull = ctx.Repo.CanWriteIssuesOrPulls(isPull) && !ctx.Repo.Repository.IsArchived + if !data.CanModifyIssueOrPull { return data } - data.retrieveProjectsDataForIssueWriter(ctx) + data.retrieveMilestonesDataForIssueWriter(ctx) if ctx.Written() { return data } - PrepareBranchList(ctx) + data.retrieveProjectsDataForIssueWriter(ctx) if ctx.Written() { return data } @@ -131,7 +139,7 @@ func (d *IssuePageMetaData) retrieveMilestonesDataForIssueWriter(ctx *context.Co } } -func (d *IssuePageMetaData) retrieveAssigneesDataForIssueWriter(ctx *context.Context) { +func (d *IssuePageMetaData) retrieveAssigneesData(ctx *context.Context) { var err error d.AssigneesData.CandidateAssignees, err = repo_model.GetRepoAssignees(ctx, d.Repository) if err != nil { @@ -193,6 +201,7 @@ func (d *IssuePageMetaData) retrieveReviewersData(ctx *context.Context) { var posterID int64 var isClosed bool var reviews issues_model.ReviewList + var err error if d.Issue == nil { if ctx.Doer != nil { @@ -206,14 +215,7 @@ func (d *IssuePageMetaData) retrieveReviewersData(ctx *context.Context) { isClosed = d.Issue.IsClosed || d.Issue.PullRequest.HasMerged - originalAuthorReviews, err := issues_model.GetReviewersFromOriginalAuthorsByIssueID(ctx, d.Issue.ID) - if err != nil { - ctx.ServerError("GetReviewersFromOriginalAuthorsByIssueID", err) - return - } - data.OriginalReviews = originalAuthorReviews - - reviews, err = issues_model.GetReviewsByIssueID(ctx, d.Issue.ID) + reviews, data.OriginalReviews, err = issues_model.GetReviewsByIssueID(ctx, d.Issue.ID) if err != nil { ctx.ServerError("GetReviewersByIssueID", err) return diff --git a/routers/web/repo/issue_pin.go b/routers/web/repo/issue_pin.go index d7d3205c378b6..0074e31f038d9 100644 --- a/routers/web/repo/issue_pin.go +++ b/routers/web/repo/issue_pin.go @@ -39,7 +39,7 @@ func IssuePinOrUnpin(ctx *context.Context) { // IssueUnpin unpins a Issue func IssueUnpin(ctx *context.Context) { - issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) + issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) if err != nil { ctx.Status(http.StatusInternalServerError) log.Error(err.Error()) diff --git a/routers/web/repo/issue_suggestions.go b/routers/web/repo/issue_suggestions.go index 46e9f339a5a53..9ef39425041f8 100644 --- a/routers/web/repo/issue_suggestions.go +++ b/routers/web/repo/issue_suggestions.go @@ -6,13 +6,10 @@ package repo import ( "net/http" - "code.gitea.io/gitea/models/db" - issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/unit" - issue_indexer "code.gitea.io/gitea/modules/indexer/issues" "code.gitea.io/gitea/modules/optional" - "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/services/context" + issue_service "code.gitea.io/gitea/services/issue" ) // IssueSuggestions returns a list of issue suggestions @@ -29,54 +26,11 @@ func IssueSuggestions(ctx *context.Context) { isPull = optional.Some(false) } - searchOpt := &issue_indexer.SearchOptions{ - Paginator: &db.ListOptions{ - Page: 0, - PageSize: 5, - }, - Keyword: keyword, - RepoIDs: []int64{ctx.Repo.Repository.ID}, - IsPull: isPull, - IsClosed: nil, - SortBy: issue_indexer.SortByUpdatedDesc, - } - - ids, _, err := issue_indexer.SearchIssues(ctx, searchOpt) - if err != nil { - ctx.ServerError("SearchIssues", err) - return - } - issues, err := issues_model.GetIssuesByIDs(ctx, ids, true) + suggestions, err := issue_service.GetSuggestion(ctx, ctx.Repo.Repository, isPull, keyword) if err != nil { - ctx.ServerError("FindIssuesByIDs", err) + ctx.ServerError("GetSuggestion", err) return } - suggestions := make([]*structs.Issue, 0, len(issues)) - - for _, issue := range issues { - suggestion := &structs.Issue{ - ID: issue.ID, - Index: issue.Index, - Title: issue.Title, - State: issue.State(), - } - - if issue.IsPull { - if err := issue.LoadPullRequest(ctx); err != nil { - ctx.ServerError("LoadPullRequest", err) - return - } - if issue.PullRequest != nil { - suggestion.PullRequest = &structs.PullRequestMeta{ - HasMerged: issue.PullRequest.HasMerged, - IsWorkInProgress: issue.PullRequest.IsWorkInProgress(ctx), - } - } - } - - suggestions = append(suggestions, suggestion) - } - ctx.JSON(http.StatusOK, suggestions) } diff --git a/routers/web/repo/issue_timetrack.go b/routers/web/repo/issue_timetrack.go index 36e931a48fb05..11ffa8bacf5a0 100644 --- a/routers/web/repo/issue_timetrack.go +++ b/routers/web/repo/issue_timetrack.go @@ -60,7 +60,7 @@ func DeleteTime(c *context.Context) { return } - t, err := issues_model.GetTrackedTimeByID(c, c.PathParamInt64("timeid")) + t, err := issues_model.GetTrackedTimeByID(c, c.PathParamInt64(":timeid")) if err != nil { if db.IsErrNotExist(err) { c.NotFound("time not found", err) @@ -81,7 +81,7 @@ func DeleteTime(c *context.Context) { return } - c.Flash.Success(c.Tr("repo.issues.del_time_history", util.SecToTime(t.Time))) + c.Flash.Success(c.Tr("repo.issues.del_time_history", util.SecToHours(t.Time))) c.JSONRedirect("") } diff --git a/routers/web/repo/issue_view.go b/routers/web/repo/issue_view.go index 61e75e211bd41..09b57f4e7833a 100644 --- a/routers/web/repo/issue_view.go +++ b/routers/web/repo/issue_view.go @@ -265,13 +265,13 @@ func combineLabelComments(issue *issues_model.Issue) { // ViewIssue render issue view page func ViewIssue(ctx *context.Context) { - if ctx.PathParam("type") == "issues" { + if ctx.PathParam(":type") == "issues" { // If issue was requested we check if repo has external tracker and redirect extIssueUnit, err := ctx.Repo.Repository.GetUnit(ctx, unit.TypeExternalTracker) if err == nil && extIssueUnit != nil { if extIssueUnit.ExternalTrackerConfig().ExternalTrackerStyle == markup.IssueNameStyleNumeric || extIssueUnit.ExternalTrackerConfig().ExternalTrackerStyle == "" { metas := ctx.Repo.Repository.ComposeMetas(ctx) - metas["index"] = ctx.PathParam("index") + metas["index"] = ctx.PathParam(":index") res, err := vars.Expand(extIssueUnit.ExternalTrackerConfig().ExternalTrackerFormat, metas) if err != nil { log.Error("unable to expand template vars for issue url. issue: %s, err: %v", metas["index"], err) @@ -287,7 +287,7 @@ func ViewIssue(ctx *context.Context) { } } - issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) + issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) if err != nil { if issues_model.IsErrIssueNotExist(err) { ctx.NotFound("GetIssueByIndex", err) @@ -301,10 +301,10 @@ func ViewIssue(ctx *context.Context) { } // Make sure type and URL matches. - if ctx.PathParam("type") == "issues" && issue.IsPull { + if ctx.PathParam(":type") == "issues" && issue.IsPull { ctx.Redirect(issue.Link()) return - } else if ctx.PathParam("type") == "pulls" && !issue.IsPull { + } else if ctx.PathParam(":type") == "pulls" && !issue.IsPull { ctx.Redirect(issue.Link()) return } diff --git a/routers/web/repo/issue_watch.go b/routers/web/repo/issue_watch.go index a2a4be1758a96..6799bf8eb292e 100644 --- a/routers/web/repo/issue_watch.go +++ b/routers/web/repo/issue_watch.go @@ -8,13 +8,13 @@ import ( "strconv" issues_model "code.gitea.io/gitea/models/issues" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/services/context" ) const ( - tplWatching templates.TplName = "repo/issue/view_content/watching" + tplWatching base.TplName = "repo/issue/view_content/watching" ) // IssueWatch sets issue watching @@ -46,7 +46,7 @@ func IssueWatch(ctx *context.Context) { return } - watch, err := strconv.ParseBool(ctx.Req.PostForm.Get("watch")) + watch, err := strconv.ParseBool(ctx.Req.PostFormValue("watch")) if err != nil { ctx.ServerError("watch is not bool", err) return diff --git a/routers/web/repo/migrate.go b/routers/web/repo/migrate.go index 3a7dc294667c9..3eaf05f38335c 100644 --- a/routers/web/repo/migrate.go +++ b/routers/web/repo/migrate.go @@ -9,17 +9,17 @@ import ( "net/url" "strings" + "code.gitea.io/gitea/models" admin_model "code.gitea.io/gitea/models/admin" "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/base" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/lfs" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/structs" - "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/services/context" @@ -29,7 +29,7 @@ import ( ) const ( - tplMigrate templates.TplName = "repo/migrate/migrate" + tplMigrate base.TplName = "repo/migrate/migrate" ) // Migrate render migration of repository page @@ -67,10 +67,10 @@ func Migrate(ctx *context.Context) { } ctx.Data["ContextUser"] = ctxUser - ctx.HTML(http.StatusOK, templates.TplName("repo/migrate/"+serviceType.Name())) + ctx.HTML(http.StatusOK, base.TplName("repo/migrate/"+serviceType.Name())) } -func handleMigrateError(ctx *context.Context, owner *user_model.User, err error, name string, tpl templates.TplName, form *forms.MigrateRepoForm) { +func handleMigrateError(ctx *context.Context, owner *user_model.User, err error, name string, tpl base.TplName, form *forms.MigrateRepoForm) { if setting.Repository.DisableMigrations { ctx.Error(http.StatusForbidden, "MigrateError: the site administrator has disabled migrations") return @@ -122,9 +122,9 @@ func handleMigrateError(ctx *context.Context, owner *user_model.User, err error, } } -func handleMigrateRemoteAddrError(ctx *context.Context, err error, tpl templates.TplName, form *forms.MigrateRepoForm) { - if git.IsErrInvalidCloneAddr(err) { - addrErr := err.(*git.ErrInvalidCloneAddr) +func handleMigrateRemoteAddrError(ctx *context.Context, err error, tpl base.TplName, form *forms.MigrateRepoForm) { + if models.IsErrInvalidCloneAddr(err) { + addrErr := err.(*models.ErrInvalidCloneAddr) switch { case addrErr.IsProtocolInvalid: ctx.RenderWithErr(ctx.Tr("repo.mirror_address_protocol_invalid"), tpl, form) @@ -169,14 +169,14 @@ func MigratePost(ctx *context.Context) { } ctx.Data["ContextUser"] = ctxUser - tpl := templates.TplName("repo/migrate/" + form.Service.Name()) + tpl := base.TplName("repo/migrate/" + form.Service.Name()) if ctx.HasError() { ctx.HTML(http.StatusOK, tpl) return } - remoteAddr, err := git.ParseRemoteAddr(form.CloneAddr, form.AuthUsername, form.AuthPassword) + remoteAddr, err := forms.ParseRemoteAddr(form.CloneAddr, form.AuthUsername, form.AuthPassword) if err == nil { err = migrations.IsMigrateURLAllowed(remoteAddr, ctx.Doer) } diff --git a/routers/web/repo/milestone.go b/routers/web/repo/milestone.go index 392d87167a56c..33c15e7767818 100644 --- a/routers/web/repo/milestone.go +++ b/routers/web/repo/milestone.go @@ -11,10 +11,10 @@ import ( "code.gitea.io/gitea/models/db" issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/renderhelper" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/markup/markdown" "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/routers/common" "code.gitea.io/gitea/services/context" @@ -25,9 +25,9 @@ import ( ) const ( - tplMilestone templates.TplName = "repo/issue/milestones" - tplMilestoneNew templates.TplName = "repo/issue/milestone_new" - tplMilestoneIssues templates.TplName = "repo/issue/milestone_issues" + tplMilestone base.TplName = "repo/issue/milestones" + tplMilestoneNew base.TplName = "repo/issue/milestone_new" + tplMilestoneIssues base.TplName = "repo/issue/milestone_issues" ) // Milestones render milestones page @@ -147,7 +147,7 @@ func EditMilestone(ctx *context.Context) { ctx.Data["PageIsMilestones"] = true ctx.Data["PageIsEditMilestone"] = true - m, err := issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("id")) + m, err := issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":id")) if err != nil { if issues_model.IsErrMilestoneNotExist(err) { ctx.NotFound("", nil) @@ -183,7 +183,7 @@ func EditMilestonePost(ctx *context.Context) { return } - m, err := issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("id")) + m, err := issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":id")) if err != nil { if issues_model.IsErrMilestoneNotExist(err) { ctx.NotFound("", nil) @@ -207,7 +207,7 @@ func EditMilestonePost(ctx *context.Context) { // ChangeMilestoneStatus response for change a milestone's status func ChangeMilestoneStatus(ctx *context.Context) { var toClose bool - switch ctx.PathParam("action") { + switch ctx.PathParam(":action") { case "open": toClose = false case "close": @@ -216,7 +216,7 @@ func ChangeMilestoneStatus(ctx *context.Context) { ctx.JSONRedirect(ctx.Repo.RepoLink + "/milestones") return } - id := ctx.PathParamInt64("id") + id := ctx.PathParamInt64(":id") if err := issues_model.ChangeMilestoneStatusByRepoIDAndID(ctx, ctx.Repo.Repository.ID, id, toClose); err != nil { if issues_model.IsErrMilestoneNotExist(err) { @@ -226,7 +226,7 @@ func ChangeMilestoneStatus(ctx *context.Context) { } return } - ctx.JSONRedirect(ctx.Repo.RepoLink + "/milestones?state=" + url.QueryEscape(ctx.PathParam("action"))) + ctx.JSONRedirect(ctx.Repo.RepoLink + "/milestones?state=" + url.QueryEscape(ctx.PathParam(":action"))) } // DeleteMilestone delete a milestone @@ -242,7 +242,7 @@ func DeleteMilestone(ctx *context.Context) { // MilestoneIssuesAndPulls lists all the issues and pull requests of the milestone func MilestoneIssuesAndPulls(ctx *context.Context) { - milestoneID := ctx.PathParamInt64("id") + milestoneID := ctx.PathParamInt64(":id") projectID := ctx.FormInt64("project") milestone, err := issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, milestoneID) if err != nil { diff --git a/routers/web/repo/packages.go b/routers/web/repo/packages.go index c8d3719bc04f6..57e578da37307 100644 --- a/routers/web/repo/packages.go +++ b/routers/web/repo/packages.go @@ -9,14 +9,14 @@ import ( "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/packages" "code.gitea.io/gitea/models/unit" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/services/context" ) const ( - tplPackagesList templates.TplName = "repo/packages" + tplPackagesList base.TplName = "repo/packages" ) // Packages displays a list of all packages in the repository diff --git a/routers/web/repo/patch.go b/routers/web/repo/patch.go index 1807cf31a1280..0dee02dd9ca9b 100644 --- a/routers/web/repo/patch.go +++ b/routers/web/repo/patch.go @@ -6,10 +6,11 @@ package repo import ( "strings" + "code.gitea.io/gitea/models" git_model "code.gitea.io/gitea/models/git" "code.gitea.io/gitea/models/unit" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/services/context" @@ -18,7 +19,7 @@ import ( ) const ( - tplPatchFile templates.TplName = "repo/editor/patch" + tplPatchFile base.TplName = "repo/editor/patch" ) // NewDiffPatch render create patch page @@ -100,7 +101,7 @@ func NewDiffPatchPost(ctx *context.Context) { ctx.Data["Err_NewBranchName"] = true ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplEditFile, &form) return - } else if files.IsErrCommitIDDoesNotMatch(err) { + } else if models.IsErrCommitIDDoesNotMatch(err) { ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+form.LastCommit+"..."+ctx.Repo.CommitID), tplPatchFile, &form) return } diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go index 4313b6c403f21..4c11ffd407e5d 100644 --- a/routers/web/repo/projects.go +++ b/routers/web/repo/projects.go @@ -16,11 +16,11 @@ import ( "code.gitea.io/gitea/models/renderhelper" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/markup/markdown" "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/routers/web/shared/issue" @@ -31,9 +31,9 @@ import ( ) const ( - tplProjects templates.TplName = "repo/projects/list" - tplProjectsNew templates.TplName = "repo/projects/new" - tplProjectsView templates.TplName = "repo/projects/view" + tplProjects base.TplName = "repo/projects/list" + tplProjectsNew base.TplName = "repo/projects/new" + tplProjectsView base.TplName = "repo/projects/view" ) // MustEnableRepoProjects check if repo projects are enabled in settings @@ -92,6 +92,11 @@ func Projects(ctx *context.Context) { return } + if err := project_service.LoadIssueNumbersForProjects(ctx, projects, ctx.Doer); err != nil { + ctx.ServerError("LoadIssueNumbersForProjects", err) + return + } + for i := range projects { rctx := renderhelper.NewRenderContextRepoComment(ctx, repo) projects[i].RenderedContent, err = markdown.RenderString(rctx, projects[i].Description) @@ -166,7 +171,7 @@ func NewProjectPost(ctx *context.Context) { // ChangeProjectStatus updates the status of a project between "open" and "close" func ChangeProjectStatus(ctx *context.Context) { var toClose bool - switch ctx.PathParam("action") { + switch ctx.PathParam(":action") { case "open": toClose = false case "close": @@ -175,18 +180,18 @@ func ChangeProjectStatus(ctx *context.Context) { ctx.JSONRedirect(ctx.Repo.RepoLink + "/projects") return } - id := ctx.PathParamInt64("id") + id := ctx.PathParamInt64(":id") if err := project_model.ChangeProjectStatusByRepoIDAndID(ctx, ctx.Repo.Repository.ID, id, toClose); err != nil { ctx.NotFoundOrServerError("ChangeProjectStatusByRepoIDAndID", project_model.IsErrProjectNotExist, err) return } - ctx.JSONRedirect(fmt.Sprintf("%s/projects/%d", ctx.Repo.RepoLink, id)) + ctx.JSONRedirect(project_model.ProjectLinkForRepo(ctx.Repo.Repository, id)) } // DeleteProject delete a project func DeleteProject(ctx *context.Context) { - p, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64("id")) + p, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64(":id")) if err != nil { if project_model.IsErrProjectNotExist(err) { ctx.NotFound("", nil) @@ -216,7 +221,7 @@ func RenderEditProject(ctx *context.Context) { ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects) ctx.Data["CardTypes"] = project_model.GetCardConfig() - p, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64("id")) + p, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64(":id")) if err != nil { if project_model.IsErrProjectNotExist(err) { ctx.NotFound("", nil) @@ -235,7 +240,7 @@ func RenderEditProject(ctx *context.Context) { ctx.Data["content"] = p.Description ctx.Data["card_type"] = p.CardType ctx.Data["redirect"] = ctx.FormString("redirect") - ctx.Data["CancelLink"] = fmt.Sprintf("%s/projects/%d", ctx.Repo.Repository.Link(), p.ID) + ctx.Data["CancelLink"] = project_model.ProjectLinkForRepo(ctx.Repo.Repository, p.ID) ctx.HTML(http.StatusOK, tplProjectsNew) } @@ -243,13 +248,13 @@ func RenderEditProject(ctx *context.Context) { // EditProjectPost response for editing a project func EditProjectPost(ctx *context.Context) { form := web.GetForm(ctx).(*forms.CreateProjectForm) - projectID := ctx.PathParamInt64("id") + projectID := ctx.PathParamInt64(":id") ctx.Data["Title"] = ctx.Tr("repo.projects.edit") ctx.Data["PageIsEditProjects"] = true ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects) ctx.Data["CardTypes"] = project_model.GetCardConfig() - ctx.Data["CancelLink"] = fmt.Sprintf("%s/projects/%d", ctx.Repo.Repository.Link(), projectID) + ctx.Data["CancelLink"] = project_model.ProjectLinkForRepo(ctx.Repo.Repository, projectID) if ctx.HasError() { ctx.HTML(http.StatusOK, tplProjectsNew) @@ -288,7 +293,7 @@ func EditProjectPost(ctx *context.Context) { // ViewProject renders the project with board view func ViewProject(ctx *context.Context) { - project, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64("id")) + project, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64(":id")) if err != nil { if project_model.IsErrProjectNotExist(err) { ctx.NotFound("", nil) @@ -312,7 +317,8 @@ func ViewProject(ctx *context.Context) { assigneeID := ctx.FormInt64("assignee") // TODO: use "optional" but not 0 in the future - issuesMap, err := issues_model.LoadIssuesFromColumnList(ctx, columns, &issues_model.IssuesOptions{ + issuesMap, err := project_service.LoadIssuesFromProject(ctx, project, &issues_model.IssuesOptions{ + RepoIDs: []int64{ctx.Repo.Repository.ID}, LabelIDs: labelIDs, AssigneeID: optional.Some(assigneeID), }) @@ -320,6 +326,9 @@ func ViewProject(ctx *context.Context) { ctx.ServerError("LoadIssuesOfColumns", err) return } + for _, column := range columns { + column.NumIssues = int64(len(issuesMap[column.ID])) + } if project.CardType != project_model.CardTypeTextOnly { issuesAttachmentMap := make(map[int64][]*repo_model.Attachment) @@ -468,7 +477,7 @@ func DeleteProjectColumn(ctx *context.Context) { return } - project, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64("id")) + project, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64(":id")) if err != nil { if project_model.IsErrProjectNotExist(err) { ctx.NotFound("", nil) @@ -478,12 +487,12 @@ func DeleteProjectColumn(ctx *context.Context) { return } - pb, err := project_model.GetColumn(ctx, ctx.PathParamInt64("columnID")) + pb, err := project_model.GetColumn(ctx, ctx.PathParamInt64(":columnID")) if err != nil { ctx.ServerError("GetProjectColumn", err) return } - if pb.ProjectID != ctx.PathParamInt64("id") { + if pb.ProjectID != ctx.PathParamInt64(":id") { ctx.JSON(http.StatusUnprocessableEntity, map[string]string{ "message": fmt.Sprintf("ProjectColumn[%d] is not in Project[%d] as expected", pb.ID, project.ID), }) @@ -497,7 +506,7 @@ func DeleteProjectColumn(ctx *context.Context) { return } - if err := project_model.DeleteColumnByID(ctx, ctx.PathParamInt64("columnID")); err != nil { + if err := project_model.DeleteColumnByID(ctx, ctx.PathParamInt64(":columnID")); err != nil { ctx.ServerError("DeleteProjectColumnByID", err) return } @@ -515,7 +524,7 @@ func AddColumnToProjectPost(ctx *context.Context) { return } - project, err := project_model.GetProjectForRepoByID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("id")) + project, err := project_model.GetProjectForRepoByID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":id")) if err != nil { if project_model.IsErrProjectNotExist(err) { ctx.NotFound("", nil) @@ -553,7 +562,7 @@ func checkProjectColumnChangePermissions(ctx *context.Context) (*project_model.P return nil, nil } - project, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64("id")) + project, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64(":id")) if err != nil { if project_model.IsErrProjectNotExist(err) { ctx.NotFound("", nil) @@ -563,12 +572,12 @@ func checkProjectColumnChangePermissions(ctx *context.Context) (*project_model.P return nil, nil } - column, err := project_model.GetColumn(ctx, ctx.PathParamInt64("columnID")) + column, err := project_model.GetColumn(ctx, ctx.PathParamInt64(":columnID")) if err != nil { ctx.ServerError("GetProjectColumn", err) return nil, nil } - if column.ProjectID != ctx.PathParamInt64("id") { + if column.ProjectID != ctx.PathParamInt64(":id") { ctx.JSON(http.StatusUnprocessableEntity, map[string]string{ "message": fmt.Sprintf("ProjectColumn[%d] is not in Project[%d] as expected", column.ID, project.ID), }) @@ -639,7 +648,7 @@ func MoveIssues(ctx *context.Context) { return } - project, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64("id")) + project, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64(":id")) if err != nil { if project_model.IsErrProjectNotExist(err) { ctx.NotFound("ProjectNotExist", nil) @@ -653,7 +662,7 @@ func MoveIssues(ctx *context.Context) { return } - column, err := project_model.GetColumn(ctx, ctx.PathParamInt64("columnID")) + column, err := project_model.GetColumn(ctx, ctx.PathParamInt64(":columnID")) if err != nil { if project_model.IsErrProjectColumnNotExist(err) { ctx.NotFound("ProjectColumnNotExist", nil) diff --git a/routers/web/repo/projects_test.go b/routers/web/repo/projects_test.go index d0690d9a4de0d..1a42c615abc55 100644 --- a/routers/web/repo/projects_test.go +++ b/routers/web/repo/projects_test.go @@ -17,8 +17,8 @@ func TestCheckProjectColumnChangePermissions(t *testing.T) { ctx, _ := contexttest.MockContext(t, "user2/repo1/projects/1/2") contexttest.LoadUser(t, ctx, 2) contexttest.LoadRepo(t, ctx, 1) - ctx.SetPathParam("id", "1") - ctx.SetPathParam("columnID", "2") + ctx.SetPathParam(":id", "1") + ctx.SetPathParam(":columnID", "2") project, column := checkProjectColumnChangePermissions(ctx) assert.NotNil(t, project) diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go index a6c46a7e74b2d..6c09f69bace0d 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -14,6 +14,7 @@ import ( "strings" "time" + "code.gitea.io/gitea/models" activities_model "code.gitea.io/gitea/models/activities" "code.gitea.io/gitea/models/db" git_model "code.gitea.io/gitea/models/git" @@ -23,13 +24,13 @@ 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/base" "code.gitea.io/gitea/modules/emoji" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/gitrepo" issue_template "code.gitea.io/gitea/modules/issue/template" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/routers/utils" @@ -49,10 +50,10 @@ import ( ) const ( - tplCompareDiff templates.TplName = "repo/diff/compare" - tplPullCommits templates.TplName = "repo/pulls/commits" - tplPullFiles templates.TplName = "repo/pulls/files" - tplPullSnapFile templates.TplName = "repo/diff/snap_diff" + tplCompareDiff base.TplName = "repo/diff/compare" + tplPullCommits base.TplName = "repo/pulls/commits" + tplPullFiles base.TplName = "repo/pulls/files" + tplPullSnapFile base.TplName = "repo/diff/snap_diff" pullRequestTemplateKey = "PullRequestTemplate" ) @@ -109,7 +110,7 @@ func getRepository(ctx *context.Context, repoID int64) *repo_model.Repository { } func getPullInfo(ctx *context.Context) (issue *issues_model.Issue, ok bool) { - issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) + issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) if err != nil { if issues_model.IsErrIssueNotExist(err) { ctx.NotFound("GetIssueByIndex", err) @@ -798,18 +799,18 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi return } + allComments := issues_model.CommentList{} for _, file := range diff.Files { for _, section := range file.Sections { for _, line := range section.Lines { - for _, comment := range line.Comments { - if err := comment.LoadAttachments(ctx); err != nil { - ctx.ServerError("LoadAttachments", err) - return - } - } + allComments = append(allComments, line.Comments...) } } } + if err := allComments.LoadAttachments(ctx); err != nil { + ctx.ServerError("LoadAttachments", err) + return + } pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pull.BaseRepoID, pull.BaseBranch) if err != nil { @@ -983,8 +984,8 @@ func UpdatePullRequest(ctx *context.Context) { message := fmt.Sprintf("Merge branch '%s' into %s", issue.PullRequest.BaseBranch, issue.PullRequest.HeadBranch) if err = pull_service.Update(ctx, issue.PullRequest, ctx.Doer, message, rebase, strategy, option); err != nil { - if pull_service.IsErrMergeConflicts(err) { - conflictError := err.(pull_service.ErrMergeConflicts) + if models.IsErrMergeConflicts(err) { + conflictError := err.(models.ErrMergeConflicts) flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{ "Message": ctx.Tr("repo.pulls.merge_conflict"), "Summary": ctx.Tr("repo.pulls.merge_conflict_summary"), @@ -997,8 +998,8 @@ func UpdatePullRequest(ctx *context.Context) { ctx.Flash.Error(flashError) ctx.Redirect(issue.Link()) return - } else if pull_service.IsErrRebaseConflicts(err) { - conflictError := err.(pull_service.ErrRebaseConflicts) + } else if models.IsErrRebaseConflicts(err) { + conflictError := err.(models.ErrRebaseConflicts) flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{ "Message": ctx.Tr("repo.pulls.rebase_conflict", utils.SanitizeFlashErrorString(conflictError.CommitSHA)), "Summary": ctx.Tr("repo.pulls.rebase_conflict_summary"), @@ -1062,7 +1063,7 @@ func MergePullRequest(ctx *context.Context) { ctx.JSONError(ctx.Tr("repo.pulls.no_merge_wip")) case errors.Is(err, pull_service.ErrNotMergeableState): ctx.JSONError(ctx.Tr("repo.pulls.no_merge_not_ready")) - case pull_service.IsErrDisallowedToMerge(err): + case models.IsErrDisallowedToMerge(err): ctx.JSONError(ctx.Tr("repo.pulls.no_merge_not_ready")) case asymkey_service.IsErrWontSign(err): ctx.JSONError(err.Error()) // has no translation ... @@ -1079,7 +1080,7 @@ func MergePullRequest(ctx *context.Context) { if manuallyMerged { if err := pull_service.MergedManually(ctx, pr, ctx.Doer, ctx.Repo.GitRepo, form.MergeCommitID); err != nil { switch { - case pull_service.IsErrInvalidMergeStyle(err): + case models.IsErrInvalidMergeStyle(err): ctx.JSONError(ctx.Tr("repo.pulls.invalid_merge_option")) case strings.Contains(err.Error(), "Wrong commit ID"): ctx.JSONError(ctx.Tr("repo.pulls.wrong_commit_id")) @@ -1126,10 +1127,10 @@ func MergePullRequest(ctx *context.Context) { } if err := pull_service.Merge(ctx, pr, ctx.Doer, ctx.Repo.GitRepo, repo_model.MergeStyle(form.Do), form.HeadCommitID, message, false); err != nil { - if pull_service.IsErrInvalidMergeStyle(err) { + if models.IsErrInvalidMergeStyle(err) { ctx.JSONError(ctx.Tr("repo.pulls.invalid_merge_option")) - } else if pull_service.IsErrMergeConflicts(err) { - conflictError := err.(pull_service.ErrMergeConflicts) + } else if models.IsErrMergeConflicts(err) { + conflictError := err.(models.ErrMergeConflicts) flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{ "Message": ctx.Tr("repo.editor.merge_conflict"), "Summary": ctx.Tr("repo.editor.merge_conflict_summary"), @@ -1141,8 +1142,8 @@ func MergePullRequest(ctx *context.Context) { } ctx.Flash.Error(flashError) ctx.JSONRedirect(issue.Link()) - } else if pull_service.IsErrRebaseConflicts(err) { - conflictError := err.(pull_service.ErrRebaseConflicts) + } else if models.IsErrRebaseConflicts(err) { + conflictError := err.(models.ErrRebaseConflicts) flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{ "Message": ctx.Tr("repo.pulls.rebase_conflict", utils.SanitizeFlashErrorString(conflictError.CommitSHA)), "Summary": ctx.Tr("repo.pulls.rebase_conflict_summary"), @@ -1154,7 +1155,7 @@ func MergePullRequest(ctx *context.Context) { } ctx.Flash.Error(flashError) ctx.JSONRedirect(issue.Link()) - } else if pull_service.IsErrMergeUnrelatedHistories(err) { + } else if models.IsErrMergeUnrelatedHistories(err) { log.Debug("MergeUnrelatedHistories error: %v", err) ctx.Flash.Error(ctx.Tr("repo.pulls.unrelated_histories")) ctx.JSONRedirect(issue.Link()) @@ -1162,7 +1163,7 @@ func MergePullRequest(ctx *context.Context) { log.Debug("MergePushOutOfDate error: %v", err) ctx.Flash.Error(ctx.Tr("repo.pulls.merge_out_of_date")) ctx.JSONRedirect(issue.Link()) - } else if pull_service.IsErrSHADoesNotMatch(err) { + } else if models.IsErrSHADoesNotMatch(err) { log.Debug("MergeHeadOutOfDate error: %v", err) ctx.Flash.Error(ctx.Tr("repo.pulls.head_out_of_date")) ctx.JSONRedirect(issue.Link()) @@ -1278,7 +1279,7 @@ func CompareAndPullRequestPost(ctx *context.Context) { ci := ParseCompareInfo(ctx) defer func() { - if ci != nil && ci.HeadGitRepo != nil { + if !ctx.Repo.PullRequest.SameRepo && ci != nil && ci.HeadGitRepo != nil { ci.HeadGitRepo.Close() } }() @@ -1560,7 +1561,7 @@ func DownloadPullPatch(ctx *context.Context) { // DownloadPullDiffOrPatch render a pull's raw diff or patch func DownloadPullDiffOrPatch(ctx *context.Context, patch bool) { - pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) + pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) if err != nil { if issues_model.IsErrPullRequestNotExist(err) { ctx.NotFound("GetPullRequestByIndex", err) @@ -1621,7 +1622,7 @@ func UpdatePullRequestTarget(ctx *context.Context) { "error": err.Error(), "user_error": errorMessage, }) - } else if pull_service.IsErrPullRequestHasMerged(err) { + } else if models.IsErrPullRequestHasMerged(err) { errorMessage := ctx.Tr("repo.pulls.has_merged") ctx.Flash.Error(errorMessage) @@ -1653,7 +1654,7 @@ func UpdatePullRequestTarget(ctx *context.Context) { func SetAllowEdits(ctx *context.Context) { form := web.GetForm(ctx).(*forms.UpdateAllowEditsForm) - pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) + pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) if err != nil { if issues_model.IsErrPullRequestNotExist(err) { ctx.NotFound("GetPullRequestByIndex", err) diff --git a/routers/web/repo/pull_review.go b/routers/web/repo/pull_review.go index 3e9e615b15dab..aa2e689e42367 100644 --- a/routers/web/repo/pull_review.go +++ b/routers/web/repo/pull_review.go @@ -12,10 +12,10 @@ import ( "code.gitea.io/gitea/models/organization" pull_model "code.gitea.io/gitea/models/pull" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context/upload" @@ -26,10 +26,10 @@ import ( ) const ( - tplDiffConversation templates.TplName = "repo/diff/conversation" - tplConversationOutdated templates.TplName = "repo/diff/conversation_outdated" - tplTimelineConversation templates.TplName = "repo/issue/view_content/conversation" - tplNewComment templates.TplName = "repo/diff/new_comment" + tplDiffConversation base.TplName = "repo/diff/conversation" + tplConversationOutdated base.TplName = "repo/diff/conversation_outdated" + tplTimelineConversation base.TplName = "repo/issue/view_content/conversation" + tplNewComment base.TplName = "repo/diff/new_comment" ) // RenderNewCodeCommentForm will render the form for creating a new review comment diff --git a/routers/web/repo/recent_commits.go b/routers/web/repo/recent_commits.go index dc72081900909..65a47333b68c0 100644 --- a/routers/web/repo/recent_commits.go +++ b/routers/web/repo/recent_commits.go @@ -4,16 +4,14 @@ package repo import ( - "errors" "net/http" - "code.gitea.io/gitea/modules/templates" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/services/context" - contributors_service "code.gitea.io/gitea/services/repository" ) const ( - tplRecentCommits templates.TplName = "repo/activity" + tplRecentCommits base.TplName = "repo/activity" ) // RecentCommits renders the page to show recent commit frequency on repository @@ -26,16 +24,3 @@ func RecentCommits(ctx *context.Context) { ctx.HTML(http.StatusOK, tplRecentCommits) } - -// RecentCommitsData returns JSON of recent commits data -func RecentCommitsData(ctx *context.Context) { - if contributorStats, err := contributors_service.GetContributorStats(ctx, ctx.Cache, ctx.Repo.Repository, ctx.Repo.CommitID); err != nil { - if errors.Is(err, contributors_service.ErrAwaitGeneration) { - ctx.Status(http.StatusAccepted) - return - } - ctx.ServerError("RecentCommitsData", err) - } else { - ctx.JSON(http.StatusOK, contributorStats["total"].Weeks) - } -} diff --git a/routers/web/repo/release.go b/routers/web/repo/release.go index b8176cb70b91c..b3a91a6070744 100644 --- a/routers/web/repo/release.go +++ b/routers/web/repo/release.go @@ -10,18 +10,19 @@ import ( "net/http" "strings" + "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/db" git_model "code.gitea.io/gitea/models/git" "code.gitea.io/gitea/models/renderhelper" 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/markup/markdown" "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/routers/web/feed" @@ -29,13 +30,13 @@ import ( "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context/upload" "code.gitea.io/gitea/services/forms" - release_service "code.gitea.io/gitea/services/release" + releaseservice "code.gitea.io/gitea/services/release" ) const ( - tplReleasesList templates.TplName = "repo/release/list" - tplReleaseNew templates.TplName = "repo/release/new" - tplTagsList templates.TplName = "repo/tag/list" + tplReleasesList base.TplName = "repo/release/list" + tplReleaseNew base.TplName = "repo/release/new" + tplTagsList base.TplName = "repo/tag/list" ) // calReleaseNumCommitsBehind calculates given release has how many commits behind release target. @@ -431,27 +432,27 @@ func NewReleasePost(ctx *context.Context) { } if len(form.TagOnly) > 0 { - if err = release_service.CreateNewTag(ctx, ctx.Doer, ctx.Repo.Repository, form.Target, form.TagName, msg); err != nil { - if release_service.IsErrTagAlreadyExists(err) { - e := err.(release_service.ErrTagAlreadyExists) + if err = releaseservice.CreateNewTag(ctx, ctx.Doer, ctx.Repo.Repository, form.Target, form.TagName, msg); err != nil { + if models.IsErrTagAlreadyExists(err) { + e := err.(models.ErrTagAlreadyExists) ctx.Flash.Error(ctx.Tr("repo.branch.tag_collision", e.TagName)) ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()) return } - if release_service.IsErrInvalidTagName(err) { + if models.IsErrInvalidTagName(err) { ctx.Flash.Error(ctx.Tr("repo.release.tag_name_invalid")) ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()) return } - if release_service.IsErrProtectedTagName(err) { + if models.IsErrProtectedTagName(err) { ctx.Flash.Error(ctx.Tr("repo.release.tag_name_protected")) ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()) return } - ctx.ServerError("release_service.CreateNewTag", err) + ctx.ServerError("releaseservice.CreateNewTag", err) return } @@ -474,14 +475,14 @@ func NewReleasePost(ctx *context.Context) { IsTag: false, } - if err = release_service.CreateRelease(ctx.Repo.GitRepo, rel, attachmentUUIDs, msg); err != nil { + if err = releaseservice.CreateRelease(ctx.Repo.GitRepo, rel, attachmentUUIDs, msg); err != nil { ctx.Data["Err_TagName"] = true switch { case repo_model.IsErrReleaseAlreadyExist(err): ctx.RenderWithErr(ctx.Tr("repo.release.tag_name_already_exist"), tplReleaseNew, &form) - case release_service.IsErrInvalidTagName(err): + case models.IsErrInvalidTagName(err): ctx.RenderWithErr(ctx.Tr("repo.release.tag_name_invalid"), tplReleaseNew, &form) - case release_service.IsErrProtectedTagName(err): + case models.IsErrProtectedTagName(err): ctx.RenderWithErr(ctx.Tr("repo.release.tag_name_protected"), tplReleaseNew, &form) default: ctx.ServerError("CreateRelease", err) @@ -503,7 +504,7 @@ func NewReleasePost(ctx *context.Context) { rel.PublisherID = ctx.Doer.ID rel.IsTag = false - if err = release_service.UpdateRelease(ctx, ctx.Doer, ctx.Repo.GitRepo, rel, attachmentUUIDs, nil, nil); err != nil { + if err = releaseservice.UpdateRelease(ctx, ctx.Doer, ctx.Repo.GitRepo, rel, attachmentUUIDs, nil, nil); err != nil { ctx.Data["Err_TagName"] = true ctx.ServerError("UpdateRelease", err) return @@ -609,7 +610,7 @@ func EditReleasePost(ctx *context.Context) { rel.Note = form.Content rel.IsDraft = len(form.Draft) > 0 rel.IsPrerelease = form.Prerelease - if err = release_service.UpdateRelease(ctx, ctx.Doer, ctx.Repo.GitRepo, + if err = releaseservice.UpdateRelease(ctx, ctx.Doer, ctx.Repo.GitRepo, rel, addAttachmentUUIDs, delAttachmentUUIDs, editAttachments); err != nil { ctx.ServerError("UpdateRelease", err) return @@ -648,8 +649,8 @@ func deleteReleaseOrTag(ctx *context.Context, isDelTag bool) { return } - if err := release_service.DeleteReleaseByID(ctx, ctx.Repo.Repository, rel, ctx.Doer, isDelTag); err != nil { - if release_service.IsErrProtectedTagName(err) { + if err := releaseservice.DeleteReleaseByID(ctx, ctx.Repo.Repository, rel, ctx.Doer, isDelTag); err != nil { + if models.IsErrProtectedTagName(err) { ctx.Flash.Error(ctx.Tr("repo.release.tag_name_protected")) } else { ctx.Flash.Error("DeleteReleaseByID: " + err.Error()) diff --git a/routers/web/repo/repo.go b/routers/web/repo/repo.go index 85a745acbd32e..0b57547c9c5c4 100644 --- a/routers/web/repo/repo.go +++ b/routers/web/repo/repo.go @@ -11,6 +11,7 @@ import ( "slices" "strings" + "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/db" git_model "code.gitea.io/gitea/models/git" "code.gitea.io/gitea/models/organization" @@ -18,6 +19,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/base" "code.gitea.io/gitea/modules/cache" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" @@ -26,7 +28,6 @@ import ( "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/storage" api "code.gitea.io/gitea/modules/structs" - "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/services/context" @@ -38,8 +39,8 @@ import ( ) const ( - tplCreate templates.TplName = "repo/create" - tplAlertDetails templates.TplName = "base/alert_details" + tplCreate base.TplName = "repo/create" + tplAlertDetails base.TplName = "base/alert_details" ) // MustBeNotEmpty render when a repo is a empty git dir @@ -185,7 +186,7 @@ func Create(ctx *context.Context) { ctx.HTML(http.StatusOK, tplCreate) } -func handleCreateError(ctx *context.Context, owner *user_model.User, err error, name string, tpl templates.TplName, form any) { +func handleCreateError(ctx *context.Context, owner *user_model.User, err error, name string, tpl base.TplName, form any) { switch { case repo_model.IsErrReachLimitOfRepo(err): maxCreationLimit := owner.MaxCreationLimit() @@ -227,8 +228,12 @@ func CreatePost(ctx *context.Context) { ctx.Data["Licenses"] = repo_module.Licenses ctx.Data["Readmes"] = repo_module.Readmes + // the logic is still buggy, the complete fix is in 1.24 + ctx.Data["IsForcedPrivate"] = setting.Repository.ForcePrivate ctx.Data["CanCreateRepo"] = ctx.Doer.CanCreateRepo() ctx.Data["MaxCreationLimit"] = ctx.Doer.MaxCreationLimit() + ctx.Data["SupportedObjectFormats"] = git.DefaultFeatures().SupportedObjectFormats + ctx.Data["DefaultObjectFormat"] = git.Sha1ObjectFormat ctxUser := checkContextUser(ctx, form.UID) if ctx.Written() { @@ -304,14 +309,14 @@ func CreatePost(ctx *context.Context) { } const ( - tplWatchUnwatch templates.TplName = "repo/watch_unwatch" - tplStarUnstar templates.TplName = "repo/star_unstar" + tplWatchUnwatch base.TplName = "repo/watch_unwatch" + tplStarUnstar base.TplName = "repo/star_unstar" ) // Action response for actions to a repository func Action(ctx *context.Context) { var err error - switch ctx.PathParam("action") { + switch ctx.PathParam(":action") { case "watch": err = repo_model.WatchRepo(ctx, ctx.Doer, ctx.Repo.Repository, true) case "unwatch": @@ -339,12 +344,12 @@ func Action(ctx *context.Context) { if errors.Is(err, user_model.ErrBlockedUser) { ctx.Flash.Error(ctx.Tr("repo.action.blocked_user")) } else { - ctx.ServerError(fmt.Sprintf("Action (%s)", ctx.PathParam("action")), err) + ctx.ServerError(fmt.Sprintf("Action (%s)", ctx.PathParam(":action")), err) return } } - switch ctx.PathParam("action") { + switch ctx.PathParam(":action") { case "watch", "unwatch": ctx.Data["IsWatchingRepo"] = repo_model.IsWatching(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID) case "star", "unstar": @@ -354,17 +359,17 @@ func Action(ctx *context.Context) { // see the `hx-trigger="refreshUserCards ..."` comments in tmpl ctx.RespHeader().Add("hx-trigger", "refreshUserCards") - switch ctx.PathParam("action") { + switch ctx.PathParam(":action") { case "watch", "unwatch", "star", "unstar": // we have to reload the repository because NumStars or NumWatching (used in the templates) has just changed ctx.Data["Repository"], err = repo_model.GetRepositoryByName(ctx, ctx.Repo.Repository.OwnerID, ctx.Repo.Repository.Name) if err != nil { - ctx.ServerError(fmt.Sprintf("Action (%s)", ctx.PathParam("action")), err) + ctx.ServerError(fmt.Sprintf("Action (%s)", ctx.PathParam(":action")), err) return } } - switch ctx.PathParam("action") { + switch ctx.PathParam(":action") { case "watch", "unwatch": ctx.HTML(http.StatusOK, tplWatchUnwatch) return @@ -377,7 +382,7 @@ func Action(ctx *context.Context) { } func acceptOrRejectRepoTransfer(ctx *context.Context, accept bool) error { - repoTransfer, err := repo_model.GetPendingRepositoryTransfer(ctx, ctx.Repo.Repository) + repoTransfer, err := models.GetPendingRepositoryTransfer(ctx, ctx.Repo.Repository) if err != nil { return err } diff --git a/routers/web/repo/search.go b/routers/web/repo/search.go index a037a348333aa..d5772ff79d2fa 100644 --- a/routers/web/repo/search.go +++ b/routers/web/repo/search.go @@ -8,14 +8,15 @@ import ( "strings" "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/git" code_indexer "code.gitea.io/gitea/modules/indexer/code" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/templates" + "code.gitea.io/gitea/routers/common" "code.gitea.io/gitea/services/context" ) -const tplSearch templates.TplName = "repo/search" +const tplSearch base.TplName = "repo/search" func indexSettingToGitGrepPathspecList() (list []string) { for _, expr := range setting.Indexer.IncludePatterns { @@ -29,18 +30,9 @@ func indexSettingToGitGrepPathspecList() (list []string) { // Search render repository search page func Search(ctx *context.Context) { - language := ctx.FormTrim("l") - keyword := ctx.FormTrim("q") - - isFuzzy := ctx.FormOptionalBool("fuzzy").ValueOrDefault(true) - - ctx.Data["Keyword"] = keyword - ctx.Data["Language"] = language - ctx.Data["IsFuzzy"] = isFuzzy ctx.Data["PageIsViewCode"] = true - ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled - - if keyword == "" { + prepareSearch := common.PrepareCodeSearch(ctx) + if prepareSearch.Keyword == "" { ctx.HTML(http.StatusOK, tplSearch) return } @@ -57,9 +49,9 @@ func Search(ctx *context.Context) { var err error total, searchResults, searchResultLanguages, err = code_indexer.PerformSearch(ctx, &code_indexer.SearchOptions{ RepoIDs: []int64{ctx.Repo.Repository.ID}, - Keyword: keyword, - IsKeywordFuzzy: isFuzzy, - Language: language, + Keyword: prepareSearch.Keyword, + IsKeywordFuzzy: prepareSearch.IsFuzzy, + Language: prepareSearch.Language, Paginator: &db.ListOptions{ Page: page, PageSize: setting.UI.RepoSearchPagingNum, @@ -75,9 +67,9 @@ func Search(ctx *context.Context) { ctx.Data["CodeIndexerUnavailable"] = !code_indexer.IsAvailable(ctx) } } else { - res, err := git.GrepSearch(ctx, ctx.Repo.GitRepo, keyword, git.GrepOptions{ + res, err := git.GrepSearch(ctx, ctx.Repo.GitRepo, prepareSearch.Keyword, git.GrepOptions{ ContextLineNumber: 1, - IsFuzzy: isFuzzy, + IsFuzzy: prepareSearch.IsFuzzy, RefName: git.RefNameFromBranch(ctx.Repo.BranchName).String(), // BranchName should be default branch or the first existing branch PathspecList: indexSettingToGitGrepPathspecList(), }) @@ -109,7 +101,7 @@ func Search(ctx *context.Context) { pager := context.NewPagination(total, setting.UI.RepoSearchPagingNum, page, 5) pager.SetDefaultParams(ctx) - pager.AddParamString("l", language) + pager.AddParamString("l", prepareSearch.Language) ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, tplSearch) diff --git a/routers/web/repo/setting/collaboration.go b/routers/web/repo/setting/collaboration.go index df7cc5e39b6c7..cdf91edf4a2c2 100644 --- a/routers/web/repo/setting/collaboration.go +++ b/routers/web/repo/setting/collaboration.go @@ -32,7 +32,7 @@ func Collaboration(ctx *context.Context) { } ctx.Data["Collaborators"] = users - teams, err := organization.GetRepoTeams(ctx, ctx.Repo.Repository.OwnerID, ctx.Repo.Repository.ID) + teams, err := organization.GetRepoTeams(ctx, ctx.Repo.Repository) if err != nil { ctx.ServerError("GetRepoTeams", err) return diff --git a/routers/web/repo/setting/deploy_key.go b/routers/web/repo/setting/deploy_key.go index 193562528bf83..abc3eb4af18b6 100644 --- a/routers/web/repo/setting/deploy_key.go +++ b/routers/web/repo/setting/deploy_key.go @@ -99,7 +99,7 @@ func DeployKeysPost(ctx *context.Context) { // DeleteDeployKey response for deleting a deploy key func DeleteDeployKey(ctx *context.Context) { - if err := asymkey_service.DeleteDeployKey(ctx, ctx.Repo.Repository, ctx.FormInt64("id")); err != nil { + if err := asymkey_service.DeleteDeployKey(ctx, ctx.Doer, ctx.FormInt64("id")); err != nil { ctx.Flash.Error("DeleteDeployKey: " + err.Error()) } else { ctx.Flash.Success(ctx.Tr("repo.settings.deploy_key_deletion_success")) diff --git a/routers/web/repo/setting/git_hooks.go b/routers/web/repo/setting/git_hooks.go index 1d9221130398c..2e9caa4c868f1 100644 --- a/routers/web/repo/setting/git_hooks.go +++ b/routers/web/repo/setting/git_hooks.go @@ -30,7 +30,7 @@ func GitHooksEdit(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("repo.settings.githooks") ctx.Data["PageIsSettingsGitHooks"] = true - name := ctx.PathParam("name") + name := ctx.PathParam(":name") hook, err := ctx.Repo.GitRepo.GetHook(name) if err != nil { if err == git.ErrNotValidHook { @@ -46,7 +46,7 @@ func GitHooksEdit(ctx *context.Context) { // GitHooksEditPost response for editing a git hook of a repository func GitHooksEditPost(ctx *context.Context) { - name := ctx.PathParam("name") + name := ctx.PathParam(":name") hook, err := ctx.Repo.GitRepo.GetHook(name) if err != nil { if err == git.ErrNotValidHook { diff --git a/routers/web/repo/setting/lfs.go b/routers/web/repo/setting/lfs.go index 2df483fa3453d..fad635966864c 100644 --- a/routers/web/repo/setting/lfs.go +++ b/routers/web/repo/setting/lfs.go @@ -15,6 +15,7 @@ import ( "strings" git_model "code.gitea.io/gitea/models/git" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/charset" "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/git" @@ -24,18 +25,17 @@ import ( repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/storage" - "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/typesniffer" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/services/context" ) const ( - tplSettingsLFS templates.TplName = "repo/settings/lfs" - tplSettingsLFSLocks templates.TplName = "repo/settings/lfs_locks" - tplSettingsLFSFile templates.TplName = "repo/settings/lfs_file" - tplSettingsLFSFileFind templates.TplName = "repo/settings/lfs_file_find" - tplSettingsLFSPointers templates.TplName = "repo/settings/lfs_pointers" + tplSettingsLFS base.TplName = "repo/settings/lfs" + tplSettingsLFSLocks base.TplName = "repo/settings/lfs_locks" + tplSettingsLFSFile base.TplName = "repo/settings/lfs_file" + tplSettingsLFSFileFind base.TplName = "repo/settings/lfs_file_find" + tplSettingsLFSPointers base.TplName = "repo/settings/lfs_pointers" ) // LFSFiles shows a repository's LFS files diff --git a/routers/web/repo/setting/protected_branch.go b/routers/web/repo/setting/protected_branch.go index 022a24a9ad465..f651d8f318db1 100644 --- a/routers/web/repo/setting/protected_branch.go +++ b/routers/web/repo/setting/protected_branch.go @@ -15,7 +15,6 @@ import ( "code.gitea.io/gitea/models/perm" access_model "code.gitea.io/gitea/models/perm/access" "code.gitea.io/gitea/modules/base" - "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/routers/web/repo" "code.gitea.io/gitea/services/context" @@ -27,7 +26,7 @@ import ( ) const ( - tplProtectedBranch templates.TplName = "repo/settings/protected_branch" + tplProtectedBranch base.TplName = "repo/settings/protected_branch" ) // ProtectedBranchRules render the page to protect the repository diff --git a/routers/web/repo/setting/protected_tag.go b/routers/web/repo/setting/protected_tag.go index 1730ad4a8bec8..fcfa77aa8ce04 100644 --- a/routers/web/repo/setting/protected_tag.go +++ b/routers/web/repo/setting/protected_tag.go @@ -14,14 +14,13 @@ import ( access_model "code.gitea.io/gitea/models/perm/access" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/forms" ) const ( - tplTags templates.TplName = "repo/settings/tags" + tplTags base.TplName = "repo/settings/tags" ) // Tags render the page to protect tags @@ -170,7 +169,7 @@ func setTagsContext(ctx *context.Context) error { func selectProtectedTagByContext(ctx *context.Context) *git_model.ProtectedTag { id := ctx.FormInt64("id") if id == 0 { - id = ctx.PathParamInt64("id") + id = ctx.PathParamInt64(":id") } tag, err := git_model.GetProtectedTagByID(ctx, id) diff --git a/routers/web/repo/setting/runners.go b/routers/web/repo/setting/runners.go deleted file mode 100644 index 94f2ae7a0c092..0000000000000 --- a/routers/web/repo/setting/runners.go +++ /dev/null @@ -1,187 +0,0 @@ -// Copyright 2022 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package setting - -import ( - "errors" - "net/http" - "net/url" - - actions_model "code.gitea.io/gitea/models/actions" - "code.gitea.io/gitea/models/db" - "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/templates" - actions_shared "code.gitea.io/gitea/routers/web/shared/actions" - shared_user "code.gitea.io/gitea/routers/web/shared/user" - "code.gitea.io/gitea/services/context" -) - -const ( - // TODO: Separate secrets from runners when layout is ready - tplRepoRunners templates.TplName = "repo/settings/actions" - tplOrgRunners templates.TplName = "org/settings/actions" - tplAdminRunners templates.TplName = "admin/actions" - tplUserRunners templates.TplName = "user/settings/actions" - tplRepoRunnerEdit templates.TplName = "repo/settings/runner_edit" - tplOrgRunnerEdit templates.TplName = "org/settings/runners_edit" - tplAdminRunnerEdit templates.TplName = "admin/runners/edit" - tplUserRunnerEdit templates.TplName = "user/settings/runner_edit" -) - -type runnersCtx struct { - OwnerID int64 - RepoID int64 - IsRepo bool - IsOrg bool - IsAdmin bool - IsUser bool - RunnersTemplate templates.TplName - RunnerEditTemplate templates.TplName - RedirectLink string -} - -func getRunnersCtx(ctx *context.Context) (*runnersCtx, error) { - if ctx.Data["PageIsRepoSettings"] == true { - return &runnersCtx{ - RepoID: ctx.Repo.Repository.ID, - OwnerID: 0, - IsRepo: true, - RunnersTemplate: tplRepoRunners, - RunnerEditTemplate: tplRepoRunnerEdit, - RedirectLink: ctx.Repo.RepoLink + "/settings/actions/runners/", - }, nil - } - - if ctx.Data["PageIsOrgSettings"] == true { - err := shared_user.LoadHeaderCount(ctx) - if err != nil { - ctx.ServerError("LoadHeaderCount", err) - return nil, nil - } - return &runnersCtx{ - RepoID: 0, - OwnerID: ctx.Org.Organization.ID, - IsOrg: true, - RunnersTemplate: tplOrgRunners, - RunnerEditTemplate: tplOrgRunnerEdit, - RedirectLink: ctx.Org.OrgLink + "/settings/actions/runners/", - }, nil - } - - if ctx.Data["PageIsAdmin"] == true { - return &runnersCtx{ - RepoID: 0, - OwnerID: 0, - IsAdmin: true, - RunnersTemplate: tplAdminRunners, - RunnerEditTemplate: tplAdminRunnerEdit, - RedirectLink: setting.AppSubURL + "/-/admin/actions/runners/", - }, nil - } - - if ctx.Data["PageIsUserSettings"] == true { - return &runnersCtx{ - OwnerID: ctx.Doer.ID, - RepoID: 0, - IsUser: true, - RunnersTemplate: tplUserRunners, - RunnerEditTemplate: tplUserRunnerEdit, - RedirectLink: setting.AppSubURL + "/user/settings/actions/runners/", - }, nil - } - - return nil, errors.New("unable to set Runners context") -} - -// Runners render settings/actions/runners page for repo level -func Runners(ctx *context.Context) { - ctx.Data["PageIsSharedSettingsRunners"] = true - ctx.Data["Title"] = ctx.Tr("actions.actions") - ctx.Data["PageType"] = "runners" - - rCtx, err := getRunnersCtx(ctx) - if err != nil { - ctx.ServerError("getRunnersCtx", err) - return - } - - page := ctx.FormInt("page") - if page <= 1 { - page = 1 - } - - opts := actions_model.FindRunnerOptions{ - ListOptions: db.ListOptions{ - Page: page, - PageSize: 100, - }, - Sort: ctx.Req.URL.Query().Get("sort"), - Filter: ctx.Req.URL.Query().Get("q"), - } - if rCtx.IsRepo { - opts.RepoID = rCtx.RepoID - opts.WithAvailable = true - } else if rCtx.IsOrg || rCtx.IsUser { - opts.OwnerID = rCtx.OwnerID - opts.WithAvailable = true - } - actions_shared.RunnersList(ctx, opts) - - ctx.HTML(http.StatusOK, rCtx.RunnersTemplate) -} - -// RunnersEdit renders runner edit page for repository level -func RunnersEdit(ctx *context.Context) { - ctx.Data["PageIsSharedSettingsRunners"] = true - ctx.Data["Title"] = ctx.Tr("actions.runners.edit_runner") - rCtx, err := getRunnersCtx(ctx) - if err != nil { - ctx.ServerError("getRunnersCtx", err) - return - } - - page := ctx.FormInt("page") - if page <= 1 { - page = 1 - } - - actions_shared.RunnerDetails(ctx, page, - ctx.PathParamInt64("runnerid"), rCtx.OwnerID, rCtx.RepoID, - ) - ctx.HTML(http.StatusOK, rCtx.RunnerEditTemplate) -} - -func RunnersEditPost(ctx *context.Context) { - rCtx, err := getRunnersCtx(ctx) - if err != nil { - ctx.ServerError("getRunnersCtx", err) - return - } - actions_shared.RunnerDetailsEditPost(ctx, ctx.PathParamInt64("runnerid"), - rCtx.OwnerID, rCtx.RepoID, - rCtx.RedirectLink+url.PathEscape(ctx.PathParam("runnerid"))) -} - -func ResetRunnerRegistrationToken(ctx *context.Context) { - rCtx, err := getRunnersCtx(ctx) - if err != nil { - ctx.ServerError("getRunnersCtx", err) - return - } - actions_shared.RunnerResetRegistrationToken(ctx, rCtx.OwnerID, rCtx.RepoID, rCtx.RedirectLink) -} - -// RunnerDeletePost response for deleting runner -func RunnerDeletePost(ctx *context.Context) { - rCtx, err := getRunnersCtx(ctx) - if err != nil { - ctx.ServerError("getRunnersCtx", err) - return - } - actions_shared.RunnerDeletePost(ctx, ctx.PathParamInt64("runnerid"), rCtx.RedirectLink, rCtx.RedirectLink+url.PathEscape(ctx.PathParam("runnerid"))) -} - -func RedirectToDefaultSetting(ctx *context.Context) { - ctx.Redirect(ctx.Repo.RepoLink + "/settings/actions/runners") -} diff --git a/routers/web/repo/setting/secrets.go b/routers/web/repo/setting/secrets.go index 46cb875f9bdac..df1172934437d 100644 --- a/routers/web/repo/setting/secrets.go +++ b/routers/web/repo/setting/secrets.go @@ -8,8 +8,8 @@ import ( "net/http" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/templates" shared "code.gitea.io/gitea/routers/web/shared/secrets" shared_user "code.gitea.io/gitea/routers/web/shared/user" "code.gitea.io/gitea/services/context" @@ -17,9 +17,9 @@ import ( const ( // TODO: Separate secrets from runners when layout is ready - tplRepoSecrets templates.TplName = "repo/settings/actions" - tplOrgSecrets templates.TplName = "org/settings/actions" - tplUserSecrets templates.TplName = "user/settings/actions" + tplRepoSecrets base.TplName = "repo/settings/actions" + tplOrgSecrets base.TplName = "org/settings/actions" + tplUserSecrets base.TplName = "user/settings/actions" ) type secretsCtx struct { @@ -28,7 +28,7 @@ type secretsCtx struct { IsRepo bool IsOrg bool IsUser bool - SecretsTemplate templates.TplName + SecretsTemplate base.TplName RedirectLink string } diff --git a/routers/web/repo/setting/setting.go b/routers/web/repo/setting/setting.go index 7399c681e2e9d..d750700ed10dc 100644 --- a/routers/web/repo/setting/setting.go +++ b/routers/web/repo/setting/setting.go @@ -11,13 +11,14 @@ import ( "strings" "time" - actions_model "code.gitea.io/gitea/models/actions" + "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/perm" repo_model "code.gitea.io/gitea/models/repo" unit_model "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/indexer/code" issue_indexer "code.gitea.io/gitea/modules/indexer/issues" @@ -26,7 +27,6 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/structs" - "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/validation" "code.gitea.io/gitea/modules/web" @@ -41,12 +41,12 @@ import ( ) const ( - tplSettingsOptions templates.TplName = "repo/settings/options" - tplCollaboration templates.TplName = "repo/settings/collaboration" - tplBranches templates.TplName = "repo/settings/branches" - tplGithooks templates.TplName = "repo/settings/githooks" - tplGithookEdit templates.TplName = "repo/settings/githook_edit" - tplDeployKeys templates.TplName = "repo/settings/deploy_keys" + tplSettingsOptions base.TplName = "repo/settings/options" + tplCollaboration base.TplName = "repo/settings/collaboration" + tplBranches base.TplName = "repo/settings/branches" + tplGithooks base.TplName = "repo/settings/githooks" + tplGithookEdit base.TplName = "repo/settings/githook_edit" + tplDeployKeys base.TplName = "repo/settings/deploy_keys" ) // SettingsCtxData is a middleware that sets all the general context data for the @@ -222,7 +222,7 @@ func SettingsPost(ctx *context.Context) { form.MirrorPassword, _ = u.User.Password() } - address, err := git.ParseRemoteAddr(form.MirrorAddress, form.MirrorUsername, form.MirrorPassword) + address, err := forms.ParseRemoteAddr(form.MirrorAddress, form.MirrorUsername, form.MirrorPassword) if err == nil { err = migrations.IsMigrateURLAllowed(address, ctx.Doer) } @@ -384,7 +384,7 @@ func SettingsPost(ctx *context.Context) { return } - address, err := git.ParseRemoteAddr(form.PushMirrorAddress, form.PushMirrorUsername, form.PushMirrorPassword) + address, err := forms.ParseRemoteAddr(form.PushMirrorAddress, form.PushMirrorUsername, form.PushMirrorPassword) if err == nil { err = migrations.IsMigrateURLAllowed(address, ctx.Doer) } @@ -787,7 +787,7 @@ func SettingsPost(ctx *context.Context) { if err := repo_service.StartRepositoryTransfer(ctx, ctx.Doer, newOwner, repo, nil); err != nil { if repo_model.IsErrRepoAlreadyExist(err) { ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplSettingsOptions, nil) - } else if repo_model.IsErrRepoTransferInProgress(err) { + } else if models.IsErrRepoTransferInProgress(err) { ctx.RenderWithErr(ctx.Tr("repo.settings.transfer_in_progress"), tplSettingsOptions, nil) } else if errors.Is(err, user_model.ErrBlockedUser) { ctx.RenderWithErr(ctx.Tr("repo.settings.transfer.blocked_user"), tplSettingsOptions, nil) @@ -813,9 +813,9 @@ func SettingsPost(ctx *context.Context) { return } - repoTransfer, err := repo_model.GetPendingRepositoryTransfer(ctx, ctx.Repo.Repository) + repoTransfer, err := models.GetPendingRepositoryTransfer(ctx, ctx.Repo.Repository) if err != nil { - if repo_model.IsErrNoPendingTransfer(err) { + if models.IsErrNoPendingTransfer(err) { ctx.Flash.Error("repo.settings.transfer_abort_invalid") ctx.Redirect(repo.Link() + "/settings") } else { @@ -901,7 +901,7 @@ func SettingsPost(ctx *context.Context) { return } - if err := actions_model.CleanRepoScheduleTasks(ctx, repo); err != nil { + if err := actions_service.CleanRepoScheduleTasks(ctx, repo); err != nil { log.Error("CleanRepoScheduleTasks for archived repo %s/%s: %v", ctx.Repo.Owner.Name, repo.Name, err) } @@ -979,8 +979,8 @@ func SettingsPost(ctx *context.Context) { } func handleSettingRemoteAddrError(ctx *context.Context, err error, form *forms.RepoSettingForm) { - if git.IsErrInvalidCloneAddr(err) { - addrErr := err.(*git.ErrInvalidCloneAddr) + if models.IsErrInvalidCloneAddr(err) { + addrErr := err.(*models.ErrInvalidCloneAddr) switch { case addrErr.IsProtocolInvalid: ctx.RenderWithErr(ctx.Tr("repo.mirror_address_protocol_invalid"), tplSettingsOptions, form) diff --git a/routers/web/repo/setting/variables.go b/routers/web/repo/setting/variables.go deleted file mode 100644 index 9b5453f043649..0000000000000 --- a/routers/web/repo/setting/variables.go +++ /dev/null @@ -1,140 +0,0 @@ -// Copyright 2023 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package setting - -import ( - "errors" - "net/http" - - "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/templates" - shared "code.gitea.io/gitea/routers/web/shared/actions" - shared_user "code.gitea.io/gitea/routers/web/shared/user" - "code.gitea.io/gitea/services/context" -) - -const ( - tplRepoVariables templates.TplName = "repo/settings/actions" - tplOrgVariables templates.TplName = "org/settings/actions" - tplUserVariables templates.TplName = "user/settings/actions" - tplAdminVariables templates.TplName = "admin/actions" -) - -type variablesCtx struct { - OwnerID int64 - RepoID int64 - IsRepo bool - IsOrg bool - IsUser bool - IsGlobal bool - VariablesTemplate templates.TplName - RedirectLink string -} - -func getVariablesCtx(ctx *context.Context) (*variablesCtx, error) { - if ctx.Data["PageIsRepoSettings"] == true { - return &variablesCtx{ - OwnerID: 0, - RepoID: ctx.Repo.Repository.ID, - IsRepo: true, - VariablesTemplate: tplRepoVariables, - RedirectLink: ctx.Repo.RepoLink + "/settings/actions/variables", - }, nil - } - - if ctx.Data["PageIsOrgSettings"] == true { - err := shared_user.LoadHeaderCount(ctx) - if err != nil { - ctx.ServerError("LoadHeaderCount", err) - return nil, nil - } - return &variablesCtx{ - OwnerID: ctx.ContextUser.ID, - RepoID: 0, - IsOrg: true, - VariablesTemplate: tplOrgVariables, - RedirectLink: ctx.Org.OrgLink + "/settings/actions/variables", - }, nil - } - - if ctx.Data["PageIsUserSettings"] == true { - return &variablesCtx{ - OwnerID: ctx.Doer.ID, - RepoID: 0, - IsUser: true, - VariablesTemplate: tplUserVariables, - RedirectLink: setting.AppSubURL + "/user/settings/actions/variables", - }, nil - } - - if ctx.Data["PageIsAdmin"] == true { - return &variablesCtx{ - OwnerID: 0, - RepoID: 0, - IsGlobal: true, - VariablesTemplate: tplAdminVariables, - RedirectLink: setting.AppSubURL + "/-/admin/actions/variables", - }, nil - } - - return nil, errors.New("unable to set Variables context") -} - -func Variables(ctx *context.Context) { - ctx.Data["Title"] = ctx.Tr("actions.variables") - ctx.Data["PageType"] = "variables" - ctx.Data["PageIsSharedSettingsVariables"] = true - - vCtx, err := getVariablesCtx(ctx) - if err != nil { - ctx.ServerError("getVariablesCtx", err) - return - } - - shared.SetVariablesContext(ctx, vCtx.OwnerID, vCtx.RepoID) - if ctx.Written() { - return - } - - ctx.HTML(http.StatusOK, vCtx.VariablesTemplate) -} - -func VariableCreate(ctx *context.Context) { - vCtx, err := getVariablesCtx(ctx) - if err != nil { - ctx.ServerError("getVariablesCtx", err) - return - } - - if ctx.HasError() { // form binding validation error - ctx.JSONError(ctx.GetErrMsg()) - return - } - - shared.CreateVariable(ctx, vCtx.OwnerID, vCtx.RepoID, vCtx.RedirectLink) -} - -func VariableUpdate(ctx *context.Context) { - vCtx, err := getVariablesCtx(ctx) - if err != nil { - ctx.ServerError("getVariablesCtx", err) - return - } - - if ctx.HasError() { // form binding validation error - ctx.JSONError(ctx.GetErrMsg()) - return - } - - shared.UpdateVariable(ctx, vCtx.RedirectLink) -} - -func VariableDelete(ctx *context.Context) { - vCtx, err := getVariablesCtx(ctx) - if err != nil { - ctx.ServerError("getVariablesCtx", err) - return - } - shared.DeleteVariable(ctx, vCtx.RedirectLink) -} diff --git a/routers/web/repo/setting/webhook.go b/routers/web/repo/setting/webhook.go index 1b0ba83af4d5d..c519b4117fc36 100644 --- a/routers/web/repo/setting/webhook.go +++ b/routers/web/repo/setting/webhook.go @@ -17,11 +17,11 @@ import ( access_model "code.gitea.io/gitea/models/perm/access" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/models/webhook" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/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/templates" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" webhook_module "code.gitea.io/gitea/modules/webhook" @@ -32,11 +32,11 @@ import ( ) const ( - tplHooks templates.TplName = "repo/settings/webhook/base" - tplHookNew templates.TplName = "repo/settings/webhook/new" - tplOrgHookNew templates.TplName = "org/settings/hook_new" - tplUserHookNew templates.TplName = "user/settings/hook_new" - tplAdminHookNew templates.TplName = "admin/hook_new" + tplHooks base.TplName = "repo/settings/webhook/base" + tplHookNew base.TplName = "repo/settings/webhook/new" + tplOrgHookNew base.TplName = "org/settings/hook_new" + tplUserHookNew base.TplName = "user/settings/hook_new" + tplAdminHookNew base.TplName = "admin/hook_new" ) // Webhooks render web hooks list page @@ -64,7 +64,7 @@ type ownerRepoCtx struct { IsSystemWebhook bool Link string LinkNew string - NewTemplate templates.TplName + NewTemplate base.TplName } // getOwnerRepoCtx determines whether this is a repo, owner, or admin (both default and system) context. @@ -99,9 +99,9 @@ func getOwnerRepoCtx(ctx *context.Context) (*ownerRepoCtx, error) { if ctx.Data["PageIsAdmin"] == true { return &ownerRepoCtx{ IsAdmin: true, - IsSystemWebhook: ctx.PathParam("configType") == "system-hooks", + IsSystemWebhook: ctx.PathParam(":configType") == "system-hooks", Link: path.Join(setting.AppSubURL, "/-/admin/hooks"), - LinkNew: path.Join(setting.AppSubURL, "/-/admin/", ctx.PathParam("configType")), + LinkNew: path.Join(setting.AppSubURL, "/-/admin/", ctx.PathParam(":configType")), NewTemplate: tplAdminHookNew, }, nil } @@ -110,7 +110,7 @@ func getOwnerRepoCtx(ctx *context.Context) (*ownerRepoCtx, error) { } func checkHookType(ctx *context.Context) string { - hookType := strings.ToLower(ctx.PathParam("type")) + hookType := strings.ToLower(ctx.PathParam(":type")) if !util.SliceContainsString(setting.Webhook.Types, hookType, true) { ctx.NotFound("checkHookType", nil) return "" @@ -184,6 +184,7 @@ func ParseHookEvent(form forms.WebhookForm) *webhook_module.HookEvent { Wiki: form.Wiki, Repository: form.Repository, Package: form.Package, + Status: form.Status, }, BranchFilter: form.BranchFilter, } @@ -592,11 +593,11 @@ func checkWebhook(ctx *context.Context) (*ownerRepoCtx, *webhook.Webhook) { var w *webhook.Webhook if orCtx.RepoID > 0 { - w, err = webhook.GetWebhookByRepoID(ctx, orCtx.RepoID, ctx.PathParamInt64("id")) + w, err = webhook.GetWebhookByRepoID(ctx, orCtx.RepoID, ctx.PathParamInt64(":id")) } else if orCtx.OwnerID > 0 { - w, err = webhook.GetWebhookByOwnerID(ctx, orCtx.OwnerID, ctx.PathParamInt64("id")) + w, err = webhook.GetWebhookByOwnerID(ctx, orCtx.OwnerID, ctx.PathParamInt64(":id")) } else if orCtx.IsAdmin { - w, err = webhook.GetSystemOrDefaultWebhook(ctx, ctx.PathParamInt64("id")) + w, err = webhook.GetSystemOrDefaultWebhook(ctx, ctx.PathParamInt64(":id")) } if err != nil || w == nil { if webhook.IsErrWebhookNotExist(err) { @@ -645,7 +646,7 @@ func WebHooksEdit(ctx *context.Context) { // TestWebhook test if web hook is work fine func TestWebhook(ctx *context.Context) { - hookID := ctx.PathParamInt64("id") + hookID := ctx.PathParamInt64(":id") w, err := webhook.GetWebhookByRepoID(ctx, ctx.Repo.Repository.ID, hookID) if err != nil { ctx.Flash.Error("GetWebhookByRepoID: " + err.Error()) @@ -706,7 +707,7 @@ func TestWebhook(ctx *context.Context) { // ReplayWebhook replays a webhook func ReplayWebhook(ctx *context.Context) { - hookTaskUUID := ctx.PathParam("uuid") + hookTaskUUID := ctx.PathParam(":uuid") orCtx, w := checkWebhook(ctx) if ctx.Written() { diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go index 14fc9038f3e6b..e43841acd365c 100644 --- a/routers/web/repo/view.go +++ b/routers/web/repo/view.go @@ -35,7 +35,6 @@ import ( "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/structs" - "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/typesniffer" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/services/context" @@ -46,12 +45,12 @@ import ( ) const ( - tplRepoEMPTY templates.TplName = "repo/empty" - tplRepoHome templates.TplName = "repo/home" - tplRepoViewList templates.TplName = "repo/view_list" - tplWatchers templates.TplName = "repo/watchers" - tplForks templates.TplName = "repo/forks" - tplMigrating templates.TplName = "repo/migrate/migrating" + tplRepoEMPTY base.TplName = "repo/empty" + tplRepoHome base.TplName = "repo/home" + tplRepoViewList base.TplName = "repo/view_list" + tplWatchers base.TplName = "repo/watchers" + tplForks base.TplName = "repo/forks" + tplMigrating base.TplName = "repo/migrate/migrating" ) type fileInfo struct { @@ -315,7 +314,7 @@ func renderDirectoryFiles(ctx *context.Context, timeout time.Duration) git.Entri } // RenderUserCards render a page show users according the input template -func RenderUserCards(ctx *context.Context, total int, getter func(opts db.ListOptions) ([]*user_model.User, error), tpl templates.TplName) { +func RenderUserCards(ctx *context.Context, total int, getter func(opts db.ListOptions) ([]*user_model.User, error), tpl base.TplName) { page := ctx.FormInt("page") if page <= 0 { page = 1 diff --git a/routers/web/repo/view_file.go b/routers/web/repo/view_file.go index 17c2821824339..63a117b60c5ca 100644 --- a/routers/web/repo/view_file.go +++ b/routers/web/repo/view_file.go @@ -9,7 +9,6 @@ import ( "image" "io" "path" - "slices" "strings" git_model "code.gitea.io/gitea/models/git" @@ -79,7 +78,7 @@ func prepareToRenderFile(ctx *context.Context, entry *git.TreeEntry) { if workFlowErr != nil { ctx.Data["FileError"] = ctx.Locale.Tr("actions.runs.invalid_workflow_helper", workFlowErr.Error()) } - } else if slices.Contains([]string{"CODEOWNERS", "docs/CODEOWNERS", ".gitea/CODEOWNERS"}, ctx.Repo.TreePath) { + } else if issue_service.IsCodeOwnerFile(ctx.Repo.TreePath) { if data, err := blob.GetBlobContent(setting.UI.MaxDisplayFileSize); err == nil { _, warnings := issue_model.GetCodeOwnersFromContent(ctx, data) if len(warnings) > 0 { diff --git a/routers/web/repo/view_home.go b/routers/web/repo/view_home.go index 70ba07f9a89a3..d7a393c5cd3b4 100644 --- a/routers/web/repo/view_home.go +++ b/routers/web/repo/view_home.go @@ -215,24 +215,67 @@ func prepareRecentlyPushedNewBranches(ctx *context.Context) { if !opts.Repo.IsMirror && !opts.BaseRepo.IsMirror && opts.BaseRepo.UnitEnabled(ctx, unit_model.TypePullRequests) && baseRepoPerm.CanRead(unit_model.TypePullRequests) { - ctx.Data["RecentlyPushedNewBranches"], err = git_model.FindRecentlyPushedNewBranches(ctx, ctx.Doer, opts) + var finalBranches []*git_model.RecentlyPushedNewBranch + branches, err := git_model.FindRecentlyPushedNewBranches(ctx, ctx.Doer, opts) if err != nil { log.Error("FindRecentlyPushedNewBranches failed: %v", err) } + + for _, branch := range branches { + divergingInfo, err := repo_service.GetBranchDivergingInfo(ctx, + branch.BranchRepo, branch.BranchName, // "base" repo for diverging info + opts.BaseRepo, opts.BaseRepo.DefaultBranch, // "head" repo for diverging info + ) + if err != nil { + log.Error("GetBranchDivergingInfo failed: %v", err) + continue + } + branchRepoHasNewCommits := divergingInfo.BaseHasNewCommits + baseRepoCommitsBehind := divergingInfo.HeadCommitsBehind + if branchRepoHasNewCommits || baseRepoCommitsBehind > 0 { + finalBranches = append(finalBranches, branch) + } + } + ctx.Data["RecentlyPushedNewBranches"] = finalBranches } } } +func updateContextRepoEmptyAndStatus(ctx *context.Context, empty bool, status repo_model.RepositoryStatus) { + if ctx.Repo.Repository.IsEmpty == empty && ctx.Repo.Repository.Status == status { + return + } + ctx.Repo.Repository.IsEmpty = empty + if ctx.Repo.Repository.Status == repo_model.RepositoryReady || ctx.Repo.Repository.Status == repo_model.RepositoryBroken { + ctx.Repo.Repository.Status = status // only handle ready and broken status, leave other status as-is + } + if err := repo_model.UpdateRepositoryColsNoAutoTime(ctx, ctx.Repo.Repository, "is_empty", "status"); err != nil { + ctx.ServerError("updateContextRepoEmptyAndStatus: UpdateRepositoryCols", err) + return + } +} + func handleRepoEmptyOrBroken(ctx *context.Context) { showEmpty := true - var err error if ctx.Repo.GitRepo != nil { - showEmpty, err = ctx.Repo.GitRepo.IsEmpty() + reallyEmpty, err := ctx.Repo.GitRepo.IsEmpty() if err != nil { + showEmpty = true // the repo is broken + updateContextRepoEmptyAndStatus(ctx, true, repo_model.RepositoryBroken) log.Error("GitRepo.IsEmpty: %v", err) - ctx.Repo.Repository.Status = repo_model.RepositoryBroken - showEmpty = true ctx.Flash.Error(ctx.Tr("error.occurred"), true) + } else if reallyEmpty { + showEmpty = true // the repo is really empty + updateContextRepoEmptyAndStatus(ctx, true, repo_model.RepositoryReady) + } else if branches, _, _ := ctx.Repo.GitRepo.GetBranches(0, 1); len(branches) == 0 { + showEmpty = true // it is not really empty, but there is no branch + // at the moment, other repo units like "actions" are not able to handle such case, + // so we just mark the repo as empty to prevent from displaying these units. + ctx.Data["RepoHasContentsWithoutBranch"] = true + updateContextRepoEmptyAndStatus(ctx, true, repo_model.RepositoryReady) + } else { + // the repo is actually not empty and has branches, need to update the database later + showEmpty = false } } if showEmpty { @@ -240,18 +283,11 @@ func handleRepoEmptyOrBroken(ctx *context.Context) { return } - // the repo is not really empty, so we should update the modal in database - // such problem may be caused by: - // 1) an error occurs during pushing/receiving. 2) the user replaces an empty git repo manually - // and even more: the IsEmpty flag is deeply broken and should be removed with the UI changed to manage to cope with empty repos. - // it's possible for a repository to be non-empty by that flag but still 500 - // because there are no branches - only tags -or the default branch is non-extant as it has been 0-pushed. - ctx.Repo.Repository.IsEmpty = false - if err = repo_model.UpdateRepositoryCols(ctx, ctx.Repo.Repository, "is_empty"); err != nil { - ctx.ServerError("UpdateRepositoryCols", err) - return - } - if err = repo_module.UpdateRepoSize(ctx, ctx.Repo.Repository); err != nil { + // The repo is not really empty, so we should update the model in database, such problem may be caused by: + // 1) an error occurs during pushing/receiving. + // 2) the user replaces an empty git repo manually. + updateContextRepoEmptyAndStatus(ctx, false, repo_model.RepositoryReady) + if err := repo_module.UpdateRepoSize(ctx, ctx.Repo.Repository); err != nil { ctx.ServerError("UpdateRepoSize", err) return } @@ -276,7 +312,7 @@ func prepareToRenderDirOrFile(entry *git.TreeEntry) func(ctx *context.Context) { func handleRepoHomeFeed(ctx *context.Context) bool { if setting.Other.EnableFeed { - isFeed, _, showFeedType := feed.GetFeedType(ctx.PathParam("reponame"), ctx.Req) + isFeed, _, showFeedType := feed.GetFeedType(ctx.PathParam(":reponame"), ctx.Req) if isFeed { switch { case ctx.Link == fmt.Sprintf("%s.%s", ctx.Repo.RepoLink, showFeedType): diff --git a/routers/web/repo/wiki.go b/routers/web/repo/wiki.go index 3fca7cebea525..fec72c9253250 100644 --- a/routers/web/repo/wiki.go +++ b/routers/web/repo/wiki.go @@ -26,7 +26,6 @@ import ( "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/markup/markdown" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" @@ -38,11 +37,11 @@ import ( ) const ( - tplWikiStart templates.TplName = "repo/wiki/start" - tplWikiView templates.TplName = "repo/wiki/view" - tplWikiRevision templates.TplName = "repo/wiki/revision" - tplWikiNew templates.TplName = "repo/wiki/new" - tplWikiPages templates.TplName = "repo/wiki/pages" + tplWikiStart base.TplName = "repo/wiki/start" + tplWikiView base.TplName = "repo/wiki/view" + tplWikiRevision base.TplName = "repo/wiki/revision" + tplWikiNew base.TplName = "repo/wiki/new" + tplWikiPages base.TplName = "repo/wiki/pages" ) // MustEnableWiki check if wiki is enabled, if external then redirect diff --git a/routers/web/shared/actions/runners.go b/routers/web/shared/actions/runners.go index 6d77bdd2fab74..2266232ebe976 100644 --- a/routers/web/shared/actions/runners.go +++ b/routers/web/shared/actions/runners.go @@ -5,18 +5,131 @@ package actions import ( "errors" + "net/http" + "net/url" actions_model "code.gitea.io/gitea/models/actions" "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" + shared_user "code.gitea.io/gitea/routers/web/shared/user" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/forms" ) -// RunnersList prepares data for runners list -func RunnersList(ctx *context.Context, opts actions_model.FindRunnerOptions) { +const ( + // TODO: Separate secrets from runners when layout is ready + tplRepoRunners base.TplName = "repo/settings/actions" + tplOrgRunners base.TplName = "org/settings/actions" + tplAdminRunners base.TplName = "admin/actions" + tplUserRunners base.TplName = "user/settings/actions" + tplRepoRunnerEdit base.TplName = "repo/settings/runner_edit" + tplOrgRunnerEdit base.TplName = "org/settings/runners_edit" + tplAdminRunnerEdit base.TplName = "admin/runners/edit" + tplUserRunnerEdit base.TplName = "user/settings/runner_edit" +) + +type runnersCtx struct { + OwnerID int64 + RepoID int64 + IsRepo bool + IsOrg bool + IsAdmin bool + IsUser bool + RunnersTemplate base.TplName + RunnerEditTemplate base.TplName + RedirectLink string +} + +func getRunnersCtx(ctx *context.Context) (*runnersCtx, error) { + if ctx.Data["PageIsRepoSettings"] == true { + return &runnersCtx{ + RepoID: ctx.Repo.Repository.ID, + OwnerID: 0, + IsRepo: true, + RunnersTemplate: tplRepoRunners, + RunnerEditTemplate: tplRepoRunnerEdit, + RedirectLink: ctx.Repo.RepoLink + "/settings/actions/runners/", + }, nil + } + + if ctx.Data["PageIsOrgSettings"] == true { + err := shared_user.LoadHeaderCount(ctx) + if err != nil { + ctx.ServerError("LoadHeaderCount", err) + return nil, nil + } + return &runnersCtx{ + RepoID: 0, + OwnerID: ctx.Org.Organization.ID, + IsOrg: true, + RunnersTemplate: tplOrgRunners, + RunnerEditTemplate: tplOrgRunnerEdit, + RedirectLink: ctx.Org.OrgLink + "/settings/actions/runners/", + }, nil + } + + if ctx.Data["PageIsAdmin"] == true { + return &runnersCtx{ + RepoID: 0, + OwnerID: 0, + IsAdmin: true, + RunnersTemplate: tplAdminRunners, + RunnerEditTemplate: tplAdminRunnerEdit, + RedirectLink: setting.AppSubURL + "/-/admin/actions/runners/", + }, nil + } + + if ctx.Data["PageIsUserSettings"] == true { + return &runnersCtx{ + OwnerID: ctx.Doer.ID, + RepoID: 0, + IsUser: true, + RunnersTemplate: tplUserRunners, + RunnerEditTemplate: tplUserRunnerEdit, + RedirectLink: setting.AppSubURL + "/user/settings/actions/runners/", + }, nil + } + + return nil, errors.New("unable to set Runners context") +} + +// Runners render settings/actions/runners page for repo level +func Runners(ctx *context.Context) { + ctx.Data["PageIsSharedSettingsRunners"] = true + ctx.Data["Title"] = ctx.Tr("actions.actions") + ctx.Data["PageType"] = "runners" + + rCtx, err := getRunnersCtx(ctx) + if err != nil { + ctx.ServerError("getRunnersCtx", err) + return + } + + page := ctx.FormInt("page") + if page <= 1 { + page = 1 + } + + opts := actions_model.FindRunnerOptions{ + ListOptions: db.ListOptions{ + Page: page, + PageSize: 100, + }, + Sort: ctx.Req.URL.Query().Get("sort"), + Filter: ctx.Req.URL.Query().Get("q"), + } + if rCtx.IsRepo { + opts.RepoID = rCtx.RepoID + opts.WithAvailable = true + } else if rCtx.IsOrg || rCtx.IsUser { + opts.OwnerID = rCtx.OwnerID + opts.WithAvailable = true + } + runners, count, err := db.FindAndCount[actions_model.ActionRunner](ctx, opts) if err != nil { ctx.ServerError("CountRunners", err) @@ -53,10 +166,29 @@ func RunnersList(ctx *context.Context, opts actions_model.FindRunnerOptions) { pager := context.NewPagination(int(count), opts.PageSize, opts.Page, 5) ctx.Data["Page"] = pager + + ctx.HTML(http.StatusOK, rCtx.RunnersTemplate) } -// RunnerDetails prepares data for runners edit page -func RunnerDetails(ctx *context.Context, page int, runnerID, ownerID, repoID int64) { +// RunnersEdit renders runner edit page for repository level +func RunnersEdit(ctx *context.Context) { + ctx.Data["PageIsSharedSettingsRunners"] = true + ctx.Data["Title"] = ctx.Tr("actions.runners.edit_runner") + rCtx, err := getRunnersCtx(ctx) + if err != nil { + ctx.ServerError("getRunnersCtx", err) + return + } + + page := ctx.FormInt("page") + if page <= 1 { + page = 1 + } + + runnerID := ctx.PathParamInt64("runnerid") + ownerID := rCtx.OwnerID + repoID := rCtx.RepoID + runner, err := actions_model.GetRunnerByID(ctx, runnerID) if err != nil { ctx.ServerError("GetRunnerByID", err) @@ -97,10 +229,22 @@ func RunnerDetails(ctx *context.Context, page int, runnerID, ownerID, repoID int ctx.Data["Tasks"] = tasks pager := context.NewPagination(int(count), opts.PageSize, opts.Page, 5) ctx.Data["Page"] = pager + + ctx.HTML(http.StatusOK, rCtx.RunnerEditTemplate) } -// RunnerDetailsEditPost response for edit runner details -func RunnerDetailsEditPost(ctx *context.Context, runnerID, ownerID, repoID int64, redirectTo string) { +func RunnersEditPost(ctx *context.Context) { + rCtx, err := getRunnersCtx(ctx) + if err != nil { + ctx.ServerError("getRunnersCtx", err) + return + } + + runnerID := ctx.PathParamInt64("runnerid") + ownerID := rCtx.OwnerID + repoID := rCtx.RepoID + redirectTo := rCtx.RedirectLink + runner, err := actions_model.GetRunnerByID(ctx, runnerID) if err != nil { log.Warn("RunnerDetailsEditPost.GetRunnerByID failed: %v, url: %s", err, ctx.Req.URL) @@ -129,10 +273,18 @@ func RunnerDetailsEditPost(ctx *context.Context, runnerID, ownerID, repoID int64 ctx.Redirect(redirectTo) } -// RunnerResetRegistrationToken reset registration token -func RunnerResetRegistrationToken(ctx *context.Context, ownerID, repoID int64, redirectTo string) { - _, err := actions_model.NewRunnerToken(ctx, ownerID, repoID) +func ResetRunnerRegistrationToken(ctx *context.Context) { + rCtx, err := getRunnersCtx(ctx) if err != nil { + ctx.ServerError("getRunnersCtx", err) + return + } + + ownerID := rCtx.OwnerID + repoID := rCtx.RepoID + redirectTo := rCtx.RedirectLink + + if _, err := actions_model.NewRunnerToken(ctx, ownerID, repoID); err != nil { ctx.ServerError("ResetRunnerRegistrationToken", err) return } @@ -140,11 +292,28 @@ func RunnerResetRegistrationToken(ctx *context.Context, ownerID, repoID int64, r ctx.JSONRedirect(redirectTo) } -// RunnerDeletePost response for deleting a runner -func RunnerDeletePost(ctx *context.Context, runnerID int64, - successRedirectTo, failedRedirectTo string, -) { - if err := actions_model.DeleteRunner(ctx, runnerID); err != nil { +// RunnerDeletePost response for deleting runner +func RunnerDeletePost(ctx *context.Context) { + rCtx, err := getRunnersCtx(ctx) + if err != nil { + ctx.ServerError("getRunnersCtx", err) + return + } + + runner := findActionsRunner(ctx, rCtx) + if ctx.Written() { + return + } + + if !runner.Editable(rCtx.OwnerID, rCtx.RepoID) { + ctx.NotFound("RunnerDeletePost", util.NewPermissionDeniedErrorf("no permission to delete this runner")) + return + } + + successRedirectTo := rCtx.RedirectLink + failedRedirectTo := rCtx.RedirectLink + url.PathEscape(ctx.PathParam("runnerid")) + + if err := actions_model.DeleteRunner(ctx, runner.ID); err != nil { log.Warn("DeleteRunnerPost.UpdateRunner failed: %v, url: %s", err, ctx.Req.URL) ctx.Flash.Warning(ctx.Tr("actions.runners.delete_runner_failed")) @@ -158,3 +327,41 @@ func RunnerDeletePost(ctx *context.Context, runnerID int64, ctx.JSONRedirect(successRedirectTo) } + +func RedirectToDefaultSetting(ctx *context.Context) { + ctx.Redirect(ctx.Repo.RepoLink + "/settings/actions/runners") +} + +func findActionsRunner(ctx *context.Context, rCtx *runnersCtx) *actions_model.ActionRunner { + runnerID := ctx.PathParamInt64("runnerid") + opts := &actions_model.FindRunnerOptions{ + IDs: []int64{runnerID}, + } + switch { + case rCtx.IsRepo: + opts.RepoID = rCtx.RepoID + if opts.RepoID == 0 { + panic("repoID is 0") + } + case rCtx.IsOrg, rCtx.IsUser: + opts.OwnerID = rCtx.OwnerID + if opts.OwnerID == 0 { + panic("ownerID is 0") + } + case rCtx.IsAdmin: + // do nothing + default: + panic("invalid actions runner context") + } + + got, err := db.Find[actions_model.ActionRunner](ctx, opts) + if err != nil { + ctx.ServerError("FindRunner", err) + return nil + } else if len(got) == 0 { + ctx.NotFound("FindRunner", errors.New("runner not found")) + return nil + } + + return got[0] +} diff --git a/routers/web/shared/actions/variables.go b/routers/web/shared/actions/variables.go index f895475748a5b..dfbd5f00eb41e 100644 --- a/routers/web/shared/actions/variables.go +++ b/routers/web/shared/actions/variables.go @@ -4,31 +4,127 @@ package actions import ( + "errors" + "net/http" + actions_model "code.gitea.io/gitea/models/actions" "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/web" + shared_user "code.gitea.io/gitea/routers/web/shared/user" actions_service "code.gitea.io/gitea/services/actions" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/forms" ) -func SetVariablesContext(ctx *context.Context, ownerID, repoID int64) { +const ( + tplRepoVariables base.TplName = "repo/settings/actions" + tplOrgVariables base.TplName = "org/settings/actions" + tplUserVariables base.TplName = "user/settings/actions" + tplAdminVariables base.TplName = "admin/actions" +) + +type variablesCtx struct { + OwnerID int64 + RepoID int64 + IsRepo bool + IsOrg bool + IsUser bool + IsGlobal bool + VariablesTemplate base.TplName + RedirectLink string +} + +func getVariablesCtx(ctx *context.Context) (*variablesCtx, error) { + if ctx.Data["PageIsRepoSettings"] == true { + return &variablesCtx{ + OwnerID: 0, + RepoID: ctx.Repo.Repository.ID, + IsRepo: true, + VariablesTemplate: tplRepoVariables, + RedirectLink: ctx.Repo.RepoLink + "/settings/actions/variables", + }, nil + } + + if ctx.Data["PageIsOrgSettings"] == true { + err := shared_user.LoadHeaderCount(ctx) + if err != nil { + ctx.ServerError("LoadHeaderCount", err) + return nil, nil + } + return &variablesCtx{ + OwnerID: ctx.ContextUser.ID, + RepoID: 0, + IsOrg: true, + VariablesTemplate: tplOrgVariables, + RedirectLink: ctx.Org.OrgLink + "/settings/actions/variables", + }, nil + } + + if ctx.Data["PageIsUserSettings"] == true { + return &variablesCtx{ + OwnerID: ctx.Doer.ID, + RepoID: 0, + IsUser: true, + VariablesTemplate: tplUserVariables, + RedirectLink: setting.AppSubURL + "/user/settings/actions/variables", + }, nil + } + + if ctx.Data["PageIsAdmin"] == true { + return &variablesCtx{ + OwnerID: 0, + RepoID: 0, + IsGlobal: true, + VariablesTemplate: tplAdminVariables, + RedirectLink: setting.AppSubURL + "/-/admin/actions/variables", + }, nil + } + + return nil, errors.New("unable to set Variables context") +} + +func Variables(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("actions.variables") + ctx.Data["PageType"] = "variables" + ctx.Data["PageIsSharedSettingsVariables"] = true + + vCtx, err := getVariablesCtx(ctx) + if err != nil { + ctx.ServerError("getVariablesCtx", err) + return + } + variables, err := db.Find[actions_model.ActionVariable](ctx, actions_model.FindVariablesOpts{ - OwnerID: ownerID, - RepoID: repoID, + OwnerID: vCtx.OwnerID, + RepoID: vCtx.RepoID, }) if err != nil { ctx.ServerError("FindVariables", err) return } ctx.Data["Variables"] = variables + + ctx.HTML(http.StatusOK, vCtx.VariablesTemplate) } -func CreateVariable(ctx *context.Context, ownerID, repoID int64, redirectURL string) { +func VariableCreate(ctx *context.Context) { + vCtx, err := getVariablesCtx(ctx) + if err != nil { + ctx.ServerError("getVariablesCtx", err) + return + } + + if ctx.HasError() { // form binding validation error + ctx.JSONError(ctx.GetErrMsg()) + return + } + form := web.GetForm(ctx).(*forms.EditVariableForm) - v, err := actions_service.CreateVariable(ctx, ownerID, repoID, form.Name, form.Data) + v, err := actions_service.CreateVariable(ctx, vCtx.OwnerID, vCtx.RepoID, form.Name, form.Data) if err != nil { log.Error("CreateVariable: %v", err) ctx.JSONError(ctx.Tr("actions.variables.creation.failed")) @@ -36,30 +132,92 @@ func CreateVariable(ctx *context.Context, ownerID, repoID int64, redirectURL str } ctx.Flash.Success(ctx.Tr("actions.variables.creation.success", v.Name)) - ctx.JSONRedirect(redirectURL) + ctx.JSONRedirect(vCtx.RedirectLink) } -func UpdateVariable(ctx *context.Context, redirectURL string) { +func VariableUpdate(ctx *context.Context) { + vCtx, err := getVariablesCtx(ctx) + if err != nil { + ctx.ServerError("getVariablesCtx", err) + return + } + + if ctx.HasError() { // form binding validation error + ctx.JSONError(ctx.GetErrMsg()) + return + } + id := ctx.PathParamInt64("variable_id") + + variable := findActionsVariable(ctx, id, vCtx) + if ctx.Written() { + return + } + form := web.GetForm(ctx).(*forms.EditVariableForm) + variable.Name = form.Name + variable.Data = form.Data - if ok, err := actions_service.UpdateVariable(ctx, id, form.Name, form.Data); err != nil || !ok { + if ok, err := actions_service.UpdateVariableNameData(ctx, variable); err != nil || !ok { log.Error("UpdateVariable: %v", err) ctx.JSONError(ctx.Tr("actions.variables.update.failed")) return } ctx.Flash.Success(ctx.Tr("actions.variables.update.success")) - ctx.JSONRedirect(redirectURL) + ctx.JSONRedirect(vCtx.RedirectLink) } -func DeleteVariable(ctx *context.Context, redirectURL string) { +func findActionsVariable(ctx *context.Context, id int64, vCtx *variablesCtx) *actions_model.ActionVariable { + opts := actions_model.FindVariablesOpts{ + IDs: []int64{id}, + } + switch { + case vCtx.IsRepo: + opts.RepoID = vCtx.RepoID + if opts.RepoID == 0 { + panic("RepoID is 0") + } + case vCtx.IsOrg, vCtx.IsUser: + opts.OwnerID = vCtx.OwnerID + if opts.OwnerID == 0 { + panic("OwnerID is 0") + } + case vCtx.IsGlobal: + // do nothing + default: + panic("invalid actions variable") + } + + got, err := actions_model.FindVariables(ctx, opts) + if err != nil { + ctx.ServerError("FindVariables", err) + return nil + } else if len(got) == 0 { + ctx.NotFound("FindVariables", nil) + return nil + } + return got[0] +} + +func VariableDelete(ctx *context.Context) { + vCtx, err := getVariablesCtx(ctx) + if err != nil { + ctx.ServerError("getVariablesCtx", err) + return + } + id := ctx.PathParamInt64("variable_id") - if err := actions_service.DeleteVariableByID(ctx, id); err != nil { + variable := findActionsVariable(ctx, id, vCtx) + if ctx.Written() { + return + } + + if err := actions_service.DeleteVariableByID(ctx, variable.ID); err != nil { log.Error("Delete variable [%d] failed: %v", id, err) ctx.JSONError(ctx.Tr("actions.variables.deletion.failed")) return } ctx.Flash.Success(ctx.Tr("actions.variables.deletion.success")) - ctx.JSONRedirect(redirectURL) + ctx.JSONRedirect(vCtx.RedirectLink) } diff --git a/routers/web/shared/packages/packages.go b/routers/web/shared/packages/packages.go index a1bcb09850698..1d3cabf71b4ec 100644 --- a/routers/web/shared/packages/packages.go +++ b/routers/web/shared/packages/packages.go @@ -11,9 +11,9 @@ import ( "code.gitea.io/gitea/models/db" packages_model "code.gitea.io/gitea/models/packages" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/optional" - "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/forms" @@ -54,11 +54,11 @@ func setRuleEditContext(ctx *context.Context, pcr *packages_model.PackageCleanup ctx.Data["AvailableTypes"] = packages_model.TypeList } -func PerformRuleAddPost(ctx *context.Context, owner *user_model.User, redirectURL string, template templates.TplName) { +func PerformRuleAddPost(ctx *context.Context, owner *user_model.User, redirectURL string, template base.TplName) { performRuleEditPost(ctx, owner, nil, redirectURL, template) } -func PerformRuleEditPost(ctx *context.Context, owner *user_model.User, redirectURL string, template templates.TplName) { +func PerformRuleEditPost(ctx *context.Context, owner *user_model.User, redirectURL string, template base.TplName) { pcr := getCleanupRuleByContext(ctx, owner) if pcr == nil { return @@ -79,7 +79,7 @@ func PerformRuleEditPost(ctx *context.Context, owner *user_model.User, redirectU } } -func performRuleEditPost(ctx *context.Context, owner *user_model.User, pcr *packages_model.PackageCleanupRule, redirectURL string, template templates.TplName) { +func performRuleEditPost(ctx *context.Context, owner *user_model.User, pcr *packages_model.PackageCleanupRule, redirectURL string, template base.TplName) { isEditRule := pcr != nil if pcr == nil { diff --git a/routers/web/shared/project/column.go b/routers/web/shared/project/column.go index 141c80716f541..ba1f527beaafe 100644 --- a/routers/web/shared/project/column.go +++ b/routers/web/shared/project/column.go @@ -11,7 +11,7 @@ import ( // MoveColumns moves or keeps columns in a project and sorts them inside that project func MoveColumns(ctx *context.Context) { - project, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64("id")) + project, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64(":id")) if err != nil { ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err) return diff --git a/routers/web/shared/user/header.go b/routers/web/shared/user/header.go index d388d2b5d9a6c..4cb0592b4b782 100644 --- a/routers/web/shared/user/header.go +++ b/routers/web/shared/user/header.go @@ -61,20 +61,11 @@ func PrepareContextForProfileBigAvatar(ctx *context.Context) { orgs, err := db.Find[organization.Organization](ctx, organization.FindOrgOptions{ UserID: ctx.ContextUser.ID, IncludePrivate: showPrivate, - ListOptions: db.ListOptions{ - Page: 1, - // query one more results (without a separate counting) to see whether we need to add the "show more orgs" link - PageSize: setting.UI.User.OrgPagingNum + 1, - }, }) if err != nil { ctx.ServerError("FindOrgs", err) return } - if len(orgs) > setting.UI.User.OrgPagingNum { - orgs = orgs[:setting.UI.User.OrgPagingNum] - ctx.Data["ShowMoreOrgs"] = true - } ctx.Data["Orgs"] = orgs ctx.Data["HasOrgsVisible"] = organization.HasOrgsVisible(ctx, orgs, ctx.Doer) diff --git a/routers/web/user/avatar.go b/routers/web/user/avatar.go index f77bd602b3d7f..81c00b3bd42d1 100644 --- a/routers/web/user/avatar.go +++ b/routers/web/user/avatar.go @@ -4,7 +4,6 @@ package user import ( - "strings" "time" "code.gitea.io/gitea/models/avatars" @@ -21,27 +20,18 @@ func cacheableRedirect(ctx *context.Context, location string) { ctx.Redirect(location) } -// AvatarByUserName redirect browser to user avatar of requested size -func AvatarByUserName(ctx *context.Context) { - userName := ctx.PathParam("username") - size := int(ctx.PathParamInt64("size")) - - var user *user_model.User - if strings.ToLower(userName) != user_model.GhostUserLowerName { +// AvatarByUsernameSize redirect browser to user avatar of requested size +func AvatarByUsernameSize(ctx *context.Context) { + username := ctx.PathParam("username") + user := user_model.GetSystemUserByName(username) + if user == nil { var err error - if user, err = user_model.GetUserByName(ctx, userName); err != nil { - if user_model.IsErrUserNotExist(err) { - ctx.NotFound("GetUserByName", err) - return - } - ctx.ServerError("Invalid user: "+userName, err) + if user, err = user_model.GetUserByName(ctx, username); err != nil { + ctx.NotFoundOrServerError("GetUserByName", user_model.IsErrUserNotExist, err) return } - } else { - user = user_model.NewGhostUser() } - - cacheableRedirect(ctx, user.AvatarLinkWithSize(ctx, size)) + cacheableRedirect(ctx, user.AvatarLinkWithSize(ctx, int(ctx.PathParamInt64("size")))) } // AvatarByEmailHash redirects the browser to the email avatar link diff --git a/routers/web/user/code.go b/routers/web/user/code.go index f805f2b028172..8f4b4b2d9ac7c 100644 --- a/routers/web/user/code.go +++ b/routers/web/user/code.go @@ -8,15 +8,16 @@ import ( "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/modules/base" code_indexer "code.gitea.io/gitea/modules/indexer/code" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/templates" + "code.gitea.io/gitea/routers/common" shared_user "code.gitea.io/gitea/routers/web/shared/user" "code.gitea.io/gitea/services/context" ) const ( - tplUserCode templates.TplName = "user/code" + tplUserCode base.TplName = "user/code" ) // CodeSearch render user/organization code search page @@ -34,20 +35,11 @@ func CodeSearch(ctx *context.Context) { } ctx.Data["IsPackageEnabled"] = setting.Packages.Enabled - ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled ctx.Data["Title"] = ctx.Tr("explore.code") - - language := ctx.FormTrim("l") - keyword := ctx.FormTrim("q") - - isFuzzy := ctx.FormOptionalBool("fuzzy").ValueOrDefault(true) - - ctx.Data["Keyword"] = keyword - ctx.Data["Language"] = language - ctx.Data["IsFuzzy"] = isFuzzy ctx.Data["IsCodePage"] = true - if keyword == "" { + prepareSearch := common.PrepareCodeSearch(ctx) + if prepareSearch.Keyword == "" { ctx.HTML(http.StatusOK, tplUserCode) return } @@ -77,9 +69,9 @@ func CodeSearch(ctx *context.Context) { if len(repoIDs) > 0 { total, searchResults, searchResultLanguages, err = code_indexer.PerformSearch(ctx, &code_indexer.SearchOptions{ RepoIDs: repoIDs, - Keyword: keyword, - IsKeywordFuzzy: isFuzzy, - Language: language, + Keyword: prepareSearch.Keyword, + IsKeywordFuzzy: prepareSearch.IsFuzzy, + Language: prepareSearch.Language, Paginator: &db.ListOptions{ Page: page, PageSize: setting.UI.RepoSearchPagingNum, @@ -122,7 +114,7 @@ func CodeSearch(ctx *context.Context) { pager := context.NewPagination(total, setting.UI.RepoSearchPagingNum, page, 5) pager.SetDefaultParams(ctx) - pager.AddParamString("l", language) + pager.AddParamString("l", prepareSearch.Language) ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, tplUserCode) diff --git a/routers/web/user/home.go b/routers/web/user/home.go index b5d3a7294bad1..c14bb2f28d52d 100644 --- a/routers/web/user/home.go +++ b/routers/web/user/home.go @@ -31,7 +31,6 @@ import ( "code.gitea.io/gitea/modules/markup/markdown" "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/routers/web/feed" "code.gitea.io/gitea/routers/web/shared/issue" @@ -41,22 +40,22 @@ import ( issue_service "code.gitea.io/gitea/services/issue" pull_service "code.gitea.io/gitea/services/pull" - "github.com/keybase/go-crypto/openpgp" - "github.com/keybase/go-crypto/openpgp/armor" + "github.com/ProtonMail/go-crypto/openpgp" + "github.com/ProtonMail/go-crypto/openpgp/armor" "xorm.io/builder" ) const ( - tplDashboard templates.TplName = "user/dashboard/dashboard" - tplIssues templates.TplName = "user/dashboard/issues" - tplMilestones templates.TplName = "user/dashboard/milestones" - tplProfile templates.TplName = "user/profile" + tplDashboard base.TplName = "user/dashboard/dashboard" + tplIssues base.TplName = "user/dashboard/issues" + tplMilestones base.TplName = "user/dashboard/milestones" + tplProfile base.TplName = "user/profile" ) // getDashboardContextUser finds out which context user dashboard is being viewed as . func getDashboardContextUser(ctx *context.Context) *user_model.User { ctxUser := ctx.Doer - orgName := ctx.PathParam("org") + orgName := ctx.PathParam(":org") if len(orgName) > 0 { ctxUser = ctx.Org.Organization.AsUser() ctx.Data["Teams"] = ctx.Org.Teams @@ -420,7 +419,7 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) { IsPull: optional.Some(isPullList), SortType: sortType, IsArchived: optional.Some(false), - User: ctx.Doer, + Doer: ctx.Doer, } // -------------------------------------------------------------------------- // Build opts (IssuesOptions), which contains filter information. @@ -432,7 +431,7 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) { // Get repository IDs where User/Org/Team has access. if ctx.Org != nil && ctx.Org.Organization != nil { - opts.Org = ctx.Org.Organization + opts.Owner = ctx.Org.Organization.AsUser() opts.Team = ctx.Org.Team issue.PrepareFilterIssueLabels(ctx, 0, ctx.Org.Organization.AsUser()) @@ -579,17 +578,9 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) { // ------------------------------- // Fill stats to post to ctx.Data. // ------------------------------- - issueStats, err := getUserIssueStats(ctx, filterMode, issue_indexer.ToSearchOptions(keyword, opts).Copy( + issueStats, err := getUserIssueStats(ctx, ctxUser, filterMode, issue_indexer.ToSearchOptions(keyword, opts).Copy( func(o *issue_indexer.SearchOptions) { o.IsFuzzyKeyword = isFuzzy - // If the doer is the same as the context user, which means the doer is viewing his own dashboard, - // it's not enough to show the repos that the doer owns or has been explicitly granted access to, - // because the doer may create issues or be mentioned in any public repo. - // So we need search issues in all public repos. - o.AllPublic = ctx.Doer.ID == ctxUser.ID - o.MentionID = nil - o.ReviewRequestedID = nil - o.ReviewedID = nil }, )) if err != nil { @@ -743,7 +734,7 @@ func UsernameSubRoute(ctx *context.Context) { switch { case strings.HasSuffix(username, ".png"): if reloadParam(".png") { - AvatarByUserName(ctx) + AvatarByUsernameSize(ctx) } case strings.HasSuffix(username, ".keys"): if reloadParam(".keys") { @@ -778,10 +769,19 @@ func UsernameSubRoute(ctx *context.Context) { } } -func getUserIssueStats(ctx *context.Context, filterMode int, opts *issue_indexer.SearchOptions) (ret *issues_model.IssueStats, err error) { +func getUserIssueStats(ctx *context.Context, ctxUser *user_model.User, filterMode int, opts *issue_indexer.SearchOptions) (ret *issues_model.IssueStats, err error) { ret = &issues_model.IssueStats{} doerID := ctx.Doer.ID + opts = opts.Copy(func(o *issue_indexer.SearchOptions) { + // If the doer is the same as the context user, which means the doer is viewing his own dashboard, + // it's not enough to show the repos that the doer owns or has been explicitly granted access to, + // because the doer may create issues or be mentioned in any public repo. + // So we need search issues in all public repos. + o.AllPublic = doerID == ctxUser.ID + }) + + // Open/Closed are for the tabs of the issue list { openClosedOpts := opts.Copy() switch filterMode { @@ -812,6 +812,15 @@ func getUserIssueStats(ctx *context.Context, filterMode int, opts *issue_indexer } } + // Below stats are for the left sidebar + opts = opts.Copy(func(o *issue_indexer.SearchOptions) { + o.AssigneeID = nil + o.PosterID = nil + o.MentionID = nil + o.ReviewRequestedID = nil + o.ReviewedID = nil + }) + ret.YourRepositoriesCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.AllPublic = false })) if err != nil { return nil, err diff --git a/routers/web/user/notification.go b/routers/web/user/notification.go index 732fac2595860..414cb0be49074 100644 --- a/routers/web/user/notification.go +++ b/routers/web/user/notification.go @@ -22,7 +22,6 @@ import ( "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/structs" - "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/services/context" issue_service "code.gitea.io/gitea/services/issue" @@ -30,9 +29,9 @@ import ( ) const ( - tplNotification templates.TplName = "user/notification/notification" - tplNotificationDiv templates.TplName = "user/notification/notification_div" - tplNotificationSubscriptions templates.TplName = "user/notification/notification_subscriptions" + tplNotification base.TplName = "user/notification/notification" + tplNotificationDiv base.TplName = "user/notification/notification_div" + tplNotificationSubscriptions base.TplName = "user/notification/notification_subscriptions" ) // GetNotificationCount is the middleware that sets the notification count in the context diff --git a/routers/web/user/package.go b/routers/web/user/package.go index 8d78da7c5a833..c6f85ac734eb9 100644 --- a/routers/web/user/package.go +++ b/routers/web/user/package.go @@ -14,6 +14,7 @@ import ( "code.gitea.io/gitea/models/perm" access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/httplib" "code.gitea.io/gitea/modules/log" @@ -23,7 +24,6 @@ import ( debian_module "code.gitea.io/gitea/modules/packages/debian" rpm_module "code.gitea.io/gitea/modules/packages/rpm" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" packages_helper "code.gitea.io/gitea/routers/api/packages/helper" @@ -34,10 +34,10 @@ import ( ) const ( - tplPackagesList templates.TplName = "user/overview/packages" - tplPackagesView templates.TplName = "package/view" - tplPackageVersionList templates.TplName = "user/overview/package_versions" - tplPackagesSettings templates.TplName = "package/settings" + tplPackagesList base.TplName = "user/overview/packages" + tplPackagesView base.TplName = "package/view" + tplPackageVersionList base.TplName = "user/overview/package_versions" + tplPackagesSettings base.TplName = "package/settings" ) // ListPackages displays a list of all packages of the context user @@ -502,7 +502,7 @@ func PackageSettingsPost(ctx *context.Context) { // DownloadPackageFile serves the content of a package file func DownloadPackageFile(ctx *context.Context) { - pf, err := packages_model.GetFileForVersionByID(ctx, ctx.Package.Descriptor.Version.ID, ctx.PathParamInt64("fileid")) + pf, err := packages_model.GetFileForVersionByID(ctx, ctx.Package.Descriptor.Version.ID, ctx.PathParamInt64(":fileid")) if err != nil { if err == packages_model.ErrPackageFileNotExist { ctx.NotFound("", err) diff --git a/routers/web/user/profile.go b/routers/web/user/profile.go index 9c014bffdb79b..c41030a5e2515 100644 --- a/routers/web/user/profile.go +++ b/routers/web/user/profile.go @@ -12,16 +12,15 @@ import ( activities_model "code.gitea.io/gitea/models/activities" "code.gitea.io/gitea/models/db" - "code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/renderhelper" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/markup/markdown" "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/routers/web/feed" "code.gitea.io/gitea/routers/web/org" @@ -31,8 +30,8 @@ import ( ) const ( - tplProfileBigAvatar templates.TplName = "shared/user/profile_big_avatar" - tplFollowUnfollow templates.TplName = "org/follow_unfollow" + tplProfileBigAvatar base.TplName = "shared/user/profile_big_avatar" + tplFollowUnfollow base.TplName = "org/follow_unfollow" ) // OwnerProfile render profile page for a user or a organization (aka, repo owner) @@ -257,21 +256,6 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb ctx.Data["ProfileReadme"] = profileContent } } - case "organizations": - orgs, count, err := db.FindAndCount[organization.Organization](ctx, organization.FindOrgOptions{ - UserID: ctx.ContextUser.ID, - IncludePrivate: showPrivate, - ListOptions: db.ListOptions{ - Page: page, - PageSize: pagingNum, - }, - }) - if err != nil { - ctx.ServerError("GetUserOrganizations", err) - return - } - ctx.Data["Cards"] = orgs - total = int(count) default: // default to "repositories" repos, count, err = repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{ ListOptions: db.ListOptions{ @@ -310,7 +294,31 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb } pager := context.NewPagination(total, pagingNum, page, 5) - pager.AddParamFromRequest(ctx.Req) + pager.SetDefaultParams(ctx) + pager.AddParamString("tab", tab) + if tab != "followers" && tab != "following" && tab != "activity" && tab != "projects" { + pager.AddParamString("language", language) + } + if tab == "activity" { + if ctx.Data["Date"] != nil { + pager.AddParamString("date", fmt.Sprint(ctx.Data["Date"])) + } + } + if archived.Has() { + pager.AddParamString("archived", fmt.Sprint(archived.Value())) + } + if fork.Has() { + pager.AddParamString("fork", fmt.Sprint(fork.Value())) + } + if mirror.Has() { + pager.AddParamString("mirror", fmt.Sprint(mirror.Value())) + } + if template.Has() { + pager.AddParamString("template", fmt.Sprint(template.Value())) + } + if private.Has() { + pager.AddParamString("private", fmt.Sprint(private.Value())) + } ctx.Data["Page"] = pager } diff --git a/routers/web/user/setting/account.go b/routers/web/user/setting/account.go index 3acc3c7a543e5..7f2dece416971 100644 --- a/routers/web/user/setting/account.go +++ b/routers/web/user/setting/account.go @@ -10,15 +10,13 @@ import ( "net/http" "time" - org_model "code.gitea.io/gitea/models/organization" - packages_model "code.gitea.io/gitea/models/packages" - repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/auth/password" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/services/auth" @@ -31,7 +29,7 @@ import ( ) const ( - tplSettingsAccount templates.TplName = "user/settings/account" + tplSettingsAccount base.TplName = "user/settings/account" ) // Account renders change user's password, user's email and user suicide page @@ -303,16 +301,16 @@ func DeleteAccount(ctx *context.Context) { if err := user.DeleteUser(ctx, ctx.Doer, false); err != nil { switch { - case repo_model.IsErrUserOwnRepos(err): + case models.IsErrUserOwnRepos(err): ctx.Flash.Error(ctx.Tr("form.still_own_repo")) ctx.Redirect(setting.AppSubURL + "/user/settings/account") - case org_model.IsErrUserHasOrgs(err): + case models.IsErrUserHasOrgs(err): ctx.Flash.Error(ctx.Tr("form.still_has_org")) ctx.Redirect(setting.AppSubURL + "/user/settings/account") - case packages_model.IsErrUserOwnPackages(err): + case models.IsErrUserOwnPackages(err): ctx.Flash.Error(ctx.Tr("form.still_own_packages")) ctx.Redirect(setting.AppSubURL + "/user/settings/account") - case user_model.IsErrDeleteLastAdminUser(err): + case models.IsErrDeleteLastAdminUser(err): ctx.Flash.Error(ctx.Tr("auth.last_admin")) ctx.Redirect(setting.AppSubURL + "/user/settings/account") default: diff --git a/routers/web/user/setting/applications.go b/routers/web/user/setting/applications.go index cf71d01dc1624..356c2ea5de37f 100644 --- a/routers/web/user/setting/applications.go +++ b/routers/web/user/setting/applications.go @@ -10,15 +10,15 @@ import ( auth_model "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/db" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/forms" ) const ( - tplSettingsApplications templates.TplName = "user/settings/applications" + tplSettingsApplications base.TplName = "user/settings/applications" ) // Applications render manage access token page diff --git a/routers/web/user/setting/block.go b/routers/web/user/setting/block.go index 3756495fd24b1..d419fb321bc00 100644 --- a/routers/web/user/setting/block.go +++ b/routers/web/user/setting/block.go @@ -7,14 +7,14 @@ import ( "net/http" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/templates" shared_user "code.gitea.io/gitea/routers/web/shared/user" "code.gitea.io/gitea/services/context" ) const ( - tplSettingsBlockedUsers templates.TplName = "user/settings/blocked_users" + tplSettingsBlockedUsers base.TplName = "user/settings/blocked_users" ) func BlockedUsers(ctx *context.Context) { diff --git a/routers/web/user/setting/keys.go b/routers/web/user/setting/keys.go index 127aed9845e95..c492715fb5f71 100644 --- a/routers/web/user/setting/keys.go +++ b/routers/web/user/setting/keys.go @@ -11,8 +11,8 @@ import ( asymkey_model "code.gitea.io/gitea/models/asymkey" "code.gitea.io/gitea/models/db" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/web" asymkey_service "code.gitea.io/gitea/services/asymkey" "code.gitea.io/gitea/services/context" @@ -20,7 +20,7 @@ import ( ) const ( - tplSettingsKeys templates.TplName = "user/settings/keys" + tplSettingsKeys base.TplName = "user/settings/keys" ) // Keys render user's SSH/GPG public keys page diff --git a/routers/web/user/setting/oauth2.go b/routers/web/user/setting/oauth2.go index d50728c24ecde..1f485e06c815e 100644 --- a/routers/web/user/setting/oauth2.go +++ b/routers/web/user/setting/oauth2.go @@ -4,13 +4,13 @@ package setting import ( + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/services/context" ) const ( - tplSettingsOAuthApplicationEdit templates.TplName = "user/settings/applications_oauth2_edit" + tplSettingsOAuthApplicationEdit base.TplName = "user/settings/applications_oauth2_edit" ) func newOAuth2CommonHandlers(userID int64) *OAuth2CommonHandlers { diff --git a/routers/web/user/setting/oauth2_common.go b/routers/web/user/setting/oauth2_common.go index 783deca710f58..e93e9e19548b4 100644 --- a/routers/web/user/setting/oauth2_common.go +++ b/routers/web/user/setting/oauth2_common.go @@ -8,7 +8,7 @@ import ( "net/http" "code.gitea.io/gitea/models/auth" - "code.gitea.io/gitea/modules/templates" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" shared_user "code.gitea.io/gitea/routers/web/shared/user" @@ -17,10 +17,10 @@ import ( ) type OAuth2CommonHandlers struct { - OwnerID int64 // 0 for instance-wide, otherwise OrgID or UserID - BasePathList string // the base URL for the application list page, eg: "/user/setting/applications" - BasePathEditPrefix string // the base URL for the application edit page, will be appended with app id, eg: "/user/setting/applications/oauth2" - TplAppEdit templates.TplName // the template for the application edit page + OwnerID int64 // 0 for instance-wide, otherwise OrgID or UserID + BasePathList string // the base URL for the application list page, eg: "/user/setting/applications" + BasePathEditPrefix string // the base URL for the application edit page, will be appended with app id, eg: "/user/setting/applications/oauth2" + TplAppEdit base.TplName // the template for the application edit page } func (oa *OAuth2CommonHandlers) renderEditPage(ctx *context.Context) { diff --git a/routers/web/user/setting/packages.go b/routers/web/user/setting/packages.go index 9692cd9db0ca6..50521c11c0e7e 100644 --- a/routers/web/user/setting/packages.go +++ b/routers/web/user/setting/packages.go @@ -8,18 +8,18 @@ import ( "strings" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/base" chef_module "code.gitea.io/gitea/modules/packages/chef" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/util" shared "code.gitea.io/gitea/routers/web/shared/packages" "code.gitea.io/gitea/services/context" ) const ( - tplSettingsPackages templates.TplName = "user/settings/packages" - tplSettingsPackagesRuleEdit templates.TplName = "user/settings/packages_cleanup_rules_edit" - tplSettingsPackagesRulePreview templates.TplName = "user/settings/packages_cleanup_rules_preview" + tplSettingsPackages base.TplName = "user/settings/packages" + tplSettingsPackagesRuleEdit base.TplName = "user/settings/packages_cleanup_rules_edit" + tplSettingsPackagesRulePreview base.TplName = "user/settings/packages_cleanup_rules_preview" ) func Packages(ctx *context.Context) { diff --git a/routers/web/user/setting/profile.go b/routers/web/user/setting/profile.go index 4b3c214096c74..3b051c9b5f4b7 100644 --- a/routers/web/user/setting/profile.go +++ b/routers/web/user/setting/profile.go @@ -19,10 +19,10 @@ import ( "code.gitea.io/gitea/models/organization" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/translation" "code.gitea.io/gitea/modules/typesniffer" "code.gitea.io/gitea/modules/util" @@ -35,10 +35,10 @@ import ( ) const ( - tplSettingsProfile templates.TplName = "user/settings/profile" - tplSettingsAppearance templates.TplName = "user/settings/appearance" - tplSettingsOrganization templates.TplName = "user/settings/organization" - tplSettingsRepositories templates.TplName = "user/settings/repos" + tplSettingsProfile base.TplName = "user/settings/profile" + tplSettingsAppearance base.TplName = "user/settings/appearance" + tplSettingsOrganization base.TplName = "user/settings/organization" + tplSettingsRepositories base.TplName = "user/settings/repos" ) // Profile render user's profile page diff --git a/routers/web/user/setting/security/security.go b/routers/web/user/setting/security/security.go index 38d1910e2d71b..b44cb4dd499fa 100644 --- a/routers/web/user/setting/security/security.go +++ b/routers/web/user/setting/security/security.go @@ -11,16 +11,16 @@ import ( auth_model "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/db" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/services/auth/source/oauth2" "code.gitea.io/gitea/services/context" ) const ( - tplSettingsSecurity templates.TplName = "user/settings/security/security" - tplSettingsTwofaEnroll templates.TplName = "user/settings/security/twofa_enroll" + tplSettingsSecurity base.TplName = "user/settings/security/security" + tplSettingsTwofaEnroll base.TplName = "user/settings/security/twofa_enroll" ) // Security render change user's password page and 2FA diff --git a/routers/web/user/setting/webhooks.go b/routers/web/user/setting/webhooks.go index ca7c7ec1aca8b..3732ca27c06f1 100644 --- a/routers/web/user/setting/webhooks.go +++ b/routers/web/user/setting/webhooks.go @@ -9,13 +9,13 @@ import ( "code.gitea.io/gitea/models/db" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/models/webhook" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/services/context" ) const ( - tplSettingsHooks templates.TplName = "user/settings/hooks" + tplSettingsHooks base.TplName = "user/settings/hooks" ) // Webhooks render webhook list page diff --git a/routers/web/web.go b/routers/web/web.go index 654646d77c5c5..4e6a126c25f15 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -4,6 +4,7 @@ package web import ( + gocontext "context" "net/http" "strings" @@ -36,6 +37,7 @@ import ( "code.gitea.io/gitea/routers/web/repo" "code.gitea.io/gitea/routers/web/repo/actions" repo_setting "code.gitea.io/gitea/routers/web/repo/setting" + shared_actions "code.gitea.io/gitea/routers/web/shared/actions" "code.gitea.io/gitea/routers/web/shared/project" "code.gitea.io/gitea/routers/web/user" user_setting "code.gitea.io/gitea/routers/web/user/setting" @@ -283,23 +285,23 @@ func Routes() *web.Router { mid = append(mid, repo.GetActiveStopwatch) mid = append(mid, goGet) - others := web.NewRouter() - others.Use(mid...) - registerRoutes(others) - routes.Mount("", others) + webRoutes := web.NewRouter() + webRoutes.Use(mid...) + webRoutes.Group("", func() { registerWebRoutes(webRoutes) }, common.BlockExpensive()) + routes.Mount("", webRoutes) return routes } var optSignInIgnoreCsrf = verifyAuthWithOptions(&common.VerifyOptions{DisableCSRF: true}) -// registerRoutes register routes -func registerRoutes(m *web.Router) { +// registerWebRoutes register routes +func registerWebRoutes(m *web.Router) { // required to be signed in or signed out reqSignIn := verifyAuthWithOptions(&common.VerifyOptions{SignInRequired: true}) reqSignOut := verifyAuthWithOptions(&common.VerifyOptions{SignOutRequired: true}) // optional sign in (if signed in, use the user as doer, if not, no doer) - optSignIn := verifyAuthWithOptions(&common.VerifyOptions{SignInRequired: setting.Service.RequireSignInView}) - optExploreSignIn := verifyAuthWithOptions(&common.VerifyOptions{SignInRequired: setting.Service.RequireSignInView || setting.Service.Explore.RequireSigninView}) + optSignIn := verifyAuthWithOptions(&common.VerifyOptions{SignInRequired: setting.Service.RequireSignInViewStrict}) + optExploreSignIn := verifyAuthWithOptions(&common.VerifyOptions{SignInRequired: setting.Service.RequireSignInViewStrict || setting.Service.Explore.RequireSigninView}) validation.AddBindingRules() @@ -441,10 +443,10 @@ func registerRoutes(m *web.Router) { addSettingsVariablesRoutes := func() { m.Group("/variables", func() { - m.Get("", repo_setting.Variables) - m.Post("/new", web.Bind(forms.EditVariableForm{}), repo_setting.VariableCreate) - m.Post("/{variable_id}/edit", web.Bind(forms.EditVariableForm{}), repo_setting.VariableUpdate) - m.Post("/{variable_id}/delete", repo_setting.VariableDelete) + m.Get("", shared_actions.Variables) + m.Post("/new", web.Bind(forms.EditVariableForm{}), shared_actions.VariableCreate) + m.Post("/{variable_id}/edit", web.Bind(forms.EditVariableForm{}), shared_actions.VariableUpdate) + m.Post("/{variable_id}/delete", shared_actions.VariableDelete) }) } @@ -458,11 +460,11 @@ func registerRoutes(m *web.Router) { addSettingsRunnersRoutes := func() { m.Group("/runners", func() { - m.Get("", repo_setting.Runners) - m.Combo("/{runnerid}").Get(repo_setting.RunnersEdit). - Post(web.Bind(forms.EditRunnerForm{}), repo_setting.RunnersEditPost) - m.Post("/{runnerid}/delete", repo_setting.RunnerDeletePost) - m.Post("/reset_registration_token", repo_setting.ResetRunnerRegistrationToken) + m.Get("", shared_actions.Runners) + m.Combo("/{runnerid}").Get(shared_actions.RunnersEdit). + Post(web.Bind(forms.EditRunnerForm{}), shared_actions.RunnersEditPost) + m.Post("/{runnerid}/delete", shared_actions.RunnerDeletePost) + m.Post("/reset_registration_token", shared_actions.ResetRunnerRegistrationToken) }) } @@ -680,7 +682,7 @@ func registerRoutes(m *web.Router) { m.Get("/activate", auth.Activate) m.Post("/activate", auth.ActivatePost) m.Any("/activate_email", auth.ActivateEmail) - m.Get("/avatar/{username}/{size}", user.AvatarByUserName) + m.Get("/avatar/{username}/{size}", user.AvatarByUsernameSize) m.Get("/recover_account", auth.ResetPasswd) m.Post("/recover_account", auth.ResetPasswdPost) m.Get("/forgot_password", auth.ForgotPasswd) @@ -876,11 +878,6 @@ func registerRoutes(m *web.Router) { m.Post("", org.TeamInvitePost) }) - // require member permissions - m.Group("/{org}/view", func() { - m.Get("", org.View) - }, context.OrgAssignment(false, false, false, false)) - m.Group("/{org}", func() { m.Get("/dashboard", user.Dashboard) m.Get("/dashboard/{team}", user.Dashboard) @@ -1024,15 +1021,14 @@ func registerRoutes(m *web.Router) { m.Get("/new", org.RenderNewProject) m.Post("/new", web.Bind(forms.CreateProjectForm{}), org.NewProjectPost) m.Group("/{id}", func() { + m.Post("", web.Bind(forms.EditProjectColumnForm{}), org.AddColumnToProjectPost) + m.Post("/move", project.MoveColumns) m.Post("/delete", org.DeleteProject) m.Get("/edit", org.RenderEditProject) m.Post("/edit", web.Bind(forms.CreateProjectForm{}), org.EditProjectPost) m.Post("/{action:open|close}", org.ChangeProjectStatus) - // TODO: improper name. Others are "delete project", "edit project", but this one is "move columns" - m.Post("/move", project.MoveColumns) - m.Post("/columns/new", web.Bind(forms.EditProjectColumnForm{}), org.AddColumnToProjectPost) m.Group("/{columnID}", func() { m.Put("", web.Bind(forms.EditProjectColumnForm{}), org.EditProjectColumn) m.Delete("", org.DeleteProjectColumn) @@ -1137,7 +1133,7 @@ func registerRoutes(m *web.Router) { }) }) m.Group("/actions", func() { - m.Get("", repo_setting.RedirectToDefaultSetting) + m.Get("", shared_actions.RedirectToDefaultSetting) addSettingsRunnersRoutes() addSettingsSecretsRoutes() addSettingsVariablesRoutes() @@ -1176,7 +1172,7 @@ func registerRoutes(m *web.Router) { Get(repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.CompareDiff). Post(reqSignIn, context.RepoMustNotBeArchived(), reqRepoPullsReader, repo.MustAllowPulls, web.Bind(forms.CreateIssueForm{}), repo.SetWhitespaceBehavior, repo.CompareAndPullRequestPost) }, optSignIn, context.RepoAssignment, reqRepoCodeReader) - // end "/{username}/{reponame}": repo code: find, compare, list + // end "/{username}/{reponame}": find, compare, list (code related) m.Group("/{username}/{reponame}", func() { m.Get("/issues/posters", repo.IssuePosters) // it can't use {type:issues|pulls} because it would conflict with other routes like "/pulls/{index}" @@ -1345,8 +1341,7 @@ func registerRoutes(m *web.Router) { m.Get(".atom", feedEnabled, repo.TagsListFeedAtom) }, ctxDataSet("EnableFeed", setting.Other.EnableFeed), repo.MustBeNotEmpty, context.RepoRefByType(context.RepoRefTag, context.RepoRefByTypeOptions{IgnoreNotExistErr: true})) - m.Post("/tags/delete", repo.DeleteTag, reqSignIn, - repo.MustBeNotEmpty, context.RepoMustNotBeArchived(), reqRepoCodeWriter, context.RepoRef()) + m.Post("/tags/delete", reqSignIn, repo.MustBeNotEmpty, context.RepoMustNotBeArchived(), reqRepoCodeWriter, repo.DeleteTag) }, optSignIn, context.RepoAssignment, reqRepoCodeReader) // end "/{username}/{reponame}": repo tags @@ -1397,15 +1392,14 @@ func registerRoutes(m *web.Router) { m.Get("/new", repo.RenderNewProject) m.Post("/new", web.Bind(forms.CreateProjectForm{}), repo.NewProjectPost) m.Group("/{id}", func() { + m.Post("", web.Bind(forms.EditProjectColumnForm{}), repo.AddColumnToProjectPost) + m.Post("/move", project.MoveColumns) m.Post("/delete", repo.DeleteProject) m.Get("/edit", repo.RenderEditProject) m.Post("/edit", web.Bind(forms.CreateProjectForm{}), repo.EditProjectPost) m.Post("/{action:open|close}", repo.ChangeProjectStatus) - // TODO: improper name. Others are "delete project", "edit project", but this one is "move columns" - m.Post("/move", project.MoveColumns) - m.Post("/columns/new", web.Bind(forms.EditProjectColumnForm{}), repo.AddColumnToProjectPost) m.Group("/{columnID}", func() { m.Put("", web.Bind(forms.EditProjectColumnForm{}), repo.EditProjectColumn) m.Delete("", repo.DeleteProjectColumn) @@ -1453,7 +1447,6 @@ func registerRoutes(m *web.Router) { m.Combo("/*"). Get(repo.Wiki). Post(context.RepoMustNotBeArchived(), reqSignIn, reqRepoWikiWriter, web.Bind(forms.NewWikiForm{}), repo.WikiPost) - m.Get("/blob_excerpt/{sha}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.ExcerptBlob) m.Get("/commit/{sha:[a-f0-9]{7,64}}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.Diff) m.Get("/commit/{sha:[a-f0-9]{7,64}}.{ext:patch|diff}", repo.RawDiff) m.Get("/raw/*", repo.WikiRaw) @@ -1466,20 +1459,23 @@ func registerRoutes(m *web.Router) { m.Group("/{username}/{reponame}/activity", func() { m.Get("", repo.Activity) m.Get("/{period}", repo.Activity) - m.Group("/contributors", func() { - m.Get("", repo.Contributors) - m.Get("/data", repo.ContributorsData) - }) - m.Group("/code-frequency", func() { - m.Get("", repo.CodeFrequency) - m.Get("/data", repo.CodeFrequencyData) - }) - m.Group("/recent-commits", func() { - m.Get("", repo.RecentCommits) - m.Get("/data", repo.RecentCommitsData) - }) + + m.Group("", func() { + m.Group("/contributors", func() { + m.Get("", repo.Contributors) + m.Get("/data", repo.ContributorsData) + }) + m.Group("/code-frequency", func() { + m.Get("", repo.CodeFrequency) + m.Get("/data", repo.CodeFrequencyData) + }) + m.Group("/recent-commits", func() { + m.Get("", repo.RecentCommits) + m.Get("/data", repo.CodeFrequencyData) // "recent-commits" also uses the same data as "code-frequency" + }) + }, reqRepoCodeReader) }, - optSignIn, context.RepoAssignment, context.RequireRepoReaderOr(unit.TypePullRequests, unit.TypeIssues, unit.TypeReleases), + optSignIn, context.RepoAssignment, context.RequireRepoReaderOr(unit.TypeCode, unit.TypePullRequests, unit.TypeIssues, unit.TypeReleases), context.RepoRef(), repo.MustBeNotEmpty, ) // end "/{username}/{reponame}/activity" @@ -1529,6 +1525,28 @@ func registerRoutes(m *web.Router) { m.Get("", repo.Branches) }, repo.MustBeNotEmpty, context.RepoRef()) + m.Group("/blob_excerpt", func() { + m.Get("/{sha}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.ExcerptBlob) + }, func(ctx *context.Context) gocontext.CancelFunc { + // FIXME: refactor this function, use separate routes for wiki/code + if ctx.FormBool("wiki") { + ctx.Data["PageIsWiki"] = true + repo.MustEnableWiki(ctx) + return nil + } + + if ctx.Written() { + return nil + } + cancel := context.RepoRef()(ctx) + if ctx.Written() { + return cancel + } + + repo.MustBeNotEmpty(ctx) + return cancel + }) + m.Group("/media", func() { m.Get("/branch/*", context.RepoRefByType(context.RepoRefBranch), repo.SingleDownloadOrLFS) m.Get("/tag/*", context.RepoRefByType(context.RepoRefTag), repo.SingleDownloadOrLFS) @@ -1568,8 +1586,6 @@ func registerRoutes(m *web.Router) { m.Get("/commit/*", context.RepoRefByType(context.RepoRefCommit), repo.RefBlame) }, repo.MustBeNotEmpty) - m.Get("/blob_excerpt/{sha}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.ExcerptBlob) - m.Group("", func() { m.Get("/graph", repo.Graph) m.Get("/commit/{sha:([a-f0-9]{7,64})$}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.Diff) @@ -1627,7 +1643,7 @@ func registerRoutes(m *web.Router) { } m.NotFound(func(w http.ResponseWriter, req *http.Request) { - ctx := context.GetWebContext(req) + ctx := context.GetWebContext(req.Context()) routing.UpdateFuncInfo(ctx, routing.GetFuncInfo(ctx.NotFound, "WebNotFound")) ctx.NotFound("", nil) }) diff --git a/services/actions/cleanup.go b/services/actions/cleanup.go index 1223ebcab6349..ee1d1677133e2 100644 --- a/services/actions/cleanup.go +++ b/services/actions/cleanup.go @@ -52,9 +52,9 @@ func cleanExpiredArtifacts(taskCtx context.Context) error { } if err := storage.ActionsArtifacts.Delete(artifact.StoragePath); err != nil { log.Error("Cannot delete artifact %d: %v", artifact.ID, err) - continue + // go on } - log.Info("Artifact %d set expired", artifact.ID) + log.Info("Artifact %d is deleted (due to expiration)", artifact.ID) } return nil } @@ -76,9 +76,9 @@ func cleanNeedDeleteArtifacts(taskCtx context.Context) error { } if err := storage.ActionsArtifacts.Delete(artifact.StoragePath); err != nil { log.Error("Cannot delete artifact %d: %v", artifact.ID, err) - continue + // go on } - log.Info("Artifact %d set deleted", artifact.ID) + log.Info("Artifact %d is deleted (due to pending deletion)", artifact.ID) } if len(artifacts) < deleteArtifactBatchSize { log.Debug("No more artifacts pending deletion") @@ -103,8 +103,7 @@ func CleanupLogs(ctx context.Context) error { for _, task := range tasks { if err := actions_module.RemoveLogs(ctx, task.LogInStorage, task.LogFilename); err != nil { log.Error("Failed to remove log %s (in storage %v) of task %v: %v", task.LogFilename, task.LogInStorage, task.ID, err) - // do not return error here, continue to next task - continue + // do not return error here, go on } task.LogIndexes = nil // clear log indexes since it's a heavy field task.LogExpired = true diff --git a/services/actions/clear_tasks.go b/services/actions/clear_tasks.go index 67373782d5c91..9d613b68a5560 100644 --- a/services/actions/clear_tasks.go +++ b/services/actions/clear_tasks.go @@ -10,10 +10,12 @@ import ( actions_model "code.gitea.io/gitea/models/actions" "code.gitea.io/gitea/models/db" + repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/modules/actions" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" + webhook_module "code.gitea.io/gitea/modules/webhook" ) // StopZombieTasks stops the task which have running status, but haven't been updated for a long time @@ -32,6 +34,24 @@ func StopEndlessTasks(ctx context.Context) error { }) } +func notifyWorkflowJobStatusUpdate(ctx context.Context, jobs []*actions_model.ActionRunJob) { + if len(jobs) > 0 { + CreateCommitStatus(ctx, jobs...) + } +} + +func CancelPreviousJobs(ctx context.Context, repoID int64, ref, workflowID string, event webhook_module.HookEventType) error { + jobs, err := actions_model.CancelPreviousJobs(ctx, repoID, ref, workflowID, event) + notifyWorkflowJobStatusUpdate(ctx, jobs) + return err +} + +func CleanRepoScheduleTasks(ctx context.Context, repo *repo_model.Repository) error { + jobs, err := actions_model.CleanRepoScheduleTasks(ctx, repo) + notifyWorkflowJobStatusUpdate(ctx, jobs) + return err +} + func stopTasks(ctx context.Context, opts actions_model.FindTaskOptions) error { tasks, err := db.Find[actions_model.ActionTask](ctx, opts) if err != nil { @@ -67,7 +87,7 @@ func stopTasks(ctx context.Context, opts actions_model.FindTaskOptions) error { remove() } - CreateCommitStatus(ctx, jobs...) + notifyWorkflowJobStatusUpdate(ctx, jobs) return nil } diff --git a/services/actions/init_test.go b/services/actions/init_test.go index 59c321ccd771e..7ef07022041da 100644 --- a/services/actions/init_test.go +++ b/services/actions/init_test.go @@ -17,9 +17,7 @@ import ( ) func TestMain(m *testing.M) { - unittest.MainTest(m, &unittest.TestOptions{ - FixtureFiles: []string{"action_runner_token.yml"}, - }) + unittest.MainTest(m) os.Exit(m.Run()) } diff --git a/services/actions/notifier.go b/services/actions/notifier.go index a4ebdf9e888ee..67e33e7cce0a8 100644 --- a/services/actions/notifier.go +++ b/services/actions/notifier.go @@ -58,7 +58,15 @@ func (n *actionsNotifier) NewIssue(ctx context.Context, issue *issues_model.Issu // IssueChangeContent notifies change content of issue func (n *actionsNotifier) IssueChangeContent(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, oldContent string) { ctx = withMethod(ctx, "IssueChangeContent") + n.notifyIssueChangeWithTitleOrContent(ctx, doer, issue) +} + +func (n *actionsNotifier) IssueChangeTitle(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, oldTitle string) { + ctx = withMethod(ctx, "IssueChangeTitle") + n.notifyIssueChangeWithTitleOrContent(ctx, doer, issue) +} +func (n *actionsNotifier) notifyIssueChangeWithTitleOrContent(ctx context.Context, doer *user_model.User, issue *issues_model.Issue) { var err error if err = issue.LoadRepo(ctx); err != nil { log.Error("LoadRepo: %v", err) diff --git a/services/actions/notifier_helper.go b/services/actions/notifier_helper.go index 323c6a76e422c..87ea1a37f591c 100644 --- a/services/actions/notifier_helper.go +++ b/services/actions/notifier_helper.go @@ -117,7 +117,7 @@ func (input *notifyInput) Notify(ctx context.Context) { func notify(ctx context.Context, input *notifyInput) error { shouldDetectSchedules := input.Event == webhook_module.HookEventPush && input.Ref.BranchName() == input.Repo.DefaultBranch - if input.Doer.IsActions() { + if input.Doer.IsGiteaActions() { // avoiding triggering cyclically, for example: // a comment of an issue will trigger the runner to add a new comment as reply, // and the new comment will trigger the runner again. @@ -136,7 +136,7 @@ func notify(ctx context.Context, input *notifyInput) error { return nil } if unit_model.TypeActions.UnitGlobalDisabled() { - if err := actions_model.CleanRepoScheduleTasks(ctx, input.Repo); err != nil { + if err := CleanRepoScheduleTasks(ctx, input.Repo); err != nil { log.Error("CleanRepoScheduleTasks: %v", err) } return nil @@ -341,7 +341,7 @@ func handleWorkflows( // cancel running jobs if the event is push or pull_request_sync if run.Event == webhook_module.HookEventPush || run.Event == webhook_module.HookEventPullRequestSync { - if err := actions_model.CancelPreviousJobs( + if err := CancelPreviousJobs( ctx, run.RepoID, run.Ref, @@ -472,7 +472,7 @@ func handleSchedules( log.Error("CountSchedules: %v", err) return err } else if count > 0 { - if err := actions_model.CleanRepoScheduleTasks(ctx, input.Repo); err != nil { + if err := CleanRepoScheduleTasks(ctx, input.Repo); err != nil { log.Error("CleanRepoScheduleTasks: %v", err) } } diff --git a/services/actions/schedule_tasks.go b/services/actions/schedule_tasks.go index 18f3324fd2c26..ad1158313b220 100644 --- a/services/actions/schedule_tasks.go +++ b/services/actions/schedule_tasks.go @@ -55,7 +55,7 @@ func startTasks(ctx context.Context) error { // cancel running jobs if the event is push if row.Schedule.Event == webhook_module.HookEventPush { // cancel running jobs of the same workflow - if err := actions_model.CancelPreviousJobs( + if err := CancelPreviousJobs( ctx, row.RepoID, row.Schedule.Ref, diff --git a/routers/api/actions/runner/utils.go b/services/actions/task.go similarity index 88% rename from routers/api/actions/runner/utils.go rename to services/actions/task.go index 539be8d88905a..f2d14b329c763 100644 --- a/routers/api/actions/runner/utils.go +++ b/services/actions/task.go @@ -1,7 +1,7 @@ // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package runner +package actions import ( "context" @@ -16,51 +16,68 @@ import ( "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/services/actions" runnerv1 "code.gitea.io/actions-proto-go/runner/v1" "google.golang.org/protobuf/types/known/structpb" ) -func pickTask(ctx context.Context, runner *actions_model.ActionRunner) (*runnerv1.Task, bool, error) { - t, ok, err := actions_model.CreateTaskForRunner(ctx, runner) - if err != nil { - return nil, false, fmt.Errorf("CreateTaskForRunner: %w", err) - } - if !ok { - return nil, false, nil - } +func PickTask(ctx context.Context, runner *actions_model.ActionRunner) (*runnerv1.Task, bool, error) { + var ( + task *runnerv1.Task + job *actions_model.ActionRunJob + ) - secrets, err := secret_model.GetSecretsOfTask(ctx, t) - if err != nil { - return nil, false, fmt.Errorf("GetSecretsOfTask: %w", err) - } + if err := db.WithTx(ctx, func(ctx context.Context) error { + t, ok, err := actions_model.CreateTaskForRunner(ctx, runner) + if err != nil { + return fmt.Errorf("CreateTaskForRunner: %w", err) + } + if !ok { + return nil + } - vars, err := actions_model.GetVariablesOfRun(ctx, t.Job.Run) - if err != nil { - return nil, false, fmt.Errorf("GetVariablesOfRun: %w", err) - } + if err := t.LoadAttributes(ctx); err != nil { + return fmt.Errorf("task LoadAttributes: %w", err) + } + job = t.Job - actions.CreateCommitStatus(ctx, t.Job) + secrets, err := secret_model.GetSecretsOfTask(ctx, t) + if err != nil { + return fmt.Errorf("GetSecretsOfTask: %w", err) + } + + vars, err := actions_model.GetVariablesOfRun(ctx, t.Job.Run) + if err != nil { + return fmt.Errorf("GetVariablesOfRun: %w", err) + } + + needs, err := findTaskNeeds(ctx, job) + if err != nil { + return fmt.Errorf("findTaskNeeds: %w", err) + } + + taskContext := generateTaskContext(t) - task := &runnerv1.Task{ - Id: t.ID, - WorkflowPayload: t.Job.WorkflowPayload, - Context: generateTaskContext(t), - Secrets: secrets, - Vars: vars, + task = &runnerv1.Task{ + Id: t.ID, + WorkflowPayload: t.Job.WorkflowPayload, + Context: taskContext, + Secrets: secrets, + Vars: vars, + Needs: needs, + } + + return nil + }); err != nil { + return nil, false, err } - if needs, err := findTaskNeeds(ctx, t); err != nil { - log.Error("Cannot find needs for task %v: %v", t.ID, err) - // Go on with empty needs. - // If return error, the task will be wild, which means the runner will never get it when it has been assigned to the runner. - // In contrast, missing needs is less serious. - // And the task will fail and the runner will report the error in the logs. - } else { - task.Needs = needs + if task == nil { + return nil, false, nil } + CreateCommitStatus(ctx, job) + return task, true, nil } @@ -95,7 +112,7 @@ func generateTaskContext(t *actions_model.ActionTask) *structpb.Struct { refName := git.RefName(ref) - giteaRuntimeToken, err := actions.CreateAuthorizationToken(t.ID, t.Job.RunID, t.JobID) + giteaRuntimeToken, err := CreateAuthorizationToken(t.ID, t.Job.RunID, t.JobID) if err != nil { log.Error("actions.CreateAuthorizationToken failed: %v", err) } @@ -148,16 +165,13 @@ func generateTaskContext(t *actions_model.ActionTask) *structpb.Struct { return taskContext } -func findTaskNeeds(ctx context.Context, task *actions_model.ActionTask) (map[string]*runnerv1.TaskNeed, error) { - if err := task.LoadAttributes(ctx); err != nil { - return nil, fmt.Errorf("LoadAttributes: %w", err) - } - if len(task.Job.Needs) == 0 { +func findTaskNeeds(ctx context.Context, job *actions_model.ActionRunJob) (map[string]*runnerv1.TaskNeed, error) { + if len(job.Needs) == 0 { return nil, nil } - needs := container.SetOf(task.Job.Needs...) + needs := container.SetOf(job.Needs...) - jobs, err := db.Find[actions_model.ActionRunJob](ctx, actions_model.FindRunJobOptions{RunID: task.Job.RunID}) + jobs, err := db.Find[actions_model.ActionRunJob](ctx, actions_model.FindRunJobOptions{RunID: job.RunID}) if err != nil { return nil, fmt.Errorf("FindRunJobs: %w", err) } diff --git a/routers/api/actions/runner/utils_test.go b/services/actions/task_test.go similarity index 80% rename from routers/api/actions/runner/utils_test.go rename to services/actions/task_test.go index d7a6f84550f1d..035b62c5bf444 100644 --- a/routers/api/actions/runner/utils_test.go +++ b/services/actions/task_test.go @@ -1,7 +1,7 @@ // Copyright 2024 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package runner +package actions import ( "context" @@ -17,8 +17,9 @@ func Test_findTaskNeeds(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) task := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: 51}) + job := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: task.JobID}) - ret, err := findTaskNeeds(context.Background(), task) + ret, err := findTaskNeeds(context.Background(), job) assert.NoError(t, err) assert.Len(t, ret, 1) assert.Contains(t, ret, "job1") diff --git a/services/actions/variables.go b/services/actions/variables.go index 8dde9c4af5c16..95f088dbd334e 100644 --- a/services/actions/variables.go +++ b/services/actions/variables.go @@ -6,7 +6,6 @@ package actions import ( "context" "regexp" - "strings" actions_model "code.gitea.io/gitea/models/actions" "code.gitea.io/gitea/modules/log" @@ -31,20 +30,18 @@ func CreateVariable(ctx context.Context, ownerID, repoID int64, name, data strin return v, nil } -func UpdateVariable(ctx context.Context, variableID int64, name, data string) (bool, error) { - if err := secret_service.ValidateName(name); err != nil { +func UpdateVariableNameData(ctx context.Context, variable *actions_model.ActionVariable) (bool, error) { + if err := secret_service.ValidateName(variable.Name); err != nil { return false, err } - if err := envNameCIRegexMatch(name); err != nil { + if err := envNameCIRegexMatch(variable.Name); err != nil { return false, err } - return actions_model.UpdateVariable(ctx, &actions_model.ActionVariable{ - ID: variableID, - Name: strings.ToUpper(name), - Data: util.ReserveLineBreakForTextarea(data), - }) + variable.Data = util.ReserveLineBreakForTextarea(variable.Data) + + return actions_model.UpdateVariableCols(ctx, variable, "name", "data") } func DeleteVariableByID(ctx context.Context, variableID int64) error { diff --git a/services/asymkey/deploy_key.go b/services/asymkey/deploy_key.go index 9e5a6d6292b98..324688c53408e 100644 --- a/services/asymkey/deploy_key.go +++ b/services/asymkey/deploy_key.go @@ -5,69 +5,21 @@ package asymkey import ( "context" - "fmt" - asymkey_model "code.gitea.io/gitea/models/asymkey" + "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/db" - repo_model "code.gitea.io/gitea/models/repo" + user_model "code.gitea.io/gitea/models/user" ) -// DeleteRepoDeployKeys deletes all deploy keys of a repository. permissions check should be done outside -func DeleteRepoDeployKeys(ctx context.Context, repoID int64) (int, error) { - deployKeys, err := db.Find[asymkey_model.DeployKey](ctx, asymkey_model.ListDeployKeysOptions{RepoID: repoID}) - if err != nil { - return 0, fmt.Errorf("listDeployKeys: %w", err) - } - - for _, dKey := range deployKeys { - if err := deleteDeployKeyFromDB(ctx, dKey); err != nil { - return 0, fmt.Errorf("deleteDeployKeys: %w", err) - } - } - return len(deployKeys), nil -} - -// deleteDeployKeyFromDB delete deploy keys from database -func deleteDeployKeyFromDB(ctx context.Context, key *asymkey_model.DeployKey) error { - if _, err := db.DeleteByID[asymkey_model.DeployKey](ctx, key.ID); err != nil { - return fmt.Errorf("delete deploy key [%d]: %w", key.ID, err) - } - - // Check if this is the last reference to same key content. - has, err := asymkey_model.IsDeployKeyExistByKeyID(ctx, key.KeyID) - if err != nil { - return err - } else if !has { - if _, err = db.DeleteByID[asymkey_model.PublicKey](ctx, key.KeyID); err != nil { - return err - } - } - - return nil -} - // DeleteDeployKey deletes deploy key from its repository authorized_keys file if needed. -// Permissions check should be done outside. -func DeleteDeployKey(ctx context.Context, repo *repo_model.Repository, id int64) error { +func DeleteDeployKey(ctx context.Context, doer *user_model.User, id int64) error { dbCtx, committer, err := db.TxContext(ctx) if err != nil { return err } defer committer.Close() - key, err := asymkey_model.GetDeployKeyByID(ctx, id) - if err != nil { - if asymkey_model.IsErrDeployKeyNotExist(err) { - return nil - } - return fmt.Errorf("GetDeployKeyByID: %w", err) - } - - if key.RepoID != repo.ID { - return fmt.Errorf("deploy key %d does not belong to repository %d", id, repo.ID) - } - - if err := deleteDeployKeyFromDB(dbCtx, key); err != nil { + if err := models.DeleteDeployKey(dbCtx, doer, id); err != nil { return err } if err := committer.Commit(); err != nil { diff --git a/services/asymkey/main_test.go b/services/asymkey/main_test.go index 1cdc39933d43a..3505b26f699a6 100644 --- a/services/asymkey/main_test.go +++ b/services/asymkey/main_test.go @@ -8,7 +8,6 @@ import ( "code.gitea.io/gitea/models/unittest" - _ "code.gitea.io/gitea/models" _ "code.gitea.io/gitea/models/actions" _ "code.gitea.io/gitea/models/activities" ) diff --git a/services/auth/auth.go b/services/auth/auth.go index 43ff95f05302e..5e186d7e69354 100644 --- a/services/auth/auth.go +++ b/services/auth/auth.go @@ -104,7 +104,7 @@ func handleSignIn(resp http.ResponseWriter, req *http.Request, sess SessionStore middleware.SetLocaleCookie(resp, user.Language, 0) // force to generate a new CSRF token - if ctx := gitea_context.GetWebContext(req); ctx != nil { + if ctx := gitea_context.GetWebContext(req.Context()); ctx != nil { ctx.Csrf.PrepareForSessionUser(ctx) } } diff --git a/services/auth/interface.go b/services/auth/interface.go index 275b4dd56ce07..ece28af12d1be 100644 --- a/services/auth/interface.go +++ b/services/auth/interface.go @@ -8,11 +8,12 @@ import ( "net/http" user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/modules/reqctx" "code.gitea.io/gitea/modules/session" + "code.gitea.io/gitea/modules/web/middleware" ) -type DataStore = reqctx.ContextDataProvider +// DataStore represents a data store +type DataStore middleware.ContextDataStore // SessionStore represents a session store type SessionStore session.Store diff --git a/services/auth/oauth2_test.go b/services/auth/oauth2_test.go index 0d9e793cf3a1a..b706847e8e1a1 100644 --- a/services/auth/oauth2_test.go +++ b/services/auth/oauth2_test.go @@ -9,7 +9,7 @@ import ( "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/modules/reqctx" + "code.gitea.io/gitea/modules/web/middleware" "code.gitea.io/gitea/services/actions" "github.com/stretchr/testify/assert" @@ -23,7 +23,7 @@ func TestUserIDFromToken(t *testing.T) { token, err := actions.CreateAuthorizationToken(RunningTaskID, 1, 2) assert.NoError(t, err) - ds := make(reqctx.ContextData) + ds := make(middleware.ContextData) o := OAuth2{} uid := o.userIDFromToken(context.Background(), token, ds) diff --git a/services/auth/sspi.go b/services/auth/sspi.go index 8ac83dcb044fc..0152f18c7480e 100644 --- a/services/auth/sspi.go +++ b/services/auth/sspi.go @@ -13,10 +13,10 @@ import ( "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/db" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/web/middleware" "code.gitea.io/gitea/services/auth/source/sspi" gitea_context "code.gitea.io/gitea/services/context" @@ -25,7 +25,7 @@ import ( ) const ( - tplSignIn templates.TplName = "user/auth/signin" + tplSignIn base.TplName = "user/auth/signin" ) type SSPIAuth interface { @@ -89,7 +89,7 @@ func (s *SSPI) Verify(req *http.Request, w http.ResponseWriter, store DataStore, store.GetData()["EnableSSPI"] = true // in this case, the Verify function is called in Gitea's web context // FIXME: it doesn't look good to render the page here, why not redirect? - gitea_context.GetWebContext(req).HTML(http.StatusUnauthorized, tplSignIn) + gitea_context.GetWebContext(req.Context()).HTML(http.StatusUnauthorized, tplSignIn) return nil, err } if outToken != "" { diff --git a/services/context/api.go b/services/context/api.go index 7b604c5ea15de..50ec0ee40b4a6 100644 --- a/services/context/api.go +++ b/services/context/api.go @@ -5,6 +5,7 @@ package context import ( + "context" "fmt" "net/http" "net/url" @@ -211,15 +212,17 @@ func (ctx *APIContext) SetLinkHeader(total, pageSize int) { func APIContexter() func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - base := NewBaseContext(w, req) + base, baseCleanUp := NewBaseContext(w, req) ctx := &APIContext{ Base: base, Cache: cache.GetCache(), Repo: &Repository{PullRequest: &PullRequest{}}, Org: &APIOrganization{}, } + defer baseCleanUp() - ctx.SetContextValue(apiContextKey, ctx) + ctx.Base.AppendContextValue(apiContextKey, ctx) + ctx.Base.AppendContextValueFunc(gitrepo.RepositoryContextKey, func() any { return ctx.Repo.GitRepo }) // If request sends files, parse them here otherwise the Query() can't be parsed and the CsrfToken will be invalid. if ctx.Req.Method == "POST" && strings.Contains(ctx.Req.Header.Get("Content-Type"), "multipart/form-data") { @@ -264,22 +267,31 @@ func (ctx *APIContext) NotFound(objs ...any) { // ReferencesGitRepo injects the GitRepo into the Context // you can optional skip the IsEmpty check -func ReferencesGitRepo(allowEmpty ...bool) func(ctx *APIContext) { - return func(ctx *APIContext) { +func ReferencesGitRepo(allowEmpty ...bool) func(ctx *APIContext) (cancel context.CancelFunc) { + return func(ctx *APIContext) (cancel context.CancelFunc) { // Empty repository does not have reference information. if ctx.Repo.Repository.IsEmpty && !(len(allowEmpty) != 0 && allowEmpty[0]) { - return + return nil } // For API calls. if ctx.Repo.GitRepo == nil { - var err error - ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx, ctx.Repo.Repository) + gitRepo, err := gitrepo.OpenRepository(ctx, ctx.Repo.Repository) if err != nil { ctx.Error(http.StatusInternalServerError, fmt.Sprintf("Open Repository %v failed", ctx.Repo.Repository.FullName()), err) - return + return cancel + } + ctx.Repo.GitRepo = gitRepo + // We opened it, we should close it + return func() { + // If it's been set to nil then assume someone else has closed it. + if ctx.Repo.GitRepo != nil { + _ = ctx.Repo.GitRepo.Close() + } } } + + return cancel } } @@ -293,8 +305,7 @@ func RepoRefForAPI(next http.Handler) http.Handler { return } - // NOTICE: the "ref" here for internal usage only (e.g. woodpecker) - refName, _ := getRefNameLegacy(ctx.Base, ctx.Repo, ctx.FormTrim("ref")) + refName, _ := getRefNameLegacy(ctx.Base, ctx.Repo, ctx.PathParam("*"), ctx.FormTrim("ref")) var err error if ctx.Repo.GitRepo.IsBranchExist(refName) { diff --git a/services/context/base.go b/services/context/base.go index 7a39353e09e5d..d62709558436e 100644 --- a/services/context/base.go +++ b/services/context/base.go @@ -9,36 +9,81 @@ import ( "html/template" "io" "net/http" + "net/url" + "strconv" "strings" + "time" "code.gitea.io/gitea/modules/httplib" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/reqctx" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/translation" "code.gitea.io/gitea/modules/web/middleware" + + "github.com/go-chi/chi/v5" ) +type contextValuePair struct { + key any + valueFn func() any +} + type BaseContextKeyType struct{} var BaseContextKey BaseContextKeyType type Base struct { - context.Context - reqctx.RequestDataStore + originCtx context.Context + contextValues []contextValuePair Resp ResponseWriter Req *http.Request // Data is prepared by ContextDataStore middleware, this field only refers to the pre-created/prepared ContextData. // Although it's mainly used for MVC templates, sometimes it's also used to pass data between middlewares/handler - Data reqctx.ContextData + Data middleware.ContextData // Locale is mainly for Web context, although the API context also uses it in some cases: message response, form validation Locale translation.Locale } +func (b *Base) Deadline() (deadline time.Time, ok bool) { + return b.originCtx.Deadline() +} + +func (b *Base) Done() <-chan struct{} { + return b.originCtx.Done() +} + +func (b *Base) Err() error { + return b.originCtx.Err() +} + +func (b *Base) Value(key any) any { + for _, pair := range b.contextValues { + if pair.key == key { + return pair.valueFn() + } + } + return b.originCtx.Value(key) +} + +func (b *Base) AppendContextValueFunc(key any, valueFn func() any) any { + b.contextValues = append(b.contextValues, contextValuePair{key, valueFn}) + return b +} + +func (b *Base) AppendContextValue(key, value any) any { + b.contextValues = append(b.contextValues, contextValuePair{key, func() any { return value }}) + return b +} + +func (b *Base) GetData() middleware.ContextData { + return b.Data +} + // AppendAccessControlExposeHeaders append headers by name to "Access-Control-Expose-Headers" header func (b *Base) AppendAccessControlExposeHeaders(names ...string) { val := b.RespHeader().Get("Access-Control-Expose-Headers") @@ -102,6 +147,93 @@ func (b *Base) RemoteAddr() string { return b.Req.RemoteAddr } +// PathParam returns the param in request path, eg: "/{var}" => "/a%2fb", then `var == "a/b"` +func (b *Base) PathParam(name string) string { + s, err := url.PathUnescape(b.PathParamRaw(name)) + if err != nil && !setting.IsProd { + panic("Failed to unescape path param: " + err.Error() + ", there seems to be a double-unescaping bug") + } + return s +} + +// PathParamRaw returns the raw param in request path, eg: "/{var}" => "/a%2fb", then `var == "a%2fb"` +func (b *Base) PathParamRaw(name string) string { + return chi.URLParam(b.Req, strings.TrimPrefix(name, ":")) +} + +// PathParamInt64 returns the param in request path as int64 +func (b *Base) PathParamInt64(p string) int64 { + v, _ := strconv.ParseInt(b.PathParam(p), 10, 64) + return v +} + +// SetPathParam set request path params into routes +func (b *Base) SetPathParam(k, v string) { + chiCtx := chi.RouteContext(b) + chiCtx.URLParams.Add(strings.TrimPrefix(k, ":"), url.PathEscape(v)) +} + +// FormString returns the first value matching the provided key in the form as a string +func (b *Base) FormString(key string) string { + return b.Req.FormValue(key) +} + +// FormStrings returns a string slice for the provided key from the form +func (b *Base) FormStrings(key string) []string { + if b.Req.Form == nil { + if err := b.Req.ParseMultipartForm(32 << 20); err != nil { + return nil + } + } + if v, ok := b.Req.Form[key]; ok { + return v + } + return nil +} + +// FormTrim returns the first value for the provided key in the form as a space trimmed string +func (b *Base) FormTrim(key string) string { + return strings.TrimSpace(b.Req.FormValue(key)) +} + +// FormInt returns the first value for the provided key in the form as an int +func (b *Base) FormInt(key string) int { + v, _ := strconv.Atoi(b.Req.FormValue(key)) + return v +} + +// FormInt64 returns the first value for the provided key in the form as an int64 +func (b *Base) FormInt64(key string) int64 { + v, _ := strconv.ParseInt(b.Req.FormValue(key), 10, 64) + return v +} + +// FormBool returns true if the value for the provided key in the form is "1", "true" or "on" +func (b *Base) FormBool(key string) bool { + s := b.Req.FormValue(key) + v, _ := strconv.ParseBool(s) + v = v || strings.EqualFold(s, "on") + return v +} + +// FormOptionalBool returns an optional.Some(true) or optional.Some(false) if the value +// for the provided key exists in the form else it returns optional.None[bool]() +func (b *Base) FormOptionalBool(key string) optional.Option[bool] { + value := b.Req.FormValue(key) + if len(value) == 0 { + return optional.None[bool]() + } + s := b.Req.FormValue(key) + v, _ := strconv.ParseBool(s) + v = v || strings.EqualFold(s, "on") + return optional.Some(v) +} + +func (b *Base) SetFormString(key, value string) { + _ = b.Req.FormValue(key) // force parse form + b.Req.Form.Set(key, value) +} + // PlainTextBytes renders bytes as plain text func (b *Base) plainTextInternal(skip, status int, bs []byte) { statusPrefix := status / 100 @@ -163,6 +295,13 @@ func (b *Base) ServeContent(r io.ReadSeeker, opts *ServeHeaderOptions) { http.ServeContent(b.Resp, b.Req, opts.Filename, opts.LastModified, r) } +// Close frees all resources hold by Context +func (b *Base) cleanUp() { + if b.Req != nil && b.Req.MultipartForm != nil { + _ = b.Req.MultipartForm.RemoveAll() // remove the temp files buffered to tmp directory + } +} + func (b *Base) Tr(msg string, args ...any) template.HTML { return b.Locale.Tr(msg, args...) } @@ -171,28 +310,17 @@ func (b *Base) TrN(cnt any, key1, keyN string, args ...any) template.HTML { return b.Locale.TrN(cnt, key1, keyN, args...) } -func NewBaseContext(resp http.ResponseWriter, req *http.Request) *Base { - ds := reqctx.GetRequestDataStore(req.Context()) - b := &Base{ - Context: req.Context(), - RequestDataStore: ds, - Req: req, - Resp: WrapResponseWriter(resp), - Locale: middleware.Locale(resp, req), - Data: ds.GetData(), +func NewBaseContext(resp http.ResponseWriter, req *http.Request) (b *Base, closeFunc func()) { + b = &Base{ + originCtx: req.Context(), + Req: req, + Resp: WrapResponseWriter(resp), + Locale: middleware.Locale(resp, req), + Data: middleware.GetContextData(req.Context()), } b.Req = b.Req.WithContext(b) - ds.SetContextValue(BaseContextKey, b) - ds.SetContextValue(translation.ContextKey, b.Locale) - ds.SetContextValue(httplib.RequestContextKey, b.Req) - return b -} - -func NewBaseContextForTest(resp http.ResponseWriter, req *http.Request) *Base { - if !setting.IsInTesting { - panic("This function is only for testing") - } - ctx := reqctx.NewRequestContextForTest(req.Context()) - *req = *req.WithContext(ctx) - return NewBaseContext(resp, req) + b.AppendContextValue(BaseContextKey, b) + b.AppendContextValue(translation.ContextKey, b.Locale) + b.AppendContextValue(httplib.RequestContextKey, b.Req) + return b, b.cleanUp } diff --git a/services/context/base_form.go b/services/context/base_form.go deleted file mode 100644 index ddf9734f5777d..0000000000000 --- a/services/context/base_form.go +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright 2024 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package context - -import ( - "strconv" - "strings" - - "code.gitea.io/gitea/modules/optional" -) - -// FormString returns the first value matching the provided key in the form as a string -func (b *Base) FormString(key string) string { - return b.Req.FormValue(key) -} - -// FormStrings returns a string slice for the provided key from the form -func (b *Base) FormStrings(key string) []string { - if b.Req.Form == nil { - if err := b.Req.ParseMultipartForm(32 << 20); err != nil { - return nil - } - } - if v, ok := b.Req.Form[key]; ok { - return v - } - return nil -} - -// FormTrim returns the first value for the provided key in the form as a space trimmed string -func (b *Base) FormTrim(key string) string { - return strings.TrimSpace(b.Req.FormValue(key)) -} - -// FormInt returns the first value for the provided key in the form as an int -func (b *Base) FormInt(key string) int { - v, _ := strconv.Atoi(b.Req.FormValue(key)) - return v -} - -// FormInt64 returns the first value for the provided key in the form as an int64 -func (b *Base) FormInt64(key string) int64 { - v, _ := strconv.ParseInt(b.Req.FormValue(key), 10, 64) - return v -} - -// FormBool returns true if the value for the provided key in the form is "1", "true" or "on" -func (b *Base) FormBool(key string) bool { - s := b.Req.FormValue(key) - v, _ := strconv.ParseBool(s) - v = v || strings.EqualFold(s, "on") - return v -} - -// FormOptionalBool returns an optional.Some(true) or optional.Some(false) if the value -// for the provided key exists in the form else it returns optional.None[bool]() -func (b *Base) FormOptionalBool(key string) optional.Option[bool] { - value := b.Req.FormValue(key) - if len(value) == 0 { - return optional.None[bool]() - } - s := b.Req.FormValue(key) - v, _ := strconv.ParseBool(s) - v = v || strings.EqualFold(s, "on") - return optional.Some(v) -} - -func (b *Base) SetFormString(key, value string) { - _ = b.Req.FormValue(key) // force parse form - b.Req.Form.Set(key, value) -} diff --git a/services/context/base_path.go b/services/context/base_path.go deleted file mode 100644 index 3678deaff98f5..0000000000000 --- a/services/context/base_path.go +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright 2024 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package context - -import ( - "net/url" - "strconv" - "strings" - - "code.gitea.io/gitea/modules/setting" - - "github.com/go-chi/chi/v5" -) - -// PathParam returns the param in request path, eg: "/{var}" => "/a%2fb", then `var == "a/b"` -func (b *Base) PathParam(name string) string { - s, err := url.PathUnescape(b.PathParamRaw(name)) - if err != nil && !setting.IsProd { - panic("Failed to unescape path param: " + err.Error() + ", there seems to be a double-unescaping bug") - } - return s -} - -// PathParamRaw returns the raw param in request path, eg: "/{var}" => "/a%2fb", then `var == "a%2fb"` -func (b *Base) PathParamRaw(name string) string { - if strings.HasPrefix(name, ":") { - setting.PanicInDevOrTesting("path param should not start with ':'") - name = name[1:] - } - return chi.URLParam(b.Req, name) -} - -// PathParamInt64 returns the param in request path as int64 -func (b *Base) PathParamInt64(p string) int64 { - v, _ := strconv.ParseInt(b.PathParam(p), 10, 64) - return v -} - -// SetPathParam set request path params into routes -func (b *Base) SetPathParam(name, value string) { - if strings.HasPrefix(name, ":") { - setting.PanicInDevOrTesting("path param should not start with ':'") - name = name[1:] - } - chi.RouteContext(b).URLParams.Add(name, url.PathEscape(value)) -} diff --git a/services/context/base_test.go b/services/context/base_test.go index b936b76f58b30..823f20e00bc0d 100644 --- a/services/context/base_test.go +++ b/services/context/base_test.go @@ -14,7 +14,6 @@ import ( ) func TestRedirect(t *testing.T) { - setting.IsInTesting = true req, _ := http.NewRequest("GET", "/", nil) cases := []struct { @@ -29,9 +28,10 @@ func TestRedirect(t *testing.T) { } for _, c := range cases { resp := httptest.NewRecorder() - b := NewBaseContextForTest(resp, req) + b, cleanup := NewBaseContext(resp, req) resp.Header().Add("Set-Cookie", (&http.Cookie{Name: setting.SessionConfig.CookieName, Value: "dummy"}).String()) b.Redirect(c.url) + cleanup() has := resp.Header().Get("Set-Cookie") == "i_like_gitea=dummy" assert.Equal(t, c.keep, has, "url = %q", c.url) } @@ -39,8 +39,9 @@ func TestRedirect(t *testing.T) { req, _ = http.NewRequest("GET", "/", nil) resp := httptest.NewRecorder() req.Header.Add("HX-Request", "true") - b := NewBaseContextForTest(resp, req) + b, cleanup := NewBaseContext(resp, req) b.Redirect("/other") + cleanup() assert.Equal(t, "/other", resp.Header().Get("HX-Redirect")) assert.Equal(t, http.StatusNoContent, resp.Code) } diff --git a/services/context/captcha.go b/services/context/captcha.go index 9272e7a65ac1b..41afe0e7d25a6 100644 --- a/services/context/captcha.go +++ b/services/context/captcha.go @@ -7,13 +7,13 @@ import ( "fmt" "sync" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/cache" "code.gitea.io/gitea/modules/hcaptcha" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/mcaptcha" "code.gitea.io/gitea/modules/recaptcha" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/turnstile" "gitea.com/go-chi/captcha" @@ -60,7 +60,7 @@ const ( // VerifyCaptcha verifies Captcha data // No-op if captchas are not enabled -func VerifyCaptcha(ctx *Context, tpl templates.TplName, form any) { +func VerifyCaptcha(ctx *Context, tpl base.TplName, form any) { if !setting.Service.EnableCaptcha { return } diff --git a/services/context/context.go b/services/context/context.go index 6715c5663dc29..2f6a465740570 100644 --- a/services/context/context.go +++ b/services/context/context.go @@ -18,6 +18,7 @@ import ( "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/cache" + "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/httpcache" "code.gitea.io/gitea/modules/session" "code.gitea.io/gitea/modules/setting" @@ -31,7 +32,7 @@ import ( // Render represents a template render type Render interface { TemplateLookup(tmpl string, templateCtx context.Context) (templates.TemplateExecutor, error) - HTML(w io.Writer, status int, name templates.TplName, data any, templateCtx context.Context) error + HTML(w io.Writer, status int, name string, data any, templateCtx context.Context) error } // Context represents context of a request. @@ -76,9 +77,9 @@ type webContextKeyType struct{} var WebContextKey = webContextKeyType{} -func GetWebContext(req *http.Request) *Context { - ctx, _ := req.Context().Value(WebContextKey).(*Context) - return ctx +func GetWebContext(ctx context.Context) *Context { + webCtx, _ := ctx.Value(WebContextKey).(*Context) + return webCtx } // ValidateContext is a special context for form validation middleware. It may be different from other contexts. @@ -132,6 +133,7 @@ func NewWebContext(base *Base, render Render, session session.Store) *Context { } ctx.TemplateContext = NewTemplateContextForWeb(ctx) ctx.Flash = &middleware.Flash{DataStore: ctx, Values: url.Values{}} + ctx.AppendContextValue(WebContextKey, ctx) return ctx } @@ -152,9 +154,14 @@ func Contexter() func(next http.Handler) http.Handler { } return func(next http.Handler) http.Handler { return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { - base := NewBaseContext(resp, req) + base, baseCleanUp := NewBaseContext(resp, req) + defer baseCleanUp() ctx := NewWebContext(base, rnd, session.GetContextSession(req)) + ctx.Data.MergeFrom(middleware.CommonTemplateContextData()) + if setting.IsProd && !setting.IsInTesting { + ctx.Data["Context"] = ctx // TODO: use "ctx" in template and remove this + } ctx.Data["CurrentURL"] = setting.AppSubURL + req.URL.RequestURI() ctx.Data["Link"] = ctx.Link @@ -162,7 +169,9 @@ func Contexter() func(next http.Handler) http.Handler { ctx.PageData = map[string]any{} ctx.Data["PageData"] = ctx.PageData - ctx.Base.SetContextValue(WebContextKey, ctx) + ctx.Base.AppendContextValue(WebContextKey, ctx) + ctx.Base.AppendContextValueFunc(gitrepo.RepositoryContextKey, func() any { return ctx.Repo.GitRepo }) + ctx.Csrf = NewCSRFProtector(csrfOpts) // Get the last flash message from cookie diff --git a/services/context/context_response.go b/services/context/context_response.go index c7044791eb6e2..14a31dad84540 100644 --- a/services/context/context_response.go +++ b/services/context/context_response.go @@ -17,6 +17,7 @@ import ( "time" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/httplib" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" @@ -62,10 +63,10 @@ func (ctx *Context) RedirectToCurrentSite(location ...string) { ctx.Redirect(setting.AppSubURL + "/") } -const tplStatus500 templates.TplName = "status/500" +const tplStatus500 base.TplName = "status/500" // HTML calls Context.HTML and renders the template to HTTP response -func (ctx *Context) HTML(status int, name templates.TplName) { +func (ctx *Context) HTML(status int, name base.TplName) { log.Debug("Template: %s", name) tmplStartTime := time.Now() @@ -76,7 +77,7 @@ func (ctx *Context) HTML(status int, name templates.TplName) { return strconv.FormatInt(time.Since(tmplStartTime).Nanoseconds()/1e6, 10) + "ms" } - err := ctx.Render.HTML(ctx.Resp, status, name, ctx.Data, ctx.TemplateContext) + err := ctx.Render.HTML(ctx.Resp, status, string(name), ctx.Data, ctx.TemplateContext) if err == nil || errors.Is(err, syscall.EPIPE) { return } @@ -93,7 +94,7 @@ func (ctx *Context) HTML(status int, name templates.TplName) { // JSONTemplate renders the template as JSON response // keep in mind that the template is processed in HTML context, so JSON-things should be handled carefully, eg: by JSEscape -func (ctx *Context) JSONTemplate(tmpl templates.TplName) { +func (ctx *Context) JSONTemplate(tmpl base.TplName) { t, err := ctx.Render.TemplateLookup(string(tmpl), nil) if err != nil { ctx.ServerError("unable to find template", err) @@ -106,14 +107,14 @@ func (ctx *Context) JSONTemplate(tmpl templates.TplName) { } // RenderToHTML renders the template content to a HTML string -func (ctx *Context) RenderToHTML(name templates.TplName, data any) (template.HTML, error) { +func (ctx *Context) RenderToHTML(name base.TplName, data map[string]any) (template.HTML, error) { var buf strings.Builder - err := ctx.Render.HTML(&buf, 0, name, data, ctx.TemplateContext) + err := ctx.Render.HTML(&buf, 0, string(name), data, ctx.TemplateContext) return template.HTML(buf.String()), err } // RenderWithErr used for page has form validation but need to prompt error to users. -func (ctx *Context) RenderWithErr(msg any, tpl templates.TplName, form any) { +func (ctx *Context) RenderWithErr(msg any, tpl base.TplName, form any) { if form != nil { middleware.AssignForm(form, ctx.Data) } @@ -150,7 +151,7 @@ func (ctx *Context) notFoundInternal(logMsg string, logErr error) { ctx.Data["IsRepo"] = ctx.Repo.Repository != nil ctx.Data["Title"] = "Page Not Found" - ctx.HTML(http.StatusNotFound, templates.TplName("status/404")) + ctx.HTML(http.StatusNotFound, "status/404") } // ServerError displays a 500 (Internal Server Error) page and prints the given error, if any. diff --git a/services/context/context_test.go b/services/context/context_test.go index 54044644f0721..984593398d439 100644 --- a/services/context/context_test.go +++ b/services/context/context_test.go @@ -26,7 +26,6 @@ func TestRemoveSessionCookieHeader(t *testing.T) { } func TestRedirectToCurrentSite(t *testing.T) { - setting.IsInTesting = true defer test.MockVariableValue(&setting.AppURL, "http://localhost:3000/sub/")() defer test.MockVariableValue(&setting.AppSubURL, "/sub")() cases := []struct { @@ -41,7 +40,8 @@ func TestRedirectToCurrentSite(t *testing.T) { t.Run(c.location, func(t *testing.T) { req := &http.Request{URL: &url.URL{Path: "/"}} resp := httptest.NewRecorder() - base := NewBaseContextForTest(resp, req) + base, baseCleanUp := NewBaseContext(resp, req) + defer baseCleanUp() ctx := NewWebContext(base, nil, nil) ctx.RedirectToCurrentSite(c.location) redirect := test.RedirectURL(resp) diff --git a/services/context/org.go b/services/context/org.go index be87cef7a316f..bf482fa7543ce 100644 --- a/services/context/org.go +++ b/services/context/org.go @@ -40,7 +40,7 @@ func (org *Organization) CanReadUnit(ctx *Context, unitType unit.Type) bool { } func GetOrganizationByParams(ctx *Context) { - orgName := ctx.PathParam("org") + orgName := ctx.PathParam(":org") var err error @@ -220,7 +220,7 @@ func HandleOrgAssignment(ctx *Context, args ...bool) { ctx.Data["NumTeams"] = len(ctx.Org.Teams) } - teamName := ctx.PathParam("team") + teamName := ctx.PathParam(":team") if len(teamName) > 0 { teamExists := false for _, team := range ctx.Org.Teams { diff --git a/services/context/package.go b/services/context/package.go index e98e01acbb0cb..33855b1101292 100644 --- a/services/context/package.go +++ b/services/context/package.go @@ -93,7 +93,7 @@ func packageAssignment(ctx *packageAssignmentCtx, errCb func(int, string, any)) } func determineAccessMode(ctx *Base, pkg *Package, doer *user_model.User) (perm.AccessMode, error) { - if setting.Service.RequireSignInView && (doer == nil || doer.IsGhost()) { + if setting.Service.RequireSignInViewStrict && (doer == nil || doer.IsGhost()) { return perm.AccessModeNone, nil } @@ -153,10 +153,12 @@ func PackageContexter() func(next http.Handler) http.Handler { renderer := templates.HTMLRenderer() return func(next http.Handler) http.Handler { return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { - base := NewBaseContext(resp, req) + base, baseCleanUp := NewBaseContext(resp, req) + defer baseCleanUp() + // it is still needed when rendering 500 page in a package handler ctx := NewWebContext(base, renderer, nil) - ctx.SetContextValue(WebContextKey, ctx) + ctx.Base.AppendContextValue(WebContextKey, ctx) next.ServeHTTP(ctx.Resp, ctx.Req) }) } diff --git a/services/context/private.go b/services/context/private.go index 51857da8fe5d5..8b41949f604e1 100644 --- a/services/context/private.go +++ b/services/context/private.go @@ -64,9 +64,11 @@ func GetPrivateContext(req *http.Request) *PrivateContext { func PrivateContexter() func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - base := NewBaseContext(w, req) + base, baseCleanUp := NewBaseContext(w, req) ctx := &PrivateContext{Base: base} - ctx.SetContextValue(privateContextKey, ctx) + defer baseCleanUp() + ctx.Base.AppendContextValue(privateContextKey, ctx) + next.ServeHTTP(ctx.Resp, ctx.Req) }) } @@ -76,15 +78,8 @@ func PrivateContexter() func(http.Handler) http.Handler { // This function should be used when there is a need for work to continue even if the request has been cancelled. // Primarily this affects hook/post-receive and hook/proc-receive both of which need to continue working even if // the underlying request has timed out from the ssh/http push -func OverrideContext() func(http.Handler) http.Handler { - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - // We now need to override the request context as the base for our work because even if the request is cancelled we have to continue this work - ctx := GetPrivateContext(req) - var finished func() - ctx.Override, _, finished = process.GetManager().AddTypedContext(graceful.GetManager().HammerContext(), fmt.Sprintf("PrivateContext: %s", ctx.Req.RequestURI), process.RequestProcessType, true) - defer finished() - next.ServeHTTP(ctx.Resp, ctx.Req) - }) - } +func OverrideContext(ctx *PrivateContext) (cancel context.CancelFunc) { + // We now need to override the request context as the base for our work because even if the request is cancelled we have to continue this work + ctx.Override, _, cancel = process.GetManager().AddTypedContext(graceful.GetManager().HammerContext(), fmt.Sprintf("PrivateContext: %s", ctx.Req.RequestURI), process.RequestProcessType, true) + return cancel } diff --git a/services/context/repo.go b/services/context/repo.go index b537a050362c5..da6731df5a211 100644 --- a/services/context/repo.go +++ b/services/context/repo.go @@ -14,6 +14,7 @@ import ( "path" "strings" + "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/db" git_model "code.gitea.io/gitea/models/git" issues_model "code.gitea.io/gitea/models/issues" @@ -316,8 +317,8 @@ func ComposeGoGetImport(ctx context.Context, owner, repo string) string { // This is particular a workaround for "go get" command which does not respect // .netrc file. func EarlyResponseForGoGetMeta(ctx *Context) { - username := ctx.PathParam("username") - reponame := strings.TrimSuffix(ctx.PathParam("reponame"), ".git") + username := ctx.PathParam(":username") + reponame := strings.TrimSuffix(ctx.PathParam(":reponame"), ".git") if username == "" || reponame == "" { ctx.PlainText(http.StatusBadRequest, "invalid repository path") return @@ -336,8 +337,8 @@ func EarlyResponseForGoGetMeta(ctx *Context) { // RedirectToRepo redirect to a differently-named repository func RedirectToRepo(ctx *Base, redirectRepoID int64) { - ownerName := ctx.PathParam("username") - previousRepoName := ctx.PathParam("reponame") + ownerName := ctx.PathParam(":username") + previousRepoName := ctx.PathParam(":reponame") repo, err := repo_model.GetRepositoryByID(ctx, redirectRepoID) if err != nil { @@ -355,7 +356,9 @@ func RedirectToRepo(ctx *Base, redirectRepoID int64) { if ctx.Req.URL.RawQuery != "" { redirectPath += "?" + ctx.Req.URL.RawQuery } - ctx.Redirect(path.Join(setting.AppSubURL, redirectPath), http.StatusTemporaryRedirect) + // Git client needs a 301 redirect by default to follow the new location + // It's not documentated in git documentation, but it's the behavior of git client + ctx.Redirect(path.Join(setting.AppSubURL, redirectPath), http.StatusMovedPermanently) } func repoAssignment(ctx *Context, repo *repo_model.Repository) { @@ -397,13 +400,11 @@ func repoAssignment(ctx *Context, repo *repo_model.Repository) { } // RepoAssignment returns a middleware to handle repository assignment -func RepoAssignment(ctx *Context) { +func RepoAssignment(ctx *Context) context.CancelFunc { if _, repoAssignmentOnce := ctx.Data["repoAssignmentExecuted"]; repoAssignmentOnce { // FIXME: it should panic in dev/test modes to have a clear behavior - if !setting.IsProd || setting.IsInTesting { - panic("RepoAssignment should not be executed twice") - } - return + log.Trace("RepoAssignment was exec already, skipping second call ...") + return nil } ctx.Data["repoAssignmentExecuted"] = true @@ -412,8 +413,8 @@ func RepoAssignment(ctx *Context) { err error ) - userName := ctx.PathParam("username") - repoName := ctx.PathParam("reponame") + userName := ctx.PathParam(":username") + repoName := ctx.PathParam(":reponame") repoName = strings.TrimSuffix(repoName, ".git") if setting.Other.EnableFeed { repoName = strings.TrimSuffix(repoName, ".rss") @@ -431,7 +432,7 @@ func RepoAssignment(ctx *Context) { // https://github.com/golang/go/issues/19760 if ctx.FormString("go-get") == "1" { EarlyResponseForGoGetMeta(ctx) - return + return nil } if redirectUserID, err := user_model.LookupUserRedirect(ctx, userName); err == nil { @@ -444,7 +445,7 @@ func RepoAssignment(ctx *Context) { } else { ctx.ServerError("GetUserByName", err) } - return + return nil } } ctx.Repo.Owner = owner @@ -456,7 +457,7 @@ func RepoAssignment(ctx *Context) { if strings.HasSuffix(repoName, ".wiki") { // ctx.Req.URL.Path does not have the preceding appSubURL - any redirect must have this added // Now we happen to know that all of our paths are: /:username/:reponame/whatever_else - originalRepoName := ctx.PathParam("reponame") + originalRepoName := ctx.PathParam(":reponame") redirectRepoName := strings.TrimSuffix(repoName, ".wiki") redirectRepoName += originalRepoName[len(redirectRepoName)+5:] redirectPath := strings.Replace( @@ -469,7 +470,7 @@ func RepoAssignment(ctx *Context) { redirectPath += "?" + ctx.Req.URL.RawQuery } ctx.Redirect(path.Join(setting.AppSubURL, redirectPath)) - return + return nil } // Get repository. @@ -482,7 +483,7 @@ func RepoAssignment(ctx *Context) { } else if repo_model.IsErrRedirectNotExist(err) { if ctx.FormString("go-get") == "1" { EarlyResponseForGoGetMeta(ctx) - return + return nil } ctx.NotFound("GetRepositoryByName", nil) } else { @@ -491,13 +492,13 @@ func RepoAssignment(ctx *Context) { } else { ctx.ServerError("GetRepositoryByName", err) } - return + return nil } repo.Owner = owner repoAssignment(ctx, repo) if ctx.Written() { - return + return nil } ctx.Repo.RepoLink = repo.Link() @@ -522,7 +523,7 @@ func RepoAssignment(ctx *Context) { }) if err != nil { ctx.ServerError("GetReleaseCountByRepoID", err) - return + return nil } ctx.Data["NumReleases"], err = db.Count[repo_model.Release](ctx, repo_model.FindReleasesOptions{ // only show draft releases for users who can write, read-only users shouldn't see draft releases. @@ -531,7 +532,7 @@ func RepoAssignment(ctx *Context) { }) if err != nil { ctx.ServerError("GetReleaseCountByRepoID", err) - return + return nil } ctx.Data["Title"] = owner.Name + "/" + repo.Name @@ -548,14 +549,14 @@ func RepoAssignment(ctx *Context) { canSignedUserFork, err := repo_module.CanUserForkRepo(ctx, ctx.Doer, ctx.Repo.Repository) if err != nil { ctx.ServerError("CanUserForkRepo", err) - return + return nil } ctx.Data["CanSignedUserFork"] = canSignedUserFork userAndOrgForks, err := repo_model.GetForksByUserAndOrgs(ctx, ctx.Doer, ctx.Repo.Repository) if err != nil { ctx.ServerError("GetForksByUserAndOrgs", err) - return + return nil } ctx.Data["UserAndOrgForks"] = userAndOrgForks @@ -589,14 +590,14 @@ func RepoAssignment(ctx *Context) { if repo.IsFork { RetrieveBaseRepo(ctx, repo) if ctx.Written() { - return + return nil } } if repo.IsGenerated() { RetrieveTemplateRepo(ctx, repo) if ctx.Written() { - return + return nil } } @@ -611,18 +612,10 @@ func RepoAssignment(ctx *Context) { if !isHomeOrSettings { ctx.Redirect(ctx.Repo.RepoLink) } - return - } - - if ctx.Repo.GitRepo != nil { - if !setting.IsProd || setting.IsInTesting { - panic("RepoAssignment: GitRepo should be nil") - } - _ = ctx.Repo.GitRepo.Close() - ctx.Repo.GitRepo = nil + return nil } - ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx, repo) + gitRepo, err := gitrepo.OpenRepository(ctx, repo) if err != nil { if strings.Contains(err.Error(), "repository does not exist") || strings.Contains(err.Error(), "no such file or directory") { log.Error("Repository %-v has a broken repository on the file system: %s Error: %v", ctx.Repo.Repository, ctx.Repo.Repository.RepoPath(), err) @@ -632,16 +625,28 @@ func RepoAssignment(ctx *Context) { if !isHomeOrSettings { ctx.Redirect(ctx.Repo.RepoLink) } - return + return nil } ctx.ServerError("RepoAssignment Invalid repo "+repo.FullName(), err) - return + return nil + } + if ctx.Repo.GitRepo != nil { + ctx.Repo.GitRepo.Close() + } + ctx.Repo.GitRepo = gitRepo + + // We opened it, we should close it + cancel := func() { + // If it's been set to nil then assume someone else has closed it. + if ctx.Repo.GitRepo != nil { + ctx.Repo.GitRepo.Close() + } } // Stop at this point when the repo is empty. if ctx.Repo.Repository.IsEmpty { ctx.Data["BranchName"] = ctx.Repo.Repository.DefaultBranch - return + return cancel } branchOpts := git_model.FindBranchOptions{ @@ -652,7 +657,7 @@ func RepoAssignment(ctx *Context) { branchesTotal, err := db.Count[git_model.Branch](ctx, branchOpts) if err != nil { ctx.ServerError("CountBranches", err) - return + return cancel } // non-empty repo should have at least 1 branch, so this repository's branches haven't been synced yet @@ -660,7 +665,7 @@ func RepoAssignment(ctx *Context) { branchesTotal, err = repo_module.SyncRepoBranches(ctx, ctx.Repo.Repository.ID, 0) if err != nil { ctx.ServerError("SyncRepoBranches", err) - return + return cancel } } @@ -668,7 +673,7 @@ func RepoAssignment(ctx *Context) { // If no branch is set in the request URL, try to guess a default one. if len(ctx.Repo.BranchName) == 0 { - if len(ctx.Repo.Repository.DefaultBranch) > 0 && ctx.Repo.GitRepo.IsBranchExist(ctx.Repo.Repository.DefaultBranch) { + if len(ctx.Repo.Repository.DefaultBranch) > 0 && gitRepo.IsBranchExist(ctx.Repo.Repository.DefaultBranch) { ctx.Repo.BranchName = ctx.Repo.Repository.DefaultBranch } else { ctx.Repo.BranchName, _ = gitrepo.GetDefaultBranch(ctx, ctx.Repo.Repository) @@ -706,15 +711,15 @@ func RepoAssignment(ctx *Context) { ctx.Data["PullRequestCtx"] = ctx.Repo.PullRequest if ctx.Repo.Repository.Status == repo_model.RepositoryPendingTransfer { - repoTransfer, err := repo_model.GetPendingRepositoryTransfer(ctx, ctx.Repo.Repository) + repoTransfer, err := models.GetPendingRepositoryTransfer(ctx, ctx.Repo.Repository) if err != nil { ctx.ServerError("GetPendingRepositoryTransfer", err) - return + return cancel } if err := repoTransfer.LoadAttributes(ctx); err != nil { ctx.ServerError("LoadRecipient", err) - return + return cancel } ctx.Data["RepoTransfer"] = repoTransfer @@ -729,6 +734,7 @@ func RepoAssignment(ctx *Context) { ctx.Data["GoDocDirectory"] = fullURLPrefix + "{/dir}" ctx.Data["GoDocFile"] = fullURLPrefix + "{/dir}/{file}#L{line}" } + return cancel } // RepoRefType type of repo reference @@ -747,7 +753,7 @@ const headRefName = "HEAD" // RepoRef handles repository reference names when the ref name is not // explicitly given -func RepoRef() func(*Context) { +func RepoRef() func(*Context) context.CancelFunc { // since no ref name is explicitly specified, ok to just use branch return RepoRefByType(RepoRefBranch) } @@ -765,35 +771,30 @@ func getRefNameFromPath(repo *Repository, path string, isExist func(string) bool return "" } -func getRefNameLegacy(ctx *Base, repo *Repository, optionalExtraRef ...string) (string, RepoRefType) { - extraRef := util.OptionalArg(optionalExtraRef) - reqPath := ctx.PathParam("*") - reqPath = path.Join(extraRef, reqPath) - - if refName := getRefName(ctx, repo, RepoRefBranch); refName != "" { +func getRefNameLegacy(ctx *Base, repo *Repository, reqPath, extraRef string) (string, RepoRefType) { + reqRefPath := path.Join(extraRef, reqPath) + reqRefPathParts := strings.Split(reqRefPath, "/") + if refName := getRefName(ctx, repo, reqRefPath, RepoRefBranch); refName != "" { return refName, RepoRefBranch } - if refName := getRefName(ctx, repo, RepoRefTag); refName != "" { + if refName := getRefName(ctx, repo, reqRefPath, RepoRefTag); refName != "" { return refName, RepoRefTag } - - // For legacy support only full commit sha - parts := strings.Split(reqPath, "/") - if git.IsStringLikelyCommitID(git.ObjectFormatFromName(repo.Repository.ObjectFormatName), parts[0]) { + if git.IsStringLikelyCommitID(git.ObjectFormatFromName(repo.Repository.ObjectFormatName), reqRefPathParts[0]) { // FIXME: this logic is different from other types. Ideally, it should also try to GetCommit to check if it exists - repo.TreePath = strings.Join(parts[1:], "/") - return parts[0], RepoRefCommit + repo.TreePath = strings.Join(reqRefPathParts[1:], "/") + return reqRefPathParts[0], RepoRefCommit } - - if refName := getRefName(ctx, repo, RepoRefBlob); len(refName) > 0 { + if refName := getRefName(ctx, repo, reqPath, RepoRefBlob); refName != "" { return refName, RepoRefBlob } + // FIXME: the old code falls back to default branch if "ref" doesn't exist, there could be an edge case: + // "README?ref=no-such" would read the README file from the default branch, but the user might expect a 404 repo.TreePath = reqPath return repo.Repository.DefaultBranch, RepoRefBranch } -func getRefName(ctx *Base, repo *Repository, pathType RepoRefType) string { - path := ctx.PathParam("*") +func getRefName(ctx *Base, repo *Repository, path string, pathType RepoRefType) string { switch pathType { case RepoRefBranch: ref := getRefNameFromPath(repo, path, repo.GitRepo.IsBranchExist) @@ -862,9 +863,9 @@ type RepoRefByTypeOptions struct { // RepoRefByType handles repository reference name for a specific type // of repository reference -func RepoRefByType(detectRefType RepoRefType, opts ...RepoRefByTypeOptions) func(*Context) { +func RepoRefByType(detectRefType RepoRefType, opts ...RepoRefByTypeOptions) func(*Context) context.CancelFunc { opt := util.OptionalArg(opts) - return func(ctx *Context) { + return func(ctx *Context) (cancel context.CancelFunc) { refType := detectRefType // Empty repository does not have reference information. if ctx.Repo.Repository.IsEmpty { @@ -872,7 +873,7 @@ func RepoRefByType(detectRefType RepoRefType, opts ...RepoRefByTypeOptions) func ctx.Repo.IsViewBranch = true ctx.Repo.BranchName = ctx.Repo.Repository.DefaultBranch ctx.Data["TreePath"] = "" - return + return nil } var ( @@ -881,15 +882,23 @@ func RepoRefByType(detectRefType RepoRefType, opts ...RepoRefByTypeOptions) func ) if ctx.Repo.GitRepo == nil { - ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx, ctx.Repo.Repository) + ctx.Repo.GitRepo, err = gitrepo.OpenRepository(ctx, ctx.Repo.Repository) if err != nil { ctx.ServerError(fmt.Sprintf("Open Repository %v failed", ctx.Repo.Repository.FullName()), err) - return + return nil + } + // We opened it, we should close it + cancel = func() { + // If it's been set to nil then assume someone else has closed it. + if ctx.Repo.GitRepo != nil { + ctx.Repo.GitRepo.Close() + } } } // Get default branch. - if len(ctx.PathParam("*")) == 0 { + reqPath := ctx.PathParam("*") + if reqPath == "" { refName = ctx.Repo.Repository.DefaultBranch if !ctx.Repo.GitRepo.IsBranchExist(refName) { brs, _, err := ctx.Repo.GitRepo.GetBranches(0, 1) @@ -897,10 +906,8 @@ func RepoRefByType(detectRefType RepoRefType, opts ...RepoRefByTypeOptions) func refName = brs[0].Name } else if len(brs) == 0 { log.Error("No branches in non-empty repository %s", ctx.Repo.GitRepo.Path) - ctx.Repo.Repository.MarkAsBrokenEmpty() } else { log.Error("GetBranches error: %v", err) - ctx.Repo.Repository.MarkAsBrokenEmpty() } } ctx.Repo.RefName = refName @@ -911,18 +918,17 @@ func RepoRefByType(detectRefType RepoRefType, opts ...RepoRefByTypeOptions) func } else if strings.Contains(err.Error(), "fatal: not a git repository") || strings.Contains(err.Error(), "object does not exist") { // if the repository is broken, we can continue to the handler code, to show "Settings -> Delete Repository" for end users log.Error("GetBranchCommit: %v", err) - ctx.Repo.Repository.MarkAsBrokenEmpty() } else { ctx.ServerError("GetBranchCommit", err) - return + return cancel } ctx.Repo.IsViewBranch = true - } else { + } else { // there is a path in request guessLegacyPath := refType == RepoRefUnknown if guessLegacyPath { - refName, refType = getRefNameLegacy(ctx.Base, ctx.Repo) + refName, refType = getRefNameLegacy(ctx.Base, ctx.Repo, reqPath, "") } else { - refName = getRefName(ctx.Base, ctx.Repo, refType) + refName = getRefName(ctx.Base, ctx.Repo, reqPath, refType) } ctx.Repo.RefName = refName isRenamedBranch, has := ctx.Data["IsRenamedBranch"].(bool) @@ -931,7 +937,7 @@ func RepoRefByType(detectRefType RepoRefType, opts ...RepoRefByTypeOptions) func ctx.Flash.Info(ctx.Tr("repo.branch.renamed", refName, renamedBranchName)) link := setting.AppSubURL + strings.Replace(ctx.Req.URL.EscapedPath(), util.PathEscapeSegments(refName), util.PathEscapeSegments(renamedBranchName), 1) ctx.Redirect(link) - return + return cancel } if refType == RepoRefBranch && ctx.Repo.GitRepo.IsBranchExist(refName) { @@ -941,7 +947,7 @@ func RepoRefByType(detectRefType RepoRefType, opts ...RepoRefByTypeOptions) func ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetBranchCommit(refName) if err != nil { ctx.ServerError("GetBranchCommit", err) - return + return cancel } ctx.Repo.CommitID = ctx.Repo.Commit.ID.String() } else if refType == RepoRefTag && ctx.Repo.GitRepo.IsTagExist(refName) { @@ -952,10 +958,10 @@ func RepoRefByType(detectRefType RepoRefType, opts ...RepoRefByTypeOptions) func if err != nil { if git.IsErrNotExist(err) { ctx.NotFound("GetTagCommit", err) - return + return cancel } ctx.ServerError("GetTagCommit", err) - return + return cancel } ctx.Repo.CommitID = ctx.Repo.Commit.ID.String() } else if git.IsStringLikelyCommitID(ctx.Repo.GetObjectFormat(), refName, 7) { @@ -965,7 +971,7 @@ func RepoRefByType(detectRefType RepoRefType, opts ...RepoRefByTypeOptions) func ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetCommit(refName) if err != nil { ctx.NotFound("GetCommit", err) - return + return cancel } // If short commit ID add canonical link header if len(refName) < ctx.Repo.GetObjectFormat().FullLength() { @@ -974,10 +980,10 @@ func RepoRefByType(detectRefType RepoRefType, opts ...RepoRefByTypeOptions) func } } else { if opt.IgnoreNotExistErr { - return + return cancel } ctx.NotFound("RepoRef invalid repo", fmt.Errorf("branch or tag not exist: %s", refName)) - return + return cancel } if guessLegacyPath { @@ -989,7 +995,7 @@ func RepoRefByType(detectRefType RepoRefType, opts ...RepoRefByTypeOptions) func ctx.Repo.BranchNameSubURL(), util.PathEscapeSegments(ctx.Repo.TreePath)) ctx.Redirect(redirect) - return + return cancel } } @@ -1007,10 +1013,12 @@ func RepoRefByType(detectRefType RepoRefType, opts ...RepoRefByTypeOptions) func ctx.Repo.CommitsCount, err = ctx.Repo.GetCommitsCount() if err != nil { ctx.ServerError("GetCommitsCount", err) - return + return cancel } ctx.Data["CommitsCount"] = ctx.Repo.CommitsCount ctx.Repo.GitRepo.LastCommitCache = git.NewLastCommitCache(ctx.Repo.CommitsCount, ctx.Repo.Repository.FullName(), ctx.Repo.GitRepo, cache.GetCache()) + + return cancel } } diff --git a/services/context/upload/upload.go b/services/context/upload/upload.go index da4370a43364c..cefd13ebb60dd 100644 --- a/services/context/upload/upload.go +++ b/services/context/upload/upload.go @@ -97,8 +97,8 @@ func AddUploadContext(ctx *context.Context, uploadType string) { } else if uploadType == "comment" { ctx.Data["UploadUrl"] = ctx.Repo.RepoLink + "/issues/attachments" ctx.Data["UploadRemoveUrl"] = ctx.Repo.RepoLink + "/issues/attachments/remove" - if len(ctx.PathParam("index")) > 0 { - ctx.Data["UploadLinkUrl"] = ctx.Repo.RepoLink + "/issues/" + url.PathEscape(ctx.PathParam("index")) + "/attachments" + if len(ctx.PathParam(":index")) > 0 { + ctx.Data["UploadLinkUrl"] = ctx.Repo.RepoLink + "/issues/" + url.PathEscape(ctx.PathParam(":index")) + "/attachments" } else { ctx.Data["UploadLinkUrl"] = ctx.Repo.RepoLink + "/issues/attachments" } diff --git a/services/context/user.go b/services/context/user.go index dbc35e198d498..b0e855e923185 100644 --- a/services/context/user.go +++ b/services/context/user.go @@ -33,7 +33,7 @@ func UserAssignmentWeb() func(ctx *Context) { // UserIDAssignmentAPI returns a middleware to handle context-user assignment for api routes func UserIDAssignmentAPI() func(ctx *APIContext) { return func(ctx *APIContext) { - userID := ctx.PathParamInt64("user-id") + userID := ctx.PathParamInt64(":user-id") if ctx.IsSigned && ctx.Doer.ID == userID { ctx.ContextUser = ctx.Doer @@ -59,7 +59,7 @@ func UserAssignmentAPI() func(ctx *APIContext) { } func userAssignment(ctx *Base, doer *user_model.User, errCb func(int, string, any)) (contextUser *user_model.User) { - username := ctx.PathParam("username") + username := ctx.PathParam(":username") if doer != nil && doer.LowerName == strings.ToLower(username) { contextUser = doer diff --git a/services/contexttest/context_tests.go b/services/contexttest/context_tests.go index b0f71cad20e7d..3c3fa76e3c49f 100644 --- a/services/contexttest/context_tests.go +++ b/services/contexttest/context_tests.go @@ -21,7 +21,6 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/cache" "code.gitea.io/gitea/modules/gitrepo" - "code.gitea.io/gitea/modules/reqctx" "code.gitea.io/gitea/modules/session" "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/translation" @@ -41,7 +40,7 @@ func mockRequest(t *testing.T, reqPath string) *http.Request { requestURL, err := url.Parse(path) assert.NoError(t, err) req := &http.Request{Method: method, Host: requestURL.Host, URL: requestURL, Form: maps.Clone(requestURL.Query()), Header: http.Header{}} - req = req.WithContext(reqctx.NewRequestContextForTest(req.Context())) + req = req.WithContext(middleware.WithContextData(req.Context())) return req } @@ -61,16 +60,17 @@ func MockContext(t *testing.T, reqPath string, opts ...MockContextOption) (*cont } resp := httptest.NewRecorder() req := mockRequest(t, reqPath) - base := context.NewBaseContext(resp, req) + base, baseCleanUp := context.NewBaseContext(resp, req) + _ = baseCleanUp // during test, it doesn't need to do clean up. TODO: this can be improved later base.Data = middleware.GetContextData(req.Context()) base.Locale = &translation.MockLocale{} chiCtx := chi.NewRouteContext() ctx := context.NewWebContext(base, opt.Render, nil) - ctx.SetContextValue(context.WebContextKey, ctx) - ctx.SetContextValue(chi.RouteCtxKey, chiCtx) + ctx.AppendContextValue(context.WebContextKey, ctx) + ctx.AppendContextValue(chi.RouteCtxKey, chiCtx) if opt.SessionStore != nil { - ctx.SetContextValue(session.MockStoreContextKey, opt.SessionStore) + ctx.AppendContextValue(session.MockStoreContextKey, opt.SessionStore) ctx.Session = opt.SessionStore } ctx.Cache = cache.GetCache() @@ -83,24 +83,27 @@ func MockContext(t *testing.T, reqPath string, opts ...MockContextOption) (*cont func MockAPIContext(t *testing.T, reqPath string) (*context.APIContext, *httptest.ResponseRecorder) { resp := httptest.NewRecorder() req := mockRequest(t, reqPath) - base := context.NewBaseContext(resp, req) + base, baseCleanUp := context.NewBaseContext(resp, req) base.Data = middleware.GetContextData(req.Context()) base.Locale = &translation.MockLocale{} ctx := &context.APIContext{Base: base} + _ = baseCleanUp // during test, it doesn't need to do clean up. TODO: this can be improved later + chiCtx := chi.NewRouteContext() - ctx.SetContextValue(chi.RouteCtxKey, chiCtx) + ctx.Base.AppendContextValue(chi.RouteCtxKey, chiCtx) return ctx, resp } func MockPrivateContext(t *testing.T, reqPath string) (*context.PrivateContext, *httptest.ResponseRecorder) { resp := httptest.NewRecorder() req := mockRequest(t, reqPath) - base := context.NewBaseContext(resp, req) + base, baseCleanUp := context.NewBaseContext(resp, req) base.Data = middleware.GetContextData(req.Context()) base.Locale = &translation.MockLocale{} ctx := &context.PrivateContext{Base: base} + _ = baseCleanUp // during test, it doesn't need to do clean up. TODO: this can be improved later chiCtx := chi.NewRouteContext() - ctx.SetContextValue(chi.RouteCtxKey, chiCtx) + ctx.Base.AppendContextValue(chi.RouteCtxKey, chiCtx) return ctx, resp } @@ -180,7 +183,7 @@ func (tr *MockRender) TemplateLookup(tmpl string, _ gocontext.Context) (template return nil, nil } -func (tr *MockRender) HTML(w io.Writer, status int, _ templates.TplName, _ any, _ gocontext.Context) error { +func (tr *MockRender) HTML(w io.Writer, status int, _ string, _ any, _ gocontext.Context) error { if resp, ok := w.(http.ResponseWriter); ok { resp.WriteHeader(status) } diff --git a/services/convert/issue.go b/services/convert/issue.go index e3124efd6402e..62d0a3b3e6827 100644 --- a/services/convert/issue.go +++ b/services/convert/issue.go @@ -16,6 +16,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/util" ) func ToIssue(ctx context.Context, doer *user_model.User, issue *issues_model.Issue) *api.Issue { @@ -66,7 +67,7 @@ func toIssue(ctx context.Context, doer *user_model.User, issue *issues_model.Iss if err := issue.LoadLabels(ctx); err != nil { return &api.Issue{} } - apiIssue.Labels = ToLabelList(issue.Labels, issue.Repo, issue.Repo.Owner) + apiIssue.Labels = util.SliceNilAsEmpty(ToLabelList(issue.Labels, issue.Repo, issue.Repo.Owner)) apiIssue.Repo = &api.RepositoryMeta{ ID: issue.Repo.ID, Name: issue.Repo.Name, @@ -186,7 +187,7 @@ func ToStopWatches(ctx context.Context, sws []*issues_model.Stopwatch) (api.Stop result = append(result, api.StopWatch{ Created: sw.CreatedUnix.AsTime(), Seconds: sw.Seconds(), - Duration: sw.Duration(), + Duration: util.SecToHours(sw.Seconds()), IssueIndex: issue.Index, IssueTitle: issue.Title, RepoOwnerName: repo.OwnerName, diff --git a/services/convert/issue_comment.go b/services/convert/issue_comment.go index b8527ae233cc0..9ad584a62f759 100644 --- a/services/convert/issue_comment.go +++ b/services/convert/issue_comment.go @@ -74,7 +74,7 @@ func ToTimelineComment(ctx context.Context, repo *repo_model.Repository, c *issu c.Content[0] == '|' { // TimeTracking Comments from v1.21 on store the seconds instead of an formatted string // so we check for the "|" delimiter and convert new to legacy format on demand - c.Content = util.SecToTime(c.Content[1:]) + c.Content = util.SecToHours(c.Content[1:]) } if c.Type == issues_model.CommentTypeChangeTimeEstimate { diff --git a/services/convert/pull.go b/services/convert/pull.go index a1ab7eeb8eca6..e0548eb6353f5 100644 --- a/services/convert/pull.go +++ b/services/convert/pull.go @@ -18,6 +18,7 @@ import ( "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/log" api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/util" ) // ToAPIPullRequest assumes following fields have been assigned with valid values: @@ -77,7 +78,7 @@ func ToAPIPullRequest(ctx context.Context, pr *issues_model.PullRequest, doer *u Labels: apiIssue.Labels, Milestone: apiIssue.Milestone, Assignee: apiIssue.Assignee, - Assignees: apiIssue.Assignees, + Assignees: util.SliceNilAsEmpty(apiIssue.Assignees), State: apiIssue.State, Draft: pr.IsWorkInProgress(ctx), IsLocked: apiIssue.IsLocked, @@ -94,6 +95,10 @@ func ToAPIPullRequest(ctx context.Context, pr *issues_model.PullRequest, doer *u Updated: pr.Issue.UpdatedUnix.AsTimePtr(), PinOrder: apiIssue.PinOrder, + // output "[]" rather than null to align to github outputs + RequestedReviewers: []*api.User{}, + RequestedReviewersTeams: []*api.Team{}, + AllowMaintainerEdit: pr.AllowMaintainerEdit, Base: &api.PRBranchInfo{ @@ -234,9 +239,11 @@ func ToAPIPullRequest(ctx context.Context, pr *issues_model.PullRequest, doer *u // Calculate diff startCommitID = pr.MergeBase - apiPullRequest.ChangedFiles, apiPullRequest.Additions, apiPullRequest.Deletions, err = gitRepo.GetDiffShortStat(startCommitID, endCommitID) + diffChangedFiles, diffAdditions, diffDeletions, err := gitRepo.GetDiffShortStat(startCommitID, endCommitID) if err != nil { log.Error("GetDiffShortStat: %v", err) + } else { + apiPullRequest.ChangedFiles, apiPullRequest.Additions, apiPullRequest.Deletions = &diffChangedFiles, &diffAdditions, &diffDeletions } } @@ -454,12 +461,6 @@ func ToAPIPullRequests(ctx context.Context, baseRepo *repo_model.Repository, prs return nil, err } - // Outer scope variables to be used in diff calculation - var ( - startCommitID string - endCommitID string - ) - if git.IsErrBranchNotExist(err) { headCommitID, err := headGitRepo.GetRefCommitID(apiPullRequest.Head.Ref) if err != nil && !git.IsErrNotExist(err) { @@ -468,7 +469,6 @@ func ToAPIPullRequests(ctx context.Context, baseRepo *repo_model.Repository, prs } if err == nil { apiPullRequest.Head.Sha = headCommitID - endCommitID = headCommitID } } else { commit, err := headBranch.GetCommit() @@ -479,17 +479,8 @@ func ToAPIPullRequests(ctx context.Context, baseRepo *repo_model.Repository, prs if err == nil { apiPullRequest.Head.Ref = pr.HeadBranch apiPullRequest.Head.Sha = commit.ID.String() - endCommitID = commit.ID.String() } } - - // Calculate diff - startCommitID = pr.MergeBase - - apiPullRequest.ChangedFiles, apiPullRequest.Additions, apiPullRequest.Deletions, err = gitRepo.GetDiffShortStat(startCommitID, endCommitID) - if err != nil { - log.Error("GetDiffShortStat: %v", err) - } } if len(apiPullRequest.Head.Sha) == 0 && len(apiPullRequest.Head.Ref) != 0 { diff --git a/services/convert/repository.go b/services/convert/repository.go index 88ccd88fcfec6..644a2a11fa2d2 100644 --- a/services/convert/repository.go +++ b/services/convert/repository.go @@ -7,6 +7,7 @@ import ( "context" "time" + "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/perm" access_model "code.gitea.io/gitea/models/perm/access" @@ -14,6 +15,7 @@ import ( unit_model "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/modules/log" api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/util" ) // ToRepo converts a Repository to api.Repository @@ -157,8 +159,8 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR var transfer *api.RepoTransfer if repo.Status == repo_model.RepositoryPendingTransfer { - t, err := repo_model.GetPendingRepositoryTransfer(ctx, repo) - if err != nil && !repo_model.IsErrNoPendingTransfer(err) { + t, err := models.GetPendingRepositoryTransfer(ctx, repo) + if err != nil && !models.IsErrNoPendingTransfer(err) { log.Warn("GetPendingRepositoryTransfer: %v", err) } else { if err := t.LoadAttributes(ctx); err != nil { @@ -240,14 +242,14 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR MirrorInterval: mirrorInterval, MirrorUpdated: mirrorUpdated, RepoTransfer: transfer, - Topics: repo.Topics, + Topics: util.SliceNilAsEmpty(repo.Topics), ObjectFormatName: repo.ObjectFormatName, Licenses: repoLicenses.StringList(), } } // ToRepoTransfer convert a models.RepoTransfer to a structs.RepeTransfer -func ToRepoTransfer(ctx context.Context, t *repo_model.RepoTransfer) *api.RepoTransfer { +func ToRepoTransfer(ctx context.Context, t *models.RepoTransfer) *api.RepoTransfer { teams, _ := ToTeams(ctx, t.Teams, false) return &api.RepoTransfer{ diff --git a/services/cron/tasks_basic.go b/services/cron/tasks_basic.go index fb5938745e61c..841981787dffd 100644 --- a/services/cron/tasks_basic.go +++ b/services/cron/tasks_basic.go @@ -54,7 +54,7 @@ func registerRepoHealthCheck() { RunAtStart: false, Schedule: "@midnight", }, - Timeout: 60 * time.Second, + Timeout: time.Duration(setting.Git.Timeout.Default) * time.Second, Args: []string{}, }, func(ctx context.Context, _ *user_model.User, config Config) error { rhcConfig := config.(*RepoHealthCheckConfig) diff --git a/services/doctor/storage.go b/services/doctor/storage.go index 3f3b562c370c7..77fc6d65dfafc 100644 --- a/services/doctor/storage.go +++ b/services/doctor/storage.go @@ -121,7 +121,7 @@ func checkStorage(opts *checkStorageOptions) func(ctx context.Context, logger lo storer: storage.LFS, isOrphaned: func(path string, obj storage.Object, stat fs.FileInfo) (bool, error) { // The oid of an LFS stored object is the name but with all the path.Separators removed - oid := strings.ReplaceAll(path, "/", "") + oid := strings.ReplaceAll(strings.ReplaceAll(path, "\\", ""), "/", "") exists, err := git.ExistsLFSObject(ctx, oid) return !exists, err }, diff --git a/services/feed/notifier.go b/services/feed/notifier.go index 702eb5ad533ad..d941027c35276 100644 --- a/services/feed/notifier.go +++ b/services/feed/notifier.go @@ -109,7 +109,7 @@ func (a *actionNotifier) CreateIssueComment(ctx context.Context, doer *user_mode IsPrivate: issue.Repo.IsPrivate, } - truncatedContent, truncatedRight := util.EllipsisDisplayStringX(comment.Content, 200) + truncatedContent, truncatedRight := util.SplitStringAtByteN(comment.Content, 200) if truncatedRight != "" { // in case the content is in a Latin family language, we remove the last broken word. lastSpaceIdx := strings.LastIndex(truncatedContent, " ") diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go index b14171787e466..f558133aeb599 100644 --- a/services/forms/repo_form.go +++ b/services/forms/repo_form.go @@ -6,8 +6,10 @@ package forms import ( "net/http" + "net/url" "strings" + "code.gitea.io/gitea/models" issues_model "code.gitea.io/gitea/models/issues" project_model "code.gitea.io/gitea/models/project" "code.gitea.io/gitea/modules/setting" @@ -88,6 +90,27 @@ func (f *MigrateRepoForm) Validate(req *http.Request, errs binding.Errors) bindi return middleware.Validate(errs, ctx.Data, f, ctx.Locale) } +// ParseRemoteAddr checks if given remote address is valid, +// and returns composed URL with needed username and password. +func ParseRemoteAddr(remoteAddr, authUsername, authPassword string) (string, error) { + remoteAddr = strings.TrimSpace(remoteAddr) + // Remote address can be HTTP/HTTPS/Git URL or local path. + if strings.HasPrefix(remoteAddr, "http://") || + strings.HasPrefix(remoteAddr, "https://") || + strings.HasPrefix(remoteAddr, "git://") { + u, err := url.Parse(remoteAddr) + if err != nil { + return "", &models.ErrInvalidCloneAddr{IsURLError: true, Host: remoteAddr} + } + if len(authUsername)+len(authPassword) > 0 { + u.User = url.UserPassword(authUsername, authPassword) + } + remoteAddr = u.String() + } + + return remoteAddr, nil +} + // RepoSettingForm form for changing repository settings type RepoSettingForm struct { RepoName string `binding:"Required;AlphaDashDot;MaxSize(100)"` @@ -240,6 +263,7 @@ type WebhookForm struct { Wiki bool Repository bool Package bool + Status bool Active bool BranchFilter string `binding:"GlobPattern"` AuthorizationHeader string diff --git a/services/gitdiff/gitdiff.go b/services/gitdiff/gitdiff.go index e1fcfb3d69511..393b222388eb5 100644 --- a/services/gitdiff/gitdiff.go +++ b/services/gitdiff/gitdiff.go @@ -82,7 +82,7 @@ type DiffLine struct { Match int Type DiffLineType Content string - Comments []*issues_model.Comment + Comments issues_model.CommentList SectionInfo *DiffLineSectionInfo } @@ -202,8 +202,6 @@ var ( addedCodePrefix = []byte(``) removedCodePrefix = []byte(``) codeTagSuffix = []byte(``) - // divTagPrefix = []byte(`
`) - // divTagSuffix = []byte(`
`) ) func diffToHTML(lineWrapperTags []string, diffs []diffmatchpatch.Diff, lineType DiffLineType) string { @@ -1290,6 +1288,8 @@ func GetDiff(ctx context.Context, gitRepo *git.Repository, opts *DiffOptions, fi if language.Has() { diffFile.Language = language.Value() } + } else { + checker = nil // CheckPath fails, it's not impossible to "check" anymore } } @@ -1474,10 +1474,8 @@ func GetWhitespaceFlag(whitespaceBehavior string) git.TrustedCmdArgs { "ignore-eol": {"--ignore-space-at-eol"}, "show-all": nil, } - if flag, ok := whitespaceFlags[whitespaceBehavior]; ok { return flag } - log.Warn("unknown whitespace behavior: %q, default to 'show-all'", whitespaceBehavior) return nil } diff --git a/services/issue/commit.go b/services/issue/commit.go index 963d0359fd35d..0579e0f5c53e6 100644 --- a/services/issue/commit.go +++ b/services/issue/commit.go @@ -188,19 +188,15 @@ func UpdateIssuesCommit(ctx context.Context, doer *user_model.User, repo *repo_m continue } } - - refIssue.Repo = refRepo - if ref.Action == references.XRefActionCloses && !refIssue.IsClosed { - if len(ref.TimeLog) > 0 { - if err := issueAddTime(ctx, refIssue, doer, c.Timestamp, ref.TimeLog); err != nil { - return err - } - } - if err := CloseIssue(ctx, refIssue, doer, c.Sha1); err != nil { + isClosed := ref.Action == references.XRefActionCloses + if isClosed && len(ref.TimeLog) > 0 { + if err := issueAddTime(ctx, refIssue, doer, c.Timestamp, ref.TimeLog); err != nil { return err } - } else if ref.Action == references.XRefActionReopens && refIssue.IsClosed { - if err := ReopenIssue(ctx, refIssue, doer, c.Sha1); err != nil { + } + if isClosed != refIssue.IsClosed { + refIssue.Repo = refRepo + if err := ChangeStatus(ctx, refIssue, doer, c.Sha1, isClosed); err != nil { return err } } diff --git a/services/issue/issue.go b/services/issue/issue.go index c6a52cc0fe512..b24ca3001965b 100644 --- a/services/issue/issue.go +++ b/services/issue/issue.go @@ -92,8 +92,12 @@ func ChangeTitle(ctx context.Context, issue *issues_model.Issue, doer *user_mode var reviewNotifiers []*ReviewRequestNotifier if issue.IsPull && issues_model.HasWorkInProgressPrefix(oldTitle) && !issues_model.HasWorkInProgressPrefix(title) { + if err := issue.LoadPullRequest(ctx); err != nil { + return err + } + var err error - reviewNotifiers, err = PullRequestCodeOwnersReview(ctx, issue, issue.PullRequest) + reviewNotifiers, err = PullRequestCodeOwnersReview(ctx, issue.PullRequest) if err != nil { log.Error("PullRequestCodeOwnersReview: %v", err) } diff --git a/services/issue/pull.go b/services/issue/pull.go index 896802108d6ca..3543b05b18f78 100644 --- a/services/issue/pull.go +++ b/services/issue/pull.go @@ -6,6 +6,7 @@ package issue import ( "context" "fmt" + "slices" "time" issues_model "code.gitea.io/gitea/models/issues" @@ -40,20 +41,27 @@ type ReviewRequestNotifier struct { ReviewTeam *org_model.Team } -func PullRequestCodeOwnersReview(ctx context.Context, issue *issues_model.Issue, pr *issues_model.PullRequest) ([]*ReviewRequestNotifier, error) { - files := []string{"CODEOWNERS", "docs/CODEOWNERS", ".gitea/CODEOWNERS"} +var codeOwnerFiles = []string{"CODEOWNERS", "docs/CODEOWNERS", ".gitea/CODEOWNERS"} +func IsCodeOwnerFile(f string) bool { + return slices.Contains(codeOwnerFiles, f) +} + +func PullRequestCodeOwnersReview(ctx context.Context, pr *issues_model.PullRequest) ([]*ReviewRequestNotifier, error) { + if err := pr.LoadIssue(ctx); err != nil { + return nil, err + } + issue := pr.Issue if pr.IsWorkInProgress(ctx) { return nil, nil } - if err := pr.LoadHeadRepo(ctx); err != nil { return nil, err } - if err := pr.LoadBaseRepo(ctx); err != nil { return nil, err } + pr.Issue.Repo = pr.BaseRepo if pr.BaseRepo.IsFork { return nil, nil @@ -71,7 +79,7 @@ func PullRequestCodeOwnersReview(ctx context.Context, issue *issues_model.Issue, } var data string - for _, file := range files { + for _, file := range codeOwnerFiles { if blob, err := commit.GetBlobByPath(file); err == nil { data, err = blob.GetBlobContent(setting.UI.MaxDisplayFileSize) if err == nil { @@ -79,8 +87,14 @@ func PullRequestCodeOwnersReview(ctx context.Context, issue *issues_model.Issue, } } } + if data == "" { + return nil, nil + } rules, _ := issues_model.GetCodeOwnersFromContent(ctx, data) + if len(rules) == 0 { + return nil, nil + } // get the mergebase mergeBase, err := getMergeBase(repo, pr, git.BranchPrefix+pr.BaseBranch, pr.GetGitRefName()) @@ -116,13 +130,31 @@ func PullRequestCodeOwnersReview(ctx context.Context, issue *issues_model.Issue, return nil, err } + // load all reviews from database + latestReivews, _, err := issues_model.GetReviewsByIssueID(ctx, pr.IssueID) + if err != nil { + return nil, err + } + + contain := func(list issues_model.ReviewList, u *user_model.User) bool { + for _, review := range list { + if review.ReviewerTeamID == 0 && review.ReviewerID == u.ID { + return true + } + } + return false + } + for _, u := range uniqUsers { - if u.ID != issue.Poster.ID { + if u.ID != issue.Poster.ID && !contain(latestReivews, u) { comment, err := issues_model.AddReviewRequest(ctx, issue, u, issue.Poster) if err != nil { log.Warn("Failed add assignee user: %s to PR review: %s#%d, error: %s", u.Name, pr.BaseRepo.Name, pr.ID, err) return nil, err } + if comment == nil { // comment maybe nil if review type is ReviewTypeRequest + continue + } notifiers = append(notifiers, &ReviewRequestNotifier{ Comment: comment, IsAdd: true, @@ -130,12 +162,16 @@ func PullRequestCodeOwnersReview(ctx context.Context, issue *issues_model.Issue, }) } } + for _, t := range uniqTeams { comment, err := issues_model.AddTeamReviewRequest(ctx, issue, t, issue.Poster) if err != nil { log.Warn("Failed add assignee team: %s to PR review: %s#%d, error: %s", t.Name, pr.BaseRepo.Name, pr.ID, err) return nil, err } + if comment == nil { // comment maybe nil if review type is ReviewTypeRequest + continue + } notifiers = append(notifiers, &ReviewRequestNotifier{ Comment: comment, IsAdd: true, diff --git a/services/issue/status.go b/services/issue/status.go index e18b891175a0a..967c29bd22230 100644 --- a/services/issue/status.go +++ b/services/issue/status.go @@ -6,54 +6,34 @@ package issue import ( "context" - "code.gitea.io/gitea/models/db" issues_model "code.gitea.io/gitea/models/issues" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/log" notify_service "code.gitea.io/gitea/services/notify" ) -// CloseIssue close an issue. -func CloseIssue(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, commitID string) error { - dbCtx, committer, err := db.TxContext(ctx) +// ChangeStatus changes issue status to open or closed. +// closed means the target status +// Fix me: you should check whether the current issue status is same to the target status before call this function +// as in function changeIssueStatus we will return WasClosedError, even the issue status and target status are both open +func ChangeStatus(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, commitID string, closed bool) error { + comment, err := issues_model.ChangeIssueStatus(ctx, issue, doer, closed) if err != nil { - return err - } - defer committer.Close() - - comment, err := issues_model.CloseIssue(dbCtx, issue, doer) - if err != nil { - if issues_model.IsErrDependenciesLeft(err) { - if err := issues_model.FinishIssueStopwatchIfPossible(dbCtx, doer, issue); err != nil { + if issues_model.IsErrDependenciesLeft(err) && closed { + if err := issues_model.FinishIssueStopwatchIfPossible(ctx, doer, issue); err != nil { log.Error("Unable to stop stopwatch for issue[%d]#%d: %v", issue.ID, issue.Index, err) } } return err } - if err := issues_model.FinishIssueStopwatchIfPossible(dbCtx, doer, issue); err != nil { - return err - } - - if err := committer.Commit(); err != nil { - return err - } - committer.Close() - - notify_service.IssueChangeStatus(ctx, doer, commitID, issue, comment, true) - - return nil -} - -// ReopenIssue reopen an issue. -// FIXME: If some issues dependent this one are closed, should we also reopen them? -func ReopenIssue(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, commitID string) error { - comment, err := issues_model.ReopenIssue(ctx, issue, doer) - if err != nil { - return err + if closed { + if err := issues_model.FinishIssueStopwatchIfPossible(ctx, doer, issue); err != nil { + return err + } } - notify_service.IssueChangeStatus(ctx, doer, commitID, issue, comment, false) + notify_service.IssueChangeStatus(ctx, doer, commitID, issue, comment, closed) return nil } diff --git a/services/issue/suggestion.go b/services/issue/suggestion.go new file mode 100644 index 0000000000000..22eddb19042bb --- /dev/null +++ b/services/issue/suggestion.go @@ -0,0 +1,73 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package issue + +import ( + "context" + "strconv" + + issues_model "code.gitea.io/gitea/models/issues" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/modules/optional" + "code.gitea.io/gitea/modules/structs" +) + +func GetSuggestion(ctx context.Context, repo *repo_model.Repository, isPull optional.Option[bool], keyword string) ([]*structs.Issue, error) { + var issues issues_model.IssueList + var err error + pageSize := 5 + if keyword == "" { + issues, err = issues_model.FindLatestUpdatedIssues(ctx, repo.ID, isPull, pageSize) + if err != nil { + return nil, err + } + } else { + indexKeyword, _ := strconv.ParseInt(keyword, 10, 64) + var issueByIndex *issues_model.Issue + var excludedID int64 + if indexKeyword > 0 { + issueByIndex, err = issues_model.GetIssueByIndex(ctx, repo.ID, indexKeyword) + if err != nil && !issues_model.IsErrIssueNotExist(err) { + return nil, err + } + if issueByIndex != nil { + excludedID = issueByIndex.ID + pageSize-- + } + } + + issues, err = issues_model.FindIssuesSuggestionByKeyword(ctx, repo.ID, keyword, isPull, excludedID, pageSize) + if err != nil { + return nil, err + } + + if issueByIndex != nil { + issues = append([]*issues_model.Issue{issueByIndex}, issues...) + } + } + + if err := issues.LoadPullRequests(ctx); err != nil { + return nil, err + } + + suggestions := make([]*structs.Issue, 0, len(issues)) + for _, issue := range issues { + suggestion := &structs.Issue{ + ID: issue.ID, + Index: issue.Index, + Title: issue.Title, + State: issue.State(), + } + + if issue.IsPull && issue.PullRequest != nil { + suggestion.PullRequest = &structs.PullRequestMeta{ + HasMerged: issue.PullRequest.HasMerged, + IsWorkInProgress: issue.PullRequest.IsWorkInProgress(ctx), + } + } + suggestions = append(suggestions, suggestion) + } + + return suggestions, nil +} diff --git a/services/issue/suggestion_test.go b/services/issue/suggestion_test.go new file mode 100644 index 0000000000000..84cfd520ac40a --- /dev/null +++ b/services/issue/suggestion_test.go @@ -0,0 +1,57 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package issue + +import ( + "testing" + + "code.gitea.io/gitea/models/db" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unittest" + "code.gitea.io/gitea/modules/optional" + + "github.com/stretchr/testify/assert" +) + +func Test_Suggestion(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + + testCases := []struct { + keyword string + isPull optional.Option[bool] + expectedIndexes []int64 + }{ + { + keyword: "", + expectedIndexes: []int64{5, 1, 4, 2, 3}, + }, + { + keyword: "1", + expectedIndexes: []int64{1}, + }, + { + keyword: "issue", + expectedIndexes: []int64{4, 1, 2, 3}, + }, + { + keyword: "pull", + expectedIndexes: []int64{5}, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.keyword, func(t *testing.T) { + issues, err := GetSuggestion(db.DefaultContext, repo1, testCase.isPull, testCase.keyword) + assert.NoError(t, err) + + issueIndexes := make([]int64, 0, len(issues)) + for _, issue := range issues { + issueIndexes = append(issueIndexes, issue.Index) + } + assert.EqualValues(t, testCase.expectedIndexes, issueIndexes) + }) + } +} diff --git a/services/lfs/server.go b/services/lfs/server.go index a77623fdc196c..c4866edaab233 100644 --- a/services/lfs/server.go +++ b/services/lfs/server.go @@ -134,7 +134,9 @@ func DownloadHandler(ctx *context.Context) { } contentLength := toByte + 1 - fromByte - ctx.Resp.Header().Set("Content-Length", strconv.FormatInt(contentLength, 10)) + contentLengthStr := strconv.FormatInt(contentLength, 10) + ctx.Resp.Header().Set("Content-Length", contentLengthStr) + ctx.Resp.Header().Set("X-Gitea-LFS-Content-Length", contentLengthStr) // we need this header to make sure it won't be affected by reverse proxy or compression ctx.Resp.Header().Set("Content-Type", "application/octet-stream") filename := ctx.PathParam("filename") diff --git a/services/mailer/mail.go b/services/mailer/mail.go index 52e19bde6f261..114a21619671b 100644 --- a/services/mailer/mail.go +++ b/services/mailer/mail.go @@ -21,11 +21,11 @@ import ( "code.gitea.io/gitea/models/renderhelper" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/emoji" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/markup/markdown" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/translation" incoming_payload "code.gitea.io/gitea/services/mailer/incoming/payload" @@ -34,14 +34,14 @@ import ( ) const ( - mailAuthActivate templates.TplName = "auth/activate" - mailAuthActivateEmail templates.TplName = "auth/activate_email" - mailAuthResetPassword templates.TplName = "auth/reset_passwd" - mailAuthRegisterNotify templates.TplName = "auth/register_notify" + mailAuthActivate base.TplName = "auth/activate" + mailAuthActivateEmail base.TplName = "auth/activate_email" + mailAuthResetPassword base.TplName = "auth/reset_passwd" + mailAuthRegisterNotify base.TplName = "auth/register_notify" - mailNotifyCollaborator templates.TplName = "notify/collaborator" + mailNotifyCollaborator base.TplName = "notify/collaborator" - mailRepoTransferNotify templates.TplName = "notify/repo_transfer" + mailRepoTransferNotify base.TplName = "notify/repo_transfer" // There's no actual limit for subject in RFC 5322 mailMaxSubjectRunes = 256 @@ -63,7 +63,7 @@ func SendTestMail(email string) error { } // sendUserMail sends a mail to the user -func sendUserMail(language string, u *user_model.User, tpl templates.TplName, code, subject, info string) { +func sendUserMail(language string, u *user_model.User, tpl base.TplName, code, subject, info string) { locale := translation.NewLocale(language) data := map[string]any{ "locale": locale, diff --git a/services/mailer/mail_release.go b/services/mailer/mail_release.go index 796d63d27aef1..1d73d77612255 100644 --- a/services/mailer/mail_release.go +++ b/services/mailer/mail_release.go @@ -10,16 +10,16 @@ import ( "code.gitea.io/gitea/models/renderhelper" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/markup/markdown" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/translation" sender_service "code.gitea.io/gitea/services/mailer/sender" ) const ( - tplNewReleaseMail templates.TplName = "release" + tplNewReleaseMail base.TplName = "release" ) // MailNewRelease send new release notify to all repo watchers. diff --git a/services/mailer/mail_team_invite.go b/services/mailer/mail_team_invite.go index 5ca44442f395a..4f2d5e4ca7f60 100644 --- a/services/mailer/mail_team_invite.go +++ b/services/mailer/mail_team_invite.go @@ -11,15 +11,15 @@ import ( org_model "code.gitea.io/gitea/models/organization" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/translation" sender_service "code.gitea.io/gitea/services/mailer/sender" ) const ( - tplTeamInviteMail templates.TplName = "team_invite" + tplTeamInviteMail base.TplName = "team_invite" ) // MailTeamInvite sends team invites diff --git a/services/mailer/sender/message.go b/services/mailer/sender/message.go index 55f03e4f7ec63..db20675572d20 100644 --- a/services/mailer/sender/message.go +++ b/services/mailer/sender/message.go @@ -10,9 +10,9 @@ import ( "strings" "time" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/util" "github.com/jaytaylor/html2text" gomail "github.com/wneessen/go-mail" @@ -54,7 +54,7 @@ func (m *Message) ToMessage() *gomail.Msg { plainBody, err := html2text.FromString(m.Body) if err != nil || setting.MailService.SendAsPlainText { - if strings.Contains(util.TruncateRunes(m.Body, 100), "") { + if strings.Contains(base.TruncateString(m.Body, 100), "") { log.Warn("Mail contains HTML but configured to send as plain text.") } msg.SetBodyString("text/plain", plainBody) diff --git a/services/markup/main_test.go b/services/markup/main_test.go index d04a18bfa17e9..5553ebc058948 100644 --- a/services/markup/main_test.go +++ b/services/markup/main_test.go @@ -11,6 +11,6 @@ import ( func TestMain(m *testing.M) { unittest.MainTest(m, &unittest.TestOptions{ - FixtureFiles: []string{"user.yml", "repository.yml", "access.yml", "repo_unit.yml", "issue.yml"}, + FixtureFiles: []string{"user.yml", "repository.yml", "access.yml", "repo_unit.yml"}, }) } diff --git a/services/markup/renderhelper.go b/services/markup/processorhelper.go similarity index 82% rename from services/markup/renderhelper.go rename to services/markup/processorhelper.go index 4b9852b48bf2f..b4ad2de711b3e 100644 --- a/services/markup/renderhelper.go +++ b/services/markup/processorhelper.go @@ -11,18 +11,17 @@ import ( gitea_context "code.gitea.io/gitea/services/context" ) -func FormalRenderHelperFuncs() *markup.RenderHelperFuncs { +func ProcessorHelper() *markup.RenderHelperFuncs { return &markup.RenderHelperFuncs{ RenderRepoFileCodePreview: renderRepoFileCodePreview, - RenderRepoIssueIconTitle: renderRepoIssueIconTitle, IsUsernameMentionable: func(ctx context.Context, username string) bool { mentionedUser, err := user.GetUserByName(ctx, username) if err != nil { return false } - giteaCtx, ok := ctx.(*gitea_context.Context) - if !ok { + giteaCtx := gitea_context.GetWebContext(ctx) + if giteaCtx == nil { // when using general context, use user's visibility to check return mentionedUser.Visibility.IsPublic() } diff --git a/services/markup/renderhelper_codepreview.go b/services/markup/processorhelper_codepreview.go similarity index 95% rename from services/markup/renderhelper_codepreview.go rename to services/markup/processorhelper_codepreview.go index 170c70c4098e3..cecfadae8655b 100644 --- a/services/markup/renderhelper_codepreview.go +++ b/services/markup/processorhelper_codepreview.go @@ -18,7 +18,6 @@ import ( "code.gitea.io/gitea/modules/indexer/code" "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/util" gitea_context "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/repository/files" ) @@ -36,8 +35,8 @@ func renderRepoFileCodePreview(ctx context.Context, opts markup.RenderCodePrevie return "", err } - webCtx, ok := ctx.Value(gitea_context.WebContextKey).(*gitea_context.Context) - if !ok { + webCtx := gitea_context.GetWebContext(ctx) + if webCtx == nil { return "", fmt.Errorf("context is not a web context") } doer := webCtx.Doer @@ -47,7 +46,7 @@ func renderRepoFileCodePreview(ctx context.Context, opts markup.RenderCodePrevie return "", err } if !perms.CanRead(unit.TypeCode) { - return "", util.ErrPermissionDenied + return "", fmt.Errorf("no permission") } gitRepo, err := gitrepo.OpenRepository(ctx, dbRepo) diff --git a/services/markup/renderhelper_codepreview_test.go b/services/markup/processorhelper_codepreview_test.go similarity index 95% rename from services/markup/renderhelper_codepreview_test.go rename to services/markup/processorhelper_codepreview_test.go index ea945584b427e..154e4e8e44118 100644 --- a/services/markup/renderhelper_codepreview_test.go +++ b/services/markup/processorhelper_codepreview_test.go @@ -9,13 +9,12 @@ import ( "code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/templates" - "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/services/contexttest" "github.com/stretchr/testify/assert" ) -func TestRenderHelperCodePreview(t *testing.T) { +func TestProcessorHelperCodePreview(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) ctx, _ := contexttest.MockContext(t, "/", contexttest.MockContextOption{Render: templates.HTMLRenderer()}) @@ -80,5 +79,5 @@ func TestRenderHelperCodePreview(t *testing.T) { LineStart: 1, LineStop: 10, }) - assert.ErrorIs(t, err, util.ErrPermissionDenied) + assert.ErrorContains(t, err, "no permission") } diff --git a/services/markup/renderhelper_mention_test.go b/services/markup/processorhelper_test.go similarity index 57% rename from services/markup/renderhelper_mention_test.go rename to services/markup/processorhelper_test.go index c244fa3d21d7f..170edae0e0beb 100644 --- a/services/markup/renderhelper_mention_test.go +++ b/services/markup/processorhelper_test.go @@ -18,7 +18,7 @@ import ( "github.com/stretchr/testify/assert" ) -func TestRenderHelperMention(t *testing.T) { +func TestProcessorHelper(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) userPublic := "user1" @@ -32,22 +32,23 @@ func TestRenderHelperMention(t *testing.T) { unittest.AssertCount(t, &user.User{Name: userNoSuch}, 0) // when using general context, use user's visibility to check - assert.True(t, FormalRenderHelperFuncs().IsUsernameMentionable(context.Background(), userPublic)) - assert.False(t, FormalRenderHelperFuncs().IsUsernameMentionable(context.Background(), userLimited)) - assert.False(t, FormalRenderHelperFuncs().IsUsernameMentionable(context.Background(), userPrivate)) - assert.False(t, FormalRenderHelperFuncs().IsUsernameMentionable(context.Background(), userNoSuch)) + assert.True(t, ProcessorHelper().IsUsernameMentionable(context.Background(), userPublic)) + assert.False(t, ProcessorHelper().IsUsernameMentionable(context.Background(), userLimited)) + assert.False(t, ProcessorHelper().IsUsernameMentionable(context.Background(), userPrivate)) + assert.False(t, ProcessorHelper().IsUsernameMentionable(context.Background(), userNoSuch)) // when using web context, use user.IsUserVisibleToViewer to check req, err := http.NewRequest("GET", "/", nil) assert.NoError(t, err) - base := gitea_context.NewBaseContextForTest(httptest.NewRecorder(), req) + base, baseCleanUp := gitea_context.NewBaseContext(httptest.NewRecorder(), req) + defer baseCleanUp() giteaCtx := gitea_context.NewWebContext(base, &contexttest.MockRender{}, nil) - assert.True(t, FormalRenderHelperFuncs().IsUsernameMentionable(giteaCtx, userPublic)) - assert.False(t, FormalRenderHelperFuncs().IsUsernameMentionable(giteaCtx, userPrivate)) + assert.True(t, ProcessorHelper().IsUsernameMentionable(giteaCtx, userPublic)) + assert.False(t, ProcessorHelper().IsUsernameMentionable(giteaCtx, userPrivate)) giteaCtx.Doer, err = user.GetUserByName(db.DefaultContext, userPrivate) assert.NoError(t, err) - assert.True(t, FormalRenderHelperFuncs().IsUsernameMentionable(giteaCtx, userPublic)) - assert.True(t, FormalRenderHelperFuncs().IsUsernameMentionable(giteaCtx, userPrivate)) + assert.True(t, ProcessorHelper().IsUsernameMentionable(giteaCtx, userPublic)) + assert.True(t, ProcessorHelper().IsUsernameMentionable(giteaCtx, userPrivate)) } diff --git a/services/markup/renderhelper_issueicontitle.go b/services/markup/renderhelper_issueicontitle.go deleted file mode 100644 index 53a508e908145..0000000000000 --- a/services/markup/renderhelper_issueicontitle.go +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright 2024 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package markup - -import ( - "context" - "fmt" - "html/template" - - "code.gitea.io/gitea/models/issues" - "code.gitea.io/gitea/models/perm/access" - "code.gitea.io/gitea/models/repo" - "code.gitea.io/gitea/modules/htmlutil" - "code.gitea.io/gitea/modules/markup" - "code.gitea.io/gitea/modules/util" - gitea_context "code.gitea.io/gitea/services/context" -) - -func renderRepoIssueIconTitle(ctx context.Context, opts markup.RenderIssueIconTitleOptions) (_ template.HTML, err error) { - webCtx, ok := ctx.Value(gitea_context.WebContextKey).(*gitea_context.Context) - if !ok { - return "", fmt.Errorf("context is not a web context") - } - - textIssueIndex := fmt.Sprintf("(#%d)", opts.IssueIndex) - dbRepo := webCtx.Repo.Repository - if opts.OwnerName != "" { - dbRepo, err = repo.GetRepositoryByOwnerAndName(ctx, opts.OwnerName, opts.RepoName) - if err != nil { - return "", err - } - textIssueIndex = fmt.Sprintf("(%s/%s#%d)", dbRepo.OwnerName, dbRepo.Name, opts.IssueIndex) - } - if dbRepo == nil { - return "", nil - } - - issue, err := issues.GetIssueByIndex(ctx, dbRepo.ID, opts.IssueIndex) - if err != nil { - return "", err - } - - if webCtx.Repo.Repository == nil || dbRepo.ID != webCtx.Repo.Repository.ID { - perms, err := access.GetUserRepoPermission(ctx, dbRepo, webCtx.Doer) - if err != nil { - return "", err - } - if !perms.CanReadIssuesOrPulls(issue.IsPull) { - return "", util.ErrPermissionDenied - } - } - - if issue.IsPull { - if err = issue.LoadPullRequest(ctx); err != nil { - return "", err - } - } - - htmlIcon, err := webCtx.RenderToHTML("shared/issueicon", issue) - if err != nil { - return "", err - } - - return htmlutil.HTMLFormat(`%s %s %s`, opts.LinkHref, htmlIcon, issue.Title, textIssueIndex), nil -} diff --git a/services/markup/renderhelper_issueicontitle_test.go b/services/markup/renderhelper_issueicontitle_test.go deleted file mode 100644 index adce8401e076b..0000000000000 --- a/services/markup/renderhelper_issueicontitle_test.go +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright 2024 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package markup - -import ( - "testing" - - "code.gitea.io/gitea/models/repo" - "code.gitea.io/gitea/models/unittest" - "code.gitea.io/gitea/modules/markup" - "code.gitea.io/gitea/modules/templates" - "code.gitea.io/gitea/modules/util" - "code.gitea.io/gitea/services/contexttest" - - "github.com/stretchr/testify/assert" -) - -func TestRenderHelperIssueIconTitle(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - ctx, _ := contexttest.MockContext(t, "/", contexttest.MockContextOption{Render: templates.HTMLRenderer()}) - ctx.Repo.Repository = unittest.AssertExistsAndLoadBean(t, &repo.Repository{ID: 1}) - htm, err := renderRepoIssueIconTitle(ctx, markup.RenderIssueIconTitleOptions{ - LinkHref: "/link", - IssueIndex: 1, - }) - assert.NoError(t, err) - assert.Equal(t, `octicon-issue-opened(16/text green) issue1 (#1)`, string(htm)) - - ctx, _ = contexttest.MockContext(t, "/", contexttest.MockContextOption{Render: templates.HTMLRenderer()}) - htm, err = renderRepoIssueIconTitle(ctx, markup.RenderIssueIconTitleOptions{ - OwnerName: "user2", - RepoName: "repo1", - LinkHref: "/link", - IssueIndex: 1, - }) - assert.NoError(t, err) - assert.Equal(t, `octicon-issue-opened(16/text green) issue1 (user2/repo1#1)`, string(htm)) - - ctx, _ = contexttest.MockContext(t, "/", contexttest.MockContextOption{Render: templates.HTMLRenderer()}) - _, err = renderRepoIssueIconTitle(ctx, markup.RenderIssueIconTitleOptions{ - OwnerName: "user2", - RepoName: "repo2", - LinkHref: "/link", - IssueIndex: 2, - }) - assert.ErrorIs(t, err, util.ErrPermissionDenied) -} diff --git a/services/migrations/codecommit.go b/services/migrations/codecommit.go index ccda62fc3d9fc..fead527f5b9d8 100644 --- a/services/migrations/codecommit.go +++ b/services/migrations/codecommit.go @@ -14,11 +14,11 @@ import ( "code.gitea.io/gitea/modules/log" base "code.gitea.io/gitea/modules/migration" "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/util" "github.com/aws/aws-sdk-go-v2/credentials" "github.com/aws/aws-sdk-go-v2/service/codecommit" "github.com/aws/aws-sdk-go-v2/service/codecommit/types" - "github.com/aws/aws-sdk-go/aws" ) var ( @@ -94,7 +94,7 @@ func (c *CodeCommitDownloader) SetContext(ctx context.Context) { // GetRepoInfo returns a repository information func (c *CodeCommitDownloader) GetRepoInfo() (*base.Repository, error) { output, err := c.codeCommitClient.GetRepository(c.ctx, &codecommit.GetRepositoryInput{ - RepositoryName: aws.String(c.repoName), + RepositoryName: util.ToPointer(c.repoName), }) if err != nil { return nil, err @@ -126,7 +126,7 @@ func (c *CodeCommitDownloader) GetComments(commentable base.Commentable) ([]*bas for { resp, err := c.codeCommitClient.GetCommentsForPullRequest(c.ctx, &codecommit.GetCommentsForPullRequestInput{ NextToken: nextToken, - PullRequestId: aws.String(strconv.FormatInt(commentable.GetForeignIndex(), 10)), + PullRequestId: util.ToPointer(strconv.FormatInt(commentable.GetForeignIndex(), 10)), }) if err != nil { return nil, false, err @@ -171,7 +171,7 @@ func (c *CodeCommitDownloader) GetPullRequests(page, perPage int) ([]*base.PullR prs := make([]*base.PullRequest, 0, len(batch)) for _, id := range batch { output, err := c.codeCommitClient.GetPullRequest(c.ctx, &codecommit.GetPullRequestInput{ - PullRequestId: aws.String(id), + PullRequestId: util.ToPointer(id), }) if err != nil { return nil, false, err @@ -243,7 +243,7 @@ func (c *CodeCommitDownloader) getAllPullRequestIDs() ([]string, error) { for { output, err := c.codeCommitClient.ListPullRequests(c.ctx, &codecommit.ListPullRequestsInput{ - RepositoryName: aws.String(c.repoName), + RepositoryName: util.ToPointer(c.repoName), NextToken: nextToken, }) if err != nil { diff --git a/services/migrations/error.go b/services/migrations/error.go index c7d912f50be2e..9b470149bf01d 100644 --- a/services/migrations/error.go +++ b/services/migrations/error.go @@ -7,7 +7,7 @@ package migrations import ( "errors" - "github.com/google/go-github/v61/github" + "github.com/google/go-github/v71/github" ) // ErrRepoNotCreated returns the error that repository not created diff --git a/services/migrations/gitea_uploader.go b/services/migrations/gitea_uploader.go index 9e06b77b66c18..eb21b6534b8f4 100644 --- a/services/migrations/gitea_uploader.go +++ b/services/migrations/gitea_uploader.go @@ -19,6 +19,7 @@ import ( issues_model "code.gitea.io/gitea/models/issues" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" + base_module "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/label" @@ -408,7 +409,7 @@ func (g *GiteaLocalUploader) CreateIssues(issues ...*base.Issue) error { RepoID: g.repo.ID, Repo: g.repo, Index: issue.Number, - Title: util.TruncateRunes(issue.Title, 255), + Title: base_module.TruncateString(issue.Title, 255), Content: issue.Content, Ref: issue.Ref, IsClosed: issue.State == "closed", diff --git a/services/migrations/github.go b/services/migrations/github.go index 604ab84b39645..82f7cb752bf7e 100644 --- a/services/migrations/github.go +++ b/services/migrations/github.go @@ -20,7 +20,7 @@ import ( "code.gitea.io/gitea/modules/proxy" "code.gitea.io/gitea/modules/structs" - "github.com/google/go-github/v61/github" + "github.com/google/go-github/v71/github" "golang.org/x/oauth2" ) @@ -135,7 +135,7 @@ func (g *GithubDownloaderV3) LogString() string { func (g *GithubDownloaderV3) addClient(client *http.Client, baseURL string) { githubClient := github.NewClient(client) if baseURL != "https://github.com" { - githubClient, _ = github.NewClient(client).WithEnterpriseURLs(baseURL, baseURL) + githubClient, _ = githubClient.WithEnterpriseURLs(baseURL, baseURL) } g.clients = append(g.clients, githubClient) g.rates = append(g.rates, nil) @@ -448,9 +448,11 @@ func (g *GithubDownloaderV3) GetIssues(page, perPage int) ([]*base.Issue, bool, if !g.SkipReactions { for i := 1; ; i++ { g.waitAndPickClient() - res, resp, err := g.getClient().Reactions.ListIssueReactions(g.ctx, g.repoOwner, g.repoName, issue.GetNumber(), &github.ListOptions{ - Page: i, - PerPage: perPage, + res, resp, err := g.getClient().Reactions.ListIssueReactions(g.ctx, g.repoOwner, g.repoName, issue.GetNumber(), &github.ListReactionOptions{ + ListOptions: github.ListOptions{ + Page: i, + PerPage: perPage, + }, }) if err != nil { return nil, false, err @@ -534,9 +536,11 @@ func (g *GithubDownloaderV3) getComments(commentable base.Commentable) ([]*base. if !g.SkipReactions { for i := 1; ; i++ { g.waitAndPickClient() - res, resp, err := g.getClient().Reactions.ListIssueCommentReactions(g.ctx, g.repoOwner, g.repoName, comment.GetID(), &github.ListOptions{ - Page: i, - PerPage: g.maxPerPage, + res, resp, err := g.getClient().Reactions.ListIssueCommentReactions(g.ctx, g.repoOwner, g.repoName, comment.GetID(), &github.ListReactionOptions{ + ListOptions: github.ListOptions{ + Page: i, + PerPage: g.maxPerPage, + }, }) if err != nil { return nil, err @@ -609,9 +613,11 @@ func (g *GithubDownloaderV3) GetAllComments(page, perPage int) ([]*base.Comment, if !g.SkipReactions { for i := 1; ; i++ { g.waitAndPickClient() - res, resp, err := g.getClient().Reactions.ListIssueCommentReactions(g.ctx, g.repoOwner, g.repoName, comment.GetID(), &github.ListOptions{ - Page: i, - PerPage: g.maxPerPage, + res, resp, err := g.getClient().Reactions.ListIssueCommentReactions(g.ctx, g.repoOwner, g.repoName, comment.GetID(), &github.ListReactionOptions{ + ListOptions: github.ListOptions{ + Page: i, + PerPage: g.maxPerPage, + }, }) if err != nil { return nil, false, err @@ -680,9 +686,11 @@ func (g *GithubDownloaderV3) GetPullRequests(page, perPage int) ([]*base.PullReq if !g.SkipReactions { for i := 1; ; i++ { g.waitAndPickClient() - res, resp, err := g.getClient().Reactions.ListIssueReactions(g.ctx, g.repoOwner, g.repoName, pr.GetNumber(), &github.ListOptions{ - Page: i, - PerPage: perPage, + res, resp, err := g.getClient().Reactions.ListIssueReactions(g.ctx, g.repoOwner, g.repoName, pr.GetNumber(), &github.ListReactionOptions{ + ListOptions: github.ListOptions{ + Page: i, + PerPage: perPage, + }, }) if err != nil { return nil, false, err @@ -767,9 +775,11 @@ func (g *GithubDownloaderV3) convertGithubReviewComments(cs []*github.PullReques if !g.SkipReactions { for i := 1; ; i++ { g.waitAndPickClient() - res, resp, err := g.getClient().Reactions.ListPullRequestCommentReactions(g.ctx, g.repoOwner, g.repoName, c.GetID(), &github.ListOptions{ - Page: i, - PerPage: g.maxPerPage, + res, resp, err := g.getClient().Reactions.ListPullRequestCommentReactions(g.ctx, g.repoOwner, g.repoName, c.GetID(), &github.ListReactionOptions{ + ListOptions: github.ListOptions{ + Page: i, + PerPage: g.maxPerPage, + }, }) if err != nil { return nil, err @@ -879,3 +889,18 @@ func (g *GithubDownloaderV3) GetReviews(reviewable base.Reviewable) ([]*base.Rev } return allReviews, nil } + +// FormatCloneURL add authentication into remote URLs +func (g *GithubDownloaderV3) FormatCloneURL(opts MigrateOptions, remoteAddr string) (string, error) { + u, err := url.Parse(remoteAddr) + if err != nil { + return "", err + } + if len(opts.AuthToken) > 0 { + // "multiple tokens" are used to benefit more "API rate limit quota" + // git clone doesn't count for rate limits, so only use the first token. + // source: https://github.com/orgs/community/discussions/44515 + u.User = url.UserPassword("oauth2", strings.Split(opts.AuthToken, ",")[0]) + } + return u.String(), nil +} diff --git a/services/migrations/github_test.go b/services/migrations/github_test.go index 2b89e6dc0fd16..13f4b358c50d3 100644 --- a/services/migrations/github_test.go +++ b/services/migrations/github_test.go @@ -13,6 +13,7 @@ import ( base "code.gitea.io/gitea/modules/migration" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestGitHubDownloadRepo(t *testing.T) { @@ -429,3 +430,36 @@ func TestGitHubDownloadRepo(t *testing.T) { }, }, reviews) } + +func TestGithubMultiToken(t *testing.T) { + testCases := []struct { + desc string + token string + expectedCloneURL string + }{ + { + desc: "Single Token", + token: "single_token", + expectedCloneURL: "https://oauth2:single_token@github.com", + }, + { + desc: "Multi Token", + token: "token1,token2", + expectedCloneURL: "https://oauth2:token1@github.com", + }, + } + factory := GithubDownloaderV3Factory{} + + for _, tC := range testCases { + t.Run(tC.desc, func(t *testing.T) { + opts := base.MigrateOptions{CloneAddr: "https://github.com/go-gitea/gitea", AuthToken: tC.token} + client, err := factory.New(context.Background(), opts) + require.NoError(t, err) + + cloneURL, err := client.FormatCloneURL(opts, "https://github.com") + require.NoError(t, err) + + assert.Equal(t, tC.expectedCloneURL, cloneURL) + }) + } +} diff --git a/services/migrations/migrate.go b/services/migrations/migrate.go index 51b22d6111b15..d0ad6d0139a95 100644 --- a/services/migrations/migrate.go +++ b/services/migrations/migrate.go @@ -12,10 +12,10 @@ import ( "path/filepath" "strings" + "code.gitea.io/gitea/models" repo_model "code.gitea.io/gitea/models/repo" system_model "code.gitea.io/gitea/models/system" user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/hostmatcher" "code.gitea.io/gitea/modules/log" base "code.gitea.io/gitea/modules/migration" @@ -43,16 +43,16 @@ func IsMigrateURLAllowed(remoteURL string, doer *user_model.User) error { // Remote address can be HTTP/HTTPS/Git URL or local path. u, err := url.Parse(remoteURL) if err != nil { - return &git.ErrInvalidCloneAddr{IsURLError: true, Host: remoteURL} + return &models.ErrInvalidCloneAddr{IsURLError: true, Host: remoteURL} } if u.Scheme == "file" || u.Scheme == "" { if !doer.CanImportLocal() { - return &git.ErrInvalidCloneAddr{Host: "", IsPermissionDenied: true, LocalPath: true} + return &models.ErrInvalidCloneAddr{Host: "", IsPermissionDenied: true, LocalPath: true} } isAbs := filepath.IsAbs(u.Host + u.Path) if !isAbs { - return &git.ErrInvalidCloneAddr{Host: "", IsInvalidPath: true, LocalPath: true} + return &models.ErrInvalidCloneAddr{Host: "", IsInvalidPath: true, LocalPath: true} } isDir, err := util.IsDir(u.Host + u.Path) if err != nil { @@ -60,18 +60,18 @@ func IsMigrateURLAllowed(remoteURL string, doer *user_model.User) error { return err } if !isDir { - return &git.ErrInvalidCloneAddr{Host: "", IsInvalidPath: true, LocalPath: true} + return &models.ErrInvalidCloneAddr{Host: "", IsInvalidPath: true, LocalPath: true} } return nil } if u.Scheme == "git" && u.Port() != "" && (strings.Contains(remoteURL, "%0d") || strings.Contains(remoteURL, "%0a")) { - return &git.ErrInvalidCloneAddr{Host: u.Host, IsURLError: true} + return &models.ErrInvalidCloneAddr{Host: u.Host, IsURLError: true} } if u.Opaque != "" || u.Scheme != "" && u.Scheme != "http" && u.Scheme != "https" && u.Scheme != "git" { - return &git.ErrInvalidCloneAddr{Host: u.Host, IsProtocolInvalid: true, IsPermissionDenied: true, IsURLError: true} + return &models.ErrInvalidCloneAddr{Host: u.Host, IsProtocolInvalid: true, IsPermissionDenied: true, IsURLError: true} } hostName, _, err := net.SplitHostPort(u.Host) @@ -95,12 +95,12 @@ func checkByAllowBlockList(hostName string, addrList []net.IP) error { } var blockedError error if blockList.MatchHostName(hostName) || ipBlocked { - blockedError = &git.ErrInvalidCloneAddr{Host: hostName, IsPermissionDenied: true} + blockedError = &models.ErrInvalidCloneAddr{Host: hostName, IsPermissionDenied: true} } // if we have an allow-list, check the allow-list before return to get the more accurate error if !allowList.IsEmpty() { if !allowList.MatchHostName(hostName) && !ipAllowed { - return &git.ErrInvalidCloneAddr{Host: hostName, IsPermissionDenied: true} + return &models.ErrInvalidCloneAddr{Host: hostName, IsPermissionDenied: true} } } // otherwise, we always follow the blocked list diff --git a/services/mirror/mirror_pull.go b/services/mirror/mirror_pull.go index 22d380e8e68a6..06f1fb3a3e2e2 100644 --- a/services/mirror/mirror_pull.go +++ b/services/mirror/mirror_pull.go @@ -41,13 +41,18 @@ func UpdateAddress(ctx context.Context, m *repo_model.Mirror, addr string) error repoPath := m.GetRepository(ctx).RepoPath() // Remove old remote _, _, err = git.NewCommand(ctx, "remote", "rm").AddDynamicArguments(remoteName).RunStdString(&git.RunOpts{Dir: repoPath}) - if err != nil && !strings.HasPrefix(err.Error(), "exit status 128 - fatal: No such remote ") { + if err != nil && !git.IsRemoteNotExistError(err) { return err } cmd := git.NewCommand(ctx, "remote", "add").AddDynamicArguments(remoteName).AddArguments("--mirror=fetch").AddDynamicArguments(addr) + if strings.Contains(addr, "://") && strings.Contains(addr, "@") { + cmd.SetDescription(fmt.Sprintf("remote add %s --mirror=fetch %s [repo_path: %s]", remoteName, util.SanitizeCredentialURLs(addr), repoPath)) + } else { + cmd.SetDescription(fmt.Sprintf("remote add %s --mirror=fetch %s [repo_path: %s]", remoteName, addr, repoPath)) + } _, _, err = cmd.RunStdString(&git.RunOpts{Dir: repoPath}) - if err != nil && !strings.HasPrefix(err.Error(), "exit status 128 - fatal: No such remote ") { + if err != nil && !git.IsRemoteNotExistError(err) { return err } @@ -56,13 +61,18 @@ func UpdateAddress(ctx context.Context, m *repo_model.Mirror, addr string) error wikiRemotePath := repo_module.WikiRemoteURL(ctx, addr) // Remove old remote of wiki _, _, err = git.NewCommand(ctx, "remote", "rm").AddDynamicArguments(remoteName).RunStdString(&git.RunOpts{Dir: wikiPath}) - if err != nil && !strings.HasPrefix(err.Error(), "exit status 128 - fatal: No such remote ") { + if err != nil && !git.IsRemoteNotExistError(err) { return err } cmd = git.NewCommand(ctx, "remote", "add").AddDynamicArguments(remoteName).AddArguments("--mirror=fetch").AddDynamicArguments(wikiRemotePath) + if strings.Contains(wikiRemotePath, "://") && strings.Contains(wikiRemotePath, "@") { + cmd.SetDescription(fmt.Sprintf("remote add %s --mirror=fetch %s [repo_path: %s]", remoteName, util.SanitizeCredentialURLs(wikiRemotePath), wikiPath)) + } else { + cmd.SetDescription(fmt.Sprintf("remote add %s --mirror=fetch %s [repo_path: %s]", remoteName, wikiRemotePath, wikiPath)) + } _, _, err = cmd.RunStdString(&git.RunOpts{Dir: wikiPath}) - if err != nil && !strings.HasPrefix(err.Error(), "exit status 128 - fatal: No such remote ") { + if err != nil && !git.IsRemoteNotExistError(err) { return err } } @@ -87,6 +97,7 @@ type mirrorSyncResult struct { /* // * [new tag] v0.1.8 -> v0.1.8 // * [new branch] master -> origin/master +// * [new ref] refs/pull/2/head -> refs/pull/2/head" // - [deleted] (none) -> origin/test // delete a branch // - [deleted] (none) -> 1 // delete a tag // 957a993..a87ba5f test -> origin/test @@ -117,10 +128,17 @@ func parseRemoteUpdateOutput(output, remoteName string) []*mirrorSyncResult { refName: git.RefNameFromBranch(refName), oldCommitID: gitShortEmptySha, }) + case strings.HasPrefix(lines[i], " * [new ref]"): // new reference + results = append(results, &mirrorSyncResult{ + refName: git.RefName(refName), + oldCommitID: gitShortEmptySha, + }) case strings.HasPrefix(lines[i], " - "): // Delete reference isTag := !strings.HasPrefix(refName, remoteName+"/") var refFullName git.RefName - if isTag { + if strings.HasPrefix(refName, "refs/") { + refFullName = git.RefName(refName) + } else if isTag { refFullName = git.RefNameFromTag(refName) } else { refFullName = git.RefNameFromBranch(strings.TrimPrefix(refName, remoteName+"/")) @@ -143,8 +161,15 @@ func parseRemoteUpdateOutput(output, remoteName string) []*mirrorSyncResult { log.Error("Expect two SHAs but not what found: %q", lines[i]) continue } + var refFullName git.RefName + if strings.HasPrefix(refName, "refs/") { + refFullName = git.RefName(refName) + } else { + refFullName = git.RefNameFromBranch(strings.TrimPrefix(refName, remoteName+"/")) + } + results = append(results, &mirrorSyncResult{ - refName: git.RefNameFromBranch(strings.TrimPrefix(refName, remoteName+"/")), + refName: refFullName, oldCommitID: shas[0], newCommitID: shas[1], }) @@ -159,8 +184,15 @@ func parseRemoteUpdateOutput(output, remoteName string) []*mirrorSyncResult { log.Error("Expect two SHAs but not what found: %q", lines[i]) continue } + var refFullName git.RefName + if strings.HasPrefix(refName, "refs/") { + refFullName = git.RefName(refName) + } else { + refFullName = git.RefNameFromBranch(strings.TrimPrefix(refName, remoteName+"/")) + } + results = append(results, &mirrorSyncResult{ - refName: git.RefNameFromBranch(strings.TrimPrefix(refName, remoteName+"/")), + refName: refFullName, oldCommitID: shas[0], newCommitID: shas[1], }) @@ -187,6 +219,7 @@ func pruneBrokenReferences(ctx context.Context, stderrBuilder.Reset() stdoutBuilder.Reset() pruneErr := git.NewCommand(ctx, "remote", "prune").AddDynamicArguments(m.GetRemoteName()). + SetDescription(fmt.Sprintf("Mirror.runSync %ssPrune references: %s ", wiki, m.Repo.FullName())). Run(&git.RunOpts{ Timeout: timeout, Dir: repoPath, @@ -237,13 +270,15 @@ func runSync(ctx context.Context, m *repo_model.Mirror) ([]*mirrorSyncResult, bo stdoutBuilder := strings.Builder{} stderrBuilder := strings.Builder{} - if err := cmd.Run(&git.RunOpts{ - Timeout: timeout, - Dir: repoPath, - Env: envs, - Stdout: &stdoutBuilder, - Stderr: &stderrBuilder, - }); err != nil { + if err := cmd. + SetDescription(fmt.Sprintf("Mirror.runSync: %s", m.Repo.FullName())). + Run(&git.RunOpts{ + Timeout: timeout, + Dir: repoPath, + Env: envs, + Stdout: &stdoutBuilder, + Stderr: &stderrBuilder, + }); err != nil { stdout := stdoutBuilder.String() stderr := stderrBuilder.String() @@ -262,12 +297,14 @@ func runSync(ctx context.Context, m *repo_model.Mirror) ([]*mirrorSyncResult, bo // Successful prune - reattempt mirror stderrBuilder.Reset() stdoutBuilder.Reset() - if err = cmd.Run(&git.RunOpts{ - Timeout: timeout, - Dir: repoPath, - Stdout: &stdoutBuilder, - Stderr: &stderrBuilder, - }); err != nil { + if err = cmd. + SetDescription(fmt.Sprintf("Mirror.runSync: %s", m.Repo.FullName())). + Run(&git.RunOpts{ + Timeout: timeout, + Dir: repoPath, + Stdout: &stdoutBuilder, + Stderr: &stderrBuilder, + }); err != nil { stdout := stdoutBuilder.String() stderr := stderrBuilder.String() @@ -331,6 +368,7 @@ func runSync(ctx context.Context, m *repo_model.Mirror) ([]*mirrorSyncResult, bo stderrBuilder.Reset() stdoutBuilder.Reset() if err := git.NewCommand(ctx, "remote", "update", "--prune").AddDynamicArguments(m.GetRemoteName()). + SetDescription(fmt.Sprintf("Mirror.runSync Wiki: %s ", m.Repo.FullName())). Run(&git.RunOpts{ Timeout: timeout, Dir: wikiPath, @@ -357,6 +395,7 @@ func runSync(ctx context.Context, m *repo_model.Mirror) ([]*mirrorSyncResult, bo stdoutBuilder.Reset() if err = git.NewCommand(ctx, "remote", "update", "--prune").AddDynamicArguments(m.GetRemoteName()). + SetDescription(fmt.Sprintf("Mirror.runSync Wiki: %s ", m.Repo.FullName())). Run(&git.RunOpts{ Timeout: timeout, Dir: wikiPath, diff --git a/services/mirror/mirror_push.go b/services/mirror/mirror_push.go index 02ff97b1f03e3..2429f686dd9c2 100644 --- a/services/mirror/mirror_push.go +++ b/services/mirror/mirror_push.go @@ -9,6 +9,7 @@ import ( "fmt" "io" "regexp" + "strings" "time" "code.gitea.io/gitea/models/db" @@ -18,6 +19,7 @@ import ( "code.gitea.io/gitea/modules/lfs" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/process" + "code.gitea.io/gitea/modules/proxy" "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" @@ -30,6 +32,11 @@ var stripExitStatus = regexp.MustCompile(`exit status \d+ - `) func AddPushMirrorRemote(ctx context.Context, m *repo_model.PushMirror, addr string) error { addRemoteAndConfig := func(addr, path string) error { cmd := git.NewCommand(ctx, "remote", "add", "--mirror=push").AddDynamicArguments(m.RemoteName, addr) + if strings.Contains(addr, "://") && strings.Contains(addr, "@") { + cmd.SetDescription(fmt.Sprintf("remote add %s --mirror=push %s [repo_path: %s]", m.RemoteName, util.SanitizeCredentialURLs(addr), path)) + } else { + cmd.SetDescription(fmt.Sprintf("remote add %s --mirror=push %s [repo_path: %s]", m.RemoteName, addr, path)) + } if _, _, err := cmd.RunStdString(&git.RunOpts{Dir: path}); err != nil { return err } @@ -161,11 +168,13 @@ func runPushSync(ctx context.Context, m *repo_model.PushMirror) error { log.Trace("Pushing %s mirror[%d] remote %s", path, m.ID, m.RemoteName) + envs := proxy.EnvWithProxy(remoteURL.URL) if err := git.Push(ctx, path, git.PushOptions{ Remote: m.RemoteName, Force: true, Mirror: true, Timeout: timeout, + Env: envs, }); err != nil { log.Error("Error pushing %s mirror[%d] remote %s: %v", path, m.ID, m.RemoteName, err) diff --git a/services/mirror/mirror_test.go b/services/mirror/mirror_test.go index 8ad524b608de4..76632b6872ca6 100644 --- a/services/mirror/mirror_test.go +++ b/services/mirror/mirror_test.go @@ -17,9 +17,13 @@ func Test_parseRemoteUpdateOutput(t *testing.T) { - [deleted] (none) -> tag1 + f895a1e...957a993 test2 -> origin/test2 (forced update) 957a993..a87ba5f test3 -> origin/test3 + * [new ref] refs/pull/26595/head -> refs/pull/26595/head + * [new ref] refs/pull/26595/merge -> refs/pull/26595/merge + e0639e38fb..6db2410489 refs/pull/25873/head -> refs/pull/25873/head + + 1c97ebc746...976d27d52f refs/pull/25873/merge -> refs/pull/25873/merge (forced update) ` results := parseRemoteUpdateOutput(output, "origin") - assert.Len(t, results, 6) + assert.Len(t, results, 10) assert.EqualValues(t, "refs/tags/v0.1.8", results[0].refName.String()) assert.EqualValues(t, gitShortEmptySha, results[0].oldCommitID) assert.EqualValues(t, "", results[0].newCommitID) @@ -43,4 +47,20 @@ func Test_parseRemoteUpdateOutput(t *testing.T) { assert.EqualValues(t, "refs/heads/test3", results[5].refName.String()) assert.EqualValues(t, "957a993", results[5].oldCommitID) assert.EqualValues(t, "a87ba5f", results[5].newCommitID) + + assert.EqualValues(t, "refs/pull/26595/head", results[6].refName.String()) + assert.EqualValues(t, gitShortEmptySha, results[6].oldCommitID) + assert.EqualValues(t, "", results[6].newCommitID) + + assert.EqualValues(t, "refs/pull/26595/merge", results[7].refName.String()) + assert.EqualValues(t, gitShortEmptySha, results[7].oldCommitID) + assert.EqualValues(t, "", results[7].newCommitID) + + assert.EqualValues(t, "refs/pull/25873/head", results[8].refName.String()) + assert.EqualValues(t, "e0639e38fb", results[8].oldCommitID) + assert.EqualValues(t, "6db2410489", results[8].newCommitID) + + assert.EqualValues(t, "refs/pull/25873/merge", results[9].refName.String()) + assert.EqualValues(t, "1c97ebc746", results[9].oldCommitID) + assert.EqualValues(t, "976d27d52f", results[9].newCommitID) } diff --git a/services/org/org.go b/services/org/org.go index 3d30ae21a39d3..c19572a123062 100644 --- a/services/org/org.go +++ b/services/org/org.go @@ -7,45 +7,17 @@ import ( "context" "fmt" - actions_model "code.gitea.io/gitea/models/actions" + "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/db" org_model "code.gitea.io/gitea/models/organization" packages_model "code.gitea.io/gitea/models/packages" repo_model "code.gitea.io/gitea/models/repo" - secret_model "code.gitea.io/gitea/models/secret" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/storage" "code.gitea.io/gitea/modules/util" repo_service "code.gitea.io/gitea/services/repository" ) -// deleteOrganization deletes models associated to an organization. -func deleteOrganization(ctx context.Context, org *org_model.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, - &org_model.Team{OrgID: org.ID}, - &org_model.OrgUser{OrgID: org.ID}, - &org_model.TeamUser{OrgID: org.ID}, - &org_model.TeamUnit{OrgID: org.ID}, - &org_model.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 -} - // DeleteOrganization completely and permanently deletes everything of organization. func DeleteOrganization(ctx context.Context, org *org_model.Organization, purge bool) error { ctx, committer, err := db.TxContext(ctx) @@ -66,17 +38,17 @@ func DeleteOrganization(ctx context.Context, org *org_model.Organization, purge if err != nil { return fmt.Errorf("GetRepositoryCount: %w", err) } else if count > 0 { - return repo_model.ErrUserOwnRepos{UID: org.ID} + return models.ErrUserOwnRepos{UID: org.ID} } // Check ownership of packages. if ownsPackages, err := packages_model.HasOwnerPackages(ctx, org.ID); err != nil { return fmt.Errorf("HasOwnerPackages: %w", err) } else if ownsPackages { - return packages_model.ErrUserOwnPackages{UID: org.ID} + return models.ErrUserOwnPackages{UID: org.ID} } - if err := deleteOrganization(ctx, org); err != nil { + if err := org_model.DeleteOrganization(ctx, org); err != nil { return fmt.Errorf("DeleteOrganization: %w", err) } diff --git a/services/org/org_test.go b/services/org/org_test.go index 791404c5c8919..e7d2a18ea9688 100644 --- a/services/org/org_test.go +++ b/services/org/org_test.go @@ -6,9 +6,9 @@ package org import ( "testing" + "code.gitea.io/gitea/models" "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" user_model "code.gitea.io/gitea/models/user" @@ -30,7 +30,7 @@ func TestDeleteOrganization(t *testing.T) { org = unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3}) err := DeleteOrganization(db.DefaultContext, org, false) assert.Error(t, err) - assert.True(t, repo_model.IsErrUserOwnRepos(err)) + assert.True(t, models.IsErrUserOwnRepos(err)) user := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 5}) assert.Error(t, DeleteOrganization(db.DefaultContext, user, false)) diff --git a/services/org/team.go b/services/org/team.go index ee3bd898ea7ed..3688e684339db 100644 --- a/services/org/team.go +++ b/services/org/team.go @@ -141,14 +141,11 @@ func UpdateTeam(ctx context.Context, t *organization.Team, authChanged, includeA // Update access for team members if needed. if authChanged { - repos, err := repo_model.GetTeamRepositories(ctx, &repo_model.SearchTeamRepoOptions{ - TeamID: t.ID, - }) - if err != nil { - return fmt.Errorf("GetTeamRepositories: %w", err) + if err = t.LoadRepositories(ctx); err != nil { + return fmt.Errorf("LoadRepositories: %w", err) } - for _, repo := range repos { + for _, repo := range t.Repos { if err = access_model.RecalculateTeamAccesses(ctx, repo, 0); err != nil { return fmt.Errorf("recalculateTeamAccesses: %w", err) } @@ -175,6 +172,10 @@ func DeleteTeam(ctx context.Context, t *organization.Team) error { } defer committer.Close() + if err := t.LoadRepositories(ctx); err != nil { + return err + } + if err := t.LoadMembers(ctx); err != nil { return err } @@ -300,11 +301,8 @@ func AddTeamMember(ctx context.Context, team *organization.Team, user *user_mode // FIXME: Update watch repos batchly if setting.Service.AutoWatchNewRepos { // Get team and its repositories. - repos, err := repo_model.GetTeamRepositories(ctx, &repo_model.SearchTeamRepoOptions{ - TeamID: team.ID, - }) - if err != nil { - log.Error("GetTeamRepositories failed: %v", err) + if err := team.LoadRepositories(ctx); err != nil { + log.Error("team.LoadRepositories failed: %v", err) } // FIXME: in the goroutine, it can't access the "ctx", it could only use db.DefaultContext at the moment @@ -314,7 +312,7 @@ func AddTeamMember(ctx context.Context, team *organization.Team, user *user_mode log.Error("watch repo failed: %v", err) } } - }(repos) + }(team.Repos) } return nil @@ -334,10 +332,7 @@ func removeTeamMember(ctx context.Context, team *organization.Team, user *user_m team.NumMembers-- - repos, err := repo_model.GetTeamRepositories(ctx, &repo_model.SearchTeamRepoOptions{ - TeamID: team.ID, - }) - if err != nil { + if err := team.LoadRepositories(ctx); err != nil { return err } @@ -355,7 +350,7 @@ func removeTeamMember(ctx context.Context, team *organization.Team, user *user_m } // Delete access to team repositories. - for _, repo := range repos { + for _, repo := range team.Repos { if err := access_model.RecalculateUserAccess(ctx, repo, user.ID); err != nil { return err } diff --git a/services/org/team_test.go b/services/org/team_test.go index 3791776e46e9f..98addac8f8a29 100644 --- a/services/org/team_test.go +++ b/services/org/team_test.go @@ -189,12 +189,9 @@ func TestIncludesAllRepositoriesTeams(t *testing.T) { testTeamRepositories := func(teamID int64, repoIDs []int64) { team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: teamID}) - repos, err := repo_model.GetTeamRepositories(db.DefaultContext, &repo_model.SearchTeamRepoOptions{ - TeamID: team.ID, - }) - assert.NoError(t, err, "%s: GetTeamRepositories", team.Name) - assert.Len(t, repos, team.NumRepos, "%s: len repo", team.Name) - assert.Len(t, repos, len(repoIDs), "%s: repo count", team.Name) + assert.NoError(t, team.LoadRepositories(db.DefaultContext), "%s: GetRepositories", team.Name) + assert.Len(t, team.Repos, team.NumRepos, "%s: len repo", team.Name) + assert.Len(t, team.Repos, len(repoIDs), "%s: repo count", team.Name) for i, rid := range repoIDs { if rid > 0 { assert.True(t, repo_service.HasRepository(db.DefaultContext, team, rid), "%s: HasRepository(%d) %d", rid, i) @@ -313,5 +310,5 @@ func TestIncludesAllRepositoriesTeams(t *testing.T) { assert.NoError(t, repo_service.DeleteRepositoryDirectly(db.DefaultContext, user, rid), "DeleteRepository %d", i) } } - assert.NoError(t, DeleteOrganization(db.DefaultContext, org, false), "DeleteOrganization") + assert.NoError(t, organization.DeleteOrganization(db.DefaultContext, org), "DeleteOrganization") } diff --git a/services/org/user.go b/services/org/user.go index 0e74d006bb764..0627860fe7c02 100644 --- a/services/org/user.go +++ b/services/org/user.go @@ -60,7 +60,7 @@ func RemoveOrgUser(ctx context.Context, org *organization.Organization, user *us } // Delete all repository accesses and unwatch them. - env, err := repo_model.AccessibleReposEnv(ctx, org, user.ID) + env, err := organization.AccessibleReposEnv(ctx, org, user.ID) if err != nil { return fmt.Errorf("AccessibleReposEnv: %w", err) } diff --git a/services/org/user_test.go b/services/org/user_test.go index 96d1a1c8cace1..56d01a3b63a31 100644 --- a/services/org/user_test.go +++ b/services/org/user_test.go @@ -47,7 +47,7 @@ func TestRemoveOrgUser(t *testing.T) { testSuccess := func(org *organization.Organization, user *user_model.User) { expectedNumMembers := org.NumMembers - if unittest.GetBean(t, &organization.OrgUser{OrgID: org.ID, UID: user.ID}) != nil { + if unittest.BeanExists(t, &organization.OrgUser{OrgID: org.ID, UID: user.ID}) { expectedNumMembers-- } assert.NoError(t, RemoveOrgUser(db.DefaultContext, org, user)) diff --git a/services/packages/arch/repository.go b/services/packages/arch/repository.go index 6731d9a1ac728..a12af82ba5f5d 100644 --- a/services/packages/arch/repository.go +++ b/services/packages/arch/repository.go @@ -26,9 +26,9 @@ import ( "code.gitea.io/gitea/modules/util" packages_service "code.gitea.io/gitea/services/packages" - "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" ) const ( @@ -235,6 +235,28 @@ func buildPackagesIndex(ctx context.Context, ownerID int64, repoVersion *package return packages_service.DeletePackageFile(ctx, pf) } + vpfs := make(map[int64]*entryOptions) + for _, pf := range pfs { + current := &entryOptions{ + File: pf, + } + current.Version, err = packages_model.GetVersionByID(ctx, pf.VersionID) + if err != nil { + return err + } + + // here we compare the versions but not using SearchLatestVersions because we shouldn't allow "downgrading" to a older version by "latest" one. + // https://wiki.archlinux.org/title/Downgrading_packages : randomly downgrading can mess up dependencies: + // If a downgrade involves a soname change, all dependencies may need downgrading or rebuilding too. + if old, ok := vpfs[current.Version.PackageID]; ok { + if compareVersions(old.Version.Version, current.Version.Version) == -1 { + vpfs[current.Version.PackageID] = current + } + } else { + vpfs[current.Version.PackageID] = current + } + } + indexContent, _ := packages_module.NewHashedBuffer() defer indexContent.Close() @@ -243,15 +265,7 @@ func buildPackagesIndex(ctx context.Context, ownerID int64, repoVersion *package cache := make(map[int64]*packages_model.Package) - for _, pf := range pfs { - opts := &entryOptions{ - File: pf, - } - - opts.Version, err = packages_model.GetVersionByID(ctx, pf.VersionID) - if err != nil { - return err - } + for _, opts := range vpfs { if err := json.Unmarshal([]byte(opts.Version.MetadataJSON), &opts.VersionMetadata); err != nil { return err } @@ -263,12 +277,12 @@ func buildPackagesIndex(ctx context.Context, ownerID int64, repoVersion *package } cache[opts.Package.ID] = opts.Package } - opts.Blob, err = packages_model.GetBlobByID(ctx, pf.BlobID) + opts.Blob, err = packages_model.GetBlobByID(ctx, opts.File.BlobID) if err != nil { return err } - sig, err := packages_model.GetPropertiesByName(ctx, packages_model.PropertyTypeFile, pf.ID, arch_module.PropertySignature) + sig, err := packages_model.GetPropertiesByName(ctx, packages_model.PropertyTypeFile, opts.File.ID, arch_module.PropertySignature) if err != nil { return err } @@ -277,7 +291,7 @@ func buildPackagesIndex(ctx context.Context, ownerID int64, repoVersion *package } opts.Signature = sig[0].Value - meta, err := packages_model.GetPropertiesByName(ctx, packages_model.PropertyTypeFile, pf.ID, arch_module.PropertyMetadata) + meta, err := packages_model.GetPropertiesByName(ctx, packages_model.PropertyTypeFile, opts.File.ID, arch_module.PropertyMetadata) if err != nil { return err } diff --git a/services/packages/arch/vercmp.go b/services/packages/arch/vercmp.go new file mode 100644 index 0000000000000..0d33dda0f1232 --- /dev/null +++ b/services/packages/arch/vercmp.go @@ -0,0 +1,113 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package arch + +import ( + "strings" + "unicode" +) + +// https://gitlab.archlinux.org/pacman/pacman/-/blob/d55b47e5512808b67bc944feb20c2bcc6c1a4c45/lib/libalpm/version.c + +import ( + "strconv" +) + +func parseEVR(evr string) (epoch, version, release string) { + if before, after, f := strings.Cut(evr, ":"); f { + epoch = before + evr = after + } else { + epoch = "0" + } + + if before, after, f := strings.Cut(evr, "-"); f { + version = before + release = after + } else { + version = evr + release = "1" + } + return epoch, version, release +} + +func compareSegments(a, b []string) int { + lenA, lenB := len(a), len(b) + var l int + if lenA > lenB { + l = lenB + } else { + l = lenA + } + for i := 0; i < l; i++ { + if r := compare(a[i], b[i]); r != 0 { + return r + } + } + if lenA == lenB { + return 0 + } else if l == lenA { + return -1 + } + return 1 +} + +func compare(a, b string) int { + if a == b { + return 0 + } + + aNumeric := isNumeric(a) + bNumeric := isNumeric(b) + + if aNumeric && bNumeric { + aInt, _ := strconv.Atoi(a) + bInt, _ := strconv.Atoi(b) + switch { + case aInt < bInt: + return -1 + case aInt > bInt: + return 1 + default: + return 0 + } + } + + if aNumeric { + return 1 + } + if bNumeric { + return -1 + } + + return strings.Compare(a, b) +} + +func isNumeric(s string) bool { + for _, c := range s { + if !unicode.IsDigit(c) { + return false + } + } + return true +} + +func compareVersions(a, b string) int { + if a == b { + return 0 + } + + epochA, versionA, releaseA := parseEVR(a) + epochB, versionB, releaseB := parseEVR(b) + + if res := compareSegments([]string{epochA}, []string{epochB}); res != 0 { + return res + } + + if res := compareSegments(strings.Split(versionA, "."), strings.Split(versionB, ".")); res != 0 { + return res + } + + return compareSegments([]string{releaseA}, []string{releaseB}) +} diff --git a/services/packages/arch/vercmp_test.go b/services/packages/arch/vercmp_test.go new file mode 100644 index 0000000000000..2014a6d429d14 --- /dev/null +++ b/services/packages/arch/vercmp_test.go @@ -0,0 +1,27 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package arch + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestCompareVersions(t *testing.T) { + // https://man.archlinux.org/man/vercmp.8.en + checks := [][]string{ + {"1.0a", "1.0b", "1.0beta", "1.0p", "1.0pre", "1.0rc", "1.0", "1.0.a", "1.0.1"}, + {"1", "1.0", "1.1", "1.1.1", "1.2", "2.0", "3.0.0"}, + } + for _, check := range checks { + for i := 0; i < len(check)-1; i++ { + require.Equal(t, -1, compareVersions(check[i], check[i+1])) + require.Equal(t, 1, compareVersions(check[i+1], check[i])) + } + } + require.Equal(t, 1, compareVersions("1.0-2", "1.0")) + require.Equal(t, 0, compareVersions("0:1.0-1", "1.0")) + require.Equal(t, 1, compareVersions("1:1.0-1", "2.0")) +} diff --git a/services/packages/cargo/index.go b/services/packages/cargo/index.go index e8a8313625dcb..ae4b96702922d 100644 --- a/services/packages/cargo/index.go +++ b/services/packages/cargo/index.go @@ -248,7 +248,7 @@ func createOrUpdateConfigFile(ctx context.Context, repo *repo_model.Repository, "Initialize Cargo Config", func(t *files_service.TemporaryUploadRepository) error { var b bytes.Buffer - err := json.NewEncoder(&b).Encode(BuildConfig(owner, setting.Service.RequireSignInView || owner.Visibility != structs.VisibleTypePublic || repo.IsPrivate)) + err := json.NewEncoder(&b).Encode(BuildConfig(owner, setting.Service.RequireSignInViewStrict || owner.Visibility != structs.VisibleTypePublic || repo.IsPrivate)) if err != nil { return err } diff --git a/services/packages/debian/repository.go b/services/packages/debian/repository.go index 13e98a820e50e..34b52b45cfdec 100644 --- a/services/packages/debian/repository.go +++ b/services/packages/debian/repository.go @@ -23,10 +23,10 @@ import ( "code.gitea.io/gitea/modules/util" packages_service "code.gitea.io/gitea/services/packages" - "github.com/keybase/go-crypto/openpgp" - "github.com/keybase/go-crypto/openpgp/armor" - "github.com/keybase/go-crypto/openpgp/clearsign" - "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/clearsign" + "github.com/ProtonMail/go-crypto/openpgp/packet" "github.com/ulikunitz/xz" ) diff --git a/services/packages/rpm/repository.go b/services/packages/rpm/repository.go index a7d196c15c5fa..5027021c52047 100644 --- a/services/packages/rpm/repository.go +++ b/services/packages/rpm/repository.go @@ -408,7 +408,6 @@ func buildPrimary(ctx context.Context, pv *packages_model.PackageVersion, pfs [] files = append(files, f) } } - packageVersion := fmt.Sprintf("%s-%s", pd.FileMetadata.Version, pd.FileMetadata.Release) packages = append(packages, &Package{ Type: "rpm", Name: pd.Package.Name, @@ -437,7 +436,7 @@ func buildPrimary(ctx context.Context, pv *packages_model.PackageVersion, pfs [] Archive: pd.FileMetadata.ArchiveSize, }, Location: Location{ - Href: fmt.Sprintf("package/%s/%s/%s/%s-%s.%s.rpm", pd.Package.Name, packageVersion, pd.FileMetadata.Architecture, pd.Package.Name, packageVersion, pd.FileMetadata.Architecture), + Href: fmt.Sprintf("package/%s/%s/%s/%s-%s.%s.rpm", pd.Package.Name, pd.Version.Version, pd.FileMetadata.Architecture, pd.Package.Name, pd.Version.Version, pd.FileMetadata.Architecture), }, Format: Format{ License: pd.VersionMetadata.License, diff --git a/services/projects/issue.go b/services/projects/issue.go index db1621a39f933..090d19d2f4700 100644 --- a/services/projects/issue.go +++ b/services/projects/issue.go @@ -11,6 +11,7 @@ import ( issues_model "code.gitea.io/gitea/models/issues" project_model "code.gitea.io/gitea/models/project" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/optional" ) // MoveIssuesOnProjectColumn moves or keeps issues in a column and sorts them inside that column @@ -55,25 +56,152 @@ func MoveIssuesOnProjectColumn(ctx context.Context, doer *user_model.User, colum continue } - _, err = db.Exec(ctx, "UPDATE `project_issue` SET project_board_id=?, sorting=? WHERE issue_id=?", column.ID, sorting, issueID) + projectColumnID, err := curIssue.ProjectColumnID(ctx) if err != nil { return err } - // add timeline to issue - if _, err := issues_model.CreateComment(ctx, &issues_model.CreateCommentOptions{ - Type: issues_model.CommentTypeProjectColumn, - Doer: doer, - Repo: curIssue.Repo, - Issue: curIssue, - ProjectID: column.ProjectID, - ProjectTitle: project.Title, - ProjectColumnID: column.ID, - ProjectColumnTitle: column.Title, - }); err != nil { + if projectColumnID != column.ID { + // add timeline to issue + if _, err := issues_model.CreateComment(ctx, &issues_model.CreateCommentOptions{ + Type: issues_model.CommentTypeProjectColumn, + Doer: doer, + Repo: curIssue.Repo, + Issue: curIssue, + ProjectID: column.ProjectID, + ProjectTitle: project.Title, + ProjectColumnID: column.ID, + ProjectColumnTitle: column.Title, + }); err != nil { + return err + } + } + + _, err = db.Exec(ctx, "UPDATE `project_issue` SET project_board_id=?, sorting=? WHERE issue_id=?", column.ID, sorting, issueID) + if err != nil { return err } } return nil }) } + +// LoadIssuesFromProject load issues assigned to each project column inside the given project +func LoadIssuesFromProject(ctx context.Context, project *project_model.Project, opts *issues_model.IssuesOptions) (map[int64]issues_model.IssueList, error) { + issueList, err := issues_model.Issues(ctx, opts.Copy(func(o *issues_model.IssuesOptions) { + o.ProjectID = project.ID + o.SortType = "project-column-sorting" + })) + if err != nil { + return nil, err + } + + if err := issueList.LoadComments(ctx); err != nil { + return nil, err + } + + defaultColumn, err := project.MustDefaultColumn(ctx) + if err != nil { + return nil, err + } + + issueColumnMap, err := issues_model.LoadProjectIssueColumnMap(ctx, project.ID, defaultColumn.ID) + if err != nil { + return nil, err + } + + results := make(map[int64]issues_model.IssueList) + for _, issue := range issueList { + projectColumnID, ok := issueColumnMap[issue.ID] + if !ok { + continue + } + if _, ok := results[projectColumnID]; !ok { + results[projectColumnID] = make(issues_model.IssueList, 0) + } + results[projectColumnID] = append(results[projectColumnID], issue) + } + return results, nil +} + +// NumClosedIssues return counter of closed issues assigned to a project +func loadNumClosedIssues(ctx context.Context, p *project_model.Project) error { + cnt, 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 { + return err + } + p.NumClosedIssues = cnt + return nil +} + +// NumOpenIssues return counter of open issues assigned to a project +func loadNumOpenIssues(ctx context.Context, p *project_model.Project) error { + cnt, 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 { + return err + } + p.NumOpenIssues = cnt + return nil +} + +func LoadIssueNumbersForProjects(ctx context.Context, projects []*project_model.Project, doer *user_model.User) error { + for _, project := range projects { + if err := LoadIssueNumbersForProject(ctx, project, doer); err != nil { + return err + } + } + return nil +} + +func LoadIssueNumbersForProject(ctx context.Context, project *project_model.Project, doer *user_model.User) error { + // for repository project, just get the numbers + if project.OwnerID == 0 { + if err := loadNumClosedIssues(ctx, project); err != nil { + return err + } + if err := loadNumOpenIssues(ctx, project); err != nil { + return err + } + project.NumIssues = project.NumClosedIssues + project.NumOpenIssues + return nil + } + + if err := project.LoadOwner(ctx); err != nil { + return err + } + + // for user or org projects, we need to check access permissions + opts := issues_model.IssuesOptions{ + ProjectID: project.ID, + Doer: doer, + AllPublic: doer == nil, + Owner: project.Owner, + } + + var err error + project.NumOpenIssues, err = issues_model.CountIssues(ctx, opts.Copy(func(o *issues_model.IssuesOptions) { + o.IsClosed = optional.Some(false) + })) + if err != nil { + return err + } + + project.NumClosedIssues, err = issues_model.CountIssues(ctx, opts.Copy(func(o *issues_model.IssuesOptions) { + o.IsClosed = optional.Some(true) + })) + if err != nil { + return err + } + + project.NumIssues = project.NumClosedIssues + project.NumOpenIssues + + return nil +} diff --git a/services/projects/issue_test.go b/services/projects/issue_test.go new file mode 100644 index 0000000000000..b6f0b1dae11a7 --- /dev/null +++ b/services/projects/issue_test.go @@ -0,0 +1,210 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package project + +import ( + "testing" + + "code.gitea.io/gitea/models/db" + issues_model "code.gitea.io/gitea/models/issues" + org_model "code.gitea.io/gitea/models/organization" + project_model "code.gitea.io/gitea/models/project" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + + "github.com/stretchr/testify/assert" +) + +func Test_Projects(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + userAdmin := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + org3 := unittest.AssertExistsAndLoadBean(t, &org_model.Organization{ID: 3}) + user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) + + t.Run("User projects", func(t *testing.T) { + pi1 := project_model.ProjectIssue{ + ProjectID: 4, + IssueID: 1, + ProjectColumnID: 4, + } + err := db.Insert(db.DefaultContext, &pi1) + assert.NoError(t, err) + defer func() { + _, err = db.DeleteByID[project_model.ProjectIssue](db.DefaultContext, pi1.ID) + assert.NoError(t, err) + }() + + pi2 := project_model.ProjectIssue{ + ProjectID: 4, + IssueID: 4, + ProjectColumnID: 4, + } + err = db.Insert(db.DefaultContext, &pi2) + assert.NoError(t, err) + defer func() { + _, err = db.DeleteByID[project_model.ProjectIssue](db.DefaultContext, pi2.ID) + assert.NoError(t, err) + }() + + projects, err := db.Find[project_model.Project](db.DefaultContext, project_model.SearchOptions{ + OwnerID: user2.ID, + }) + assert.NoError(t, err) + assert.Len(t, projects, 3) + assert.EqualValues(t, 4, projects[0].ID) + + t.Run("Authenticated user", func(t *testing.T) { + columnIssues, err := LoadIssuesFromProject(db.DefaultContext, projects[0], &issues_model.IssuesOptions{ + Owner: user2, + Doer: user2, + }) + assert.NoError(t, err) + assert.Len(t, columnIssues, 1) // 4 has 2 issues, 6 will not contains here because 0 issues + assert.Len(t, columnIssues[4], 2) // user2 can visit both issues, one from public repository one from private repository + }) + + t.Run("Anonymous user", func(t *testing.T) { + columnIssues, err := LoadIssuesFromProject(db.DefaultContext, projects[0], &issues_model.IssuesOptions{ + AllPublic: true, + }) + assert.NoError(t, err) + assert.Len(t, columnIssues, 1) + assert.Len(t, columnIssues[4], 1) // anonymous user can only visit public repo issues + }) + + t.Run("Authenticated user with no permission to the private repo", func(t *testing.T) { + columnIssues, err := LoadIssuesFromProject(db.DefaultContext, projects[0], &issues_model.IssuesOptions{ + Owner: user2, + Doer: user4, + }) + assert.NoError(t, err) + assert.Len(t, columnIssues, 1) + assert.Len(t, columnIssues[4], 1) // user4 can only visit public repo issues + }) + }) + + t.Run("Org projects", func(t *testing.T) { + project1 := project_model.Project{ + Title: "project in an org", + OwnerID: org3.ID, + Type: project_model.TypeOrganization, + TemplateType: project_model.TemplateTypeBasicKanban, + } + err := project_model.NewProject(db.DefaultContext, &project1) + assert.NoError(t, err) + defer func() { + err := project_model.DeleteProjectByID(db.DefaultContext, project1.ID) + assert.NoError(t, err) + }() + + column1 := project_model.Column{ + Title: "column 1", + ProjectID: project1.ID, + } + err = project_model.NewColumn(db.DefaultContext, &column1) + assert.NoError(t, err) + + column2 := project_model.Column{ + Title: "column 2", + ProjectID: project1.ID, + } + err = project_model.NewColumn(db.DefaultContext, &column2) + assert.NoError(t, err) + + // issue 6 belongs to private repo 3 under org 3 + issue6 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 6}) + err = issues_model.IssueAssignOrRemoveProject(db.DefaultContext, issue6, user2, project1.ID, column1.ID) + assert.NoError(t, err) + + // issue 16 belongs to public repo 16 under org 3 + issue16 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 16}) + err = issues_model.IssueAssignOrRemoveProject(db.DefaultContext, issue16, user2, project1.ID, column1.ID) + assert.NoError(t, err) + + projects, err := db.Find[project_model.Project](db.DefaultContext, project_model.SearchOptions{ + OwnerID: org3.ID, + }) + assert.NoError(t, err) + assert.Len(t, projects, 1) + assert.EqualValues(t, project1.ID, projects[0].ID) + + t.Run("Authenticated user", func(t *testing.T) { + columnIssues, err := LoadIssuesFromProject(db.DefaultContext, projects[0], &issues_model.IssuesOptions{ + Owner: org3.AsUser(), + Doer: userAdmin, + }) + assert.NoError(t, err) + assert.Len(t, columnIssues, 1) // column1 has 2 issues, 6 will not contains here because 0 issues + assert.Len(t, columnIssues[column1.ID], 2) // user2 can visit both issues, one from public repository one from private repository + }) + + t.Run("Anonymous user", func(t *testing.T) { + columnIssues, err := LoadIssuesFromProject(db.DefaultContext, projects[0], &issues_model.IssuesOptions{ + AllPublic: true, + }) + assert.NoError(t, err) + assert.Len(t, columnIssues, 1) + assert.Len(t, columnIssues[column1.ID], 1) // anonymous user can only visit public repo issues + }) + + t.Run("Authenticated user with no permission to the private repo", func(t *testing.T) { + columnIssues, err := LoadIssuesFromProject(db.DefaultContext, projects[0], &issues_model.IssuesOptions{ + Owner: org3.AsUser(), + Doer: user2, + }) + assert.NoError(t, err) + assert.Len(t, columnIssues, 1) + assert.Len(t, columnIssues[column1.ID], 1) // user4 can only visit public repo issues + }) + }) + + t.Run("Repository projects", func(t *testing.T) { + repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + + projects, err := db.Find[project_model.Project](db.DefaultContext, project_model.SearchOptions{ + RepoID: repo1.ID, + }) + assert.NoError(t, err) + assert.Len(t, projects, 1) + assert.EqualValues(t, 1, projects[0].ID) + + t.Run("Authenticated user", func(t *testing.T) { + columnIssues, err := LoadIssuesFromProject(db.DefaultContext, projects[0], &issues_model.IssuesOptions{ + RepoIDs: []int64{repo1.ID}, + Doer: userAdmin, + }) + assert.NoError(t, err) + assert.Len(t, columnIssues, 3) + assert.Len(t, columnIssues[1], 2) + assert.Len(t, columnIssues[2], 1) + assert.Len(t, columnIssues[3], 1) + }) + + t.Run("Anonymous user", func(t *testing.T) { + columnIssues, err := LoadIssuesFromProject(db.DefaultContext, projects[0], &issues_model.IssuesOptions{ + AllPublic: true, + }) + assert.NoError(t, err) + assert.Len(t, columnIssues, 3) + assert.Len(t, columnIssues[1], 2) + assert.Len(t, columnIssues[2], 1) + assert.Len(t, columnIssues[3], 1) + }) + + t.Run("Authenticated user with no permission to the private repo", func(t *testing.T) { + columnIssues, err := LoadIssuesFromProject(db.DefaultContext, projects[0], &issues_model.IssuesOptions{ + RepoIDs: []int64{repo1.ID}, + Doer: user2, + }) + assert.NoError(t, err) + assert.Len(t, columnIssues, 3) + assert.Len(t, columnIssues[1], 2) + assert.Len(t, columnIssues[2], 1) + assert.Len(t, columnIssues[3], 1) + }) + }) +} diff --git a/services/projects/main_test.go b/services/projects/main_test.go new file mode 100644 index 0000000000000..93e4887b55bc8 --- /dev/null +++ b/services/projects/main_test.go @@ -0,0 +1,18 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package project + +import ( + "testing" + + "code.gitea.io/gitea/models/unittest" + + _ "code.gitea.io/gitea/models" + _ "code.gitea.io/gitea/models/actions" + _ "code.gitea.io/gitea/models/activities" +) + +func TestMain(m *testing.M) { + unittest.MainTest(m) +} diff --git a/services/pull/check.go b/services/pull/check.go index bffca394a8b80..baca1511a2a39 100644 --- a/services/pull/check.go +++ b/services/pull/check.go @@ -11,6 +11,7 @@ import ( "strconv" "strings" + "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/db" git_model "code.gitea.io/gitea/models/git" issues_model "code.gitea.io/gitea/models/issues" @@ -35,7 +36,7 @@ var prPatchCheckerQueue *queue.WorkerPoolQueue[string] var ( ErrIsClosed = errors.New("pull is closed") - ErrUserNotAllowedToMerge = ErrDisallowedToMerge{} + ErrUserNotAllowedToMerge = models.ErrDisallowedToMerge{} ErrHasMerged = errors.New("has already been merged") ErrIsWorkInProgress = errors.New("work in progress PRs cannot be merged") ErrIsChecking = errors.New("cannot merge while conflict checking is in progress") @@ -105,7 +106,7 @@ func CheckPullMergeable(stdCtx context.Context, doer *user_model.User, perm *acc } if err := CheckPullBranchProtections(ctx, pr, false); err != nil { - if !IsErrDisallowedToMerge(err) { + if !models.IsErrDisallowedToMerge(err) { log.Error("Error whilst checking pull branch protection for %-v: %v", pr, err) return err } @@ -300,11 +301,16 @@ func manuallyMerged(ctx context.Context, pr *issues_model.PullRequest) bool { pr.Merger = merger pr.MergerID = merger.ID - if merged, err := pr.SetMerged(ctx); err != nil { + if err := db.WithTx(ctx, func(ctx context.Context) error { + if merged, err := pr.SetMerged(ctx); err != nil { + return err + } else if !merged { + return errors.New("setMerged failed") + } + return nil + }); err != nil { log.Error("%-v setMerged : %v", pr, err) return false - } else if !merged { - return false } notify_service.MergePullRequest(ctx, merger, pr) diff --git a/services/pull/merge.go b/services/pull/merge.go index b86e26804a899..e10b08ab870de 100644 --- a/services/pull/merge.go +++ b/services/pull/merge.go @@ -13,6 +13,7 @@ import ( "strconv" "strings" + "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/db" git_model "code.gitea.io/gitea/models/git" issues_model "code.gitea.io/gitea/models/issues" @@ -29,7 +30,6 @@ import ( repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" - "code.gitea.io/gitea/modules/util" issue_service "code.gitea.io/gitea/services/issue" notify_service "code.gitea.io/gitea/services/notify" ) @@ -134,14 +134,14 @@ func getMergeMessage(ctx context.Context, baseGitRepo *git.Repository, pr *issue } if pr.BaseRepoID == pr.HeadRepoID { - return fmt.Sprintf("Merge revision '%s' (%s%d) from %s into %s", pr.Issue.Title, issueReference, pr.Issue.Index, pr.HeadBranch, pr.BaseBranch), body, nil + return fmt.Sprintf("Merge pull request '%s' (%s%d) from %s into %s", pr.Issue.Title, issueReference, pr.Issue.Index, pr.HeadBranch, pr.BaseBranch), body, nil } if pr.HeadRepo == nil { - return fmt.Sprintf("Merge revision '%s' (%s%d) from :%s into %s", pr.Issue.Title, issueReference, pr.Issue.Index, pr.HeadBranch, pr.BaseBranch), body, nil + return fmt.Sprintf("Merge pull request '%s' (%s%d) from :%s into %s", pr.Issue.Title, issueReference, pr.Issue.Index, pr.HeadBranch, pr.BaseBranch), body, nil } - return fmt.Sprintf("Merge revision '%s' (%s%d) from %s:%s into %s", pr.Issue.Title, issueReference, pr.Issue.Index, pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseBranch), body, nil + return fmt.Sprintf("Merge pull request '%s' (%s%d) from %s:%s into %s", pr.Issue.Title, issueReference, pr.Issue.Index, pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseBranch), body, nil } func expandDefaultMergeMessage(template string, vars map[string]string) (message, body string) { @@ -159,27 +159,6 @@ func GetDefaultMergeMessage(ctx context.Context, baseGitRepo *git.Repository, pr return getMergeMessage(ctx, baseGitRepo, pr, mergeStyle, nil) } -// 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 -} - // Merge merges pull request to base repository. // Caller should check PR is ready to be merged (review and status checks) func Merge(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.User, baseGitRepo *git.Repository, mergeStyle repo_model.MergeStyle, expectedHeadCommitID, message string, wasAutoMerged bool) error { @@ -200,7 +179,7 @@ func Merge(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.U // Check if merge style is correct and allowed if !prConfig.IsMergeStyleAllowed(mergeStyle) { - return ErrInvalidMergeStyle{ID: pr.BaseRepo.ID, Style: mergeStyle} + return models.ErrInvalidMergeStyle{ID: pr.BaseRepo.ID, Style: mergeStyle} } releaser, err := globallock.Lock(ctx, getPullWorkingLockKey(pr.ID)) @@ -210,7 +189,15 @@ func Merge(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.U } defer releaser() defer func() { - go AddTestPullRequestTask(doer, pr.BaseRepo.ID, pr.BaseBranch, false, "", "") + go AddTestPullRequestTask(TestPullRequestOptions{ + RepoID: pr.BaseRepo.ID, + Doer: doer, + Branch: pr.BaseBranch, + IsSync: false, + IsForcePush: false, + OldCommitID: "", + NewCommitID: "", + }) }() _, err = doMergeAndPush(ctx, pr, doer, mergeStyle, expectedHeadCommitID, message, repo_module.PushTriggerPRMergeToBase, repo_model.MergeStrategyDefault, repo_model.MergeStrategyOptionNone) @@ -263,17 +250,14 @@ func handleCloseCrossReferences(ctx context.Context, pr *issues_model.PullReques if err = ref.Issue.LoadRepo(ctx); err != nil { return err } - if ref.RefAction == references.XRefActionCloses && !ref.Issue.IsClosed { - if err = issue_service.CloseIssue(ctx, ref.Issue, doer, pr.MergedCommitID); err != nil { + isClosed := ref.RefAction == references.XRefActionCloses + if isClosed != ref.Issue.IsClosed { + if err = issue_service.ChangeStatus(ctx, ref.Issue, doer, pr.MergedCommitID, isClosed); err != nil { // Allow ErrDependenciesLeft if !issues_model.IsErrDependenciesLeft(err) { return err } } - } else if ref.RefAction == references.XRefActionReopens && ref.Issue.IsClosed { - if err = issue_service.ReopenIssue(ctx, ref.Issue, doer, pr.MergedCommitID); err != nil { - return err - } } } return nil @@ -307,7 +291,7 @@ func doMergeAndPush(ctx context.Context, pr *issues_model.PullRequest, doer *use return "", err } default: - return "", ErrInvalidMergeStyle{ID: pr.BaseRepo.ID, Style: mergeStyle} + return "", models.ErrInvalidMergeStyle{ID: pr.BaseRepo.ID, Style: mergeStyle} } // OK we should cache our current head and origin/headbranch @@ -398,66 +382,13 @@ func commitAndSignNoAuthor(ctx *mergeContext, message string) error { return nil } -// 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) -} - func runMergeCommand(ctx *mergeContext, mergeStyle repo_model.MergeStyle, cmd *git.Command) error { if err := cmd.Run(ctx.RunOpts()); err != nil { // Merge will leave a MERGE_HEAD file in the .git folder if there is a conflict if _, statErr := os.Stat(filepath.Join(ctx.tmpBasePath, ".git", "MERGE_HEAD")); statErr == nil { // We have a merge conflict error log.Debug("MergeConflict %-v: %v\n%s\n%s", ctx.pr, err, ctx.outbuf.String(), ctx.errbuf.String()) - return ErrMergeConflicts{ + return models.ErrMergeConflicts{ Style: mergeStyle, StdOut: ctx.outbuf.String(), StdErr: ctx.errbuf.String(), @@ -465,7 +396,7 @@ func runMergeCommand(ctx *mergeContext, mergeStyle repo_model.MergeStyle, cmd *g } } else if strings.Contains(ctx.errbuf.String(), "refusing to merge unrelated histories") { log.Debug("MergeUnrelatedHistories %-v: %v\n%s\n%s", ctx.pr, err, ctx.outbuf.String(), ctx.errbuf.String()) - return ErrMergeUnrelatedHistories{ + return models.ErrMergeUnrelatedHistories{ Style: mergeStyle, StdOut: ctx.outbuf.String(), StdErr: ctx.errbuf.String(), @@ -473,7 +404,7 @@ func runMergeCommand(ctx *mergeContext, mergeStyle repo_model.MergeStyle, cmd *g } } else if mergeStyle == repo_model.MergeStyleFastForwardOnly && strings.Contains(ctx.errbuf.String(), "Not possible to fast-forward, aborting") { log.Debug("MergeDivergingFastForwardOnly %-v: %v\n%s\n%s", ctx.pr, err, ctx.outbuf.String(), ctx.errbuf.String()) - return ErrMergeDivergingFastForwardOnly{ + return models.ErrMergeDivergingFastForwardOnly{ StdOut: ctx.outbuf.String(), StdErr: ctx.errbuf.String(), Err: err, @@ -508,25 +439,6 @@ func IsUserAllowedToMerge(ctx context.Context, pr *issues_model.PullRequest, p a return false, nil } -// 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 -} - // CheckPullBranchProtections checks whether the PR is ready to be merged (reviews and status checks) func CheckPullBranchProtections(ctx context.Context, pr *issues_model.PullRequest, skipProtectedFilesCheck bool) (err error) { if err = pr.LoadBaseRepo(ctx); err != nil { @@ -546,29 +458,29 @@ func CheckPullBranchProtections(ctx context.Context, pr *issues_model.PullReques return err } if !isPass { - return ErrDisallowedToMerge{ + return models.ErrDisallowedToMerge{ Reason: "Not all required status checks successful", } } if !issues_model.HasEnoughApprovals(ctx, pb, pr) { - return ErrDisallowedToMerge{ + return models.ErrDisallowedToMerge{ Reason: "Does not have enough approvals", } } if issues_model.MergeBlockedByRejectedReview(ctx, pb, pr) { - return ErrDisallowedToMerge{ + return models.ErrDisallowedToMerge{ Reason: "There are requested changes", } } if issues_model.MergeBlockedByOfficialReviewRequests(ctx, pb, pr) { - return ErrDisallowedToMerge{ + return models.ErrDisallowedToMerge{ Reason: "There are official review requests", } } if issues_model.MergeBlockedByOutdatedBranch(pb, pr) { - return ErrDisallowedToMerge{ + return models.ErrDisallowedToMerge{ Reason: "The head branch is behind the base branch", } } @@ -578,7 +490,7 @@ func CheckPullBranchProtections(ctx context.Context, pr *issues_model.PullReques } if pb.MergeBlockedByProtectedFiles(pr.ChangedProtectedFiles) { - return ErrDisallowedToMerge{ + return models.ErrDisallowedToMerge{ Reason: "Changed protected files", } } @@ -607,7 +519,7 @@ func MergedManually(ctx context.Context, pr *issues_model.PullRequest, doer *use // Check if merge style is correct and allowed if !prConfig.IsMergeStyleAllowed(repo_model.MergeStyleManuallyMerged) { - return ErrInvalidMergeStyle{ID: pr.BaseRepo.ID, Style: repo_model.MergeStyleManuallyMerged} + return models.ErrInvalidMergeStyle{ID: pr.BaseRepo.ID, Style: repo_model.MergeStyleManuallyMerged} } objectFormat := git.ObjectFormatFromName(pr.BaseRepo.ObjectFormatName) diff --git a/services/pull/merge_prepare.go b/services/pull/merge_prepare.go index 2e1cc8cf85ced..88f6c037ebc90 100644 --- a/services/pull/merge_prepare.go +++ b/services/pull/merge_prepare.go @@ -14,6 +14,7 @@ import ( "strings" "time" + "code.gitea.io/gitea/models" issues_model "code.gitea.io/gitea/models/issues" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" @@ -42,23 +43,6 @@ func (ctx *mergeContext) RunOpts() *git.RunOpts { } } -// 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) -} - func createTemporaryRepoForMerge(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.User, expectedHeadCommitID string) (mergeCtx *mergeContext, cancel context.CancelFunc, err error) { // Clone base repo. prCtx, cancel, err := createTemporaryRepoForPR(ctx, pr) @@ -81,7 +65,7 @@ func createTemporaryRepoForMerge(ctx context.Context, pr *issues_model.PullReque } if strings.TrimSpace(trackingCommitID) != expectedHeadCommitID { defer cancel() - return nil, nil, ErrSHADoesNotMatch{ + return nil, nil, models.ErrSHADoesNotMatch{ GivenSHA: expectedHeadCommitID, CurrentSHA: trackingCommitID, } @@ -249,27 +233,8 @@ func getDiffTree(ctx context.Context, repoPath, baseBranch, headBranch string, o return err } -// 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) -} - // rebaseTrackingOnToBase checks out the tracking branch as staging and rebases it on to the base branch -// if there is a conflict it will return an ErrRebaseConflicts +// if there is a conflict it will return a models.ErrRebaseConflicts func rebaseTrackingOnToBase(ctx *mergeContext, mergeStyle repo_model.MergeStyle) error { // Checkout head branch if err := git.NewCommand(ctx, "checkout", "-b").AddDynamicArguments(stagingBranch, trackingBranch). @@ -303,11 +268,11 @@ func rebaseTrackingOnToBase(ctx *mergeContext, mergeStyle repo_model.MergeStyle) } } if !ok { - log.Error("Unable to determine failing commit sha for failing rebase in temp repo for %-v. Cannot cast as ErrRebaseConflicts.", ctx.pr) + log.Error("Unable to determine failing commit sha for failing rebase in temp repo for %-v. Cannot cast as models.ErrRebaseConflicts.", ctx.pr) return fmt.Errorf("unable to git rebase staging on to base in temp repo for %v: %w\n%s\n%s", ctx.pr, err, ctx.outbuf.String(), ctx.errbuf.String()) } log.Debug("Conflict when rebasing staging on to base in %-v at %s: %v\n%s\n%s", ctx.pr, commitSha, err, ctx.outbuf.String(), ctx.errbuf.String()) - return ErrRebaseConflicts{ + return models.ErrRebaseConflicts{ CommitSHA: commitSha, Style: mergeStyle, StdOut: ctx.outbuf.String(), diff --git a/services/pull/merge_squash.go b/services/pull/merge_squash.go index 8f8a5d82e7d66..6dda46eaece80 100644 --- a/services/pull/merge_squash.go +++ b/services/pull/merge_squash.go @@ -5,11 +5,13 @@ package pull import ( "fmt" + "strings" 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/git" + "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" ) @@ -24,12 +26,12 @@ func getAuthorSignatureSquash(ctx *mergeContext) (*git.Signature, error) { // Try to get an signature from the same user in one of the commits, as the // poster email might be private or commits might have a different signature // than the primary email address of the poster. - gitRepo, err := git.OpenRepository(ctx, ctx.tmpBasePath) + gitRepo, closer, err := gitrepo.RepositoryFromContextOrOpenPath(ctx, ctx.tmpBasePath) if err != nil { log.Error("%-v Unable to open base repository: %v", ctx.pr, err) return nil, err } - defer gitRepo.Close() + defer closer.Close() commits, err := gitRepo.CommitsBetweenIDs(trackingBranch, "HEAD") if err != nil { @@ -65,7 +67,10 @@ func doMergeStyleSquash(ctx *mergeContext, message string) error { if setting.Repository.PullRequest.AddCoCommitterTrailers && ctx.committer.String() != sig.String() { // add trailer - message += fmt.Sprintf("\nCo-authored-by: %s\nCo-committed-by: %s\n", sig.String(), sig.String()) + if !strings.Contains(message, fmt.Sprintf("Co-authored-by: %s", sig.String())) { + message += fmt.Sprintf("\nCo-authored-by: %s", sig.String()) + } + message += fmt.Sprintf("\nCo-committed-by: %s\n", sig.String()) } cmdCommit := git.NewCommand(ctx, "commit"). AddOptionFormat("--author='%s <%s>'", sig.Name, sig.Email). diff --git a/services/pull/patch.go b/services/pull/patch.go index 13623d73c6699..0934a86c89a65 100644 --- a/services/pull/patch.go +++ b/services/pull/patch.go @@ -13,6 +13,7 @@ import ( "path/filepath" "strings" + "code.gitea.io/gitea/models" git_model "code.gitea.io/gitea/models/git" issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/unit" @@ -41,19 +42,9 @@ func DownloadDiffOrPatch(ctx context.Context, pr *issues_model.PullRequest, w io } defer closer.Close() - compareArg := pr.MergeBase + "..." + pr.GetGitRefName() - switch { - case patch: - err = gitRepo.GetPatch(compareArg, w) - case binary: - err = gitRepo.GetDiffBinary(compareArg, w) - default: - err = gitRepo.GetDiff(compareArg, w) - } - - if err != nil { - log.Error("unable to get patch file from %s to %s in %s Error: %v", pr.MergeBase, pr.HeadBranch, pr.BaseRepo.FullName(), err) - return fmt.Errorf("unable to get patch file from %s to %s in %s Error: %w", pr.MergeBase, pr.HeadBranch, pr.BaseRepo.FullName(), err) + if err := gitRepo.GetDiffOrPatch(pr.MergeBase, pr.GetGitRefName(), w, patch, binary); err != nil { + log.Error("Unable to get patch file from %s to %s in %s Error: %v", pr.MergeBase, pr.HeadBranch, pr.BaseRepo.FullName(), err) + return fmt.Errorf("Unable to get patch file from %s to %s in %s Error: %w", pr.MergeBase, pr.HeadBranch, pr.BaseRepo.FullName(), err) } return nil } @@ -364,7 +355,7 @@ func checkConflicts(ctx context.Context, pr *issues_model.PullRequest, gitRepo * _ = util.Remove(tmpPatchFile.Name()) }() - if err := gitRepo.GetDiffBinary(pr.MergeBase+"...tracking", tmpPatchFile); err != nil { + if err := gitRepo.GetDiffBinary(pr.MergeBase, "tracking", tmpPatchFile); err != nil { tmpPatchFile.Close() log.Error("Unable to get patch file from %s to %s in %s Error: %v", pr.MergeBase, pr.HeadBranch, pr.BaseRepo.FullName(), err) return false, fmt.Errorf("unable to get patch file from %s to %s in %s Error: %w", pr.MergeBase, pr.HeadBranch, pr.BaseRepo.FullName(), err) @@ -511,29 +502,6 @@ func checkConflicts(ctx context.Context, pr *issues_model.PullRequest, gitRepo * return false, nil } -// 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 -} - // CheckFileProtection check file Protection func CheckFileProtection(repo *git.Repository, branchName, oldCommitID, newCommitID string, patterns []glob.Glob, limit int, env []string) ([]string, error) { if len(patterns) == 0 { @@ -557,7 +525,7 @@ func CheckFileProtection(repo *git.Repository, branchName, oldCommitID, newCommi } } if len(changedProtectedFiles) > 0 { - err = ErrFilePathProtected{ + err = models.ErrFilePathProtected{ Path: changedProtectedFiles[0], } } @@ -607,7 +575,7 @@ func checkPullFilesProtection(ctx context.Context, pr *issues_model.PullRequest, } pr.ChangedProtectedFiles, err = CheckFileProtection(gitRepo, pr.HeadBranch, pr.MergeBase, "tracking", pb.GetProtectedFilePatterns(), 10, os.Environ()) - if err != nil && !IsErrFilePathProtected(err) { + if err != nil && !models.IsErrFilePathProtected(err) { return err } return nil diff --git a/services/pull/pull.go b/services/pull/pull.go index 85c36bb16aff8..3027059405009 100644 --- a/services/pull/pull.go +++ b/services/pull/pull.go @@ -13,6 +13,7 @@ import ( "strings" "time" + "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/db" git_model "code.gitea.io/gitea/models/git" issues_model "code.gitea.io/gitea/models/issues" @@ -175,7 +176,7 @@ func NewPullRequest(ctx context.Context, opts *NewPullRequestOptions) error { } if !pr.IsWorkInProgress(ctx) { - reviewNotifiers, err = issue_service.PullRequestCodeOwnersReview(ctx, issue, pr) + reviewNotifiers, err = issue_service.PullRequestCodeOwnersReview(ctx, pr) if err != nil { return err } @@ -224,28 +225,6 @@ func NewPullRequest(ctx context.Context, opts *NewPullRequestOptions) error { return nil } -// 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) -} - // ChangeTargetBranch changes the target branch of this pull request, as the given user. func ChangeTargetBranch(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.User, targetBranch string) (err error) { releaser, err := globallock.Lock(ctx, getPullWorkingLockKey(pr.ID)) @@ -269,7 +248,7 @@ func ChangeTargetBranch(ctx context.Context, pr *issues_model.PullRequest, doer } if pr.HasMerged { - return ErrPullRequestHasMerged{ + return models.ErrPullRequestHasMerged{ ID: pr.ID, IssueID: pr.Index, HeadRepoID: pr.HeadRepoID, @@ -370,19 +349,29 @@ func checkForInvalidation(ctx context.Context, requests issues_model.PullRequest return nil } +type TestPullRequestOptions struct { + RepoID int64 + Doer *user_model.User + Branch string + IsSync bool // True means it's a pull request synchronization, false means it's triggered for pull request merging or updating + IsForcePush bool + OldCommitID string + NewCommitID string +} + // AddTestPullRequestTask adds new test tasks by given head/base repository and head/base branch, // and generate new patch for testing as needed. -func AddTestPullRequestTask(doer *user_model.User, repoID int64, branch string, isSync bool, oldCommitID, newCommitID string) { - log.Trace("AddTestPullRequestTask [head_repo_id: %d, head_branch: %s]: finding pull requests", repoID, branch) +func AddTestPullRequestTask(opts TestPullRequestOptions) { + log.Trace("AddTestPullRequestTask [head_repo_id: %d, head_branch: %s]: finding pull requests", opts.RepoID, opts.Branch) graceful.GetManager().RunWithShutdownContext(func(ctx context.Context) { // There is no sensible way to shut this down ":-(" // If you don't let it run all the way then you will lose data // TODO: graceful: AddTestPullRequestTask needs to become a queue! // GetUnmergedPullRequestsByHeadInfo() only return open and unmerged PR. - prs, err := issues_model.GetUnmergedPullRequestsByHeadInfo(ctx, repoID, branch) + prs, err := issues_model.GetUnmergedPullRequestsByHeadInfo(ctx, opts.RepoID, opts.Branch) if err != nil { - log.Error("Find pull requests [head_repo_id: %d, head_branch: %s]: %v", repoID, branch, err) + log.Error("Find pull requests [head_repo_id: %d, head_branch: %s]: %v", opts.RepoID, opts.Branch, err) return } @@ -398,25 +387,24 @@ func AddTestPullRequestTask(doer *user_model.User, repoID int64, branch string, } AddToTaskQueue(ctx, pr) - comment, err := CreatePushPullComment(ctx, doer, pr, oldCommitID, newCommitID) + comment, err := CreatePushPullComment(ctx, opts.Doer, pr, opts.OldCommitID, opts.NewCommitID) if err == nil && comment != nil { - notify_service.PullRequestPushCommits(ctx, doer, pr, comment) + notify_service.PullRequestPushCommits(ctx, opts.Doer, pr, comment) } } - if isSync { - requests := issues_model.PullRequestList(prs) - if err = requests.LoadAttributes(ctx); err != nil { + if opts.IsSync { + if err = issues_model.PullRequestList(prs).LoadAttributes(ctx); err != nil { log.Error("PullRequestList.LoadAttributes: %v", err) } - if invalidationErr := checkForInvalidation(ctx, requests, repoID, doer, branch); invalidationErr != nil { + if invalidationErr := checkForInvalidation(ctx, prs, opts.RepoID, opts.Doer, opts.Branch); invalidationErr != nil { log.Error("checkForInvalidation: %v", invalidationErr) } if err == nil { for _, pr := range prs { objectFormat := git.ObjectFormatFromName(pr.BaseRepo.ObjectFormatName) - if newCommitID != "" && newCommitID != objectFormat.EmptyObjectID().String() { - changed, err := checkIfPRContentChanged(ctx, pr, oldCommitID, newCommitID) + if opts.NewCommitID != "" && opts.NewCommitID != objectFormat.EmptyObjectID().String() { + changed, err := checkIfPRContentChanged(ctx, pr, opts.OldCommitID, opts.NewCommitID) if err != nil { log.Error("checkIfPRContentChanged: %v", err) } @@ -432,12 +420,12 @@ func AddTestPullRequestTask(doer *user_model.User, repoID int64, branch string, log.Error("GetFirstMatchProtectedBranchRule: %v", err) } if pb != nil && pb.DismissStaleApprovals { - if err := DismissApprovalReviews(ctx, doer, pr); err != nil { + if err := DismissApprovalReviews(ctx, opts.Doer, pr); err != nil { log.Error("DismissApprovalReviews: %v", err) } } } - if err := issues_model.MarkReviewsAsNotStale(ctx, pr.IssueID, newCommitID); err != nil { + if err := issues_model.MarkReviewsAsNotStale(ctx, pr.IssueID, opts.NewCommitID); err != nil { log.Error("MarkReviewsAsNotStale: %v", err) } divergence, err := GetDiverging(ctx, pr) @@ -451,15 +439,25 @@ func AddTestPullRequestTask(doer *user_model.User, repoID int64, branch string, } } - notify_service.PullRequestSynchronized(ctx, doer, pr) + if !pr.IsWorkInProgress(ctx) { + reviewNotifiers, err := issue_service.PullRequestCodeOwnersReview(ctx, pr) + if err != nil { + log.Error("PullRequestCodeOwnersReview: %v", err) + } + if len(reviewNotifiers) > 0 { + issue_service.ReviewRequestNotify(ctx, pr.Issue, opts.Doer, reviewNotifiers) + } + } + + notify_service.PullRequestSynchronized(ctx, opts.Doer, pr) } } } - log.Trace("AddTestPullRequestTask [base_repo_id: %d, base_branch: %s]: finding pull requests", repoID, branch) - prs, err = issues_model.GetUnmergedPullRequestsByBaseInfo(ctx, repoID, branch) + log.Trace("AddTestPullRequestTask [base_repo_id: %d, base_branch: %s]: finding pull requests", opts.RepoID, opts.Branch) + prs, err = issues_model.GetUnmergedPullRequestsByBaseInfo(ctx, opts.RepoID, opts.Branch) if err != nil { - log.Error("Find pull requests [base_repo_id: %d, base_branch: %s]: %v", repoID, branch, err) + log.Error("Find pull requests [base_repo_id: %d, base_branch: %s]: %v", opts.RepoID, opts.Branch, err) return } for _, pr := range prs { @@ -676,7 +674,7 @@ func RetargetBranchPulls(ctx context.Context, doer *user_model.User, repoID int6 if err = pr.Issue.LoadRepo(ctx); err != nil { errs = append(errs, err) } else if err = ChangeTargetBranch(ctx, pr, doer, targetBranch); err != nil && - !issues_model.IsErrIssueIsClosed(err) && !IsErrPullRequestHasMerged(err) && + !issues_model.IsErrIssueIsClosed(err) && !models.IsErrPullRequestHasMerged(err) && !issues_model.IsErrPullRequestAlreadyExists(err) { errs = append(errs, err) } @@ -707,7 +705,7 @@ func CloseBranchPulls(ctx context.Context, doer *user_model.User, repoID int64, var errs errlist for _, pr := range prs { - if err = issue_service.CloseIssue(ctx, pr.Issue, doer, ""); err != nil && !issues_model.IsErrPullWasClosed(err) && !issues_model.IsErrDependenciesLeft(err) { + if err = issue_service.ChangeStatus(ctx, pr.Issue, doer, "", true); err != nil && !issues_model.IsErrPullWasClosed(err) && !issues_model.IsErrDependenciesLeft(err) { errs = append(errs, err) } } @@ -741,7 +739,7 @@ func CloseRepoBranchesPulls(ctx context.Context, doer *user_model.User, repo *re if pr.BaseRepoID == repo.ID { continue } - if err = issue_service.CloseIssue(ctx, pr.Issue, doer, ""); err != nil && !issues_model.IsErrPullWasClosed(err) { + if err = issue_service.ChangeStatus(ctx, pr.Issue, doer, "", true); err != nil && !issues_model.IsErrPullWasClosed(err) { errs = append(errs, err) } } diff --git a/services/pull/pull_test.go b/services/pull/pull_test.go index b92e87eae050e..787910bf760f3 100644 --- a/services/pull/pull_test.go +++ b/services/pull/pull_test.go @@ -48,13 +48,13 @@ func TestPullRequest_GetDefaultMergeMessage_InternalTracker(t *testing.T) { mergeMessage, _, err := GetDefaultMergeMessage(db.DefaultContext, gitRepo, pr, "") assert.NoError(t, err) - assert.Equal(t, "Merge revision 'issue3' (#3) from branch2 into master", mergeMessage) + assert.Equal(t, "Merge pull request 'issue3' (#3) from branch2 into master", mergeMessage) pr.BaseRepoID = 1 pr.HeadRepoID = 2 mergeMessage, _, err = GetDefaultMergeMessage(db.DefaultContext, gitRepo, pr, "") assert.NoError(t, err) - assert.Equal(t, "Merge revision 'issue3' (#3) from user2/repo1:branch2 into master", mergeMessage) + assert.Equal(t, "Merge pull request 'issue3' (#3) from user2/repo1:branch2 into master", mergeMessage) } func TestPullRequest_GetDefaultMergeMessage_ExternalTracker(t *testing.T) { @@ -79,7 +79,7 @@ func TestPullRequest_GetDefaultMergeMessage_ExternalTracker(t *testing.T) { mergeMessage, _, err := GetDefaultMergeMessage(db.DefaultContext, gitRepo, pr, "") assert.NoError(t, err) - assert.Equal(t, "Merge revision 'issue3' (!3) from branch2 into master", mergeMessage) + assert.Equal(t, "Merge pull request 'issue3' (!3) from branch2 into master", mergeMessage) pr.BaseRepoID = 1 pr.HeadRepoID = 2 @@ -88,5 +88,5 @@ func TestPullRequest_GetDefaultMergeMessage_ExternalTracker(t *testing.T) { mergeMessage, _, err = GetDefaultMergeMessage(db.DefaultContext, gitRepo, pr, "") assert.NoError(t, err) - assert.Equal(t, "Merge revision 'issue3' (#3) from user2/repo2:branch2 into master", mergeMessage) + assert.Equal(t, "Merge pull request 'issue3' (#3) from user2/repo2:branch2 into master", mergeMessage) } diff --git a/services/pull/update.go b/services/pull/update.go index 60ef649840f97..0ca92d86dd943 100644 --- a/services/pull/update.go +++ b/services/pull/update.go @@ -42,7 +42,15 @@ func Update(ctx context.Context, pr *issues_model.PullRequest, doer *user_model. if rebase { defer func() { - go AddTestPullRequestTask(doer, pr.BaseRepo.ID, pr.BaseBranch, false, "", "") + go AddTestPullRequestTask(TestPullRequestOptions{ + RepoID: pr.BaseRepo.ID, + Doer: doer, + Branch: pr.BaseBranch, + IsSync: false, + IsForcePush: false, + OldCommitID: "", + NewCommitID: "", + }) }() return updateHeadByRebaseOnToBase(ctx, pr, doer) @@ -83,7 +91,15 @@ func Update(ctx context.Context, pr *issues_model.PullRequest, doer *user_model. _, err = doMergeAndPush(ctx, reversePR, doer, repo_model.MergeStyleMerge, "", message, repository.PushTriggerPRUpdateWithBase, strategy, option) defer func() { - go AddTestPullRequestTask(doer, reversePR.HeadRepo.ID, reversePR.HeadBranch, false, "", "") + go AddTestPullRequestTask(TestPullRequestOptions{ + RepoID: reversePR.HeadRepo.ID, + Doer: doer, + Branch: reversePR.HeadBranch, + IsSync: false, + IsForcePush: false, + OldCommitID: "", + NewCommitID: "", + }) }() return err diff --git a/services/release/release.go b/services/release/release.go index 835a5943b1423..980a5e98e7fa1 100644 --- a/services/release/release.go +++ b/services/release/release.go @@ -9,6 +9,7 @@ import ( "fmt" "strings" + "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/db" git_model "code.gitea.io/gitea/models/git" repo_model "code.gitea.io/gitea/models/repo" @@ -25,44 +26,6 @@ import ( notify_service "code.gitea.io/gitea/services/notify" ) -// 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 -} - func createTag(ctx context.Context, gitRepo *git.Repository, rel *repo_model.Release, msg string) (bool, error) { err := rel.LoadAttributes(ctx) if err != nil { @@ -95,7 +58,7 @@ func createTag(ctx context.Context, gitRepo *git.Repository, rel *repo_model.Rel return false, err } if !isAllowed { - return false, ErrProtectedTagName{ + return false, models.ErrProtectedTagName{ TagName: rel.TagName, } } @@ -108,7 +71,7 @@ func createTag(ctx context.Context, gitRepo *git.Repository, rel *repo_model.Rel if len(msg) > 0 { if err = gitRepo.CreateAnnotatedTag(rel.TagName, msg, commit.ID.String()); err != nil { if strings.Contains(err.Error(), "is not a valid tag name") { - return false, ErrInvalidTagName{ + return false, models.ErrInvalidTagName{ TagName: rel.TagName, } } @@ -116,7 +79,7 @@ func createTag(ctx context.Context, gitRepo *git.Repository, rel *repo_model.Rel } } else if err = gitRepo.CreateTag(rel.TagName, commit.ID.String()); err != nil { if strings.Contains(err.Error(), "is not a valid tag name") { - return false, ErrInvalidTagName{ + return false, models.ErrInvalidTagName{ TagName: rel.TagName, } } @@ -179,7 +142,7 @@ func CreateRelease(gitRepo *git.Repository, rel *repo_model.Release, attachmentU return err } - rel.Title = util.EllipsisDisplayString(rel.Title, 255) + rel.Title, _ = util.SplitStringAtByteN(rel.Title, 255) rel.LowerTagName = strings.ToLower(rel.TagName) if err = db.Insert(gitRepo.Ctx, rel); err != nil { return err @@ -196,32 +159,13 @@ func CreateRelease(gitRepo *git.Repository, rel *repo_model.Release, attachmentU return nil } -// 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 -} - // CreateNewTag creates a new repository tag func CreateNewTag(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, commit, tagName, msg string) error { has, err := repo_model.IsReleaseExist(ctx, repo.ID, tagName) if err != nil { return err } else if has { - return ErrTagAlreadyExists{ + return models.ErrTagAlreadyExists{ TagName: tagName, } } @@ -376,12 +320,13 @@ func DeleteReleaseByID(ctx context.Context, repo *repo_model.Repository, rel *re return err } if !isAllowed { - return ErrProtectedTagName{ + return models.ErrProtectedTagName{ TagName: rel.TagName, } } if stdout, _, err := git.NewCommand(ctx, "tag", "-d").AddDashesAndList(rel.TagName). + SetDescription(fmt.Sprintf("DeleteReleaseByID (git tag -d): %d", rel.ID)). RunStdString(&git.RunOpts{Dir: repo.RepoPath()}); err != nil && !strings.Contains(err.Error(), "not found") { log.Error("DeleteReleaseByID (git tag -d): %d in %v Failed:\nStdout: %s\nError: %v", rel.ID, repo, stdout, err) return fmt.Errorf("git tag -d: %w", err) diff --git a/services/repository/adopt.go b/services/repository/adopt.go index e37909e7ab216..1f18702601f51 100644 --- a/services/repository/adopt.go +++ b/services/repository/adopt.go @@ -52,8 +52,9 @@ func AdoptRepository(ctx context.Context, doer, u *user_model.User, opts CreateR IsEmpty: !opts.AutoInit, } + repoPath := repo_model.RepoPath(u.Name, repo.Name) + if err := db.WithTx(ctx, func(ctx context.Context) error { - repoPath := repo_model.RepoPath(u.Name, repo.Name) isExist, err := util.IsExist(repoPath) if err != nil { log.Error("Unable to check if %s exists. Error: %v", repoPath, err) @@ -75,7 +76,12 @@ func AdoptRepository(ctx context.Context, doer, u *user_model.User, opts CreateR if repo, err = repo_model.GetRepositoryByID(ctx, repo.ID); err != nil { return fmt.Errorf("getRepositoryByID: %w", err) } + return nil + }); err != nil { + return nil, err + } + if err := func() error { if err := adoptRepository(ctx, repoPath, repo, opts.DefaultBranch); err != nil { return fmt.Errorf("adoptRepository: %w", err) } @@ -84,23 +90,19 @@ func AdoptRepository(ctx context.Context, doer, u *user_model.User, opts CreateR return fmt.Errorf("checkDaemonExportOK: %w", err) } - // Initialize Issue Labels if selected - if len(opts.IssueLabels) > 0 { - if err := repo_module.InitializeLabels(ctx, repo.ID, opts.IssueLabels, false); err != nil { - return fmt.Errorf("InitializeLabels: %w", err) - } - } - if stdout, _, err := git.NewCommand(ctx, "update-server-info"). + SetDescription(fmt.Sprintf("CreateRepository(git update-server-info): %s", repoPath)). RunStdString(&git.RunOpts{Dir: repoPath}); err != nil { log.Error("CreateRepository(git update-server-info) in %v: Stdout: %s\nError: %v", repo, stdout, err) return fmt.Errorf("CreateRepository(git update-server-info): %w", err) } return nil - }); err != nil { + }(); err != nil { + if errDel := DeleteRepository(ctx, doer, repo, false /* no notify */); errDel != nil { + log.Error("Failed to delete repository %s that could not be adopted: %v", repo.FullName(), errDel) + } return nil, err } - notify_service.AdoptRepository(ctx, doer, u, repo) return repo, nil diff --git a/services/repository/branch.go b/services/repository/branch.go index fc476298ca5c4..13a5265d081f2 100644 --- a/services/repository/branch.go +++ b/services/repository/branch.go @@ -9,6 +9,7 @@ import ( "fmt" "strings" + "code.gitea.io/gitea/models" actions_model "code.gitea.io/gitea/models/actions" "code.gitea.io/gitea/models/db" git_model "code.gitea.io/gitea/models/git" @@ -29,8 +30,8 @@ import ( "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" webhook_module "code.gitea.io/gitea/modules/webhook" + actions_service "code.gitea.io/gitea/services/actions" notify_service "code.gitea.io/gitea/services/notify" - release_service "code.gitea.io/gitea/services/release" files_service "code.gitea.io/gitea/services/repository/files" "xorm.io/builder" @@ -274,7 +275,7 @@ func checkBranchName(ctx context.Context, repo *repo_model.Repository, name stri BranchName: branchRefName, } case refName == git.TagPrefix+name: - return release_service.ErrTagAlreadyExists{ + return models.ErrTagAlreadyExists{ TagName: name, } } @@ -428,7 +429,7 @@ func RenameBranch(ctx context.Context, repo *repo_model.Repository, doer *user_m log.Error("DeleteCronTaskByRepo: %v", err) } // cancel running cron jobs of this repository and delete old schedules - if err := actions_model.CancelPreviousJobs( + if err := actions_service.CancelPreviousJobs( ctx, repo.ID, from, @@ -609,7 +610,7 @@ func SetRepoDefaultBranch(ctx context.Context, repo *repo_model.Repository, gitR log.Error("DeleteCronTaskByRepo: %v", err) } // cancel running cron jobs of this repository and delete old schedules - if err := actions_model.CancelPreviousJobs( + if err := actions_service.CancelPreviousJobs( ctx, repo.ID, oldDefaultBranchName, @@ -636,3 +637,74 @@ func SetRepoDefaultBranch(ctx context.Context, repo *repo_model.Repository, gitR return nil } + +// BranchDivergingInfo contains the information about the divergence of a head branch to the base branch. +type BranchDivergingInfo struct { + // whether the base branch contains new commits which are not in the head branch + BaseHasNewCommits bool + + // behind/after are number of commits that the head branch is behind/after the base branch, it's 0 if it's unable to calculate. + // there could be a case that BaseHasNewCommits=true while the behind/after are both 0 (unable to calculate). + HeadCommitsBehind int + HeadCommitsAhead int +} + +// GetBranchDivergingInfo returns the information about the divergence of a patch branch to the base branch. +func GetBranchDivergingInfo(ctx context.Context, baseRepo *repo_model.Repository, baseBranch string, headRepo *repo_model.Repository, headBranch string) (*BranchDivergingInfo, error) { + headGitBranch, err := git_model.GetBranch(ctx, headRepo.ID, headBranch) + if err != nil { + return nil, err + } + if headGitBranch.IsDeleted { + return nil, git_model.ErrBranchNotExist{ + BranchName: headBranch, + } + } + baseGitBranch, err := git_model.GetBranch(ctx, baseRepo.ID, baseBranch) + if err != nil { + return nil, err + } + if baseGitBranch.IsDeleted { + return nil, git_model.ErrBranchNotExist{ + BranchName: baseBranch, + } + } + + info := &BranchDivergingInfo{} + if headGitBranch.CommitID == baseGitBranch.CommitID { + return info, nil + } + + // if the fork repo has new commits, this call will fail because they are not in the base repo + // exit status 128 - fatal: Invalid symmetric difference expression aaaaaaaaaaaa...bbbbbbbbbbbb + // so at the moment, we first check the update time, then check whether the fork branch has base's head + diff, err := git.GetDivergingCommits(ctx, baseRepo.RepoPath(), baseGitBranch.CommitID, headGitBranch.CommitID) + if err != nil { + info.BaseHasNewCommits = baseGitBranch.UpdatedUnix > headGitBranch.UpdatedUnix + if headRepo.IsFork && info.BaseHasNewCommits { + return info, nil + } + // if the base's update time is before the fork, check whether the base's head is in the fork + headGitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, headRepo) + if err != nil { + return nil, err + } + defer closer.Close() + + headCommit, err := headGitRepo.GetCommit(headGitBranch.CommitID) + if err != nil { + return nil, err + } + baseCommitID, err := git.NewIDFromString(baseGitBranch.CommitID) + if err != nil { + return nil, err + } + hasPreviousCommit, _ := headCommit.HasPreviousCommit(baseCommitID) + info.BaseHasNewCommits = !hasPreviousCommit + return info, nil + } + + info.HeadCommitsBehind, info.HeadCommitsAhead = diff.Behind, diff.Ahead + info.BaseHasNewCommits = info.HeadCommitsBehind > 0 + return info, nil +} diff --git a/services/repository/check.go b/services/repository/check.go index acca15daf2ab6..5cdcc146797c9 100644 --- a/services/repository/check.go +++ b/services/repository/check.go @@ -86,7 +86,8 @@ func GitGcRepos(ctx context.Context, timeout time.Duration, args git.TrustedCmdA // GitGcRepo calls 'git gc' to remove unnecessary files and optimize the local repository func GitGcRepo(ctx context.Context, repo *repo_model.Repository, timeout time.Duration, args git.TrustedCmdArgs) error { log.Trace("Running git gc on %-v", repo) - command := git.NewCommand(ctx, "gc").AddArguments(args...) + command := git.NewCommand(ctx, "gc").AddArguments(args...). + SetDescription(fmt.Sprintf("Repository Garbage Collection: %s", repo.FullName())) var stdout string var err error stdout, _, err = command.RunStdString(&git.RunOpts{Timeout: timeout, Dir: repo.RepoPath()}) diff --git a/services/repository/create.go b/services/repository/create.go index a3199f2a40aac..14e625d962a07 100644 --- a/services/repository/create.go +++ b/services/repository/create.go @@ -68,6 +68,7 @@ func prepareRepoCommit(ctx context.Context, repo *repo_model.Repository, tmpDir, // Clone to temporary path and do the init commit. if stdout, _, err := git.NewCommand(ctx, "clone").AddDynamicArguments(repoPath, tmpDir). + SetDescription(fmt.Sprintf("prepareRepoCommit (git clone): %s to %s", repoPath, tmpDir)). RunStdString(&git.RunOpts{Dir: "", Env: env}); err != nil { log.Error("Failed to clone from %v into %s: stdout: %s\nError: %v", repo, tmpDir, stdout, err) return fmt.Errorf("git clone: %w", err) @@ -300,6 +301,7 @@ func CreateRepositoryDirectly(ctx context.Context, doer, u *user_model.User, opt } if stdout, _, err := git.NewCommand(ctx, "update-server-info"). + SetDescription(fmt.Sprintf("CreateRepository(git update-server-info): %s", repoPath)). RunStdString(&git.RunOpts{Dir: repoPath}); err != nil { log.Error("CreateRepository(git update-server-info) in %v: Stdout: %s\nError: %v", repo, stdout, err) rollbackRepo = repo @@ -312,7 +314,9 @@ func CreateRepositoryDirectly(ctx context.Context, doer, u *user_model.User, opt if len(opts.License) > 0 { licenses = append(licenses, ConvertLicenseName(opts.License)) - stdout, _, err := git.NewCommand(ctx, "rev-parse", "HEAD").RunStdString(&git.RunOpts{Dir: repoPath}) + stdout, _, err := git.NewCommand(ctx, "rev-parse", "HEAD"). + SetDescription(fmt.Sprintf("CreateRepository(git rev-parse HEAD): %s", repoPath)). + RunStdString(&git.RunOpts{Dir: repoPath}) if err != nil { log.Error("CreateRepository(git rev-parse HEAD) in %v: Stdout: %s\nError: %v", repo, stdout, err) rollbackRepo = repo diff --git a/services/repository/delete.go b/services/repository/delete.go index 2166b4dd5c352..96b18ac811def 100644 --- a/services/repository/delete.go +++ b/services/repository/delete.go @@ -7,13 +7,16 @@ import ( "context" "fmt" + "code.gitea.io/gitea/models" actions_model "code.gitea.io/gitea/models/actions" activities_model "code.gitea.io/gitea/models/activities" admin_model "code.gitea.io/gitea/models/admin" + asymkey_model "code.gitea.io/gitea/models/asymkey" "code.gitea.io/gitea/models/db" git_model "code.gitea.io/gitea/models/git" issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/organization" + packages_model "code.gitea.io/gitea/models/packages" 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" @@ -74,11 +77,16 @@ func DeleteRepositoryDirectly(ctx context.Context, doer *user_model.User, repoID } // Delete Deploy Keys - deleted, err := asymkey_service.DeleteRepoDeployKeys(ctx, repoID) + deployKeys, err := db.Find[asymkey_model.DeployKey](ctx, asymkey_model.ListDeployKeysOptions{RepoID: repoID}) if err != nil { - return err + return fmt.Errorf("listDeployKeys: %w", err) + } + needRewriteKeysFile := len(deployKeys) > 0 + for _, dKey := range deployKeys { + if err := models.DeleteDeployKey(ctx, doer, dKey.ID); err != nil { + return fmt.Errorf("deleteDeployKeys: %w", err) + } } - needRewriteKeysFile := deleted > 0 if cnt, err := sess.ID(repoID).Delete(&repo_model.Repository{}); err != nil { return err @@ -266,6 +274,11 @@ func DeleteRepositoryDirectly(ctx context.Context, doer *user_model.User, repoID return err } + // unlink packages linked to this repository + if err = packages_model.UnlinkRepositoryFromAllPackages(ctx, repoID); err != nil { + return err + } + if err = committer.Commit(); err != nil { return err } diff --git a/services/repository/files/cherry_pick.go b/services/repository/files/cherry_pick.go index 10545e9e03807..451a182155786 100644 --- a/services/repository/files/cherry_pick.go +++ b/services/repository/files/cherry_pick.go @@ -8,6 +8,7 @@ import ( "fmt" "strings" + "code.gitea.io/gitea/models" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" @@ -16,22 +17,6 @@ import ( "code.gitea.io/gitea/services/pull" ) -// 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) -} - // CherryPick cherrypicks or reverts a commit to the given repository func CherryPick(ctx context.Context, repo *repo_model.Repository, doer *user_model.User, revert bool, opts *ApplyDiffPatchOptions) (*structs.FileResponse, error) { if err := opts.Validate(ctx, repo, doer); err != nil { @@ -72,7 +57,7 @@ func CherryPick(ctx context.Context, repo *repo_model.Repository, doer *user_mod } opts.LastCommitID = lastCommitID.String() if commit.ID.String() != opts.LastCommitID { - return nil, ErrCommitIDDoesNotMatch{ + return nil, models.ErrCommitIDDoesNotMatch{ GivenCommitID: opts.LastCommitID, CurrentCommitID: opts.LastCommitID, } diff --git a/services/repository/files/content.go b/services/repository/files/content.go index 0ab7422ce2ebf..95e7c7087c03d 100644 --- a/services/repository/files/content.go +++ b/services/repository/files/content.go @@ -10,6 +10,7 @@ import ( "path" "strings" + "code.gitea.io/gitea/models" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/gitrepo" @@ -52,7 +53,7 @@ func GetContentsOrList(ctx context.Context, repo *repo_model.Repository, treePat // Check that the path given in opts.treePath is valid (not a git path) cleanTreePath := CleanUploadFileName(treePath) if cleanTreePath == "" && treePath != "" { - return nil, ErrFilenameInvalid{ + return nil, models.ErrFilenameInvalid{ Path: treePath, } } @@ -127,7 +128,7 @@ func GetContents(ctx context.Context, repo *repo_model.Repository, treePath, ref // Check that the path given in opts.treePath is valid (not a git path) cleanTreePath := CleanUploadFileName(treePath) if cleanTreePath == "" && treePath != "" { - return nil, ErrFilenameInvalid{ + return nil, models.ErrFilenameInvalid{ Path: treePath, } } diff --git a/services/repository/files/content_test.go b/services/repository/files/content_test.go index 7cb46c0bb67ef..a899be70e3386 100644 --- a/services/repository/files/content_test.go +++ b/services/repository/files/content_test.go @@ -53,7 +53,7 @@ func getExpectedReadmeContentsResponse() *api.ContentsResponse { func TestGetContents(t *testing.T) { unittest.PrepareTestEnv(t) ctx, _ := contexttest.MockContext(t, "user2/repo1") - ctx.SetPathParam("id", "1") + ctx.SetPathParam(":id", "1") contexttest.LoadRepo(t, ctx, 1) contexttest.LoadRepoCommit(t, ctx) contexttest.LoadUser(t, ctx, 2) @@ -81,7 +81,7 @@ func TestGetContents(t *testing.T) { func TestGetContentsOrListForDir(t *testing.T) { unittest.PrepareTestEnv(t) ctx, _ := contexttest.MockContext(t, "user2/repo1") - ctx.SetPathParam("id", "1") + ctx.SetPathParam(":id", "1") contexttest.LoadRepo(t, ctx, 1) contexttest.LoadRepoCommit(t, ctx) contexttest.LoadUser(t, ctx, 2) @@ -116,7 +116,7 @@ func TestGetContentsOrListForDir(t *testing.T) { func TestGetContentsOrListForFile(t *testing.T) { unittest.PrepareTestEnv(t) ctx, _ := contexttest.MockContext(t, "user2/repo1") - ctx.SetPathParam("id", "1") + ctx.SetPathParam(":id", "1") contexttest.LoadRepo(t, ctx, 1) contexttest.LoadRepoCommit(t, ctx) contexttest.LoadUser(t, ctx, 2) @@ -144,7 +144,7 @@ func TestGetContentsOrListForFile(t *testing.T) { func TestGetContentsErrors(t *testing.T) { unittest.PrepareTestEnv(t) ctx, _ := contexttest.MockContext(t, "user2/repo1") - ctx.SetPathParam("id", "1") + ctx.SetPathParam(":id", "1") contexttest.LoadRepo(t, ctx, 1) contexttest.LoadRepoCommit(t, ctx) contexttest.LoadUser(t, ctx, 2) @@ -175,7 +175,7 @@ func TestGetContentsErrors(t *testing.T) { func TestGetContentsOrListErrors(t *testing.T) { unittest.PrepareTestEnv(t) ctx, _ := contexttest.MockContext(t, "user2/repo1") - ctx.SetPathParam("id", "1") + ctx.SetPathParam(":id", "1") contexttest.LoadRepo(t, ctx, 1) contexttest.LoadRepoCommit(t, ctx) contexttest.LoadUser(t, ctx, 2) @@ -206,7 +206,7 @@ func TestGetContentsOrListErrors(t *testing.T) { func TestGetContentsOrListOfEmptyRepos(t *testing.T) { unittest.PrepareTestEnv(t) ctx, _ := contexttest.MockContext(t, "user30/empty") - ctx.SetPathParam("id", "52") + ctx.SetPathParam(":id", "52") contexttest.LoadRepo(t, ctx, 52) contexttest.LoadUser(t, ctx, 30) contexttest.LoadGitRepo(t, ctx) @@ -231,15 +231,15 @@ func TestGetBlobBySHA(t *testing.T) { defer ctx.Repo.GitRepo.Close() sha := "65f1bf27bc3bf70f64657658635e66094edbcb4d" - ctx.SetPathParam("id", "1") - ctx.SetPathParam("sha", sha) + ctx.SetPathParam(":id", "1") + ctx.SetPathParam(":sha", sha) gitRepo, err := gitrepo.OpenRepository(ctx, ctx.Repo.Repository) if err != nil { t.Fail() } - gbr, err := GetBlobBySHA(ctx, ctx.Repo.Repository, gitRepo, ctx.PathParam("sha")) + gbr, err := GetBlobBySHA(ctx, ctx.Repo.Repository, gitRepo, ctx.PathParam(":sha")) expectedGBR := &api.GitBlobResponse{ Content: "dHJlZSAyYTJmMWQ0NjcwNzI4YTJlMTAwNDllMzQ1YmQ3YTI3NjQ2OGJlYWI2CmF1dGhvciB1c2VyMSA8YWRkcmVzczFAZXhhbXBsZS5jb20+IDE0ODk5NTY0NzkgLTA0MDAKY29tbWl0dGVyIEV0aGFuIEtvZW5pZyA8ZXRoYW50a29lbmlnQGdtYWlsLmNvbT4gMTQ4OTk1NjQ3OSAtMDQwMAoKSW5pdGlhbCBjb21taXQK", Encoding: "base64", diff --git a/services/repository/files/diff_test.go b/services/repository/files/diff_test.go index b7bdcd8ecf444..ea6ffe60c3f88 100644 --- a/services/repository/files/diff_test.go +++ b/services/repository/files/diff_test.go @@ -18,7 +18,7 @@ import ( func TestGetDiffPreview(t *testing.T) { unittest.PrepareTestEnv(t) ctx, _ := contexttest.MockContext(t, "user2/repo1") - ctx.SetPathParam("id", "1") + ctx.SetPathParam(":id", "1") contexttest.LoadRepo(t, ctx, 1) contexttest.LoadRepoCommit(t, ctx) contexttest.LoadUser(t, ctx, 2) @@ -140,7 +140,7 @@ func TestGetDiffPreview(t *testing.T) { func TestGetDiffPreviewErrors(t *testing.T) { unittest.PrepareTestEnv(t) ctx, _ := contexttest.MockContext(t, "user2/repo1") - ctx.SetPathParam("id", "1") + ctx.SetPathParam(":id", "1") contexttest.LoadRepo(t, ctx, 1) contexttest.LoadRepoCommit(t, ctx) contexttest.LoadUser(t, ctx, 2) diff --git a/services/repository/files/file.go b/services/repository/files/file.go index d7ca8e79e5ba8..16783f5b5f8a9 100644 --- a/services/repository/files/file.go +++ b/services/repository/files/file.go @@ -156,25 +156,6 @@ func GetAuthorAndCommitterUsers(author, committer *IdentityOptions, doer *user_m return authorUser, committerUser } -// 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 -} - // CleanUploadFileName Trims a filename and returns empty string if it is a .git directory func CleanUploadFileName(name string) string { // Rebase the filename diff --git a/services/repository/files/file_test.go b/services/repository/files/file_test.go index 52c0574883436..b2f51e3c820f7 100644 --- a/services/repository/files/file_test.go +++ b/services/repository/files/file_test.go @@ -99,7 +99,7 @@ func getExpectedFileResponse() *api.FileResponse { func TestGetFileResponseFromCommit(t *testing.T) { unittest.PrepareTestEnv(t) ctx, _ := contexttest.MockContext(t, "user2/repo1") - ctx.SetPathParam("id", "1") + ctx.SetPathParam(":id", "1") contexttest.LoadRepo(t, ctx, 1) contexttest.LoadRepoCommit(t, ctx) contexttest.LoadUser(t, ctx, 2) diff --git a/services/repository/files/patch.go b/services/repository/files/patch.go index 38c17b4073d13..ab0e7ffd36fdc 100644 --- a/services/repository/files/patch.go +++ b/services/repository/files/patch.go @@ -8,6 +8,7 @@ import ( "fmt" "strings" + "code.gitea.io/gitea/models" git_model "code.gitea.io/gitea/models/git" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" @@ -15,29 +16,9 @@ import ( "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/structs" - "code.gitea.io/gitea/modules/util" asymkey_service "code.gitea.io/gitea/services/asymkey" ) -// 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 -} - // ApplyDiffPatchOptions holds the repository diff patch update options type ApplyDiffPatchOptions struct { LastCommitID string @@ -93,7 +74,7 @@ func (opts *ApplyDiffPatchOptions) Validate(ctx context.Context, repo *repo_mode if protectedBranch != nil { protectedBranch.Repo = repo if !protectedBranch.CanUserPush(ctx, doer) { - return ErrUserCannotCommit{ + return models.ErrUserCannotCommit{ UserName: doer.LowerName, } } @@ -104,7 +85,7 @@ func (opts *ApplyDiffPatchOptions) Validate(ctx context.Context, repo *repo_mode if !asymkey_service.IsErrWontSign(err) { return err } - return ErrUserCannotCommit{ + return models.ErrUserCannotCommit{ UserName: doer.LowerName, } } @@ -156,7 +137,7 @@ func ApplyDiffPatch(ctx context.Context, repo *repo_model.Repository, doer *user } opts.LastCommitID = lastCommitID.String() if commit.ID.String() != opts.LastCommitID { - return nil, ErrCommitIDDoesNotMatch{ + return nil, models.ErrCommitIDDoesNotMatch{ GivenCommitID: opts.LastCommitID, CurrentCommitID: opts.LastCommitID, } diff --git a/services/repository/files/temp_repo.go b/services/repository/files/temp_repo.go index af05565d45b20..c8631ffd1ab5d 100644 --- a/services/repository/files/temp_repo.go +++ b/services/repository/files/temp_repo.go @@ -13,6 +13,7 @@ import ( "strings" "time" + "code.gitea.io/gitea/models" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" @@ -186,7 +187,7 @@ func (t *TemporaryUploadRepository) AddObjectToIndex(mode, objectHash, objectPat if _, _, err := git.NewCommand(t.ctx, "update-index", "--add", "--replace", "--cacheinfo").AddDynamicArguments(mode, objectHash, objectPath).RunStdString(&git.RunOpts{Dir: t.basePath}); err != nil { stderr := err.Error() if matched, _ := regexp.MatchString(".*Invalid path '.*", stderr); matched { - return ErrFilePathInvalid{ + return models.ErrFilePathInvalid{ Message: objectPath, Path: objectPath, } diff --git a/services/repository/files/tree.go b/services/repository/files/tree.go index 6775186afdd8e..e3a7f3b8b0e3b 100644 --- a/services/repository/files/tree.go +++ b/services/repository/files/tree.go @@ -8,37 +8,18 @@ import ( "fmt" "net/url" + "code.gitea.io/gitea/models" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" - "code.gitea.io/gitea/modules/util" ) -// 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 -} - // GetTreeBySHA get the GitTreeResponse of a repository using a sha hash. func GetTreeBySHA(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, sha string, page, perPage int, recursive bool) (*api.GitTreeResponse, error) { gitTree, err := gitRepo.GetTree(sha) if err != nil || gitTree == nil { - return nil, ErrSHANotFound{ // TODO: this error has never been catch outside of this function + return nil, models.ErrSHANotFound{ SHA: sha, } } diff --git a/services/repository/files/tree_test.go b/services/repository/files/tree_test.go index 0c60fddf7b425..786bc15857c48 100644 --- a/services/repository/files/tree_test.go +++ b/services/repository/files/tree_test.go @@ -25,10 +25,10 @@ func TestGetTreeBySHA(t *testing.T) { sha := ctx.Repo.Repository.DefaultBranch page := 1 perPage := 10 - ctx.SetPathParam("id", "1") - ctx.SetPathParam("sha", sha) + ctx.SetPathParam(":id", "1") + ctx.SetPathParam(":sha", sha) - tree, err := GetTreeBySHA(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, ctx.PathParam("sha"), page, perPage, true) + tree, err := GetTreeBySHA(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, ctx.PathParam(":sha"), page, perPage, true) assert.NoError(t, err) expectedTree := &api.GitTreeResponse{ SHA: "65f1bf27bc3bf70f64657658635e66094edbcb4d", diff --git a/services/repository/files/update.go b/services/repository/files/update.go index a2763105b0c66..b1b64bacd9924 100644 --- a/services/repository/files/update.go +++ b/services/repository/files/update.go @@ -11,6 +11,7 @@ import ( "strings" "time" + "code.gitea.io/gitea/models" git_model "code.gitea.io/gitea/models/git" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" @@ -20,9 +21,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/structs" - "code.gitea.io/gitea/modules/util" asymkey_service "code.gitea.io/gitea/services/asymkey" - pull_service "code.gitea.io/gitea/services/pull" ) // IdentityOptions for a person's identity like an author or committer @@ -65,26 +64,6 @@ type RepoFileOptions struct { executable bool } -// 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 -} - // ChangeRepoFiles adds, updates or removes multiple files in the given repository func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *user_model.User, opts *ChangeRepoFilesOptions) (*structs.FilesResponse, error) { err := repo.MustNotBeArchived() @@ -121,14 +100,14 @@ func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use // Check that the path given in opts.treePath is valid (not a git path) treePath := CleanUploadFileName(file.TreePath) if treePath == "" { - return nil, ErrFilenameInvalid{ + return nil, models.ErrFilenameInvalid{ Path: file.TreePath, } } // If there is a fromTreePath (we are copying it), also clean it up fromTreePath := CleanUploadFileName(file.FromTreePath) if fromTreePath == "" && file.FromTreePath != "" { - return nil, ErrFilenameInvalid{ + return nil, models.ErrFilenameInvalid{ Path: file.FromTreePath, } } @@ -206,7 +185,7 @@ func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use } } if !inFilelist { - return nil, ErrRepoFileDoesNotExist{ + return nil, models.ErrRepoFileDoesNotExist{ Path: file.TreePath, } } @@ -297,63 +276,6 @@ func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use return filesResponse, nil } -// 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 -} - -// 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 -} - -// 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" -} - // handles the check for various issues for ChangeRepoFiles func handleCheckErrors(file *ChangeRepoFile, commit *git.Commit, opts *ChangeRepoFilesOptions) error { if file.Operation == "update" || file.Operation == "delete" { @@ -364,7 +286,7 @@ func handleCheckErrors(file *ChangeRepoFile, commit *git.Commit, opts *ChangeRep if file.SHA != "" { // If a SHA was given and the SHA given doesn't match the SHA of the fromTreePath, throw error if file.SHA != fromEntry.ID.String() { - return pull_service.ErrSHADoesNotMatch{ + return models.ErrSHADoesNotMatch{ Path: file.Options.treePath, GivenSHA: file.SHA, CurrentSHA: fromEntry.ID.String(), @@ -377,7 +299,7 @@ func handleCheckErrors(file *ChangeRepoFile, commit *git.Commit, opts *ChangeRep if changed, err := commit.FileChangedSinceCommit(file.Options.treePath, opts.LastCommitID); err != nil { return err } else if changed { - return ErrCommitIDDoesNotMatch{ + return models.ErrCommitIDDoesNotMatch{ GivenCommitID: opts.LastCommitID, CurrentCommitID: opts.LastCommitID, } @@ -387,7 +309,7 @@ func handleCheckErrors(file *ChangeRepoFile, commit *git.Commit, opts *ChangeRep } else { // When updating a file, a lastCommitID or SHA needs to be given to make sure other commits // haven't been made. We throw an error if one wasn't provided. - return ErrSHAOrCommitIDNotProvided{} + return models.ErrSHAOrCommitIDNotProvided{} } file.Options.executable = fromEntry.IsExecutable() } @@ -410,7 +332,7 @@ func handleCheckErrors(file *ChangeRepoFile, commit *git.Commit, opts *ChangeRep } if index < len(treePathParts)-1 { if !entry.IsDir() { - return ErrFilePathInvalid{ + return models.ErrFilePathInvalid{ Message: fmt.Sprintf("a file exists where you’re trying to create a subdirectory [path: %s]", subTreePath), Path: subTreePath, Name: part, @@ -418,14 +340,14 @@ func handleCheckErrors(file *ChangeRepoFile, commit *git.Commit, opts *ChangeRep } } } else if entry.IsLink() { - return ErrFilePathInvalid{ + return models.ErrFilePathInvalid{ Message: fmt.Sprintf("a symbolic link exists where you’re trying to create a subdirectory [path: %s]", subTreePath), Path: subTreePath, Name: part, Type: git.EntryModeSymlink, } } else if entry.IsDir() { - return ErrFilePathInvalid{ + return models.ErrFilePathInvalid{ Message: fmt.Sprintf("a directory exists where you’re trying to create a file [path: %s]", subTreePath), Path: subTreePath, Name: part, @@ -433,7 +355,7 @@ func handleCheckErrors(file *ChangeRepoFile, commit *git.Commit, opts *ChangeRep } } else if file.Options.fromTreePath != file.Options.treePath || file.Operation == "create" { // The entry shouldn't exist if we are creating new file or moving to a new path - return ErrRepoFileAlreadyExists{ + return models.ErrRepoFileAlreadyExists{ Path: file.Options.treePath, } } @@ -454,7 +376,7 @@ func CreateOrUpdateFile(ctx context.Context, t *TemporaryUploadRepository, file if file.Operation == "create" { for _, indexFile := range filesInIndex { if indexFile == file.TreePath { - return ErrRepoFileAlreadyExists{ + return models.ErrRepoFileAlreadyExists{ Path: file.TreePath, } } @@ -557,12 +479,12 @@ func VerifyBranchProtection(ctx context.Context, repo *repo_model.Repository, do isUnprotectedFile = protectedBranch.IsUnprotectedFile(globUnprotected, treePath) } if !canUserPush && !isUnprotectedFile { - return ErrUserCannotCommit{ + return models.ErrUserCannotCommit{ UserName: doer.LowerName, } } if protectedBranch.IsProtectedFile(globProtected, treePath) { - return pull_service.ErrFilePathProtected{ + return models.ErrFilePathProtected{ Path: treePath, } } @@ -573,7 +495,7 @@ func VerifyBranchProtection(ctx context.Context, repo *repo_model.Repository, do if !asymkey_service.IsErrWontSign(err) { return err } - return ErrUserCannotCommit{ + return models.ErrUserCannotCommit{ UserName: doer.LowerName, } } diff --git a/services/repository/fork.go b/services/repository/fork.go index cff0b1a40365d..b91ad6889c555 100644 --- a/services/repository/fork.go +++ b/services/repository/fork.go @@ -158,6 +158,7 @@ func ForkRepository(ctx context.Context, doer, owner *user_model.User, opts Fork } repoPath := repo_model.RepoPath(owner.Name, repo.Name) if stdout, _, err := cloneCmd.AddDynamicArguments(oldRepoPath, repoPath). + SetDescription(fmt.Sprintf("ForkRepository(git clone): %s to %s", opts.BaseRepo.FullName(), repo.FullName())). RunStdBytes(&git.RunOpts{Timeout: 10 * time.Minute}); err != nil { log.Error("Fork Repository (git clone) Failed for %v (from %v):\nStdout: %s\nError: %v", repo, opts.BaseRepo, stdout, err) return fmt.Errorf("git clone: %w", err) @@ -168,6 +169,7 @@ func ForkRepository(ctx context.Context, doer, owner *user_model.User, opts Fork } if stdout, _, err := git.NewCommand(txCtx, "update-server-info"). + SetDescription(fmt.Sprintf("ForkRepository(git update-server-info): %s", repo.FullName())). RunStdString(&git.RunOpts{Dir: repoPath}); err != nil { log.Error("Fork Repository (git update-server-info) failed for %v:\nStdout: %s\nError: %v", repo, stdout, err) return fmt.Errorf("git update-server-info: %w", err) @@ -256,9 +258,11 @@ type findForksOptions struct { } func (opts findForksOptions) ToConds() builder.Cond { - return builder.Eq{"fork_id": opts.RepoID}.And( - repo_model.AccessibleRepositoryCondition(opts.Doer, unit.TypeInvalid), - ) + cond := builder.Eq{"fork_id": opts.RepoID} + if opts.Doer != nil && opts.Doer.IsAdmin { + return cond + } + return cond.And(repo_model.AccessibleRepositoryCondition(opts.Doer, unit.TypeInvalid)) } // FindForks returns all the forks of the repository diff --git a/services/repository/generate.go b/services/repository/generate.go index 24cf9d1b9bfc1..d3b750f7d4830 100644 --- a/services/repository/generate.go +++ b/services/repository/generate.go @@ -9,7 +9,6 @@ import ( "context" "fmt" "os" - "path" "path/filepath" "regexp" "strconv" @@ -123,7 +122,7 @@ func (gt *GiteaTemplate) Globs() []glob.Glob { return gt.globs } -func checkGiteaTemplate(tmpDir string) (*GiteaTemplate, error) { +func readGiteaTemplateFile(tmpDir string) (*GiteaTemplate, error) { gtPath := filepath.Join(tmpDir, ".gitea", "template") if _, err := os.Stat(gtPath); os.IsNotExist(err) { return nil, nil @@ -136,12 +135,55 @@ func checkGiteaTemplate(tmpDir string) (*GiteaTemplate, error) { return nil, err } - gt := &GiteaTemplate{ - Path: gtPath, - Content: content, + return &GiteaTemplate{Path: gtPath, Content: content}, nil +} + +func processGiteaTemplateFile(tmpDir string, templateRepo, generateRepo *repo_model.Repository, giteaTemplateFile *GiteaTemplate) error { + if err := util.Remove(giteaTemplateFile.Path); err != nil { + return fmt.Errorf("remove .giteatemplate: %w", err) } + if len(giteaTemplateFile.Globs()) == 0 { + return nil // Avoid walking tree if there are no globs + } + tmpDirSlash := strings.TrimSuffix(filepath.ToSlash(tmpDir), "/") + "/" + return filepath.WalkDir(tmpDirSlash, func(path string, d os.DirEntry, walkErr error) error { + if walkErr != nil { + return walkErr + } + + if d.IsDir() { + return nil + } + + base := strings.TrimPrefix(filepath.ToSlash(path), tmpDirSlash) + for _, g := range giteaTemplateFile.Globs() { + if g.Match(base) { + content, err := os.ReadFile(path) + if err != nil { + return err + } - return gt, nil + generatedContent := []byte(generateExpansion(string(content), templateRepo, generateRepo, false)) + if err := os.WriteFile(path, generatedContent, 0o644); err != nil { + return err + } + + substPath := filepath.FromSlash(filepath.Join(tmpDirSlash, generateExpansion(base, templateRepo, generateRepo, true))) + + // Create parent subdirectories if needed or continue silently if it exists + if err = os.MkdirAll(filepath.Dir(substPath), 0o755); err != nil { + return err + } + + // Substitute filename variables + if err = os.Rename(path, substPath); err != nil { + return err + } + break + } + } + return nil + }) // end: WalkDir } func generateRepoCommit(ctx context.Context, repo, templateRepo, generateRepo *repo_model.Repository, tmpDir string) error { @@ -167,81 +209,45 @@ func generateRepoCommit(ctx context.Context, repo, templateRepo, generateRepo *r return fmt.Errorf("git clone: %w", err) } - if err := util.RemoveAll(path.Join(tmpDir, ".git")); err != nil { + // Get active submodules from the template + submodules, err := git.GetTemplateSubmoduleCommits(ctx, tmpDir) + if err != nil { + return fmt.Errorf("GetTemplateSubmoduleCommits: %w", err) + } + + if err = util.RemoveAll(filepath.Join(tmpDir, ".git")); err != nil { return fmt.Errorf("remove git dir: %w", err) } // Variable expansion - gt, err := checkGiteaTemplate(tmpDir) + giteaTemplateFile, err := readGiteaTemplateFile(tmpDir) if err != nil { - return fmt.Errorf("checkGiteaTemplate: %w", err) + return fmt.Errorf("readGiteaTemplateFile: %w", err) } - if gt != nil { - if err := util.Remove(gt.Path); err != nil { - return fmt.Errorf("remove .giteatemplate: %w", err) - } - - // Avoid walking tree if there are no globs - if len(gt.Globs()) > 0 { - tmpDirSlash := strings.TrimSuffix(filepath.ToSlash(tmpDir), "/") + "/" - if err := filepath.WalkDir(tmpDirSlash, func(path string, d os.DirEntry, walkErr error) error { - if walkErr != nil { - return walkErr - } - - if d.IsDir() { - return nil - } - - base := strings.TrimPrefix(filepath.ToSlash(path), tmpDirSlash) - for _, g := range gt.Globs() { - if g.Match(base) { - content, err := os.ReadFile(path) - if err != nil { - return err - } - - if err := os.WriteFile(path, - []byte(generateExpansion(string(content), templateRepo, generateRepo, false)), - 0o644); err != nil { - return err - } - - substPath := filepath.FromSlash(filepath.Join(tmpDirSlash, - generateExpansion(base, templateRepo, generateRepo, true))) - - // Create parent subdirectories if needed or continue silently if it exists - if err := os.MkdirAll(filepath.Dir(substPath), 0o755); err != nil { - return err - } - - // Substitute filename variables - if err := os.Rename(path, substPath); err != nil { - return err - } - - break - } - } - return nil - }); err != nil { - return err - } + if giteaTemplateFile != nil { + err = processGiteaTemplateFile(tmpDir, templateRepo, generateRepo, giteaTemplateFile) + if err != nil { + return err } } - if err := git.InitRepository(ctx, tmpDir, false, templateRepo.ObjectFormatName); err != nil { + if err = git.InitRepository(ctx, tmpDir, false, templateRepo.ObjectFormatName); err != nil { return err } repoPath := repo.RepoPath() if stdout, _, err := git.NewCommand(ctx, "remote", "add", "origin").AddDynamicArguments(repoPath). + SetDescription(fmt.Sprintf("generateRepoCommit (git remote add): %s to %s", templateRepoPath, tmpDir)). RunStdString(&git.RunOpts{Dir: tmpDir, Env: env}); err != nil { log.Error("Unable to add %v as remote origin to temporary repo to %s: stdout %s\nError: %v", repo, tmpDir, stdout, err) return fmt.Errorf("git remote add: %w", err) } + if err = git.AddTemplateSubmoduleIndexes(ctx, tmpDir, submodules); err != nil { + return fmt.Errorf("failed to add submodules: %v", err) + } + // set default branch based on whether it's specified in the newly generated repo or not defaultBranch := repo.DefaultBranch if strings.TrimSpace(defaultBranch) == "" { @@ -368,6 +374,7 @@ func generateRepository(ctx context.Context, doer, owner *user_model.User, templ } if stdout, _, err := git.NewCommand(ctx, "update-server-info"). + SetDescription(fmt.Sprintf("GenerateRepository(git update-server-info): %s", repoPath)). RunStdString(&git.RunOpts{Dir: repoPath}); err != nil { log.Error("GenerateRepository(git update-server-info) in %v: Stdout: %s\nError: %v", generateRepo, stdout, err) return generateRepo, fmt.Errorf("error in GenerateRepository(git update-server-info): %w", err) diff --git a/services/repository/init.go b/services/repository/init.go index c719e11786311..817fa4abd7769 100644 --- a/services/repository/init.go +++ b/services/repository/init.go @@ -34,6 +34,7 @@ func initRepoCommit(ctx context.Context, tmpPath string, repo *repo_model.Reposi committerEmail := sig.Email if stdout, _, err := git.NewCommand(ctx, "add", "--all"). + SetDescription(fmt.Sprintf("initRepoCommit (git add): %s", tmpPath)). RunStdString(&git.RunOpts{Dir: tmpPath}); err != nil { log.Error("git add --all failed: Stdout: %s\nError: %v", stdout, err) return fmt.Errorf("git add --all: %w", err) @@ -61,8 +62,9 @@ func initRepoCommit(ctx context.Context, tmpPath string, repo *repo_model.Reposi ) if stdout, _, err := cmd. + SetDescription(fmt.Sprintf("initRepoCommit (git commit): %s", tmpPath)). RunStdString(&git.RunOpts{Dir: tmpPath, Env: env}); err != nil { - log.Error("Failed to commit: %v: Stdout: %s\nError: %v", cmd.LogString(), stdout, err) + log.Error("Failed to commit: %v: Stdout: %s\nError: %v", cmd.String(), stdout, err) return fmt.Errorf("git commit: %w", err) } @@ -71,6 +73,7 @@ func initRepoCommit(ctx context.Context, tmpPath string, repo *repo_model.Reposi } if stdout, _, err := git.NewCommand(ctx, "push", "origin").AddDynamicArguments("HEAD:" + defaultBranch). + SetDescription(fmt.Sprintf("initRepoCommit (git push): %s", tmpPath)). RunStdString(&git.RunOpts{Dir: tmpPath, Env: repo_module.InternalPushingEnvironment(u, repo)}); err != nil { log.Error("Failed to push back to HEAD: Stdout: %s\nError: %v", stdout, err) return fmt.Errorf("git push: %w", err) diff --git a/services/repository/merge_upstream.go b/services/repository/merge_upstream.go index eae384aa5e46d..2a514b522ea40 100644 --- a/services/repository/merge_upstream.go +++ b/services/repository/merge_upstream.go @@ -5,9 +5,9 @@ package repository import ( "context" + "errors" "fmt" - git_model "code.gitea.io/gitea/models/git" issue_model "code.gitea.io/gitea/models/issues" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" @@ -17,12 +17,7 @@ import ( "code.gitea.io/gitea/services/pull" ) -type UpstreamDivergingInfo struct { - BaseIsNewer bool - CommitsBehind int - CommitsAhead int -} - +// MergeUpstream merges the base repository's default branch into the fork repository's current branch. func MergeUpstream(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, branch string) (mergeStyle string, err error) { if err = repo.MustNotBeArchived(); err != nil { return "", err @@ -30,9 +25,17 @@ func MergeUpstream(ctx context.Context, doer *user_model.User, repo *repo_model. if err = repo.GetBaseRepo(ctx); err != nil { return "", err } + divergingInfo, err := GetUpstreamDivergingInfo(ctx, repo, branch) + if err != nil { + return "", err + } + if !divergingInfo.BaseBranchHasNewCommits { + return "up-to-date", nil + } + err = git.Push(ctx, repo.BaseRepo.RepoPath(), git.PushOptions{ Remote: repo.RepoPath(), - Branch: fmt.Sprintf("%s:%s", branch, branch), + Branch: fmt.Sprintf("%s:%s", divergingInfo.BaseBranchName, branch), Env: repo_module.PushingEnvironment(doer, repo), }) if err == nil { @@ -64,7 +67,7 @@ func MergeUpstream(ctx context.Context, doer *user_model.User, repo *repo_model. BaseRepoID: repo.BaseRepo.ID, BaseRepo: repo.BaseRepo, HeadBranch: branch, // maybe HeadCommitID is not needed - BaseBranch: branch, + BaseBranch: divergingInfo.BaseBranchName, } fakeIssue.PullRequest = fakePR err = pull.Update(ctx, fakePR, doer, "merge upstream", false, repo_model.MergeStrategyDefault, repo_model.MergeStrategyOptionNone) @@ -74,42 +77,47 @@ func MergeUpstream(ctx context.Context, doer *user_model.User, repo *repo_model. return "merge", nil } -func GetUpstreamDivergingInfo(ctx context.Context, repo *repo_model.Repository, branch string) (*UpstreamDivergingInfo, error) { - if !repo.IsFork { +// UpstreamDivergingInfo is also used in templates, so it needs to search for all references before changing it. +type UpstreamDivergingInfo struct { + BaseBranchName string + BaseBranchHasNewCommits bool + HeadBranchCommitsBehind int +} + +// GetUpstreamDivergingInfo returns the information about the divergence between the fork repository's branch and the base repository's default branch. +func GetUpstreamDivergingInfo(ctx context.Context, forkRepo *repo_model.Repository, forkBranch string) (*UpstreamDivergingInfo, error) { + if !forkRepo.IsFork { return nil, util.NewInvalidArgumentErrorf("repo is not a fork") } - if repo.IsArchived { + if forkRepo.IsArchived { return nil, util.NewInvalidArgumentErrorf("repo is archived") } - if err := repo.GetBaseRepo(ctx); err != nil { + if err := forkRepo.GetBaseRepo(ctx); err != nil { return nil, err } - forkBranch, err := git_model.GetBranch(ctx, repo.ID, branch) - if err != nil { - return nil, err - } - - baseBranch, err := git_model.GetBranch(ctx, repo.BaseRepo.ID, branch) - if err != nil { - return nil, err - } - - info := &UpstreamDivergingInfo{} - if forkBranch.CommitID == baseBranch.CommitID { - return info, nil + // Do the best to follow the GitHub's behavior, suppose there is a `branch-a` in fork repo: + // * if `branch-a` exists in base repo: try to sync `base:branch-a` to `fork:branch-a` + // * if `branch-a` doesn't exist in base repo: try to sync `base:main` to `fork:branch-a` + info, err := GetBranchDivergingInfo(ctx, forkRepo.BaseRepo, forkBranch, forkRepo, forkBranch) + if err == nil { + return &UpstreamDivergingInfo{ + BaseBranchName: forkBranch, + BaseBranchHasNewCommits: info.BaseHasNewCommits, + HeadBranchCommitsBehind: info.HeadCommitsBehind, + }, nil } - - // TODO: if the fork repo has new commits, this call will fail: - // exit status 128 - fatal: Invalid symmetric difference expression aaaaaaaaaaaa...bbbbbbbbbbbb - // so at the moment, we are not able to handle this case, should be improved in the future - diff, err := git.GetDivergingCommits(ctx, repo.BaseRepo.RepoPath(), baseBranch.CommitID, forkBranch.CommitID) - if err != nil { - info.BaseIsNewer = baseBranch.UpdatedUnix > forkBranch.UpdatedUnix - return info, nil + if errors.Is(err, util.ErrNotExist) { + info, err = GetBranchDivergingInfo(ctx, forkRepo.BaseRepo, forkRepo.BaseRepo.DefaultBranch, forkRepo, forkBranch) + if err == nil { + return &UpstreamDivergingInfo{ + BaseBranchName: forkRepo.BaseRepo.DefaultBranch, + BaseBranchHasNewCommits: info.BaseHasNewCommits, + HeadBranchCommitsBehind: info.HeadCommitsBehind, + }, nil + } } - info.CommitsBehind, info.CommitsAhead = diff.Behind, diff.Ahead - return info, nil + return nil, err } diff --git a/services/repository/migrate.go b/services/repository/migrate.go index 7fe63eb5ca7c6..262d3bd51a9a2 100644 --- a/services/repository/migrate.go +++ b/services/repository/migrate.go @@ -8,7 +8,6 @@ import ( "errors" "fmt" "net/http" - "strings" "time" "code.gitea.io/gitea/models/db" @@ -122,6 +121,7 @@ func MigrateRepositoryGitData(ctx context.Context, u *user_model.User, } if stdout, _, err := git.NewCommand(ctx, "update-server-info"). + SetDescription(fmt.Sprintf("MigrateRepositoryGitData(git update-server-info): %s", repoPath)). RunStdString(&git.RunOpts{Dir: repoPath}); err != nil { log.Error("MigrateRepositoryGitData(git update-server-info) in %v: Stdout: %s\nError: %v", repo, stdout, err) return repo, fmt.Errorf("error in MigrateRepositoryGitData(git update-server-info): %w", err) @@ -254,10 +254,10 @@ func MigrateRepositoryGitData(ctx context.Context, u *user_model.User, func cleanUpMigrateGitConfig(ctx context.Context, repoPath string) error { cmd := git.NewCommand(ctx, "remote", "rm", "origin") // if the origin does not exist - _, stderr, err := cmd.RunStdString(&git.RunOpts{ + _, _, err := cmd.RunStdString(&git.RunOpts{ Dir: repoPath, }) - if err != nil && !strings.HasPrefix(stderr, "fatal: No such remote") { + if err != nil && !git.IsRemoteNotExistError(err) { return err } return nil @@ -276,7 +276,7 @@ func CleanUpMigrateInfo(ctx context.Context, repo *repo_model.Repository) (*repo } _, _, err := git.NewCommand(ctx, "remote", "rm", "origin").RunStdString(&git.RunOpts{Dir: repoPath}) - if err != nil && !strings.HasPrefix(err.Error(), "exit status 128 - fatal: No such remote ") { + if err != nil && !git.IsRemoteNotExistError(err) { return repo, fmt.Errorf("CleanUpMigrateInfo: %w", err) } diff --git a/services/repository/push.go b/services/repository/push.go index 06ad65e48fb6b..211bfddde28a1 100644 --- a/services/repository/push.go +++ b/services/repository/push.go @@ -23,6 +23,7 @@ import ( repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/modules/util" issue_service "code.gitea.io/gitea/services/issue" notify_service "code.gitea.io/gitea/services/notify" pull_service "code.gitea.io/gitea/services/pull" @@ -133,23 +134,26 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error { } else { // is new tag newCommit, err := gitRepo.GetCommit(opts.NewCommitID) if err != nil { - return fmt.Errorf("gitRepo.GetCommit(%s) in %s/%s[%d]: %w", opts.NewCommitID, repo.OwnerName, repo.Name, repo.ID, err) + // in case there is dirty data, for example, the "github.com/git/git" repository has tags pointing to non-existing commits + if !errors.Is(err, util.ErrNotExist) { + log.Error("Unable to get tag commit: gitRepo.GetCommit(%s) in %s/%s[%d]: %v", opts.NewCommitID, repo.OwnerName, repo.Name, repo.ID, err) + } + } else { + commits := repo_module.NewPushCommits() + commits.HeadCommit = repo_module.CommitToPushCommit(newCommit) + commits.CompareURL = repo.ComposeCompareURL(objectFormat.EmptyObjectID().String(), opts.NewCommitID) + + notify_service.PushCommits( + ctx, pusher, repo, + &repo_module.PushUpdateOptions{ + RefFullName: opts.RefFullName, + OldCommitID: objectFormat.EmptyObjectID().String(), + NewCommitID: opts.NewCommitID, + }, commits) + + addTags = append(addTags, tagName) + notify_service.CreateRef(ctx, pusher, repo, opts.RefFullName, opts.NewCommitID) } - - commits := repo_module.NewPushCommits() - commits.HeadCommit = repo_module.CommitToPushCommit(newCommit) - commits.CompareURL = repo.ComposeCompareURL(objectFormat.EmptyObjectID().String(), opts.NewCommitID) - - notify_service.PushCommits( - ctx, pusher, repo, - &repo_module.PushUpdateOptions{ - RefFullName: opts.RefFullName, - OldCommitID: objectFormat.EmptyObjectID().String(), - NewCommitID: opts.NewCommitID, - }, commits) - - addTags = append(addTags, tagName) - notify_service.CreateRef(ctx, pusher, repo, opts.RefFullName, opts.NewCommitID) } } else if opts.RefFullName.IsBranch() { if pusher == nil || pusher.ID != opts.PusherID { @@ -166,7 +170,6 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error { branch := opts.RefFullName.BranchName() if !opts.IsDelRef() { log.Trace("TriggerTask '%s/%s' by %s", repo.Name, branch, pusher.Name) - go pull_service.AddTestPullRequestTask(pusher, repo.ID, branch, true, opts.OldCommitID, opts.NewCommitID) newCommit, err := gitRepo.GetCommit(opts.NewCommitID) if err != nil { @@ -208,6 +211,17 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error { log.Error("IsForcePush %s:%s failed: %v", repo.FullName(), branch, err) } + // only update branch can trigger pull request task because the pull request hasn't been created yet when creaing a branch + go pull_service.AddTestPullRequestTask(pull_service.TestPullRequestOptions{ + RepoID: repo.ID, + Doer: pusher, + Branch: branch, + IsSync: true, + IsForcePush: isForcePush, + OldCommitID: opts.OldCommitID, + NewCommitID: opts.NewCommitID, + }) + if isForcePush { log.Trace("Push %s is a force push", opts.NewCommitID) diff --git a/services/repository/repo_team.go b/services/repository/repo_team.go index 672ee49fea9eb..29c67893b23ca 100644 --- a/services/repository/repo_team.go +++ b/services/repository/repo_team.go @@ -63,7 +63,7 @@ func addRepositoryToTeam(ctx context.Context, t *organization.Team, repo *repo_m // If the team already has some repositories they will be left unchanged. func AddAllRepositoriesToTeam(ctx context.Context, t *organization.Team) error { return db.WithTx(ctx, func(ctx context.Context) error { - orgRepos, err := repo_model.GetOrgRepositories(ctx, t.OrgID) + orgRepos, err := organization.GetOrgRepositories(ctx, t.OrgID) if err != nil { return fmt.Errorf("get org repos: %w", err) } @@ -103,15 +103,8 @@ func RemoveAllRepositoriesFromTeam(ctx context.Context, t *organization.Team) (e // Note: Shall not be called if team includes all repositories func removeAllRepositoriesFromTeam(ctx context.Context, t *organization.Team) (err error) { e := db.GetEngine(ctx) - repos, err := repo_model.GetTeamRepositories(ctx, &repo_model.SearchTeamRepoOptions{ - TeamID: t.ID, - }) - if err != nil { - return fmt.Errorf("GetTeamRepositories: %w", err) - } - // Delete all accesses. - for _, repo := range repos { + for _, repo := range t.Repos { if err := access_model.RecalculateTeamAccesses(ctx, repo, t.ID); err != nil { return err } diff --git a/services/repository/repository.go b/services/repository/repository.go index 59b4491132da9..fcc617979ef29 100644 --- a/services/repository/repository.go +++ b/services/repository/repository.go @@ -11,7 +11,6 @@ import ( "code.gitea.io/gitea/models/git" issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/organization" - packages_model "code.gitea.io/gitea/models/packages" repo_model "code.gitea.io/gitea/models/repo" system_model "code.gitea.io/gitea/models/system" "code.gitea.io/gitea/models/unit" @@ -63,11 +62,7 @@ func DeleteRepository(ctx context.Context, doer *user_model.User, repo *repo_mod notify_service.DeleteRepository(ctx, doer, repo) } - if err := DeleteRepositoryDirectly(ctx, doer, repo.ID); err != nil { - return err - } - - return packages_model.UnlinkRepositoryFromAllPackages(ctx, repo.ID) + return DeleteRepositoryDirectly(ctx, doer, repo.ID) } // PushCreateRepo creates a repository when a new repository is pushed to an appropriate namespace diff --git a/services/repository/setting.go b/services/repository/setting.go index b82f24271ea34..e0c787dd2d887 100644 --- a/services/repository/setting.go +++ b/services/repository/setting.go @@ -7,7 +7,6 @@ import ( "context" "slices" - actions_model "code.gitea.io/gitea/models/actions" "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" @@ -29,7 +28,7 @@ func UpdateRepositoryUnits(ctx context.Context, repo *repo_model.Repository, uni } if slices.Contains(deleteUnitTypes, unit.TypeActions) { - if err := actions_model.CleanRepoScheduleTasks(ctx, repo); err != nil { + if err := actions_service.CleanRepoScheduleTasks(ctx, repo); err != nil { log.Error("CleanRepoScheduleTasks: %v", err) } } diff --git a/services/repository/transfer.go b/services/repository/transfer.go index 9ef28ddeb9fa6..9a643469d9db8 100644 --- a/services/repository/transfer.go +++ b/services/repository/transfer.go @@ -9,6 +9,7 @@ import ( "os" "strings" + "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/db" issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/organization" @@ -284,7 +285,7 @@ func transferOwnership(ctx context.Context, doer *user_model.User, newOwnerName wikiRenamed = true } - if err := repo_model.DeleteRepositoryTransfer(ctx, repo.ID); err != nil { + if err := models.DeleteRepositoryTransfer(ctx, repo.ID); err != nil { return fmt.Errorf("deleteRepositoryTransfer: %w", err) } repo.Status = repo_model.RepositoryReady @@ -387,7 +388,7 @@ func ChangeRepositoryName(ctx context.Context, doer *user_model.User, repo *repo // StartRepositoryTransfer transfer a repo from one owner to a new one. // it make repository into pending transfer state, if doer can not create repo for new owner. func StartRepositoryTransfer(ctx context.Context, doer, newOwner *user_model.User, repo *repo_model.Repository, teams []*organization.Team) error { - if err := repo_model.TestRepositoryReadyForTransfer(repo.Status); err != nil { + if err := models.TestRepositoryReadyForTransfer(repo.Status); err != nil { return err } @@ -424,7 +425,7 @@ func StartRepositoryTransfer(ctx context.Context, doer, newOwner *user_model.Use // Make repo as pending for transfer repo.Status = repo_model.RepositoryPendingTransfer - if err := repo_model.CreatePendingRepositoryTransfer(ctx, doer, newOwner, repo.ID, teams); err != nil { + if err := models.CreatePendingRepositoryTransfer(ctx, doer, newOwner, repo.ID, teams); err != nil { return err } @@ -448,7 +449,7 @@ func CancelRepositoryTransfer(ctx context.Context, repo *repo_model.Repository) return err } - if err := repo_model.DeleteRepositoryTransfer(ctx, repo.ID); err != nil { + if err := models.DeleteRepositoryTransfer(ctx, repo.ID); err != nil { return err } diff --git a/services/repository/transfer_test.go b/services/repository/transfer_test.go index 91722fb8ae14b..0401701ba55f4 100644 --- a/services/repository/transfer_test.go +++ b/services/repository/transfer_test.go @@ -7,6 +7,7 @@ import ( "sync" "testing" + "code.gitea.io/gitea/models" activities_model "code.gitea.io/gitea/models/activities" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/organization" @@ -85,23 +86,23 @@ func TestRepositoryTransfer(t *testing.T) { doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}) - transfer, err := repo_model.GetPendingRepositoryTransfer(db.DefaultContext, repo) + transfer, err := models.GetPendingRepositoryTransfer(db.DefaultContext, repo) assert.NoError(t, err) assert.NotNil(t, transfer) // Cancel transfer assert.NoError(t, CancelRepositoryTransfer(db.DefaultContext, repo)) - transfer, err = repo_model.GetPendingRepositoryTransfer(db.DefaultContext, repo) + transfer, err = models.GetPendingRepositoryTransfer(db.DefaultContext, repo) assert.Error(t, err) assert.Nil(t, transfer) - assert.True(t, repo_model.IsErrNoPendingTransfer(err)) + assert.True(t, models.IsErrNoPendingTransfer(err)) user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) - assert.NoError(t, repo_model.CreatePendingRepositoryTransfer(db.DefaultContext, doer, user2, repo.ID, nil)) + assert.NoError(t, models.CreatePendingRepositoryTransfer(db.DefaultContext, doer, user2, repo.ID, nil)) - transfer, err = repo_model.GetPendingRepositoryTransfer(db.DefaultContext, repo) + transfer, err = models.GetPendingRepositoryTransfer(db.DefaultContext, repo) assert.NoError(t, err) assert.NoError(t, transfer.LoadAttributes(db.DefaultContext)) assert.Equal(t, "user2", transfer.Recipient.Name) @@ -109,12 +110,12 @@ func TestRepositoryTransfer(t *testing.T) { org6 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) // Only transfer can be started at any given time - err = repo_model.CreatePendingRepositoryTransfer(db.DefaultContext, doer, org6, repo.ID, nil) + err = models.CreatePendingRepositoryTransfer(db.DefaultContext, doer, org6, repo.ID, nil) assert.Error(t, err) - assert.True(t, repo_model.IsErrRepoTransferInProgress(err)) + assert.True(t, models.IsErrRepoTransferInProgress(err)) // Unknown user - err = repo_model.CreatePendingRepositoryTransfer(db.DefaultContext, doer, &user_model.User{ID: 1000, LowerName: "user1000"}, repo.ID, nil) + err = models.CreatePendingRepositoryTransfer(db.DefaultContext, doer, &user_model.User{ID: 1000, LowerName: "user1000"}, repo.ID, nil) assert.Error(t, err) // Cancel transfer diff --git a/services/user/block.go b/services/user/block.go index c24ce5273ca69..0b3b618aae670 100644 --- a/services/user/block.go +++ b/services/user/block.go @@ -6,6 +6,7 @@ package user import ( "context" + "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/db" issues_model "code.gitea.io/gitea/models/issues" org_model "code.gitea.io/gitea/models/organization" @@ -193,7 +194,7 @@ func unwatchRepos(ctx context.Context, watcher, repoOwner *user_model.User) erro } func cancelRepositoryTransfers(ctx context.Context, sender, recipient *user_model.User) error { - transfers, err := repo_model.GetPendingRepositoryTransfers(ctx, &repo_model.PendingRepositoryTransferOptions{ + transfers, err := models.GetPendingRepositoryTransfers(ctx, &models.PendingRepositoryTransferOptions{ SenderID: sender.ID, RecipientID: recipient.ID, }) diff --git a/services/user/update.go b/services/user/update.go index 4a39f4f783647..cbaf90053a229 100644 --- a/services/user/update.go +++ b/services/user/update.go @@ -7,6 +7,7 @@ import ( "context" "fmt" + "code.gitea.io/gitea/models" auth_model "code.gitea.io/gitea/models/auth" user_model "code.gitea.io/gitea/models/user" password_module "code.gitea.io/gitea/modules/auth/password" @@ -112,7 +113,7 @@ func UpdateUser(ctx context.Context, u *user_model.User, opts *UpdateOptions) er } if opts.IsAdmin.Has() { if !opts.IsAdmin.Value() && user_model.IsLastAdminUser(ctx, u) { - return user_model.ErrDeleteLastAdminUser{UID: u.ID} + return models.ErrDeleteLastAdminUser{UID: u.ID} } u.IsAdmin = opts.IsAdmin.Value() diff --git a/services/user/user.go b/services/user/user.go index 1aeebff142d32..7bde642412be0 100644 --- a/services/user/user.go +++ b/services/user/user.go @@ -10,6 +10,7 @@ import ( "strings" "time" + "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/organization" packages_model "code.gitea.io/gitea/models/packages" @@ -126,7 +127,7 @@ func DeleteUser(ctx context.Context, u *user_model.User, purge bool) error { } if u.IsActive && user_model.IsLastAdminUser(ctx, u) { - return user_model.ErrDeleteLastAdminUser{UID: u.ID} + return models.ErrDeleteLastAdminUser{UID: u.ID} } if purge { @@ -224,7 +225,7 @@ func DeleteUser(ctx context.Context, u *user_model.User, purge bool) error { if err != nil { return fmt.Errorf("GetRepositoryCount: %w", err) } else if count > 0 { - return repo_model.ErrUserOwnRepos{UID: u.ID} + return models.ErrUserOwnRepos{UID: u.ID} } // Check membership of organization. @@ -232,14 +233,14 @@ func DeleteUser(ctx context.Context, u *user_model.User, purge bool) error { if err != nil { return fmt.Errorf("GetOrganizationCount: %w", err) } else if count > 0 { - return organization.ErrUserHasOrgs{UID: u.ID} + return models.ErrUserHasOrgs{UID: u.ID} } // Check ownership of packages. if ownsPackages, err := packages_model.HasOwnerPackages(ctx, u.ID); err != nil { return fmt.Errorf("HasOwnerPackages: %w", err) } else if ownsPackages { - return packages_model.ErrUserOwnPackages{UID: u.ID} + return models.ErrUserOwnPackages{UID: u.ID} } if err := deleteUser(ctx, u, purge); err != nil { @@ -287,7 +288,7 @@ func DeleteInactiveUsers(ctx context.Context, olderThan time.Duration) error { for _, u := range inactiveUsers { if err = DeleteUser(ctx, u, false); err != nil { // Ignore inactive users that were ever active but then were set inactive by admin - if repo_model.IsErrUserOwnRepos(err) || organization.IsErrUserHasOrgs(err) || packages_model.IsErrUserOwnPackages(err) { + if models.IsErrUserOwnRepos(err) || models.IsErrUserHasOrgs(err) || models.IsErrUserOwnPackages(err) { log.Warn("Inactive user %q has repositories, organizations or packages, skipping deletion: %v", u.Name, err) continue } diff --git a/services/user/user_test.go b/services/user/user_test.go index 162a735cd4392..c668b005c57e9 100644 --- a/services/user/user_test.go +++ b/services/user/user_test.go @@ -9,6 +9,7 @@ import ( "testing" "time" + "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/organization" @@ -36,7 +37,7 @@ func TestDeleteUser(t *testing.T) { if len(ownedRepos) > 0 { err := DeleteUser(db.DefaultContext, user, false) assert.Error(t, err) - assert.True(t, repo_model.IsErrUserOwnRepos(err)) + assert.True(t, models.IsErrUserOwnRepos(err)) return } diff --git a/services/webhook/dingtalk.go b/services/webhook/dingtalk.go index e382f5a9df732..3ea8f50764d62 100644 --- a/services/webhook/dingtalk.go +++ b/services/webhook/dingtalk.go @@ -170,6 +170,12 @@ func (dc dingtalkConvertor) Package(p *api.PackagePayload) (DingtalkPayload, err return createDingtalkPayload(text, text, "view package", p.Package.HTMLURL), nil } +func (dc dingtalkConvertor) Status(p *api.CommitStatusPayload) (DingtalkPayload, error) { + text, _ := getStatusPayloadInfo(p, noneLinkFormatter, true) + + return createDingtalkPayload(text, text, "Status Changed", p.TargetURL), nil +} + func createDingtalkPayload(title, text, singleTitle, singleURL string) DingtalkPayload { return DingtalkPayload{ MsgType: "actionCard", @@ -190,3 +196,7 @@ func newDingtalkRequest(_ context.Context, w *webhook_model.Webhook, t *webhook_ var pc payloadConvertor[DingtalkPayload] = dingtalkConvertor{} return newJSONRequest(pc, w, t, true) } + +func init() { + RegisterWebhookRequester(webhook_module.DINGTALK, newDingtalkRequest) +} diff --git a/services/webhook/discord.go b/services/webhook/discord.go index c562d981680a8..d8d8e984374dd 100644 --- a/services/webhook/discord.go +++ b/services/webhook/discord.go @@ -14,6 +14,7 @@ import ( "unicode/utf8" webhook_model "code.gitea.io/gitea/models/webhook" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" @@ -101,6 +102,13 @@ var ( redColor = color("ff3232") ) +// https://discord.com/developers/docs/resources/message#embed-object-embed-limits +// Discord has some limits in place for the embeds. +// According to some tests, there is no consistent limit for different character sets. +// For example: 4096 ASCII letters are allowed, but only 2490 emoji characters are allowed. +// To keep it simple, we currently truncate at 2000. +const discordDescriptionCharactersLimit = 2000 + type discordConvertor struct { Username string AvatarURL string @@ -265,6 +273,12 @@ func (d discordConvertor) Package(p *api.PackagePayload) (DiscordPayload, error) return d.createPayload(p.Sender, text, "", p.Package.HTMLURL, color), nil } +func (d discordConvertor) Status(p *api.CommitStatusPayload) (DiscordPayload, error) { + text, color := getStatusPayloadInfo(p, noneLinkFormatter, false) + + return d.createPayload(p.Sender, text, "", p.TargetURL, color), nil +} + func newDiscordRequest(_ context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { meta := &DiscordMeta{} if err := json.Unmarshal([]byte(w.Meta), meta); err != nil { @@ -277,6 +291,10 @@ func newDiscordRequest(_ context.Context, w *webhook_model.Webhook, t *webhook_m return newJSONRequest(pc, w, t, true) } +func init() { + RegisterWebhookRequester(webhook_module.DISCORD, newDiscordRequest) +} + func parseHookPullRequestEventType(event webhook_module.HookEventType) (string, error) { switch event { case webhook_module.HookEventPullRequestReviewApproved: @@ -297,7 +315,7 @@ func (d discordConvertor) createPayload(s *api.User, title, text, url string, co Embeds: []DiscordEmbed{ { Title: title, - Description: text, + Description: base.TruncateString(text, discordDescriptionCharactersLimit), URL: url, Color: color, Author: DiscordEmbedAuthor{ diff --git a/services/webhook/feishu.go b/services/webhook/feishu.go index 7ca7d1cf5f369..639118d2a5eee 100644 --- a/services/webhook/feishu.go +++ b/services/webhook/feishu.go @@ -166,7 +166,17 @@ func (fc feishuConvertor) Package(p *api.PackagePayload) (FeishuPayload, error) return newFeishuTextPayload(text), nil } +func (fc feishuConvertor) Status(p *api.CommitStatusPayload) (FeishuPayload, error) { + text, _ := getStatusPayloadInfo(p, noneLinkFormatter, true) + + return newFeishuTextPayload(text), nil +} + func newFeishuRequest(_ context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { var pc payloadConvertor[FeishuPayload] = feishuConvertor{} return newJSONRequest(pc, w, t, true) } + +func init() { + RegisterWebhookRequester(webhook_module.FEISHU, newFeishuRequest) +} diff --git a/services/webhook/general.go b/services/webhook/general.go index dde43bb3495c6..c3e2ded2043e5 100644 --- a/services/webhook/general.go +++ b/services/webhook/general.go @@ -9,7 +9,9 @@ import ( "net/url" "strings" + user_model "code.gitea.io/gitea/models/user" webhook_model "code.gitea.io/gitea/models/webhook" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" @@ -307,6 +309,22 @@ func getPackagePayloadInfo(p *api.PackagePayload, linkFormatter linkFormatter, w return text, color } +func getStatusPayloadInfo(p *api.CommitStatusPayload, linkFormatter linkFormatter, withSender bool) (text string, color int) { + refLink := linkFormatter(p.TargetURL, fmt.Sprintf("%s [%s]", p.Context, base.ShortSha(p.SHA))) + + text = fmt.Sprintf("Commit Status changed: %s - %s", refLink, p.Description) + color = greenColor + if withSender { + if user_model.IsGiteaActionsUserName(p.Sender.UserName) { + text += fmt.Sprintf(" by %s", p.Sender.FullName) + } else { + text += fmt.Sprintf(" by %s", linkFormatter(setting.AppURL+url.PathEscape(p.Sender.UserName), p.Sender.UserName)) + } + } + + return text, color +} + // ToHook convert models.Webhook to api.Hook // This function is not part of the convert package to prevent an import cycle func ToHook(repoLink string, w *webhook_model.Webhook) (*api.Hook, error) { diff --git a/services/webhook/general_test.go b/services/webhook/general_test.go index ef1ec7f324c82..ec735d785a524 100644 --- a/services/webhook/general_test.go +++ b/services/webhook/general_test.go @@ -319,8 +319,8 @@ func packageTestPayload() *api.PackagePayload { AvatarURL: "http://localhost:3000/user1/avatar", }, Repository: nil, - Organization: &api.User{ - UserName: "org1", + Organization: &api.Organization{ + Name: "org1", AvatarURL: "http://localhost:3000/org1/avatar", }, Package: &api.Package{ diff --git a/services/webhook/matrix.go b/services/webhook/matrix.go index 5e9f808d8b6cb..f43354726c569 100644 --- a/services/webhook/matrix.go +++ b/services/webhook/matrix.go @@ -15,6 +15,7 @@ import ( "strings" webhook_model "code.gitea.io/gitea/models/webhook" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" @@ -24,6 +25,10 @@ import ( webhook_module "code.gitea.io/gitea/modules/webhook" ) +func init() { + RegisterWebhookRequester(webhook_module.MATRIX, newMatrixRequest) +} + func newMatrixRequest(_ context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { meta := &MatrixMeta{} if err := json.Unmarshal([]byte(w.Meta), meta); err != nil { @@ -240,6 +245,13 @@ func (m matrixConvertor) Package(p *api.PackagePayload) (MatrixPayload, error) { return m.newPayload(text) } +func (m matrixConvertor) Status(p *api.CommitStatusPayload) (MatrixPayload, error) { + refLink := htmlLinkFormatter(p.TargetURL, fmt.Sprintf("%s [%s]", p.Context, base.ShortSha(p.SHA))) + text := fmt.Sprintf("Commit Status changed: %s - %s", refLink, p.Description) + + return m.newPayload(text) +} + var urlRegex = regexp.MustCompile(`]*?href="([^">]*?)">(.*?)`) func getMessageBody(htmlText string) string { diff --git a/services/webhook/msteams.go b/services/webhook/msteams.go index 7ef96ffa2734c..485f695be2042 100644 --- a/services/webhook/msteams.go +++ b/services/webhook/msteams.go @@ -303,6 +303,20 @@ func (m msteamsConvertor) Package(p *api.PackagePayload) (MSTeamsPayload, error) ), nil } +func (m msteamsConvertor) Status(p *api.CommitStatusPayload) (MSTeamsPayload, error) { + title, color := getStatusPayloadInfo(p, noneLinkFormatter, false) + + return createMSTeamsPayload( + p.Repo, + p.Sender, + title, + "", + p.TargetURL, + color, + &MSTeamsFact{"CommitStatus:", p.Context}, + ), nil +} + func createMSTeamsPayload(r *api.Repository, s *api.User, title, text, actionTarget string, color int, fact *MSTeamsFact) MSTeamsPayload { facts := make([]MSTeamsFact, 0, 2) if r != nil { @@ -349,3 +363,7 @@ func newMSTeamsRequest(_ context.Context, w *webhook_model.Webhook, t *webhook_m var pc payloadConvertor[MSTeamsPayload] = msteamsConvertor{} return newJSONRequest(pc, w, t, true) } + +func init() { + RegisterWebhookRequester(webhook_module.MSTEAMS, newMSTeamsRequest) +} diff --git a/services/webhook/notifier.go b/services/webhook/notifier.go index a3d5cb34b19d4..335eabedd5111 100644 --- a/services/webhook/notifier.go +++ b/services/webhook/notifier.go @@ -8,12 +8,14 @@ import ( git_model "code.gitea.io/gitea/models/git" issues_model "code.gitea.io/gitea/models/issues" + "code.gitea.io/gitea/models/organization" packages_model "code.gitea.io/gitea/models/packages" "code.gitea.io/gitea/models/perm" access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/httplib" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" @@ -874,6 +876,11 @@ func (m *webhookNotifier) CreateCommitStatus(ctx context.Context, repo *repo_mod return } + // as a webhook url, target should be an absolute url. But for internal actions target url + // the target url is a url path with no host and port to make it easy to be visited + // from multiple hosts. So we need to convert it to an absolute url here. + target := httplib.MakeAbsoluteURL(ctx, status.TargetURL) + payload := api.CommitStatusPayload{ Context: status.Context, CreatedAt: status.CreatedUnix.AsTime().UTC(), @@ -881,7 +888,7 @@ func (m *webhookNotifier) CreateCommitStatus(ctx context.Context, repo *repo_mod ID: status.ID, SHA: commit.Sha1, State: status.State.String(), - TargetURL: status.TargetURL, + TargetURL: target, Commit: apiCommit, Repo: convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeOwner}), @@ -924,10 +931,16 @@ func notifyPackage(ctx context.Context, sender *user_model.User, pd *packages_mo return } + var org *api.Organization + if pd.Owner.IsOrganization() { + org = convert.ToOrganization(ctx, organization.OrgFromUser(pd.Owner)) + } + if err := PrepareWebhooks(ctx, source, webhook_module.HookEventPackage, &api.PackagePayload{ - Action: action, - Package: apiPackage, - Sender: convert.ToUser(ctx, sender, nil), + Action: action, + Package: apiPackage, + Organization: org, + Sender: convert.ToUser(ctx, sender, nil), }); err != nil { log.Error("PrepareWebhooks: %v", err) } diff --git a/services/webhook/packagist.go b/services/webhook/packagist.go index 4d809ab3a6a5f..6864fc822abde 100644 --- a/services/webhook/packagist.go +++ b/services/webhook/packagist.go @@ -110,6 +110,10 @@ func (pc packagistConvertor) Package(_ *api.PackagePayload) (PackagistPayload, e return PackagistPayload{}, nil } +func (pc packagistConvertor) Status(_ *api.CommitStatusPayload) (PackagistPayload, error) { + return PackagistPayload{}, nil +} + func newPackagistRequest(_ context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { meta := &PackagistMeta{} if err := json.Unmarshal([]byte(w.Meta), meta); err != nil { @@ -120,3 +124,7 @@ func newPackagistRequest(_ context.Context, w *webhook_model.Webhook, t *webhook } return newJSONRequest(pc, w, t, true) } + +func init() { + RegisterWebhookRequester(webhook_module.PACKAGIST, newPackagistRequest) +} diff --git a/services/webhook/payloader.go b/services/webhook/payloader.go index ab280a25b661e..c29ad8ac9206e 100644 --- a/services/webhook/payloader.go +++ b/services/webhook/payloader.go @@ -28,6 +28,7 @@ type payloadConvertor[T any] interface { Release(*api.ReleasePayload) (T, error) Wiki(*api.WikiPayload) (T, error) Package(*api.PackagePayload) (T, error) + Status(*api.CommitStatusPayload) (T, error) } func convertUnmarshalledJSON[T, P any](convert func(P) (T, error), data []byte) (t T, err error) { @@ -77,6 +78,8 @@ func newPayload[T any](rc payloadConvertor[T], data []byte, event webhook_module return convertUnmarshalledJSON(rc.Wiki, data) case webhook_module.HookEventPackage: return convertUnmarshalledJSON(rc.Package, data) + case webhook_module.HookEventStatus: + return convertUnmarshalledJSON(rc.Status, data) } return t, fmt.Errorf("newPayload unsupported event: %s", event) } diff --git a/services/webhook/slack.go b/services/webhook/slack.go index c905e7a89f48f..a5edb830f3d8d 100644 --- a/services/webhook/slack.go +++ b/services/webhook/slack.go @@ -167,6 +167,12 @@ func (s slackConvertor) Package(p *api.PackagePayload) (SlackPayload, error) { return s.createPayload(text, nil), nil } +func (s slackConvertor) Status(p *api.CommitStatusPayload) (SlackPayload, error) { + text, _ := getStatusPayloadInfo(p, SlackLinkFormatter, true) + + return s.createPayload(text, nil), nil +} + // Push implements payloadConvertor Push method func (s slackConvertor) Push(p *api.PushPayload) (SlackPayload, error) { // n new commits @@ -295,6 +301,10 @@ func newSlackRequest(_ context.Context, w *webhook_model.Webhook, t *webhook_mod return newJSONRequest(pc, w, t, true) } +func init() { + RegisterWebhookRequester(webhook_module.SLACK, newSlackRequest) +} + var slackChannel = regexp.MustCompile(`^#?[a-z0-9_-]{1,80}$`) // IsValidSlackChannel validates a channel name conforms to what slack expects: diff --git a/services/webhook/telegram.go b/services/webhook/telegram.go index e54d6f2947d21..485e2d990bae7 100644 --- a/services/webhook/telegram.go +++ b/services/webhook/telegram.go @@ -174,6 +174,12 @@ func (t telegramConvertor) Package(p *api.PackagePayload) (TelegramPayload, erro return createTelegramPayloadHTML(text), nil } +func (t telegramConvertor) Status(p *api.CommitStatusPayload) (TelegramPayload, error) { + text, _ := getStatusPayloadInfo(p, htmlLinkFormatter, true) + + return createTelegramPayloadHTML(text), nil +} + func createTelegramPayloadHTML(msgHTML string) TelegramPayload { // https://core.telegram.org/bots/api#formatting-options return TelegramPayload{ @@ -187,3 +193,7 @@ func newTelegramRequest(_ context.Context, w *webhook_model.Webhook, t *webhook_ var pc payloadConvertor[TelegramPayload] = telegramConvertor{} return newJSONRequest(pc, w, t, true) } + +func init() { + RegisterWebhookRequester(webhook_module.TELEGRAM, newTelegramRequest) +} diff --git a/services/webhook/webhook.go b/services/webhook/webhook.go index e0e8fa2fc19b5..b5e0864f15dee 100644 --- a/services/webhook/webhook.go +++ b/services/webhook/webhook.go @@ -27,16 +27,12 @@ import ( "github.com/gobwas/glob" ) -var webhookRequesters = map[webhook_module.HookType]func(context.Context, *webhook_model.Webhook, *webhook_model.HookTask) (req *http.Request, body []byte, err error){ - webhook_module.SLACK: newSlackRequest, - webhook_module.DISCORD: newDiscordRequest, - webhook_module.DINGTALK: newDingtalkRequest, - webhook_module.TELEGRAM: newTelegramRequest, - webhook_module.MSTEAMS: newMSTeamsRequest, - webhook_module.FEISHU: newFeishuRequest, - webhook_module.MATRIX: newMatrixRequest, - webhook_module.WECHATWORK: newWechatworkRequest, - webhook_module.PACKAGIST: newPackagistRequest, +type Requester func(context.Context, *webhook_model.Webhook, *webhook_model.HookTask) (req *http.Request, body []byte, err error) + +var webhookRequesters = map[webhook_module.HookType]Requester{} + +func RegisterWebhookRequester(hookType webhook_module.HookType, requester Requester) { + webhookRequesters[hookType] = requester } // IsValidHookTaskType returns true if a webhook registered diff --git a/services/webhook/webhook_test.go b/services/webhook/webhook_test.go index 6bac02712bb6b..63cbce177121e 100644 --- a/services/webhook/webhook_test.go +++ b/services/webhook/webhook_test.go @@ -9,15 +9,11 @@ import ( "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" - user_model "code.gitea.io/gitea/models/user" webhook_model "code.gitea.io/gitea/models/webhook" - "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" webhook_module "code.gitea.io/gitea/modules/webhook" - "code.gitea.io/gitea/services/convert" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) func TestWebhook_GetSlackHook(t *testing.T) { @@ -81,11 +77,3 @@ func TestPrepareWebhooksBranchFilterNoMatch(t *testing.T) { unittest.AssertNotExistsBean(t, hookTask) } } - -func TestWebhookUserMail(t *testing.T) { - require.NoError(t, unittest.PrepareTestDatabase()) - setting.Service.NoReplyAddress = "no-reply.com" - user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) - assert.Equal(t, user.GetPlaceholderEmail(), convert.ToUser(db.DefaultContext, user, nil).Email) - assert.Equal(t, user.Email, convert.ToUser(db.DefaultContext, user, user).Email) -} diff --git a/services/webhook/wechatwork.go b/services/webhook/wechatwork.go index 1d8c1d7dacd83..1c834b4020aee 100644 --- a/services/webhook/wechatwork.go +++ b/services/webhook/wechatwork.go @@ -175,7 +175,17 @@ func (wc wechatworkConvertor) Package(p *api.PackagePayload) (WechatworkPayload, return newWechatworkMarkdownPayload(text), nil } +func (wc wechatworkConvertor) Status(p *api.CommitStatusPayload) (WechatworkPayload, error) { + text, _ := getStatusPayloadInfo(p, noneLinkFormatter, true) + + return newWechatworkMarkdownPayload(text), nil +} + func newWechatworkRequest(_ context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { var pc payloadConvertor[WechatworkPayload] = wechatworkConvertor{} return newJSONRequest(pc, w, t, true) } + +func init() { + RegisterWebhookRequester(webhook_module.WECHATWORK, newWechatworkRequest) +} diff --git a/templates/admin/config.tmpl b/templates/admin/config.tmpl index 29a5e1b473d3b..88dadeb3ee5db 100644 --- a/templates/admin/config.tmpl +++ b/templates/admin/config.tmpl @@ -148,7 +148,7 @@
{{ctx.Locale.Tr "admin.config.enable_openid_signin"}}
{{svg (Iif .Service.EnableOpenIDSignIn "octicon-check" "octicon-x")}}
{{ctx.Locale.Tr "admin.config.require_sign_in_view"}}
-
{{svg (Iif .Service.RequireSignInView "octicon-check" "octicon-x")}}
+
{{svg (Iif .Service.RequireSignInViewStrict "octicon-check" "octicon-x")}}
{{ctx.Locale.Tr "admin.config.mail_notify"}}
{{svg (Iif .Service.EnableNotifyMail "octicon-check" "octicon-x")}}
{{ctx.Locale.Tr "admin.config.enable_captcha"}}
diff --git a/templates/base/head_navbar.tmpl b/templates/base/head_navbar.tmpl index ed7a8d6f2433e..86d73a235dbed 100644 --- a/templates/base/head_navbar.tmpl +++ b/templates/base/head_navbar.tmpl @@ -13,7 +13,7 @@
foo
diff --git a/templates/package/content/maven.tmpl b/templates/package/content/maven.tmpl index f56595a8308ec..e98fc53692479 100644 --- a/templates/package/content/maven.tmpl +++ b/templates/package/content/maven.tmpl @@ -11,7 +11,7 @@
<repositories>
 	<repository>
 		<id>gitea</id>
-			<url></url>
+		<url></url>
 	</repository>
 </repositories>
 
diff --git a/templates/projects/list.tmpl b/templates/projects/list.tmpl
index f5a48f72414c1..31a31527a92c4 100644
--- a/templates/projects/list.tmpl
+++ b/templates/projects/list.tmpl
@@ -52,11 +52,11 @@
 				
{{svg "octicon-issue-opened" 14}} - {{ctx.Locale.PrettyNumber (.NumOpenIssues ctx)}} {{ctx.Locale.Tr "repo.issues.open_title"}} + {{ctx.Locale.PrettyNumber .NumOpenIssues}} {{ctx.Locale.Tr "repo.issues.open_title"}}
{{svg "octicon-check" 14}} - {{ctx.Locale.PrettyNumber (.NumClosedIssues ctx)}} {{ctx.Locale.Tr "repo.issues.closed_title"}} + {{ctx.Locale.PrettyNumber .NumClosedIssues}} {{ctx.Locale.Tr "repo.issues.closed_title"}}
{{if and $.CanWriteProjects (not $.Repository.IsArchived)}} diff --git a/templates/projects/view.tmpl b/templates/projects/view.tmpl index 2550cae69c2bc..217ffe635589b 100644 --- a/templates/projects/view.tmpl +++ b/templates/projects/view.tmpl @@ -1,10 +1,10 @@ {{$canWriteProject := and .CanWriteProjects (or (not .Repository) (not .Repository.IsArchived))}}
-
-

{{.Project.Title}}

-
-
-
+
+
{{range .Columns}} -
+ -{{if $canWriteProject}} -

{{svg "octicon-git-commit" 16 "tw-mr-1"}}{{ShortSha .DefaultBranchBranch.DBBranch.CommitID}} · {{ctx.RenderUtils.RenderCommitMessage .DefaultBranchBranch.DBBranch.CommitMessage (.Repository.ComposeMetas ctx)}} · {{ctx.Locale.Tr "org.repo_updated"}} {{DateUtils.TimeSince .DefaultBranchBranch.DBBranch.CommitTime}}{{if .DefaultBranchBranch.DBBranch.Pusher}}  {{template "shared/user/avatarlink" dict "user" .DefaultBranchBranch.DBBranch.Pusher}}{{template "shared/user/namelink" .DefaultBranchBranch.DBBranch.Pusher}}{{end}}

- {{/* FIXME: here and below, the tw-overflow-visible is not quite right but it is still needed the moment: to show the important buttons when the width is narrow */}} -
- {{/* FIXME: here and above, the tw-overflow-visible is not quite right */}} - @@ -7,48 +6,42 @@ - + {{else}} {{$inlineDiff := $.section.GetComputedInlineDiffFor $line ctx.Locale}} - + - + {{end}} {{end}} @@ -60,17 +53,17 @@ @@ -20,24 +20,26 @@ {{$inlineDiff := $section.GetComputedInlineDiffFor $line ctx.Locale}} - + {{else if and (eq .GetType 3) $hasmatch}}{{/* DEL */}} {{$match := index $section.Lines $line.Match}} {{- $leftDiff := ""}}{{if $line.LeftIdx}}{{$leftDiff = $section.GetComputedInlineDiffFor $line ctx.Locale}}{{end}} @@ -45,65 +47,65 @@ - + - + {{else}} {{$inlineDiff := $section.GetComputedInlineDiffFor $line ctx.Locale}} - + - + {{end}} {{if and (eq .GetType 3) $hasmatch}} diff --git a/templates/repo/diff/section_unified.tmpl b/templates/repo/diff/section_unified.tmpl index cb612bc27c4e8..708b333291641 100644 --- a/templates/repo/diff/section_unified.tmpl +++ b/templates/repo/diff/section_unified.tmpl @@ -1,7 +1,5 @@ {{$file := .file}} -{{$repoLink := or ctx.RootData.CommitRepoLink ctx.RootData.RepoLink}} -{{$afterCommitID := or $.root.AfterCommitID "no-after-commit-id"}}{{/* this tmpl is also used by the PR Conversation page, so the "AfterCommitID" may not exist */}} -{{$blobExcerptLink := print $repoLink (Iif $.root.PageIsWiki "/wiki" "") "/blob_excerpt/" (PathEscape $afterCommitID) "?"}} +{{$blobExcerptRepoLink := or ctx.RootData.CommitRepoLink ctx.RootData.RepoLink}} @@ -18,17 +16,17 @@ {{if eq .GetType 4}} - + {{else}} - + {{end}} {{if $line.Comments}} diff --git a/templates/repo/empty.tmpl b/templates/repo/empty.tmpl index 7170fe360203d..ae3f95045bc58 100644 --- a/templates/repo/empty.tmpl +++ b/templates/repo/empty.tmpl @@ -14,14 +14,13 @@ {{end}} {{end}} + {{if .Repository.IsBroken}} -
- {{ctx.Locale.Tr "repo.broken_message"}} -
+
{{ctx.Locale.Tr "repo.broken_message"}}
+ {{else if .RepoHasContentsWithoutBranch}} +
{{ctx.Locale.Tr "repo.no_branch"}}
{{else if .CanWriteCode}} -

- {{ctx.Locale.Tr "repo.quick_guide"}} -

+

{{ctx.Locale.Tr "repo.quick_guide"}}

{{ctx.Locale.Tr "repo.clone_this_repo"}} {{ctx.Locale.Tr "repo.clone_helper" "http://git-scm.com/book/en/v2/Git-Basics-Getting-a-Git-Repository"}}

@@ -66,12 +65,10 @@ git push -u origin {{.Repository.DefaultBranch}}
{{end}} - {{else}} -
- {{ctx.Locale.Tr "repo.empty_message"}} -
- {{end}} - + + {{else}} +
{{ctx.Locale.Tr "repo.empty_message"}}
+ {{end}} diff --git a/templates/repo/graph/commits.tmpl b/templates/repo/graph/commits.tmpl index 6af0ba1f0fe9d..f1d0e62330274 100644 --- a/templates/repo/graph/commits.tmpl +++ b/templates/repo/graph/commits.tmpl @@ -5,13 +5,33 @@ {{if $commit.OnlyRelation}} {{else}} - {{template "repo/commit_sign_badge" dict "Commit" $commit.Commit "CommitBaseLink" (print $.RepoLink "/commit") "CommitSignVerification" $commit.Verification}} - - + + {{$class := "ui sha label"}} + {{if $commit.Commit.Signature}} + {{$class = (print $class " isSigned")}} + {{if $commit.Verification.Verified}} + {{if eq $commit.Verification.TrustStatus "trusted"}} + {{$class = (print $class " isVerified")}} + {{else if eq $commit.Verification.TrustStatus "untrusted"}} + {{$class = (print $class " isVerifiedUntrusted")}} + {{else}} + {{$class = (print $class " isVerifiedUnmatched")}} + {{end}} + {{else if $commit.Verification.Warning}} + {{$class = (print $class " isWarning")}} + {{end}} + {{end}} + + {{ShortSha $commit.Commit.ID.String}} + {{- if $commit.Commit.Signature -}} + {{template "repo/shabox_badge" dict "root" $ "verification" $commit.Verification}} + {{- end -}} + + + {{ctx.RenderUtils.RenderCommitMessage $commit.Subject ($.Repository.ComposeMetas ctx)}} - - + {{range $commit.Refs}} {{$refGroup := .RefGroup}} {{if eq $refGroup "pull"}} @@ -36,8 +56,7 @@ {{end}} {{end}} - - + {{$userName := $commit.Commit.Author.Name}} {{if $commit.User}} {{if and $commit.User.FullName DefaultShowFullName}} @@ -50,8 +69,7 @@ {{$userName}} {{end}} - - {{DateUtils.FullTime $commit.Date}} + {{DateUtils.FullTime $commit.Date}} {{end}} {{end}} diff --git a/templates/repo/header.tmpl b/templates/repo/header.tmpl index c3ae697f31fbf..e187ef1a87dd7 100644 --- a/templates/repo/header.tmpl +++ b/templates/repo/header.tmpl @@ -162,7 +162,7 @@ {{end}} - {{if and .EnableActions (.Permission.CanRead ctx.Consts.RepoUnitTypeActions)}} + {{if and .EnableActions (.Permission.CanRead ctx.Consts.RepoUnitTypeActions) (not .IsEmptyRepo)}} {{svg "octicon-play"}} {{ctx.Locale.Tr "actions.actions"}} {{if .Repository.NumOpenActionRuns}} diff --git a/templates/repo/home.tmpl b/templates/repo/home.tmpl index d73b7470bc5cd..f80c9c1f4b37d 100644 --- a/templates/repo/home.tmpl +++ b/templates/repo/home.tmpl @@ -24,13 +24,19 @@ {{template "repo/sub_menu" .}}
- {{$branchDropdownCurrentRefType := "branch"}} - {{$branchDropdownCurrentRefShortName := .BranchName}} - {{if .IsViewTag}} - {{$branchDropdownCurrentRefType = "tag"}} - {{$branchDropdownCurrentRefShortName = .TagName}} - {{end}} - {{template "repo/branch_dropdown" dict + {{- /* for repo home (default branch) and /owner/repo/src/branch/the-name */ -}} + {{- $branchDropdownCurrentRefType := "branch" -}} + {{- $branchDropdownCurrentRefShortName := .BranchName -}} + {{- if .IsViewTag -}} + {{- /* for /owner/repo/src/tag/the-name */ -}} + {{- $branchDropdownCurrentRefType = "tag" -}} + {{- $branchDropdownCurrentRefShortName = .TagName -}} + {{- else if .IsViewCommit -}} + {{- /* for /owner/repo/src/commit/000000 */ -}} + {{- $branchDropdownCurrentRefType = "commit" -}} + {{- $branchDropdownCurrentRefShortName = ShortSha .CommitID -}} + {{- end -}} + {{- template "repo/branch_dropdown" dict "Repository" .Repository "ShowTabBranches" true "ShowTabTags" true @@ -40,7 +46,7 @@ "RefLinkTemplate" "{RepoLink}/src/{RefType}/{RefShortName}/{TreePath}" "AllowCreateNewRef" .CanCreateBranch "ShowViewAllRefsEntry" true - }} + -}} {{if and .CanCompareOrPull .IsViewBranch (not .Repository.IsArchived)}} {{$cmpBranch := ""}} {{if ne .Repository.ID .BaseRepo.ID}} diff --git a/templates/repo/home_sidebar_bottom.tmpl b/templates/repo/home_sidebar_bottom.tmpl index f780dc122d5af..f66faf6d10f76 100644 --- a/templates/repo/home_sidebar_bottom.tmpl +++ b/templates/repo/home_sidebar_bottom.tmpl @@ -4,7 +4,7 @@
- + {{ctx.Locale.Tr "repo.releases"}} {{.NumReleases}} diff --git a/templates/repo/home_sidebar_top.tmpl b/templates/repo/home_sidebar_top.tmpl index 607dc62e2e717..8d3d7c6f86974 100644 --- a/templates/repo/home_sidebar_top.tmpl +++ b/templates/repo/home_sidebar_top.tmpl @@ -1,5 +1,5 @@
-
+
{{template "shared/search/button"}}
@@ -62,6 +62,11 @@ {{svg "octicon-cross-reference"}} {{ctx.Locale.Tr "repo.cite_this_repo"}} {{end}} + + {{$fileSizeFormatted := FileSize .Repository.Size}}{{/* the formatted string is always "{val} {unit}" */}} + {{$fileSizeFields := StringUtils.Split $fileSizeFormatted " "}} + {{svg "octicon-database"}} {{ctx.Locale.PrettyNumber (index $fileSizeFields 0)}} {{index $fileSizeFields 1}} +
diff --git a/templates/repo/issue/card.tmpl b/templates/repo/issue/card.tmpl index 2e19e86d7af30..a3f361004417e 100644 --- a/templates/repo/issue/card.tmpl +++ b/templates/repo/issue/card.tmpl @@ -45,7 +45,7 @@ {{if $.Page.LinkedPRs}} {{range index $.Page.LinkedPRs .ID}}
- + {{svg "octicon-git-merge" 16 "tw-mr-1 tw-align-middle"}} {{.Title}} #{{.Index}} diff --git a/templates/repo/issue/filter_item_label.tmpl b/templates/repo/issue/filter_item_label.tmpl index 0883d9380416b..88e2e43120552 100644 --- a/templates/repo/issue/filter_item_label.tmpl +++ b/templates/repo/issue/filter_item_label.tmpl @@ -1,4 +1,4 @@ -{{/* Template Attributes: +{{/* * "labels" from query string (needed by JS) * QueryLink * Labels diff --git a/templates/repo/issue/labels/label_edit_modal.tmpl b/templates/repo/issue/labels/label_edit_modal.tmpl index 527b7ff900caf..f04d499737aba 100644 --- a/templates/repo/issue/labels/label_edit_modal.tmpl +++ b/templates/repo/issue/labels/label_edit_modal.tmpl @@ -41,7 +41,7 @@
-
+
diff --git a/templates/repo/issue/list.tmpl b/templates/repo/issue/list.tmpl index 01b610b39db0b..ee783b420ae9b 100644 --- a/templates/repo/issue/list.tmpl +++ b/templates/repo/issue/list.tmpl @@ -14,9 +14,10 @@
{{end}} -
- {{template "repo/issue/navbar" .}} +
{{template "repo/issue/search" .}} + {{ctx.Locale.Tr "repo.labels"}} + {{ctx.Locale.Tr "repo.milestones"}} {{if not .Repository.IsArchived}} {{if .PageIsIssueList}} {{ctx.Locale.Tr "repo.issues.new"}} diff --git a/templates/repo/issue/openclose.tmpl b/templates/repo/issue/openclose.tmpl index b9dd04a7db226..00a31b5fad91a 100644 --- a/templates/repo/issue/openclose.tmpl +++ b/templates/repo/issue/openclose.tmpl @@ -17,7 +17,7 @@ {{ctx.Locale.PrettyNumber .OpenCount}} {{ctx.Locale.Tr "repo.issues.open_title"}} - {{svg "octicon-check"}} + {{svg "octicon-issue-closed"}} {{ctx.Locale.PrettyNumber .ClosedCount}} {{ctx.Locale.Tr "repo.issues.closed_title"}}
diff --git a/templates/repo/issue/sidebar/assignee_list.tmpl b/templates/repo/issue/sidebar/assignee_list.tmpl index d8ccd73387de7..f124c3f1ced8d 100644 --- a/templates/repo/issue/sidebar/assignee_list.tmpl +++ b/templates/repo/issue/sidebar/assignee_list.tmpl @@ -15,10 +15,11 @@ {{svg "octicon-search" 16}}
-
{{ctx.Locale.Tr "repo.issues.new.clear_assignees"}}
-
-
+
+ {{if and $.IsWriter (not $.Repository.IsArchived) (not .IsDeleted)}} + {{if and $.IsWriter (not $.Repository.IsArchived) (not .DBBranch.IsDeleted)}}
diff --git a/templates/repo/code/upstream_diverging_info.tmpl b/templates/repo/code/upstream_diverging_info.tmpl index 51402598f95cb..b3d35c05e5149 100644 --- a/templates/repo/code/upstream_diverging_info.tmpl +++ b/templates/repo/code/upstream_diverging_info.tmpl @@ -1,16 +1,21 @@ -{{if and .UpstreamDivergingInfo (or .UpstreamDivergingInfo.BaseIsNewer .UpstreamDivergingInfo.CommitsBehind)}} +{{if and .UpstreamDivergingInfo .UpstreamDivergingInfo.BaseBranchHasNewCommits}}
- {{$upstreamLink := printf "%s/src/branch/%s" .Repository.BaseRepo.Link (.BranchName|PathEscapeSegments)}} - {{$upstreamHtml := HTMLFormat `%s:%s` $upstreamLink .Repository.BaseRepo.FullName .BranchName}} - {{if .UpstreamDivergingInfo.CommitsBehind}} - {{ctx.Locale.TrN .UpstreamDivergingInfo.CommitsBehind "repo.pulls.upstream_diverging_prompt_behind_1" "repo.pulls.upstream_diverging_prompt_behind_n" .UpstreamDivergingInfo.CommitsBehind $upstreamHtml}} + {{$upstreamLink := printf "%s/src/branch/%s" .Repository.BaseRepo.Link (.UpstreamDivergingInfo.BaseBranchName|PathEscapeSegments)}} + {{$upstreamRepoBranchDisplay := HTMLFormat "%s:%s" .Repository.BaseRepo.FullName .UpstreamDivergingInfo.BaseBranchName}} + {{$thisRepoBranchDisplay := HTMLFormat "%s:%s" .Repository.FullName .BranchName}} + {{$upstreamHtml := HTMLFormat `%s` $upstreamLink $upstreamRepoBranchDisplay}} + {{if .UpstreamDivergingInfo.HeadBranchCommitsBehind}} + {{ctx.Locale.TrN .UpstreamDivergingInfo.HeadBranchCommitsBehind "repo.pulls.upstream_diverging_prompt_behind_1" "repo.pulls.upstream_diverging_prompt_behind_n" .UpstreamDivergingInfo.HeadBranchCommitsBehind $upstreamHtml}} {{else}} {{ctx.Locale.Tr "repo.pulls.upstream_diverging_prompt_base_newer" $upstreamHtml}} {{end}}
{{if .CanWriteCode}} - {{end}} diff --git a/templates/repo/commit_page.tmpl b/templates/repo/commit_page.tmpl index 3d95e8a715df6..38e3df8e709ef 100644 --- a/templates/repo/commit_page.tmpl +++ b/templates/repo/commit_page.tmpl @@ -1,9 +1,23 @@ {{template "base/head" .}} -{{$commitLinkBase := print $.RepoLink (Iif $.PageIsWiki "/wiki" "") "/commit"}}
{{template "repo/header" .}}
-
+ {{$class := ""}} + {{if .Commit.Signature}} + {{$class = (print $class " isSigned")}} + {{if .Verification.Verified}} + {{if eq .Verification.TrustStatus "trusted"}} + {{$class = (print $class " isVerified")}} + {{else if eq .Verification.TrustStatus "untrusted"}} + {{$class = (print $class " isVerifiedUntrusted")}} + {{else}} + {{$class = (print $class " isVerifiedUnmatched")}} + {{end}} + {{else if .Verification.Warning}} + {{$class = (print $class " isWarning")}} + {{end}} + {{end}} +

{{ctx.RenderUtils.RenderCommitMessage .Commit.Message ($.Repository.ComposeMetas ctx)}}{{template "repo/commit_statuses" dict "Status" .CommitStatus "Statuses" .CommitStatuses}}

{{if not $.PageIsWiki}} @@ -16,7 +30,7 @@ {{ctx.Locale.Tr "repo.commit.operations"}} {{svg "octicon-triangle-down" 14 "dropdown icon"}}
- {{$commitBaseLink := ""}} + {{$class := "ui sha label"}} + {{if .Signature}} + {{$class = (print $class " isSigned")}} + {{if .Verification.Verified}} + {{if eq .Verification.TrustStatus "trusted"}} + {{$class = (print $class " isVerified")}} + {{else if eq .Verification.TrustStatus "untrusted"}} + {{$class = (print $class " isVerifiedUntrusted")}} + {{else}} + {{$class = (print $class " isVerifiedUnmatched")}} + {{end}} + {{else if .Verification.Warning}} + {{$class = (print $class " isWarning")}} + {{end}} + {{end}} + {{$commitShaLink := ""}} {{if $.PageIsWiki}} - {{$commitBaseLink = printf "%s/wiki/commit" $commitRepoLink}} + {{$commitShaLink = (printf "%s/wiki/commit/%s" $commitRepoLink (PathEscape .ID.String))}} {{else if $.PageIsPullCommits}} - {{$commitBaseLink = printf "%s/pulls/%d/commits" $commitRepoLink $.Issue.Index}} + {{$commitShaLink = (printf "%s/pulls/%d/commits/%s" $commitRepoLink $.Issue.Index (PathEscape .ID.String))}} {{else if $.Reponame}} - {{$commitBaseLink = printf "%s/commit" $commitRepoLink}} + {{$commitShaLink = (printf "%s/commit/%s" $commitRepoLink (PathEscape .ID.String))}} {{end}} - {{template "repo/commit_sign_badge" dict "Commit" . "CommitBaseLink" $commitBaseLink "CommitSignVerification" .Verification}} + + {{ShortSha .ID.String}} + {{if .Signature}}{{template "repo/shabox_badge" dict "root" $ "verification" .Verification}}{{end}} + diff --git a/templates/repo/commits_list_small.tmpl b/templates/repo/commits_list_small.tmpl index 2acf7c58b81a7..0657eaba1d3bd 100644 --- a/templates/repo/commits_list_small.tmpl +++ b/templates/repo/commits_list_small.tmpl @@ -3,7 +3,7 @@ {{range .comment.Commits}} {{$tag := printf "%s-%d" $.comment.HashTag $index}} {{$index = Eval $index "+" 1}} -
{{/*singular-commit*/}} +
{{svg "octicon-git-commit"}} {{if .User}} {{ctx.AvatarUtils.Avatar .User 20}} @@ -11,8 +11,7 @@ {{ctx.AvatarUtils.AvatarByEmail .Author.Email .Author.Name 20}} {{end}} - {{$commitBaseLink := printf "%s/commit" $.comment.Issue.PullRequest.BaseRepo.Link}} - {{$commitLink:= printf "%s/%s" $commitBaseLink (PathEscape .ID.String)}} + {{$commitLink:= printf "%s/commit/%s" $.comment.Issue.PullRequest.BaseRepo.Link (PathEscape .ID.String)}} {{- ctx.RenderUtils.RenderCommitMessageLinkSubject .Message $commitLink ($.comment.Issue.PullRequest.BaseRepo.ComposeMetas ctx) -}} @@ -22,9 +21,29 @@ {{end}} - + {{template "repo/commit_statuses" dict "Status" .Status "Statuses" .Statuses}} - {{template "repo/commit_sign_badge" dict "Commit" . "CommitBaseLink" $commitBaseLink "CommitSignVerification" .Verification}} + {{$class := "ui sha label"}} + {{if .Signature}} + {{$class = (print $class " isSigned")}} + {{if .Verification.Verified}} + {{if eq .Verification.TrustStatus "trusted"}} + {{$class = (print $class " isVerified")}} + {{else if eq .Verification.TrustStatus "untrusted"}} + {{$class = (print $class " isVerifiedUntrusted")}} + {{else}} + {{$class = (print $class " isVerifiedUnmatched")}} + {{end}} + {{else if .Verification.Warning}} + {{$class = (print $class " isWarning")}} + {{end}} + {{end}} + + {{ShortSha .ID.String}} + {{if .Signature}} + {{template "repo/shabox_badge" dict "root" $.root "verification" .Verification}} + {{end}} +
{{if IsMultilineCommitMessage .Message}} diff --git a/templates/repo/create.tmpl b/templates/repo/create.tmpl index 2e1de244ea19c..07d581b2e801a 100644 --- a/templates/repo/create.tmpl +++ b/templates/repo/create.tmpl @@ -10,12 +10,7 @@
{{template "base/alert" .}} {{template "repo/create_helper" .}} - - {{if not .CanCreateRepo}} -
-

{{ctx.Locale.TrN .MaxCreationLimit "repo.form.reach_limit_of_creation_1" "repo.form.reach_limit_of_creation_n" .MaxCreationLimit}}

-
- {{end}} +
{{if or (eq $expandDirection 3) (eq $expandDirection 5)}} - {{end}} {{if or (eq $expandDirection 3) (eq $expandDirection 4)}} - {{end}} {{if eq $expandDirection 2}} - {{end}}
- {{- $inlineDiff := $.section.GetComputedInlineDiffFor $line ctx.Locale -}} - {{- template "repo/diff/section_code" dict "diff" $inlineDiff -}} - {{$inlineDiff := $.section.GetComputedInlineDiffFor $line ctx.Locale}}{{/* + */}}{{template "repo/diff/section_code" dict "diff" $inlineDiff}} {{if and $line.LeftIdx $inlineDiff.EscapeStatus.Escaped}}{{end}} {{if $line.LeftIdx}}{{end}} - {{- if $line.LeftIdx -}} - {{- template "repo/diff/section_code" dict "diff" $inlineDiff -}} - {{- else -}} - - {{- end -}} - {{/* + */}}{{if $line.LeftIdx}}{{template "repo/diff/section_code" dict "diff" $inlineDiff}}{{else}}{{/* + */}}{{/* + */}}{{end}}{{/* + */}} {{if and $line.RightIdx $inlineDiff.EscapeStatus.Escaped}}{{end}} {{if $line.RightIdx}}{{end}} - {{- if $line.RightIdx -}} - {{- template "repo/diff/section_code" dict "diff" $inlineDiff -}} - {{- else -}} - - {{- end -}} - {{/* + */}}{{if $line.RightIdx}}{{template "repo/diff/section_code" dict "diff" $inlineDiff}}{{else}}{{/* + */}}{{/* + */}}{{end}}{{/* + */}}
{{if or (eq $expandDirection 3) (eq $expandDirection 5)}} - {{end}} {{if or (eq $expandDirection 3) (eq $expandDirection 4)}} - {{end}} {{if eq $expandDirection 2}} - {{end}} diff --git a/templates/repo/diff/box.tmpl b/templates/repo/diff/box.tmpl index 9733e5f980680..53ea4fd2e3918 100644 --- a/templates/repo/diff/box.tmpl +++ b/templates/repo/diff/box.tmpl @@ -36,7 +36,10 @@ {{template "repo/diff/options_dropdown" .}} {{if .PageIsPullFiles}}
- {{/* the following will be replaced by vue component, but this avoids any loading artifacts till the vue component is initialized */}} + {{/* + the following will be replaced by vue component + but this avoids any loading artifacts till the vue component is initialized + */}} @@ -235,7 +238,7 @@ {{if and (not $.Repository.IsArchived) (not .DiffNotAvailable)}} {{end}} {{if (not .DiffNotAvailable)}} diff --git a/templates/repo/diff/escape_title.tmpl b/templates/repo/diff/escape_title.tmpl index 9787ae1d42dfc..e70f4021c7d7c 100644 --- a/templates/repo/diff/escape_title.tmpl +++ b/templates/repo/diff/escape_title.tmpl @@ -1,2 +1,2 @@ -{{if .diff.EscapeStatus.HasInvisible}}{{ctx.Locale.Tr "repo.invisible_runes_line"}} {{end -}} -{{- if .diff.EscapeStatus.HasAmbiguous}}{{ctx.Locale.Tr "repo.ambiguous_runes_line"}}{{end}} +{{if .diff.EscapeStatus.HasInvisible}}{{ctx.Locale.Tr "repo.invisible_runes_line"}} {{end}}{{/* +*/}}{{if .diff.EscapeStatus.HasAmbiguous}}{{ctx.Locale.Tr "repo.ambiguous_runes_line"}}{{end}} diff --git a/templates/repo/diff/section_split.tmpl b/templates/repo/diff/section_split.tmpl index 9953db5eb234c..37b42bcb376ab 100644 --- a/templates/repo/diff/section_split.tmpl +++ b/templates/repo/diff/section_split.tmpl @@ -1,5 +1,5 @@ {{$file := .file}} -{{$blobExcerptLink := print (or ctx.RootData.CommitRepoLink ctx.RootData.RepoLink) (Iif $.root.PageIsWiki "/wiki" "") "/blob_excerpt/" (PathEscape $.root.AfterCommitID) "?"}} +{{$blobExcerptRepoLink := or ctx.RootData.CommitRepoLink ctx.RootData.RepoLink}}
{{if or (eq $expandDirection 3) (eq $expandDirection 5)}} - {{end}} {{if or (eq $expandDirection 3) (eq $expandDirection 4)}} - {{end}} {{if eq $expandDirection 2}} - {{end}}
{{if $inlineDiff.EscapeStatus.Escaped}}{{end}}{{template "repo/diff/section_code" dict "diff" $inlineDiff}}{{/* + */}}{{template "repo/diff/section_code" dict "diff" $inlineDiff}}{{/* + */}} {{if $line.LeftIdx}}{{if $leftDiff.EscapeStatus.Escaped}}{{end}}{{end}} - {{- if and $.root.SignedUserID $.root.PageIsPullFiles -}} - - {{- end -}} - {{- if $line.LeftIdx -}} - {{- template "repo/diff/section_code" dict "diff" $leftDiff -}} - {{- else -}} - - {{- end -}} - {{/* + */}}{{if and $.root.SignedUserID $.root.PageIsPullFiles}}{{/* + */}}{{/* + */}}{{end}}{{/* + */}}{{if $line.LeftIdx}}{{/* + */}}{{template "repo/diff/section_code" dict "diff" $leftDiff}}{{/* + */}}{{else}}{{/* + */}}{{/* + */}}{{end}}{{/* + */}} {{if $match.RightIdx}}{{if $rightDiff.EscapeStatus.Escaped}}{{end}}{{end}} {{if $match.RightIdx}}{{end}} - {{- if and $.root.SignedUserID $.root.PageIsPullFiles -}} - - {{- end -}} - {{- if $match.RightIdx -}} - {{- template "repo/diff/section_code" dict "diff" $rightDiff -}} - {{- else -}} - - {{- end -}} - {{/* + */}}{{if and $.root.SignedUserID $.root.PageIsPullFiles}}{{/* + */}}{{/* + */}}{{end}}{{/* + */}}{{if $match.RightIdx}}{{/* + */}}{{template "repo/diff/section_code" dict "diff" $rightDiff}}{{/* + */}}{{else}}{{/* + */}}{{/* + */}}{{end}}{{/* + */}} {{if $line.LeftIdx}}{{if $inlineDiff.EscapeStatus.Escaped}}{{end}}{{end}} {{if $line.LeftIdx}}{{end}} - {{- if and $.root.SignedUserID $.root.PageIsPullFiles (not (eq .GetType 2)) -}} - - {{- end -}} - {{- if $line.LeftIdx -}} - {{- template "repo/diff/section_code" dict "diff" $inlineDiff -}} - {{- else -}} - - {{- end -}} - {{/* + */}}{{if and $.root.SignedUserID $.root.PageIsPullFiles (not (eq .GetType 2))}}{{/* + */}}{{/* + */}}{{end}}{{/* + */}}{{if $line.LeftIdx}}{{/* + */}}{{template "repo/diff/section_code" dict "diff" $inlineDiff}}{{/* + */}}{{else}}{{/* + */}}{{/* + */}}{{end}}{{/* + */}} {{if $line.RightIdx}}{{if $inlineDiff.EscapeStatus.Escaped}}{{end}}{{end}} {{if $line.RightIdx}}{{end}} - {{- if and $.root.SignedUserID $.root.PageIsPullFiles (not (eq .GetType 3)) -}} - - {{- end -}} - {{- if $line.RightIdx -}} - {{- template "repo/diff/section_code" dict "diff" $inlineDiff -}} - {{- else -}} - - {{- end -}} - {{/* + */}}{{if and $.root.SignedUserID $.root.PageIsPullFiles (not (eq .GetType 3))}}{{/* + */}}{{/* + */}}{{end}}{{/* + */}}{{if $line.RightIdx}}{{/* + */}}{{template "repo/diff/section_code" dict "diff" $inlineDiff}}{{/* + */}}{{else}}{{/* + */}}{{/* + */}}{{end}}{{/* + */}}
{{if or (eq $expandDirection 3) (eq $expandDirection 5)}} - {{end}} {{if or (eq $expandDirection 3) (eq $expandDirection 4)}} - {{end}} {{if eq $expandDirection 2}} - {{end}} @@ -50,16 +48,18 @@
{{template "repo/diff/section_code" dict "diff" $inlineDiff}}{{/* + */}}{{template "repo/diff/section_code" dict "diff" $inlineDiff}}{{/* + */}} - {{- if and $.root.SignedUserID $.root.PageIsPullFiles -}} - - {{- end -}} - {{- template "repo/diff/section_code" dict "diff" $inlineDiff -}} - {{/* + */}}{{if and $.root.SignedUserID $.root.PageIsPullFiles}}{{/* + */}}{{/* + */}}{{end}}{{/* + */}}{{template "repo/diff/section_code" dict "diff" $inlineDiff}}{{/* + */}}