Merge branch 'main' into lunny/uniform-temp-dir

This commit is contained in:
Lunny Xiao 2025-03-11 16:39:10 -07:00
commit 887c8a5599
2836 changed files with 95917 additions and 114366 deletions

View File

@ -22,20 +22,25 @@ groups:
name: FEATURES name: FEATURES
labels: labels:
- type/feature - type/feature
-
name: API
labels:
- modifies/api
- -
name: ENHANCEMENTS name: ENHANCEMENTS
labels: labels:
- type/enhancement - type/enhancement
- type/refactoring -
- topic/ui name: PERFORMANCE
labels:
- performance/memory
- performance/speed
- performance/bigrepo
- performance/cpu
- -
name: BUGFIXES name: BUGFIXES
labels: labels:
- type/bug - type/bug
-
name: API
labels:
- modifies/api
- -
name: TESTING name: TESTING
labels: labels:

View File

@ -79,18 +79,6 @@ cpu.out
/public/assets/fonts /public/assets/fonts
/public/assets/img/avatar /public/assets/img/avatar
/vendor /vendor
/web_src/fomantic/node_modules
/web_src/fomantic/build/*
!/web_src/fomantic/build/semantic.js
!/web_src/fomantic/build/semantic.css
!/web_src/fomantic/build/themes
/web_src/fomantic/build/themes/*
!/web_src/fomantic/build/themes/default
/web_src/fomantic/build/themes/default/assets/*
!/web_src/fomantic/build/themes/default/assets/fonts
/web_src/fomantic/build/themes/default/assets/fonts/*
!/web_src/fomantic/build/themes/default/assets/fonts/icons.woff2
!/web_src/fomantic/build/themes/default/assets/fonts/outline-icons.woff2
/VERSION /VERSION
/.air /.air
/.go-licenses /.go-licenses

View File

@ -17,6 +17,7 @@ insert_final_newline = false
[templates/swagger/v1_json.tmpl] [templates/swagger/v1_json.tmpl]
indent_style = space indent_style = space
insert_final_newline = false
[templates/user/auth/oidc_wellknown.tmpl] [templates/user/auth/oidc_wellknown.tmpl]
indent_style = space indent_style = space

1004
.eslintrc.cjs Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,963 +0,0 @@
root: true
reportUnusedDisableDirectives: true
ignorePatterns:
- /web_src/js/vendor
- /web_src/fomantic
- /public/assets/js
parser: "@typescript-eslint/parser"
parserOptions:
sourceType: module
ecmaVersion: latest
project: true
extraFileExtensions: [".vue"]
parser: "@typescript-eslint/parser" # for vue plugin - https://eslint.vuejs.org/user-guide/#how-to-use-a-custom-parser
settings:
import/extensions: [".js", ".ts"]
import/parsers:
"@typescript-eslint/parser": [".js", ".ts"]
import/resolver:
typescript: true
plugins:
- "@eslint-community/eslint-plugin-eslint-comments"
- "@stylistic/eslint-plugin-js"
- "@typescript-eslint/eslint-plugin"
- eslint-plugin-array-func
- eslint-plugin-deprecation
- eslint-plugin-github
- eslint-plugin-i
- eslint-plugin-no-jquery
- eslint-plugin-no-use-extend-native
- eslint-plugin-regexp
- eslint-plugin-sonarjs
- eslint-plugin-unicorn
- eslint-plugin-vitest
- eslint-plugin-vitest-globals
- eslint-plugin-wc
env:
es2024: true
node: true
overrides:
- files: ["web_src/**/*"]
globals:
__webpack_public_path__: true
process: false # https://github.com/webpack/webpack/issues/15833
- files: ["web_src/**/*", "docs/**/*"]
env:
browser: true
node: false
- files: ["web_src/**/*worker.*"]
env:
worker: true
rules:
no-restricted-globals: [2, addEventListener, blur, close, closed, confirm, defaultStatus, defaultstatus, error, event, external, find, focus, frameElement, frames, history, innerHeight, innerWidth, isFinite, isNaN, length, locationbar, menubar, moveBy, moveTo, name, onblur, onerror, onfocus, onload, onresize, onunload, open, opener, opera, outerHeight, outerWidth, pageXOffset, pageYOffset, parent, print, removeEventListener, resizeBy, resizeTo, screen, screenLeft, screenTop, screenX, screenY, scroll, scrollbars, scrollBy, scrollTo, scrollX, scrollY, status, statusbar, stop, toolbar, top]
- files: ["*.config.*"]
rules:
i/no-unused-modules: [0]
- files: ["**/*.d.ts"]
rules:
i/no-unused-modules: [0]
"@typescript-eslint/consistent-type-definitions": [0]
"@typescript-eslint/consistent-type-imports": [0]
- files: ["web_src/js/types.ts"]
rules:
i/no-unused-modules: [0]
- files: ["**/*.test.*", "web_src/js/test/setup.ts"]
env:
vitest-globals/env: true
rules:
vitest/consistent-test-filename: [0]
vitest/consistent-test-it: [0]
vitest/expect-expect: [0]
vitest/max-expects: [0]
vitest/max-nested-describe: [0]
vitest/no-alias-methods: [0]
vitest/no-commented-out-tests: [0]
vitest/no-conditional-expect: [0]
vitest/no-conditional-in-test: [0]
vitest/no-conditional-tests: [0]
vitest/no-disabled-tests: [0]
vitest/no-done-callback: [0]
vitest/no-duplicate-hooks: [0]
vitest/no-focused-tests: [0]
vitest/no-hooks: [0]
vitest/no-identical-title: [2]
vitest/no-interpolation-in-snapshots: [0]
vitest/no-large-snapshots: [0]
vitest/no-mocks-import: [0]
vitest/no-restricted-matchers: [0]
vitest/no-restricted-vi-methods: [0]
vitest/no-standalone-expect: [0]
vitest/no-test-prefixes: [0]
vitest/no-test-return-statement: [0]
vitest/prefer-called-with: [0]
vitest/prefer-comparison-matcher: [0]
vitest/prefer-each: [0]
vitest/prefer-equality-matcher: [0]
vitest/prefer-expect-resolves: [0]
vitest/prefer-hooks-in-order: [0]
vitest/prefer-hooks-on-top: [2]
vitest/prefer-lowercase-title: [0]
vitest/prefer-mock-promise-shorthand: [0]
vitest/prefer-snapshot-hint: [0]
vitest/prefer-spy-on: [0]
vitest/prefer-strict-equal: [0]
vitest/prefer-to-be: [0]
vitest/prefer-to-be-falsy: [0]
vitest/prefer-to-be-object: [0]
vitest/prefer-to-be-truthy: [0]
vitest/prefer-to-contain: [0]
vitest/prefer-to-have-length: [0]
vitest/prefer-todo: [0]
vitest/require-hook: [0]
vitest/require-to-throw-message: [0]
vitest/require-top-level-describe: [0]
vitest/valid-describe-callback: [2]
vitest/valid-expect: [2]
vitest/valid-title: [2]
- files: ["web_src/js/modules/fetch.ts", "web_src/js/standalone/**/*"]
rules:
no-restricted-syntax: [2, WithStatement, ForInStatement, LabeledStatement, SequenceExpression]
- files: ["**/*.vue"]
plugins:
- eslint-plugin-vue
- eslint-plugin-vue-scoped-css
extends:
- plugin:vue/vue3-recommended
- plugin:vue-scoped-css/vue3-recommended
rules:
vue/attributes-order: [0]
vue/html-closing-bracket-spacing: [2, {startTag: never, endTag: never, selfClosingTag: never}]
vue/max-attributes-per-line: [0]
vue/singleline-html-element-content-newline: [0]
- files: ["tests/e2e/**"]
plugins:
- eslint-plugin-playwright
extends: plugin:playwright/recommended
rules:
"@eslint-community/eslint-comments/disable-enable-pair": [2]
"@eslint-community/eslint-comments/no-aggregating-enable": [2]
"@eslint-community/eslint-comments/no-duplicate-disable": [2]
"@eslint-community/eslint-comments/no-restricted-disable": [0]
"@eslint-community/eslint-comments/no-unlimited-disable": [2]
"@eslint-community/eslint-comments/no-unused-disable": [2]
"@eslint-community/eslint-comments/no-unused-enable": [2]
"@eslint-community/eslint-comments/no-use": [0]
"@eslint-community/eslint-comments/require-description": [0]
"@stylistic/js/array-bracket-newline": [0]
"@stylistic/js/array-bracket-spacing": [2, never]
"@stylistic/js/array-element-newline": [0]
"@stylistic/js/arrow-parens": [2, always]
"@stylistic/js/arrow-spacing": [2, {before: true, after: true}]
"@stylistic/js/block-spacing": [0]
"@stylistic/js/brace-style": [2, 1tbs, {allowSingleLine: true}]
"@stylistic/js/comma-dangle": [2, always-multiline]
"@stylistic/js/comma-spacing": [2, {before: false, after: true}]
"@stylistic/js/comma-style": [2, last]
"@stylistic/js/computed-property-spacing": [2, never]
"@stylistic/js/dot-location": [2, property]
"@stylistic/js/eol-last": [2]
"@stylistic/js/function-call-argument-newline": [0]
"@stylistic/js/function-call-spacing": [2, never]
"@stylistic/js/function-paren-newline": [0]
"@stylistic/js/generator-star-spacing": [0]
"@stylistic/js/implicit-arrow-linebreak": [0]
"@stylistic/js/indent": [2, 2, {ignoreComments: true, SwitchCase: 1}]
"@stylistic/js/key-spacing": [2]
"@stylistic/js/keyword-spacing": [2]
"@stylistic/js/line-comment-position": [0]
"@stylistic/js/linebreak-style": [2, unix]
"@stylistic/js/lines-around-comment": [0]
"@stylistic/js/lines-between-class-members": [0]
"@stylistic/js/max-len": [0]
"@stylistic/js/max-statements-per-line": [0]
"@stylistic/js/multiline-comment-style": [0]
"@stylistic/js/multiline-ternary": [0]
"@stylistic/js/new-parens": [2]
"@stylistic/js/newline-per-chained-call": [0]
"@stylistic/js/no-confusing-arrow": [0]
"@stylistic/js/no-extra-parens": [0]
"@stylistic/js/no-extra-semi": [2]
"@stylistic/js/no-floating-decimal": [0]
"@stylistic/js/no-mixed-operators": [0]
"@stylistic/js/no-mixed-spaces-and-tabs": [2]
"@stylistic/js/no-multi-spaces": [2, {ignoreEOLComments: true, exceptions: {Property: true}}]
"@stylistic/js/no-multiple-empty-lines": [2, {max: 1, maxEOF: 0, maxBOF: 0}]
"@stylistic/js/no-tabs": [2]
"@stylistic/js/no-trailing-spaces": [2]
"@stylistic/js/no-whitespace-before-property": [2]
"@stylistic/js/nonblock-statement-body-position": [2]
"@stylistic/js/object-curly-newline": [0]
"@stylistic/js/object-curly-spacing": [2, never]
"@stylistic/js/object-property-newline": [0]
"@stylistic/js/one-var-declaration-per-line": [0]
"@stylistic/js/operator-linebreak": [2, after]
"@stylistic/js/padded-blocks": [2, never]
"@stylistic/js/padding-line-between-statements": [0]
"@stylistic/js/quote-props": [0]
"@stylistic/js/quotes": [2, single, {avoidEscape: true, allowTemplateLiterals: true}]
"@stylistic/js/rest-spread-spacing": [2, never]
"@stylistic/js/semi": [2, always, {omitLastInOneLineBlock: true}]
"@stylistic/js/semi-spacing": [2, {before: false, after: true}]
"@stylistic/js/semi-style": [2, last]
"@stylistic/js/space-before-blocks": [2, always]
"@stylistic/js/space-before-function-paren": [2, {anonymous: ignore, named: never, asyncArrow: always}]
"@stylistic/js/space-in-parens": [2, never]
"@stylistic/js/space-infix-ops": [2]
"@stylistic/js/space-unary-ops": [2]
"@stylistic/js/spaced-comment": [2, always]
"@stylistic/js/switch-colon-spacing": [2]
"@stylistic/js/template-curly-spacing": [2, never]
"@stylistic/js/template-tag-spacing": [2, never]
"@stylistic/js/wrap-iife": [2, inside]
"@stylistic/js/wrap-regex": [0]
"@stylistic/js/yield-star-spacing": [2, after]
"@typescript-eslint/adjacent-overload-signatures": [0]
"@typescript-eslint/array-type": [0]
"@typescript-eslint/await-thenable": [2]
"@typescript-eslint/ban-ts-comment": [2, {'ts-expect-error': false, 'ts-ignore': true, 'ts-nocheck': false, 'ts-check': false}]
"@typescript-eslint/ban-tslint-comment": [0]
"@typescript-eslint/class-literal-property-style": [0]
"@typescript-eslint/class-methods-use-this": [0]
"@typescript-eslint/consistent-generic-constructors": [0]
"@typescript-eslint/consistent-indexed-object-style": [0]
"@typescript-eslint/consistent-return": [0]
"@typescript-eslint/consistent-type-assertions": [2, {assertionStyle: as, objectLiteralTypeAssertions: allow}]
"@typescript-eslint/consistent-type-definitions": [2, type]
"@typescript-eslint/consistent-type-exports": [2, {fixMixedExportsWithInlineTypeSpecifier: false}]
"@typescript-eslint/consistent-type-imports": [2, {prefer: type-imports, fixStyle: separate-type-imports, disallowTypeAnnotations: true}]
"@typescript-eslint/default-param-last": [0]
"@typescript-eslint/dot-notation": [0]
"@typescript-eslint/explicit-function-return-type": [0]
"@typescript-eslint/explicit-member-accessibility": [0]
"@typescript-eslint/explicit-module-boundary-types": [0]
"@typescript-eslint/init-declarations": [0]
"@typescript-eslint/max-params": [0]
"@typescript-eslint/member-ordering": [0]
"@typescript-eslint/method-signature-style": [0]
"@typescript-eslint/naming-convention": [0]
"@typescript-eslint/no-array-constructor": [2]
"@typescript-eslint/no-array-delete": [2]
"@typescript-eslint/no-base-to-string": [0]
"@typescript-eslint/no-confusing-non-null-assertion": [2]
"@typescript-eslint/no-confusing-void-expression": [0]
"@typescript-eslint/no-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]
deprecation/deprecation: [2]
dot-notation: [0]
eqeqeq: [2]
for-direction: [2]
func-name-matching: [2]
func-names: [0]
func-style: [0]
getter-return: [2]
github/a11y-aria-label-is-well-formatted: [0]
github/a11y-no-title-attribute: [0]
github/a11y-no-visually-hidden-interactive-element: [0]
github/a11y-role-supports-aria-props: [0]
github/a11y-svg-has-accessible-name: [0]
github/array-foreach: [0]
github/async-currenttarget: [2]
github/async-preventdefault: [2]
github/authenticity-token: [0]
github/get-attribute: [0]
github/js-class-name: [0]
github/no-blur: [0]
github/no-d-none: [0]
github/no-dataset: [2]
github/no-dynamic-script-tag: [2]
github/no-implicit-buggy-globals: [2]
github/no-inner-html: [0]
github/no-innerText: [2]
github/no-then: [2]
github/no-useless-passive: [2]
github/prefer-observers: [2]
github/require-passive-events: [2]
github/unescaped-html-literal: [0]
grouped-accessor-pairs: [2]
guard-for-in: [0]
id-blacklist: [0]
id-length: [0]
id-match: [0]
i/consistent-type-specifier-style: [0]
i/default: [0]
i/dynamic-import-chunkname: [0]
i/export: [2]
i/exports-last: [0]
i/extensions: [2, always, {ignorePackages: true}]
i/first: [2]
i/group-exports: [0]
i/max-dependencies: [0]
i/named: [2]
i/namespace: [0]
i/newline-after-import: [0]
i/no-absolute-path: [0]
i/no-amd: [2]
i/no-anonymous-default-export: [0]
i/no-commonjs: [2]
i/no-cycle: [2, {ignoreExternal: true, maxDepth: 1}]
i/no-default-export: [0]
i/no-deprecated: [0]
i/no-dynamic-require: [0]
i/no-empty-named-blocks: [2]
i/no-extraneous-dependencies: [2]
i/no-import-module-exports: [0]
i/no-internal-modules: [0]
i/no-mutable-exports: [0]
i/no-named-as-default-member: [0]
i/no-named-as-default: [0]
i/no-named-default: [0]
i/no-named-export: [0]
i/no-namespace: [0]
i/no-nodejs-modules: [0]
i/no-relative-packages: [0]
i/no-relative-parent-imports: [0]
i/no-restricted-paths: [0]
i/no-self-import: [2]
i/no-unassigned-import: [0]
i/no-unresolved: [2, {commonjs: true, ignore: ["\\?.+$"]}]
i/no-unused-modules: [2, {unusedExports: true}]
i/no-useless-path-segments: [2, {commonjs: true}]
i/no-webpack-loader-syntax: [2]
i/order: [0]
i/prefer-default-export: [0]
i/unambiguous: [0]
init-declarations: [0]
line-comment-position: [0]
logical-assignment-operators: [0]
max-classes-per-file: [0]
max-depth: [0]
max-lines-per-function: [0]
max-lines: [0]
max-nested-callbacks: [0]
max-params: [0]
max-statements: [0]
multiline-comment-style: [2, separate-lines]
new-cap: [0]
no-alert: [0]
no-array-constructor: [0] # handled by @typescript-eslint/no-array-constructor
no-async-promise-executor: [0]
no-await-in-loop: [0]
no-bitwise: [0]
no-buffer-constructor: [0]
no-caller: [2]
no-case-declarations: [2]
no-class-assign: [2]
no-compare-neg-zero: [2]
no-cond-assign: [2, except-parens]
no-console: [1, {allow: [debug, info, warn, error]}]
no-const-assign: [2]
no-constant-binary-expression: [2]
no-constant-condition: [0]
no-constructor-return: [2]
no-continue: [0]
no-control-regex: [0]
no-debugger: [1]
no-delete-var: [2]
no-div-regex: [0]
no-dupe-args: [2]
no-dupe-class-members: [2]
no-dupe-else-if: [2]
no-dupe-keys: [2]
no-duplicate-case: [2]
no-duplicate-imports: [0]
no-else-return: [2]
no-empty-character-class: [2]
no-empty-function: [0]
no-empty-pattern: [2]
no-empty-static-block: [2]
no-empty: [2, {allowEmptyCatch: true}]
no-eq-null: [2]
no-eval: [2]
no-ex-assign: [2]
no-extend-native: [2]
no-extra-bind: [2]
no-extra-boolean-cast: [2]
no-extra-label: [0]
no-fallthrough: [2]
no-func-assign: [2]
no-global-assign: [2]
no-implicit-coercion: [2]
no-implicit-globals: [0]
no-implied-eval: [0] # handled by @typescript-eslint/no-implied-eval
no-import-assign: [2]
no-inline-comments: [0]
no-inner-declarations: [2]
no-invalid-regexp: [2]
no-invalid-this: [0]
no-irregular-whitespace: [2]
no-iterator: [2]
no-jquery/no-ajax-events: [2]
no-jquery/no-ajax: [2]
no-jquery/no-and-self: [2]
no-jquery/no-animate-toggle: [2]
no-jquery/no-animate: [2]
no-jquery/no-append-html: [2]
no-jquery/no-attr: [2]
no-jquery/no-bind: [2]
no-jquery/no-box-model: [2]
no-jquery/no-browser: [2]
no-jquery/no-camel-case: [2]
no-jquery/no-class-state: [2]
no-jquery/no-class: [0]
no-jquery/no-clone: [2]
no-jquery/no-closest: [0]
no-jquery/no-constructor-attributes: [2]
no-jquery/no-contains: [2]
no-jquery/no-context-prop: [2]
no-jquery/no-css: [2]
no-jquery/no-data: [0]
no-jquery/no-deferred: [2]
no-jquery/no-delegate: [2]
no-jquery/no-each-collection: [0]
no-jquery/no-each-util: [0]
no-jquery/no-each: [0]
no-jquery/no-error-shorthand: [2]
no-jquery/no-error: [2]
no-jquery/no-escape-selector: [2]
no-jquery/no-event-shorthand: [2]
no-jquery/no-extend: [2]
no-jquery/no-fade: [2]
no-jquery/no-filter: [0]
no-jquery/no-find-collection: [0]
no-jquery/no-find-util: [2]
no-jquery/no-find: [0]
no-jquery/no-fx-interval: [2]
no-jquery/no-global-eval: [2]
no-jquery/no-global-selector: [0]
no-jquery/no-grep: [2]
no-jquery/no-has: [2]
no-jquery/no-hold-ready: [2]
no-jquery/no-html: [0]
no-jquery/no-in-array: [2]
no-jquery/no-is-array: [2]
no-jquery/no-is-empty-object: [2]
no-jquery/no-is-function: [2]
no-jquery/no-is-numeric: [2]
no-jquery/no-is-plain-object: [2]
no-jquery/no-is-window: [2]
no-jquery/no-is: [2]
no-jquery/no-jquery-constructor: [0]
no-jquery/no-live: [2]
no-jquery/no-load-shorthand: [2]
no-jquery/no-load: [2]
no-jquery/no-map-collection: [0]
no-jquery/no-map-util: [2]
no-jquery/no-map: [2]
no-jquery/no-merge: [2]
no-jquery/no-node-name: [2]
no-jquery/no-noop: [2]
no-jquery/no-now: [2]
no-jquery/no-on-ready: [2]
no-jquery/no-other-methods: [0]
no-jquery/no-other-utils: [2]
no-jquery/no-param: [2]
no-jquery/no-parent: [0]
no-jquery/no-parents: [2]
no-jquery/no-parse-html-literal: [2]
no-jquery/no-parse-html: [2]
no-jquery/no-parse-json: [2]
no-jquery/no-parse-xml: [2]
no-jquery/no-prop: [2]
no-jquery/no-proxy: [2]
no-jquery/no-ready-shorthand: [2]
no-jquery/no-ready: [2]
no-jquery/no-selector-prop: [2]
no-jquery/no-serialize: [2]
no-jquery/no-size: [2]
no-jquery/no-sizzle: [2]
no-jquery/no-slide: [2]
no-jquery/no-sub: [2]
no-jquery/no-support: [2]
no-jquery/no-text: [2]
no-jquery/no-trigger: [0]
no-jquery/no-trim: [2]
no-jquery/no-type: [2]
no-jquery/no-unique: [2]
no-jquery/no-unload-shorthand: [2]
no-jquery/no-val: [0]
no-jquery/no-visibility: [2]
no-jquery/no-when: [2]
no-jquery/no-wrap: [2]
no-jquery/variable-pattern: [2]
no-label-var: [2]
no-labels: [0] # handled by no-restricted-syntax
no-lone-blocks: [2]
no-lonely-if: [0]
no-loop-func: [0]
no-loss-of-precision: [2]
no-magic-numbers: [0]
no-misleading-character-class: [2]
no-multi-assign: [0]
no-multi-str: [2]
no-negated-condition: [0]
no-nested-ternary: [0]
no-new-func: [2]
no-new-native-nonconstructor: [2]
no-new-object: [2]
no-new-symbol: [2]
no-new-wrappers: [2]
no-new: [0]
no-nonoctal-decimal-escape: [2]
no-obj-calls: [2]
no-octal-escape: [2]
no-octal: [2]
no-param-reassign: [0]
no-plusplus: [0]
no-promise-executor-return: [0]
no-proto: [2]
no-prototype-builtins: [2]
no-redeclare: [0] # must be disabled for typescript overloads
no-regex-spaces: [2]
no-restricted-exports: [0]
no-restricted-globals: [2, addEventListener, blur, close, closed, confirm, defaultStatus, defaultstatus, error, event, external, find, focus, frameElement, frames, history, innerHeight, innerWidth, isFinite, isNaN, length, location, locationbar, menubar, moveBy, moveTo, name, onblur, onerror, onfocus, onload, onresize, onunload, open, opener, opera, outerHeight, outerWidth, pageXOffset, pageYOffset, parent, print, removeEventListener, resizeBy, resizeTo, screen, screenLeft, screenTop, screenX, screenY, scroll, scrollbars, scrollBy, scrollTo, scrollX, scrollY, self, status, statusbar, stop, toolbar, top, __dirname, __filename]
no-restricted-imports: [0]
no-restricted-syntax: [2, WithStatement, ForInStatement, LabeledStatement, SequenceExpression, {selector: "CallExpression[callee.name='fetch']", message: "use modules/fetch.ts instead"}]
no-return-assign: [0]
no-script-url: [2]
no-self-assign: [2, {props: true}]
no-self-compare: [2]
no-sequences: [2]
no-setter-return: [2]
no-shadow-restricted-names: [2]
no-shadow: [0]
no-sparse-arrays: [2]
no-template-curly-in-string: [2]
no-ternary: [0]
no-this-before-super: [2]
no-throw-literal: [2]
no-undef-init: [2]
no-undef: [2, {typeof: true}]
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-function-scoping: [2]
unicorn/custom-error-definition: [0]
unicorn/empty-brace-spaces: [2]
unicorn/error-message: [0]
unicorn/escape-case: [0]
unicorn/expiring-todo-comments: [0]
unicorn/explicit-length-check: [0]
unicorn/filename-case: [0]
unicorn/import-index: [0]
unicorn/import-style: [0]
unicorn/new-for-builtins: [2]
unicorn/no-abusive-eslint-disable: [0]
unicorn/no-anonymous-default-export: [0]
unicorn/no-array-callback-reference: [0]
unicorn/no-array-for-each: [2]
unicorn/no-array-method-this-argument: [2]
unicorn/no-array-push-push: [2]
unicorn/no-array-reduce: [2]
unicorn/no-await-expression-member: [0]
unicorn/no-await-in-promise-methods: [2]
unicorn/no-console-spaces: [0]
unicorn/no-document-cookie: [2]
unicorn/no-empty-file: [2]
unicorn/no-for-loop: [0]
unicorn/no-hex-escape: [0]
unicorn/no-instanceof-array: [0]
unicorn/no-invalid-fetch-options: [2]
unicorn/no-invalid-remove-event-listener: [2]
unicorn/no-keyword-prefix: [0]
unicorn/no-length-as-slice-end: [2]
unicorn/no-lonely-if: [2]
unicorn/no-magic-array-flat-depth: [0]
unicorn/no-negated-condition: [0]
unicorn/no-negation-in-equality-check: [2]
unicorn/no-nested-ternary: [0]
unicorn/no-new-array: [0]
unicorn/no-new-buffer: [0]
unicorn/no-null: [0]
unicorn/no-object-as-default-parameter: [0]
unicorn/no-process-exit: [0]
unicorn/no-single-promise-in-promise-methods: [2]
unicorn/no-static-only-class: [2]
unicorn/no-thenable: [2]
unicorn/no-this-assignment: [2]
unicorn/no-typeof-undefined: [2]
unicorn/no-unnecessary-await: [2]
unicorn/no-unnecessary-polyfills: [2]
unicorn/no-unreadable-array-destructuring: [0]
unicorn/no-unreadable-iife: [2]
unicorn/no-unused-properties: [2]
unicorn/no-useless-fallback-in-spread: [2]
unicorn/no-useless-length-check: [2]
unicorn/no-useless-promise-resolve-reject: [2]
unicorn/no-useless-spread: [2]
unicorn/no-useless-switch-case: [2]
unicorn/no-useless-undefined: [0]
unicorn/no-zero-fractions: [2]
unicorn/number-literal-case: [0]
unicorn/numeric-separators-style: [0]
unicorn/prefer-add-event-listener: [2]
unicorn/prefer-array-find: [2]
unicorn/prefer-array-flat-map: [2]
unicorn/prefer-array-flat: [2]
unicorn/prefer-array-index-of: [2]
unicorn/prefer-array-some: [2]
unicorn/prefer-at: [0]
unicorn/prefer-blob-reading-methods: [2]
unicorn/prefer-code-point: [0]
unicorn/prefer-date-now: [2]
unicorn/prefer-default-parameters: [0]
unicorn/prefer-dom-node-append: [2]
unicorn/prefer-dom-node-dataset: [0]
unicorn/prefer-dom-node-remove: [2]
unicorn/prefer-dom-node-text-content: [2]
unicorn/prefer-event-target: [2]
unicorn/prefer-export-from: [0]
unicorn/prefer-includes: [2]
unicorn/prefer-json-parse-buffer: [0]
unicorn/prefer-keyboard-event-key: [2]
unicorn/prefer-logical-operator-over-ternary: [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]

2
.gitattributes vendored
View File

@ -5,7 +5,5 @@
/public/assets/img/svg/*.svg linguist-generated /public/assets/img/svg/*.svg linguist-generated
/templates/swagger/v1_json.tmpl linguist-generated /templates/swagger/v1_json.tmpl linguist-generated
/vendor/** -text -eol linguist-vendored /vendor/** -text -eol linguist-vendored
/web_src/fomantic/build/** linguist-generated
/web_src/fomantic/_site/globals/site.variables linguist-language=Less
/web_src/js/vendor/** -text -eol linguist-vendored /web_src/js/vendor/** -text -eol linguist-vendored
Dockerfile.* linguist-language=Dockerfile Dockerfile.* linguist-language=Dockerfile

View File

@ -13,5 +13,5 @@ contact_links:
url: https://docs.gitea.com/help/faq url: https://docs.gitea.com/help/faq
about: Please check if your question isn't mentioned here. about: Please check if your question isn't mentioned here.
- name: Crowdin Translations - name: Crowdin Translations
url: https://crowdin.com/project/gitea url: https://translate.gitea.com
about: Translations are managed here. about: Translations are managed here.

View File

@ -3,3 +3,5 @@ self-hosted-runner:
- actuated-4cpu-8gb - actuated-4cpu-8gb
- actuated-4cpu-16gb - actuated-4cpu-16gb
- nscloud - nscloud
- namespace-profile-gitea-release-docker
- namespace-profile-gitea-release-binary

10
.github/labeler.yml vendored
View File

@ -41,7 +41,7 @@ modifies/internal:
- ".dockerignore" - ".dockerignore"
- "docker/**" - "docker/**"
- ".editorconfig" - ".editorconfig"
- ".eslintrc.yaml" - ".eslintrc.cjs"
- ".golangci.yml" - ".golangci.yml"
- ".gitpod.yml" - ".gitpod.yml"
- ".markdownlint.yaml" - ".markdownlint.yaml"
@ -49,7 +49,7 @@ modifies/internal:
- "stylelint.config.js" - "stylelint.config.js"
- ".yamllint.yaml" - ".yamllint.yaml"
- ".github/**" - ".github/**"
- ".gitea/" - ".gitea/**"
- ".devcontainer/**" - ".devcontainer/**"
- "build.go" - "build.go"
- "build/**" - "build/**"
@ -73,9 +73,9 @@ modifies/go:
modifies/frontend: modifies/frontend:
- changed-files: - changed-files:
- any-glob-to-any-file: - any-glob-to-any-file:
- "**/*.js" - "*.js"
- "**/*.ts" - "*.ts"
- "**/*.vue" - "web_src/**"
docs-update-needed: docs-update-needed:
- changed-files: - changed-files:

View File

@ -1,8 +1,8 @@
name: cron-licenses name: cron-licenses
on: on:
schedule: # schedule:
- cron: "7 0 * * 1" # every Monday at 00:07 UTC # - cron: "7 0 * * 1" # every Monday at 00:07 UTC
workflow_dispatch: workflow_dispatch:
jobs: jobs:
@ -15,7 +15,7 @@ jobs:
with: with:
go-version-file: go.mod go-version-file: go.mod
check-latest: true check-latest: true
- run: make generate-license generate-gitignore - run: make generate-gitignore
timeout-minutes: 40 timeout-minutes: 40
- name: push translations to repo - name: push translations to repo
uses: appleboy/git-push-action@v0.0.3 uses: appleboy/git-push-action@v0.0.3

View File

@ -51,14 +51,16 @@ jobs:
- "options/locale/locale_en-US.ini" - "options/locale/locale_en-US.ini"
frontend: frontend:
- "**/*.js" - "*.js"
- "*.ts"
- "web_src/**" - "web_src/**"
- "tools/*.js"
- "tools/*.ts"
- "assets/emoji.json" - "assets/emoji.json"
- "package.json" - "package.json"
- "package-lock.json" - "package-lock.json"
- "Makefile" - "Makefile"
- ".eslintrc.yaml" - ".eslintrc.cjs"
- "stylelint.config.js"
- ".npmrc" - ".npmrc"
docs: docs:
@ -85,6 +87,7 @@ jobs:
swagger: swagger:
- "templates/swagger/v1_json.tmpl" - "templates/swagger/v1_json.tmpl"
- "templates/swagger/v1_input.json"
- "Makefile" - "Makefile"
- "package.json" - "package.json"
- "package-lock.json" - "package-lock.json"

View File

@ -37,7 +37,7 @@ jobs:
python-version: "3.12" python-version: "3.12"
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: 20 node-version: 22
cache: npm cache: npm
cache-dependency-path: package-lock.json cache-dependency-path: package-lock.json
- run: pip install poetry - run: pip install poetry
@ -66,7 +66,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: 20 node-version: 22
cache: npm cache: npm
cache-dependency-path: package-lock.json cache-dependency-path: package-lock.json
- run: make deps-frontend - run: make deps-frontend
@ -95,7 +95,7 @@ jobs:
go-version-file: go.mod go-version-file: go.mod
check-latest: true check-latest: true
- run: make deps-backend deps-tools - run: make deps-backend deps-tools
- run: make lint-go-windows lint-go-vet - run: make lint-go-windows lint-go-gitea-vet
env: env:
TAGS: bindata sqlite sqlite_unlock_notify TAGS: bindata sqlite sqlite_unlock_notify
GOOS: windows GOOS: windows
@ -137,7 +137,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: 20 node-version: 22
cache: npm cache: npm
cache-dependency-path: package-lock.json cache-dependency-path: package-lock.json
- run: make deps-frontend - run: make deps-frontend
@ -186,7 +186,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: 20 node-version: 22
cache: npm cache: npm
cache-dependency-path: package-lock.json cache-dependency-path: package-lock.json
- run: make deps-frontend - run: make deps-frontend

View File

@ -154,12 +154,15 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
services: services:
mysql: mysql:
image: mysql:8.0 # the bitnami mysql image has more options than the official one, it's easier to customize
image: bitnami/mysql:8.0
env: env:
MYSQL_ALLOW_EMPTY_PASSWORD: true ALLOW_EMPTY_PASSWORD: true
MYSQL_DATABASE: testgitea MYSQL_DATABASE: testgitea
ports: ports:
- "3306:3306" - "3306:3306"
options: >-
--mount type=tmpfs,destination=/bitnami/mysql/data
elasticsearch: elasticsearch:
image: elasticsearch:7.5.0 image: elasticsearch:7.5.0
env: env:
@ -188,7 +191,8 @@ jobs:
- name: run migration tests - name: run migration tests
run: make test-mysql-migration run: make test-mysql-migration
- name: run tests - name: run tests
run: make integration-test-coverage # run: make integration-test-coverage (at the moment, no coverage is really handled)
run: make test-mysql
env: env:
TAGS: bindata TAGS: bindata
RACE_ENABLED: true RACE_ENABLED: true
@ -198,12 +202,10 @@ jobs:
test-mssql: test-mssql:
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true' if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
needs: files-changed needs: files-changed
# specifying the version of ubuntu in use as mssql fails on newer kernels runs-on: ubuntu-latest
# pending resolution from vendor
runs-on: ubuntu-20.04
services: services:
mssql: mssql:
image: mcr.microsoft.com/mssql/server:2017-latest image: mcr.microsoft.com/mssql/server:2019-latest
env: env:
ACCEPT_EULA: Y ACCEPT_EULA: Y
MSSQL_PID: Standard MSSQL_PID: Standard

View File

@ -23,7 +23,7 @@ jobs:
check-latest: true check-latest: true
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: 20 node-version: 22
cache: npm cache: npm
cache-dependency-path: package-lock.json cache-dependency-path: package-lock.json
- run: make deps-frontend frontend deps-backend - run: make deps-frontend frontend deps-backend

View File

@ -10,7 +10,7 @@ concurrency:
jobs: jobs:
nightly-binary: nightly-binary:
runs-on: nscloud runs-on: namespace-profile-gitea-release-binary
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
# fetch all commits instead of only the last as some branches are long lived and could have many between versions # fetch all commits instead of only the last as some branches are long lived and could have many between versions
@ -22,7 +22,7 @@ jobs:
check-latest: true check-latest: true
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: 20 node-version: 22
cache: npm cache: npm
cache-dependency-path: package-lock.json cache-dependency-path: package-lock.json
- run: make deps-frontend deps-backend - run: make deps-frontend deps-backend
@ -58,7 +58,7 @@ jobs:
run: | run: |
aws s3 sync dist/release s3://${{ secrets.AWS_S3_BUCKET }}/gitea/${{ steps.clean_name.outputs.branch }} --no-progress aws s3 sync dist/release s3://${{ secrets.AWS_S3_BUCKET }}/gitea/${{ steps.clean_name.outputs.branch }} --no-progress
nightly-docker-rootful: nightly-docker-rootful:
runs-on: ubuntu-latest runs-on: namespace-profile-gitea-release-docker
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
# fetch all commits instead of only the last as some branches are long lived and could have many between versions # fetch all commits instead of only the last as some branches are long lived and could have many between versions
@ -95,7 +95,7 @@ jobs:
push: true push: true
tags: gitea/gitea:${{ steps.clean_name.outputs.branch }} tags: gitea/gitea:${{ steps.clean_name.outputs.branch }}
nightly-docker-rootless: nightly-docker-rootless:
runs-on: ubuntu-latest runs-on: namespace-profile-gitea-release-docker
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
# fetch all commits instead of only the last as some branches are long lived and could have many between versions # fetch all commits instead of only the last as some branches are long lived and could have many between versions

View File

@ -11,7 +11,7 @@ concurrency:
jobs: jobs:
binary: binary:
runs-on: nscloud runs-on: namespace-profile-gitea-release-binary
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
# fetch all commits instead of only the last as some branches are long lived and could have many between versions # fetch all commits instead of only the last as some branches are long lived and could have many between versions
@ -23,7 +23,7 @@ jobs:
check-latest: true check-latest: true
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: 20 node-version: 22
cache: npm cache: npm
cache-dependency-path: package-lock.json cache-dependency-path: package-lock.json
- run: make deps-frontend deps-backend - run: make deps-frontend deps-backend
@ -68,7 +68,7 @@ jobs:
env: env:
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
docker-rootful: docker-rootful:
runs-on: ubuntu-latest runs-on: namespace-profile-gitea-release-docker
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
# fetch all commits instead of only the last as some branches are long lived and could have many between versions # fetch all commits instead of only the last as some branches are long lived and could have many between versions
@ -99,7 +99,7 @@ jobs:
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
docker-rootless: docker-rootless:
runs-on: ubuntu-latest runs-on: namespace-profile-gitea-release-docker
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
# fetch all commits instead of only the last as some branches are long lived and could have many between versions # fetch all commits instead of only the last as some branches are long lived and could have many between versions

View File

@ -13,7 +13,7 @@ concurrency:
jobs: jobs:
binary: binary:
runs-on: nscloud runs-on: namespace-profile-gitea-release-binary
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
# fetch all commits instead of only the last as some branches are long lived and could have many between versions # fetch all commits instead of only the last as some branches are long lived and could have many between versions
@ -25,7 +25,7 @@ jobs:
check-latest: true check-latest: true
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: 20 node-version: 22
cache: npm cache: npm
cache-dependency-path: package-lock.json cache-dependency-path: package-lock.json
- run: make deps-frontend deps-backend - run: make deps-frontend deps-backend
@ -70,7 +70,7 @@ jobs:
env: env:
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
docker-rootful: docker-rootful:
runs-on: ubuntu-latest runs-on: namespace-profile-gitea-release-docker
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
# fetch all commits instead of only the last as some branches are long lived and could have many between versions # fetch all commits instead of only the last as some branches are long lived and could have many between versions
@ -88,9 +88,9 @@ jobs:
# 1.2 # 1.2
# 1.2.3 # 1.2.3
tags: | tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}} type=semver,pattern={{major}}
type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{version}}
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
@ -105,7 +105,7 @@ jobs:
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
docker-rootless: docker-rootless:
runs-on: ubuntu-latest runs-on: namespace-profile-gitea-release-docker
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
# fetch all commits instead of only the last as some branches are long lived and could have many between versions # fetch all commits instead of only the last as some branches are long lived and could have many between versions
@ -126,9 +126,9 @@ jobs:
# 1.2 # 1.2
# 1.2.3 # 1.2.3
tags: | tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}} type=semver,pattern={{major}}
type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{version}}
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:

18
.gitignore vendored
View File

@ -9,6 +9,11 @@ _test
# IntelliJ # IntelliJ
.idea .idea
.run
# IntelliJ Gateway
.uuid
# Goland's output filename can not be set manually # Goland's output filename can not be set manually
/go_build_* /go_build_*
/gitea_* /gitea_*
@ -28,6 +33,7 @@ _testmain.go
*.exe *.exe
*.test *.test
*.prof *.prof
*.tsbuildinfo
*coverage.out *coverage.out
coverage.all coverage.all
@ -78,18 +84,6 @@ cpu.out
/public/assets/fonts /public/assets/fonts
/public/assets/licenses.txt /public/assets/licenses.txt
/vendor /vendor
/web_src/fomantic/node_modules
/web_src/fomantic/build/*
!/web_src/fomantic/build/semantic.js
!/web_src/fomantic/build/semantic.css
!/web_src/fomantic/build/themes
/web_src/fomantic/build/themes/*
!/web_src/fomantic/build/themes/default
/web_src/fomantic/build/themes/default/assets/*
!/web_src/fomantic/build/themes/default/assets/fonts
/web_src/fomantic/build/themes/default/assets/fonts/*
!/web_src/fomantic/build/themes/default/assets/fonts/icons.woff2
!/web_src/fomantic/build/themes/default/assets/fonts/outline-icons.woff2
/VERSION /VERSION
/.air /.air
/.go-licenses /.go-licenses

View File

@ -19,10 +19,12 @@ linters:
- revive - revive
- staticcheck - staticcheck
- stylecheck - stylecheck
- testifylint
- typecheck - typecheck
- unconvert - unconvert
- unused - unused
- unparam - unparam
- usetesting
- wastedassign - wastedassign
run: run:
@ -34,6 +36,10 @@ output:
show-stats: true show-stats: true
linters-settings: linters-settings:
testifylint:
disable:
- go-require
- require-error
stylecheck: stylecheck:
checks: ["all", "-ST1005", "-ST1003"] checks: ["all", "-ST1005", "-ST1003"]
nakedret: nakedret:
@ -96,6 +102,8 @@ linters-settings:
desc: do not use the ini package, use gitea's config system instead desc: do not use the ini package, use gitea's config system instead
- pkg: gitea.com/go-chi/cache - pkg: gitea.com/go-chi/cache
desc: do not use the go-chi cache package, use gitea's cache system desc: do not use the go-chi cache package, use gitea's cache system
usetesting:
os-temp-dir: true
issues: issues:
max-issues-per-linter: 0 max-issues-per-linter: 0

2
.mailmap Normal file
View File

@ -0,0 +1,2 @@
Unknwon <u@gogs.io> <joe2010xtmf@163.com>
Unknwon <u@gogs.io> 无闻 <u@gogs.io>

View File

@ -4,6 +4,932 @@ 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 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). been added to each release, please refer to the [blog](https://blog.gitea.com).
## [1.23.5](https://github.com/go-gitea/gitea/releases/tag/v1.23.5) - 2025-03-04
* SECURITY
* Bump x/oauth2 & x/crypto (#33704) (#33727)
* PERFORMANCE
* Optimize user dashboard loading (#33686) (#33708)
* BUGFIXES
* Fix navbar dropdown item align (#33782)
* Fix inconsistent closed issue list icon (#33722) (#33728)
* Fix for Maven Package Naming Convention Handling (#33678) (#33679)
* Improve Open-with URL encoding (#33666) (#33680)
* Deleting repository should unlink all related packages (#33653) (#33673)
* Fix omitempty bug (#33663) (#33670)
* Upgrade go-crypto from 1.1.4 to 1.1.6 (#33745) (#33754)
* Fix OCI image.version annotation for releases to use full semver (#33698) (#33701)
* Try to fix ACME path when renew (#33668) (#33693)
* Fix mCaptcha bug (#33659) (#33661)
* Git graph: don't show detached commits (#33645) (#33650)
* Use MatchPhraseQuery for bleve code search (#33628)
* Adjust appearence of commit status webhook (#33778) #33789
* Upgrade golang net from 0.35.0 -> 0.36.0 (#33795) #33796
## [1.23.4](https://github.com/go-gitea/gitea/releases/tag/v1.23.4) - 2025-02-16
* SECURITY
* Enhance routers for the Actions variable operations (#33547) (#33553)
* Enhance routers for the Actions runner operations (#33549) (#33555)
* Fix project issues list and counting (#33594) #33619
* PERFORMANCES
* Performance optimization for pull request files loading comments attachments (#33585) (#33592)
* BUGFIXES
* Add a transaction to `pickTask` (#33543) (#33563)
* Fix mirror bug (#33597) (#33607)
* Use default Git timeout when checking repo health (#33593) (#33598)
* Fix PR's target branch dropdown (#33589) (#33591)
* Fix various problems (artifact order, api empty slice, assignee check, fuzzy prompt, mirror proxy, adopt git) (#33569) (#33577)
* Rework suggestion backend (#33538) (#33546)
* Fix context usage (#33554) (#33557)
* Only show the latest version in the Arch index (#33262) (#33580)
* Skip deletion error for action artifacts (#33476) (#33568)
* Make actions URL in commit status webhooks absolute (#33620) #33632
* Add missing locale (#33641) #33642
## [1.23.3](https://github.com/go-gitea/gitea/releases/tag/v1.23.3) - 2025-02-06
* Security
* Build Gitea with Golang v1.23.6 to fix security bugs
* BUGFIXES
* Fix a bug caused by status webhook template #33512
## [1.23.2](https://github.com/go-gitea/gitea/releases/tag/1.23.2) - 2025-02-04
* BREAKING
* Add tests for webhook and fix some webhook bugs (#33396) (#33442)
* Package webhooks 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.<job_id>.if` (#31070)
* Clean up revive linter config, tweak golangci output (#30980)
* Use CSS `inset` shorthand (#30939)
* Forbid deprecated `break-word` in CSS (#30934)
* Remove obsolete monaco workaround (#30893)
* Update JS dependencies, add new eslint rules (#30840)
* Fix body margin shifting with modals, fix error on project column edit (#30831)
* Remove disk-clean workflow (#30741)
* Bump `github.com/google/go-github` to v61 (#30738)
* Add built js files to eslint ignore (#30737)
* Use `ProtonMail/go-crypto` for `opengpg` in tests (#30736)
* Upgrade xorm to v1.3.9 and improve some migrations Sync (#29899)
* Added default sorting milestones by name (#27084)
* Enable `unparam` linter (#31277)
* Use Alpine 3.21 for the docker images (#32924) #32951
* Bump x/net (#32896) #32899
* Use -s -w ldflags for release artifacts (#33041) #33042
* Remove aws go sdk package dependency (#33029) #33047
## [1.22.6](https://github.com/go-gitea/gitea/releases/tag/v1.22.6) - 2024-12-12
* SECURITY
* Fix misuse of PublicKeyCallback(#32810)
* BUGFIXES
* Fix lfs migration (#32812) (#32818)
* Add missing two sync feed for refs/pull (#32815)
* TESTING
* Avoid MacOS keychain dialog in integration tests (#32813) (#32816)
## [1.22.5](https://github.com/go-gitea/gitea/releases/tag/v1.22.5) - 2024-12-11
* SECURITY
* Upgrade crypto library (#32791)
* Fix delete branch perm checking (#32654) (#32707)
* BUGFIXES
* Add standard-compliant route to serve outdated R packages (#32783) (#32789)
* Fix internal server error when updating labels without write permission (#32776) (#32785)
* Add Swift login endpoint (#32693) (#32701)
* Fix fork page branch selection (#32711) (#32725)
* Fix word overflow in file search page (#32695) (#32699)
* Fix gogit `GetRefCommitID` (#32705) (#32712)
* Fix race condition in mermaid observer (#32599) (#32673)
* Fixe a keystring misuse and refactor duplicates keystrings (#32668) (#32792)
* Bump relative-time-element to v4.4.4 (#32739)
* PERFORMANCE
* Make wiki pages visit fast (#32732) (#32745)
* MISC
* Don't create action when syncing mirror pull refs (#32659) (#32664)
## [1.22.4](https://github.com/go-gitea/gitea/releases/tag/v1.22.4) - 2024-11-14
* SECURITY
* Fix basic auth with webauthn (#32531) (#32536)
* Refactor internal routers (partial backport, auth token const time comparing) (#32473) (#32479)
* PERFORMANCE
* Remove transaction for archive download (#32186) (#32520)
* BUGFIXES
* Fix `missing signature key` error when pulling Docker images with `SERVE_DIRECT` enabled (#32365) (#32397)
* Fix get reviewers fails when selecting user without pull request permissions unit (#32415) (#32616)
* Fix adding index files to tmp directory (#32360) (#32593)
* Fix PR creation on forked repositories via API (#31863) (#32591)
* Fix missing menu tabs in organization project view page (#32313) (#32592)
* Support HTTP POST requests to `/userinfo`, aligning to OpenID Core specification (#32578) (#32594)
* Fix debian package clean up cron job (#32351) (#32590)
* Fix GetInactiveUsers (#32540) (#32588)
* Allow the actions user to login via the jwt token (#32527) (#32580)
* Fix submodule parsing (#32571) (#32577)
* Refactor find forks and fix possible bugs that weaken permissions check (#32528) (#32547)
* Fix some places that don't respect org full name setting (#32243) (#32550)
* Refactor push mirror find and add check for updating push mirror (#32539) (#32549)
* Fix basic auth with webauthn (#32531) (#32536)
* Fix artifact v4 upload above 8MB (#31664) (#32523)
* Fix oauth2 error handle not return immediately (#32514) (#32516)
* Fix action not triggered when commit message is too long (#32498) (#32507)
* Fix `GetRepoLink` nil pointer dereference on dashboard feed page when repo is deleted with actions enabled (#32501) (#32502)
* Fix `missing signature key` error when pulling Docker images with `SERVE_DIRECT` enabled (#32397) (#32397)
* Fix the permission check for user search API and limit the number of returned users for `/user/search` (#32310)
* Fix SearchIssues swagger docs (#32208) (#32298)
* Fix dropdown content overflow (#31610) (#32250)
* Disable Oauth check if oauth disabled (#32368) (#32480)
* Respect renamed dependencies of Cargo registry (#32430) (#32478)
* Fix mermaid diagram height when initially hidden (#32457) (#32464)
* Fix broken releases when re-pushing tags (#32435) (#32449)
* Only provide the commit summary for Discord webhook push events (#32432) (#32447)
* Only query team tables if repository is under org when getting assignees (#32414) (#32426)
* Fix created_unix for mirroring (#32342) (#32406)
* Respect UI.ExploreDefaultSort setting again (#32357) (#32385)
* Fix broken image when editing comment with non-image attachments (#32319) (#32345)
* Fix disable 2fa bug (#32320) (#32330)
* Always update expiration time when creating an artifact (#32281) (#32285)
* Fix null errors on conversation holder (#32258) (#32266) (#32282)
* Only rename a user when they should receive a different name (#32247) (#32249)
* Fix checkbox bug on private/archive filter (#32236) (#32240)
* Add a doctor check to disable the "Actions" unit for mirrors (#32424) (#32497)
* Quick fix milestone deadline 9999 (#32423)
* Make `show stats` work when only one file changed (#32244) (#32268)
* Make `owner/repo/pulls` handlers use "PR reader" permission (#32254) (#32265)
* Update scheduled tasks even if changes are pushed by "ActionsUser" (#32246) (#32252)
* MISC
* Remove unnecessary code: `GetPushMirrorsByRepoID` called on all repo pages (#32560) (#32567)
* Improve some sanitizer rules (#32534)
* Update nix development environment vor v1.22.x (#32495)
* Add warn log when deleting inactive users (#32318) (#32321)
* Update github.com/go-enry/go-enry to v2.9.1 (#32295) (#32296)
* Warn users when they try to use a non-root-url to sign in/up (#32272) (#32273)
## [1.22.3](https://github.com/go-gitea/gitea/releases/tag/v1.22.3) - 2024-10-08
* SECURITY
* Fix bug when a token is given public only (#32204) (#32218)
* PERFORMANCE
* Increase `cacheContextLifetime` to reduce false reports (#32011) (#32023)
* Don't join repository when loading action table data (#32127) (#32143)
* BUGFIXES
* Fix javascript error when an anonymous user visits migration page (#32144) (#32179)
* Don't init signing keys if oauth2 provider is disabled (#32177)
* Fix wrong status of `Set up Job` when first step is skipped (#32120) (#32125)
* Fix bug when deleting a migrated branch (#32075) (#32123)
* Truncate commit message during Discord webhook push events (#31970) (#32121)
* Allow to set branch protection in an empty repository (#32095) (#32119)
* Fix panic when cloning with wrong ssh format. (#32076) (#32118)
* Fix rename branch permission bug (#32066) (#32108)
* Fix: database not update release when using `git push --tags --force` (#32040) (#32074)
* Add missing comment reply handling (#32050) (#32065)
* Do not escape relative path in RPM primary index (#32038) (#32054)
* Fix `/repos/{owner}/{repo}/pulls/{index}/files` endpoint not populating `previous_filename` (#32017) (#32028)
* Support allowed hosts for migrations to work with proxy (#32025) (#32026)
* Fix the logic of finding the latest pull review commit ID (#32139) (#32165)
* Fix bug in getting merged pull request by commit (#32079) (#32117)
* Fix wrong last modify time (#32102) (#32104)
* Fix incorrect `/tokens` api (#32085) (#32092)
* Handle invalid target when creating releases using API (#31841) (#32043)
* Check if the `due_date` is nil when editing issues (#32035) (#32042)
* Fix container parallel upload bugs (#32022)
* Fixed race condition when deleting documents by repoId in ElasticSearch (#32185) (#32188)
* Refactor CSRF protector (#32057) (#32069)
* Fix Bug in Issue/pulls list (#32081) (#32115)
* Include collaboration repositories on dashboard source/forks/mirrors list (#31946) (#32122)
* Add null check for responseData.invalidTopics (#32212) (#32217)
* TESTING
* Fix mssql ci with a new mssql version on ci (#32094)
* MISC
* Upgrade some dependencies include minio-go (#32166)
* Add bin to Composer Metadata (#32099) (#32106)
* Lazy load avatar images (#32051) (#32063)
* Upgrade cache to v0.2.1 (#32003) (#32009)
## [1.22.2](https://github.com/go-gitea/gitea/releases/tag/v1.22.2) - 2024-08-28
* Security
* Replace v-html with v-text in search inputbox (#31966) (#31973)
* Fix nuget/conan/container packages upload bugs (#31967) (#31982)
* PERFORMANCE
* Refactor the usage of batch catfile (#31754) (#31889)
* BUGFIXES
* Fix overflowing content in action run log (#31842) (#31853)
* Scroll images in project issues separately from the remaining issue (#31683) (#31823)
* Add `:focus-visible` style to buttons (#31799) (#31819)
* Fix the display of project type for deleted projects (#31732) (#31734)
* Fix API owner ID should be zero when created repo secret (#31715) (#31811)
* Set owner id to zero when GetRegistrationToken for repo (#31725) (#31729)
* Fix API endpoint for registration-token (#31722) (#31728)
* Add permission check when creating PR (#31033) (#31720)
* Don't return 500 if mirror url contains special chars (#31859) (#31895)
* Fix agit automerge (#31207) (#31881)
* Add CfTurnstileSitekey context data to all captcha templates (#31874) (#31876)
* Avoid returning without written ctx when posting PR (#31843) (#31848)
* Fix raw wiki links (#31825) (#31845)
* Fix panic of ssh public key page after deletion of auth source (#31829) (#31836)
* Fixes for unreachable project issues when transfer repository from organization (#31770) (#31828)
* Show lock owner instead of repo owner on LFS setting page (#31788) (#31817)
* Fix `IsObjectExist` with gogit (#31790) (#31806)
* Fix protected branch files detection on pre_receive hook (#31778) (#31796)
* Add `TAGS` to `TEST_TAGS` and fix bugs found with gogit (#31791) (#31795)
* Rename head branch of pull requests when renaming a branch (#31759) (#31774)
* Fix wiki revision pagination (#31760) (#31772)
* Bump vue-bar-graph (#31705) (#31753)
* Distinguish LFS object errors to ignore missing objects during migration (#31702) (#31745)
* Make GetRepositoryByName more safer (#31712) (#31718)
* Fix a branch divergence cache bug (#31659) (#31661)
* Allow org team names of length 255 in create team form (#31564) (#31603)
* Use old behavior for telegram webhook (#31588)
* Bug fix for translation in ru (#31892)
* Fix actions notify bug (#31866) (#31875)
* Fix the component of access token list not mounted (#31824) (#31868)
* Add missing repository type filter parameters to pager (#31832) (#31837)
* Fix dates displaying in a wrong manner when we're close to the end of… (#31750)
* Fix "Filter by commit" Dropdown (#31695) (#31696)
* Properly filter issue list given no assignees filter (#31522) (#31685)
* Prevent update pull refs manually and will not affect other refs update (#31931)(#31955)
* Fix sort order for organization home and user profile page (#31921) (#31922)
* Fix search team (#31923) (#31942)
* Fix 500 error when state params is set when editing issue/PR by API (#31880) (#31952)
* Fix index too many file names bug (#31903) (#31953)
* Add lock for parallel maven upload (#31851) (#31954)
* MISC
* Remove "dsa-1024" testcases from Test_SSHParsePublicKey and Test_calcFingerprint (#31905) (#31914)
* Upgrade bleve to 2.4.2 (#31894)
* Remove unneccessary uses of `word-break: break-all` (#31637) (#31652)
* Return an empty string when a repo has no avatar in the repo API (#31187) (#31567)
* Upgrade micromatch to 4.0.8 (#31944)
* Update webpack to 5.94.0 (#31941)
## [1.22.1](https://github.com/go-gitea/gitea/releases/tag/v1.22.1) - 2024-07-04
* SECURITY
* Add replacement module for `mholt/archiver` (#31267) (#31270)
* API
* Fix missing images in editor preview due to wrong links (#31299) (#31393)
* Fix duplicate sub-path for avatars (#31365) (#31368)
* Reduce memory usage for chunked artifact uploads to MinIO (#31325) (#31338)
* Remove sub-path from container registry realm (#31293) (#31300)
* Fix NuGet Package API for $filter with Id equality (#31188) (#31242)
* Add an immutable tarball link to archive download headers for Nix (#31139) (#31145)
* Add missed return after `ctx.ServerError` (#31130) (#31133)
* BUGFIXES
* Fix avatar radius problem on the new issue page (#31506) (#31508)
* Fix overflow menu flickering on mobile (#31484) (#31488)
* Fix poor table column width due to breaking words (#31473) (#31477)
* Support relative paths to videos from Wiki pages (#31061) (#31453)
* Fix new issue/pr avatar (#31419) (#31424)
* Increase max length of org team names from 30 to 255 characters (#31410) (#31421)
* Fix line number width in code preview (#31307) (#31316)
* Optimize runner-tags layout to enhance visual experience (#31258) (#31263)
* Fix overflow on push notification (#31179) (#31238)
* Fix overflow on notifications (#31178) (#31237)
* Fix overflow in issue card (#31203) (#31225)
* Split sanitizer functions and fine-tune some tests (#31192) (#31200)
* use correct l10n string (#31487) (#31490)
* Fix dropzone JS error when attachment is disabled (#31486)
* Fix web notification icon not updated once you read all notifications (#31447) (#31466)
* Switch to "Write" tab when edit comment again (#31445) (#31461)
* Fix the link for .git-blame-ignore-revs bypass (#31432) (#31442)
* Fix the wrong line number in the diff view page when expanded twice. (#31431) (#31440)
* Fix labels and projects menu overflow on issue page (#31435) (#31439)
* Fix Account Linking UpdateMigrationsByType (#31428) (#31434)
* Fix markdown math brackets render problem (#31420) (#31430)
* Fix rendered wiki page link (#31398) (#31407)
* Fix natural sort (#31384) (#31394)
* Allow downloading attachments of draft releases (#31369) (#31380)
* Fix repo graph JS (#31377)
* Fix incorrect localization `explorer.go` (#31348) (#31350)
* Fix hash render end with colon (#31319) (#31346)
* Fix line number widths (#31341) (#31343)
* Fix navbar `+` menu flashing on page load (#31281) (#31342)
* Fix adopt repository has empty object name in database (#31333) (#31335)
* Delete legacy cookie before setting new cookie (#31306) (#31317)
* Fix some URLs whose sub-path is missing (#31289) (#31292)
* Fix admin oauth2 custom URL settings (#31246) (#31247)
* Make pasted "img" tag has the same behavior as markdown image (#31235) (#31243)
* Fix agit checkout command line hint & fix ShowMergeInstructions checking (#31219) (#31222)
* Fix the possible migration failure on 286 with postgres 16 (#31209) (#31218)
* Fix branch order (#31174) (#31193)
* Fix markup preview (#31158) (#31166)
* Fix push multiple branches error with tests (#31151) (#31153)
* Fix API repository object format missed (#31118) (#31132)
* Fix missing memcache import (#31105) (#31109)
* Upgrade `github.com/hashicorp/go-retryablehttp` (#31499)
* Fix double border in system status table (#31363) (#31401)
* Fix bug filtering issues which have no project (#31337) (#31367)
* Fix #31185 try fix lfs download from bitbucket failed (#31201) (#31329)
* Add nix flake for dev shell (#30967) (#31310)
* Fix and clean up `ConfirmModal` (#31283) (#31291)
* Optimize repo-list layout to enhance visual experience (#31272) (#31276)
* fixed the dropdown menu for the top New button to expand to the left (#31273) (#31275)
* Fix Activity Page Contributors dropdown (#31264) (#31269)
* fix: allow actions artifacts storage migration to complete succesfully (#31251) (#31257)
* Make blockquote attention recognize more syntaxes (#31240) (#31250)
* Remove .segment from .project-column (#31204) (#31239)
* Ignore FindRecentlyPushedNewBranches err (#31164) (#31171)
* Use vertical layout for multiple code expander buttons (#31122) (#31152)
* Remove duplicate `ProxyPreserveHost` in Apache httpd doc (#31143) (#31147)
* Improve mobile review ui (#31091) (#31136)
* Fix DashboardRepoList margin (#31121) (#31128)
* Update pip related commands for docker (#31106) (#31111)
## [1.22.0](https://github.com/go-gitea/gitea/releases/tag/v1.22.0) - 2024-05-27
This release stands as a monumental milestone in our development journey with a record-breaking incorporation of [1528](https://github.com/go-gitea/gitea/pulls?q=is%3Apr+milestone%3A1.22.0+is%3Amerged) pull requests. It marks the most extensive update in Gitea's history, showcasing a plethora of new features and infrastructure improvements.
Noteworthy advancements in this release include the introduction of `HTMX` and `Tailwind`, signaling a strategic shift as we gradually phase out `jquery` and `Fomantic UI`. These changes reflect our commitment to embracing modern technologies and enhancing the user experience.
Key highlights of this release encompass significant changes categorized under `BREAKING`, `FEATURES`, `ENHANCEMENTS`, and `PERFORMANCE`, each contributing to a more robust and efficient Gitea platform.
* BREAKING
* Improve reverse proxy documents and clarify the AppURL guessing behavior (#31003) (#31020)
* Remember log in for a month by default (#30150)
* Breaking summary for template refactoring (#29395)
* All custom templates need to follow these changes
* Recommend/convert to use case-sensitive collation for MySQL/MSSQL (#28662)
* Make offline mode as default to not connect external avatar service by default (#28548)
* Include public repos in the doer's dashboard for issue search (#28304)
* Use restricted sanitizer for repository description (#28141)
* Support storage base path as prefix (#27827)
* Enhanced auth token / remember me (#27606)
* Rename the default themes to `gitea-light`, `gitea-dark`, `gitea-auto` (#27419)
* If you didn't see the new themes, please remove the `[ui].THEMES` config option from `app.ini`
* Require MySQL 8.0, PostgreSQL 12, MSSQL 2012 (#27337)
* FEATURES
* Allow everyone to read or write a wiki by a repo unit setting (#30495)
* Use raw Wiki links for non-renderable Wiki files (#30273)
* Render embedded code preview by permalink in markdown (#30234) (#30249)
* Support repo code search without setting up an indexer (#29998)
* Support pasting URLs over markdown text (#29566)
* Allow to change primary email before account activation (#29412)
* Customizable "Open with" applications for repository clone (#29320)
* Allow options to disable user deletion from the interface on app.ini (#29275)
* Extend issue template YAML engine (#29274)
* Add support for `linguist-detectable` and `linguist-documentation` (#29267)
* Implement code frequency graph (#29191)
* Show commit status for releases (#29149)
* Add user blocking (#29028)
* Actions Artifacts v4 backend (#28965)
* Add merge style `fast-forward-only` (#28954)
* Retarget depending pulls when the parent branch is deleted (#28686)
* Add global setting on how timestamps should be rendered (#28657)
* Implement actions badge SVGs (#28102)
* Add skip ci functionality (#28075)
* Show latest commit for file (#28067)
* Allow to sync tags from the admin dashboard (#28045)
* Add Profile Readme for Organisations (#27955)
* Implement contributors graph (#27882)
* Artifact deletion in actions ui (#27172)
* Add API routes to get runner registration token (#27144)
* Add support for forking single branch (#25821)
* Add support for sha256 repositories (#23894)
* Add admin API route for managing user's badges (#23106)
* ENHANCEMENTS
* Make gitea webhooks openproject compatible (#28435) (#31081)
* Support using label names when changing issue labels (#30943) (#30958)
* Fix various problems around project board view (#30696) (#30902)
* Improve context popup rendering (#30824) (#30829)
* Allow to save empty comment (#30706)
* Prevent allow/reject reviews on merged/closed PRs (#30686)
* Initial support for colorblindness-friendly themes (#30625)
* Some NuGet package enhancements (#30280) (#30324)
* Markup color and font size fixes (#30282) (#30310)
* Show 12 lines in markup code preview (#30255) (#30257)
* Add `[other].SHOW_FOOTER_POWERED_BY` setting to hide `Powered by` (#30253)
* Pulse page improvements (#30149)
* Render code tags in commit messages (#30146)
* Prevent re-review and dismiss review actions on closed and merged PRs (#30065)
* Cancel previous runs of the same PR automatically (#29961)
* Drag-and-drop improvements for projects and issue pins (#29875)
* Add default board to new projects, remove uncategorized pseudo-board (#29874)
* Prevent layout shift in `<overflow-menu>` items (#29831)
* Add skip ci support for pull request title (#29774)
* Add more stats tables (#29730)
* Update API to return 'source_id' for users (#29718)
* Determine fuzziness of bleve indexer by keyword length (#29706)
* Expose fuzzy search for issues/pulls (#29701)
* Put an edit file button on pull request files to allow a quick operation (#29697)
* Fix action runner offline label padding (#29691)
* Update allowed attachment types (#29688)
* Completely style the webkit autofill (#29683)
* Highlight archived labels (#29680)
* Add a warning for disallowed email domains (#29658)
* Set user's 24h preference from their current OS locale (#29651)
* Add setting to disable user features when user login type is not plain (#29615)
* Improve natural sort (#29611)
* Make wiki default branch name changeable (#29603)
* Unify search boxes (#29530)
* Add support for API blob upload of release attachments (#29507)
* Detect broken git hooks (#29494)
* Sync branches to DB immediately when handling git hook calling (#29493)
* Allow options to disable user GPG key configuration from the interface on app.ini (#29486)
* Allow options to disable user SSH key configuration from the interface on app.ini (#29447)
* Use relative links for commits, mentions, and issues in markdown (#29427)
* Add `<overflow-menu>`, rename webcomponents (#29400)
* Include resource state events in Gitlab downloads (#29382)
* Properly migrate target branch change GitLab comment (#29340)
* Recolor dark theme to blue shade (#29283)
* Partially enable MSSQL case-sensitive collation support (#29238)
* Auto-update the system status in the admin dashboard (#29163)
* Integrate alpine `noarch` packages into other architectures index (#29137)
* Document how the TOC election process works (#29135)
* Tweak repo header (#29134)
* Make blockquote border size less aggressive (#29124)
* Downscale pasted PNG images based on metadata (#29123)
* Show `View at this point in history` for every commit (#29122)
* Add support for action artifact serve direct (#29120)
* Change webhook-type in create-view (#29114)
* Drop "@" from the email sender to avoid spam filters (#29109)
* Allow non-admin users to delete review requests (#29057)
* Improve user search display name (#29002)
* Include username in email headers (#28981)
* Show whether a PR is WIP inside popups (#28975)
* Also match weakly validated ETags (#28957)
* Support nuspec manifest download for Nuget packages (#28921)
* Fix hardcoded GitHub icon used as migrated release avatar (#28910)
* Propagate install_if and provider_priority to APKINDEX (#28899)
* Add artifacts v4 JWT to job message and accept it (#28885)
* Enable/disable owner and repo projects independently (#28805)
* Add non-JS fallback for reaction tooltips (#28785)
* Add the ability to see open and closed issues at the same time (#28757)
* Move sign-in labels to be above inputs (#28753)
* Display the latest sync time for pull mirrors on the repo page (#28712)
* Show in Web UI if the file is vendored and generated (#28620)
* Add orphaned topic consistency check (#28507)
* Add branch protection setting for ignoring stale approvals (#28498)
* Add option to set language in admin user view (#28449)
* Fix incorrect run order of action jobs (#28367)
* Add missing exclusive in advanced label options (#28322)
* Added instance-level variables (#28115)
* Add edit option for README.md (#28071)
* Fix link to `Code` tab on wiki commits (#28041)
* Allow to set explore page default sort (#27951)
* Improve PR diff view on mobile (#27883)
* Properly migrate automatic merge GitLab comments (#27873)
* Display issue task list on project cards (#27865)
* Add Index to pull_auto_merge.doer_id (#27811)
* Fix display member unit in the menu bar if there are no hidden members in public org (#27795)
* List all Debian package versions in `Packages` (#27786)
* Allow pull requests Manually Merged option to be used by non-admins (#27780)
* Only show diff file tree when more than one file changed (#27775)
* Show placeholder email in privacy popup (#27770)
* Revamp repo header (#27760)
* Add `must-change-password` command line parameter (#27626)
* Unify password changing and invalidate auth tokens (#27625)
* Add border to file tree 'sub-items' and add padding to 'item-file' (#27593)
* Add slow SQL query warning (#27545)
* Pre-register OAuth application for tea (#27509)
* Differentiate between `push` and `pull` `mirror sync in progress` (#27390)
* Link to file from its history (#27354)
* Add a shortcut to user's profile page to admin user details (#27299)
* Doctor: delete action entries without existing user (#27292)
* Show total TrackedTime on issue/pull/milestone lists (#26672)
* Don't show the new pull request button when the page is not compare pull (#26431)
* Add `Hide/Show all checks` button to commit status check (#26284)
* Improvements of releases list and tags list (#25859)
* PERFORMANCE
* Fix package list performance (#30520) (#30616)
* Add commit status summary table to reduce query from commit status table (#30223)
* Refactor markup/csv: don't read all to memory (#29760)
* Lazy load object format with command line and don't do it in OpenRepository (#29712)
* Add cache for branch divergence on branch list page (#29577)
* Do some performance optimization for issues list and view issue/pull (#29515)
* Cache repository default branch commit status to reduce query on commit status table (#29444)
* Use `crypto/sha256` (#29386)
* Some performance optimization on the dashboard and issues page (#29010)
* Add combined index for issue_user.uid and issue_id (#28080)
## [1.21.11](https://github.com/go-gitea/gitea/releases/tag/v1.21.11) - 2024-04-07 ## [1.21.11](https://github.com/go-gitea/gitea/releases/tag/v1.21.11) - 2024-04-07
* SECURITY * SECURITY

View File

@ -182,7 +182,7 @@ Here's how to run the test suite:
## Translation ## Translation
All translation work happens on [Crowdin](https://crowdin.com/project/gitea). All translation work happens on [Crowdin](https://translate.gitea.com).
The only translation that is maintained in this repository is [the English translation](https://github.com/go-gitea/gitea/blob/main/options/locale/locale_en-US.ini). The only translation that is maintained in this repository is [the English translation](https://github.com/go-gitea/gitea/blob/main/options/locale/locale_en-US.ini).
It is synced regularly with Crowdin. \ It is synced regularly with Crowdin. \
Other locales on main branch **should not** be updated manually as they will be overwritten with each sync. \ Other locales on main branch **should not** be updated manually as they will be overwritten with each sync. \

View File

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

View File

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

View File

@ -31,7 +31,6 @@ Gary Kim <gary@garykim.dev> (@gary-kim)
Guillermo Prandi <gitea.maint@mailfilter.com.ar> (@guillep2k) Guillermo Prandi <gitea.maint@mailfilter.com.ar> (@guillep2k)
Mura Li <typeless@ctli.io> (@typeless) Mura Li <typeless@ctli.io> (@typeless)
6543 <6543@obermui.de> (@6543) 6543 <6543@obermui.de> (@6543)
jaqra <jaqra@hotmail.com> (@jaqra)
David Svantesson <davidsvantesson@gmail.com> (@davidsvantesson) David Svantesson <davidsvantesson@gmail.com> (@davidsvantesson)
a1012112796 <1012112796@qq.com> (@a1012112796) a1012112796 <1012112796@qq.com> (@a1012112796)
Karl Heinz Marbaise <kama@soebes.de> (@khmarbaise) Karl Heinz Marbaise <kama@soebes.de> (@khmarbaise)
@ -46,7 +45,6 @@ Wim <wim@42.be> (@42wim)
Jason Song <i@wolfogre.com> (@wolfogre) Jason Song <i@wolfogre.com> (@wolfogre)
Yarden Shoham <git@yardenshoham.com> (@yardenshoham) Yarden Shoham <git@yardenshoham.com> (@yardenshoham)
Yu Tian <zettat123@gmail.com> (@Zettat123) Yu Tian <zettat123@gmail.com> (@Zettat123)
Eddie Yang <576951401@qq.com> (@yp05327)
Dong Ge <gedong_1994@163.com> (@sillyguodong) Dong Ge <gedong_1994@163.com> (@sillyguodong)
Xinyi Gong <hestergong@gmail.com> (@HesterG) Xinyi Gong <hestergong@gmail.com> (@HesterG)
wxiaoguang <wxiaoguang@gmail.com> (@wxiaoguang) wxiaoguang <wxiaoguang@gmail.com> (@wxiaoguang)
@ -63,3 +61,6 @@ Tim-Niclas Oelschläger <zokki.softwareschmiede@gmail.com> (@zokkis)
Yu Liu <1240335630@qq.com> (@HEREYUA) Yu Liu <1240335630@qq.com> (@HEREYUA)
Kemal Zebari <kemalzebra@gmail.com> (@kemzeb) Kemal Zebari <kemalzebra@gmail.com> (@kemzeb)
Rowan Bohde <rowan.bohde@gmail.com> (@bohde) Rowan Bohde <rowan.bohde@gmail.com> (@bohde)
hiifong <i@hiif.ong> (@hiifong)
metiftikci <metiftikci@hotmail.com> (@metiftikci)
Christopher Homberger <christopher.homberger@web.de> (@ChristopherHX)

263
Makefile
View File

@ -23,20 +23,20 @@ SHASUM ?= shasum -a 256
HAS_GO := $(shell hash $(GO) > /dev/null 2>&1 && echo yes) HAS_GO := $(shell hash $(GO) > /dev/null 2>&1 && echo yes)
COMMA := , COMMA := ,
XGO_VERSION := go-1.23.x XGO_VERSION := go-1.24.x
AIR_PACKAGE ?= github.com/air-verse/air@v1 AIR_PACKAGE ?= github.com/air-verse/air@v1
EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/cmd/editorconfig-checker@2.7.0 EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/v3/cmd/editorconfig-checker@v3.1.2
GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.7.0 GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.7.0
GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/cmd/golangci-lint@v1.60.3 GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/cmd/golangci-lint@v1.64.5
GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.11 GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.12
MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.5.1 MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.6.0
SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.31.0 SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.31.0
XGO_PACKAGE ?= src.techknowlogick.com/xgo@latest XGO_PACKAGE ?= src.techknowlogick.com/xgo@latest
GO_LICENSES_PACKAGE ?= github.com/google/go-licenses@v1 GO_LICENSES_PACKAGE ?= github.com/google/go-licenses@v1
GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1 GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1
ACTIONLINT_PACKAGE ?= github.com/rhysd/actionlint/cmd/actionlint@v1 ACTIONLINT_PACKAGE ?= github.com/rhysd/actionlint/cmd/actionlint@v1
GOPLS_PACKAGE ?= golang.org/x/tools/gopls@v0.15.3 GOPLS_PACKAGE ?= golang.org/x/tools/gopls@v0.17.1
DOCKER_IMAGE ?= gitea/gitea DOCKER_IMAGE ?= gitea/gitea
DOCKER_TAG ?= latest DOCKER_TAG ?= latest
@ -73,6 +73,7 @@ EXTRA_GOFLAGS ?=
MAKE_VERSION := $(shell "$(MAKE)" -v | cat | head -n 1) MAKE_VERSION := $(shell "$(MAKE)" -v | cat | head -n 1)
MAKE_EVIDENCE_DIR := .make_evidence MAKE_EVIDENCE_DIR := .make_evidence
GOTESTFLAGS ?=
ifeq ($(RACE_ENABLED),true) ifeq ($(RACE_ENABLED),true)
GOFLAGS += -race GOFLAGS += -race
GOTESTFLAGS += -race GOTESTFLAGS += -race
@ -114,8 +115,6 @@ LINUX_ARCHS ?= linux/amd64,linux/386,linux/arm-5,linux/arm-6,linux/arm64
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/)) 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/...) MIGRATE_TEST_PACKAGES ?= $(shell $(GO) list code.gitea.io/gitea/models/migrations/...)
FOMANTIC_WORK_DIR := web_src/fomantic
WEBPACK_SOURCES := $(shell find web_src/js web_src/css -type f) WEBPACK_SOURCES := $(shell find web_src/js web_src/css -type f)
WEBPACK_CONFIGS := webpack.config.js tailwind.config.js WEBPACK_CONFIGS := webpack.config.js tailwind.config.js
WEBPACK_DEST := public/assets/js/index.js public/assets/css/index.css WEBPACK_DEST := public/assets/js/index.js public/assets/css/index.css
@ -139,14 +138,14 @@ TAGS_EVIDENCE := $(MAKE_EVIDENCE_DIR)/tags
TEST_TAGS ?= $(TAGS_SPLIT) sqlite sqlite_unlock_notify TEST_TAGS ?= $(TAGS_SPLIT) sqlite sqlite_unlock_notify
TAR_EXCLUDES := .git data indexers queues log node_modules $(EXECUTABLE) $(FOMANTIC_WORK_DIR)/node_modules $(DIST) $(MAKE_EVIDENCE_DIR) $(AIR_TMP_DIR) $(GO_LICENSE_TMP_DIR) TAR_EXCLUDES := .git data indexers queues log node_modules $(EXECUTABLE) $(DIST) $(MAKE_EVIDENCE_DIR) $(AIR_TMP_DIR) $(GO_LICENSE_TMP_DIR)
GO_DIRS := build cmd models modules routers services tests GO_DIRS := build cmd models modules routers services tests
WEB_DIRS := web_src/js web_src/css WEB_DIRS := web_src/js web_src/css
ESLINT_FILES := web_src/js tools *.js *.ts tests/e2e ESLINT_FILES := web_src/js tools *.js *.ts *.cjs tests/e2e
STYLELINT_FILES := web_src/css web_src/js/components/*.vue STYLELINT_FILES := web_src/css web_src/js/components/*.vue
SPELLCHECK_FILES := $(GO_DIRS) $(WEB_DIRS) templates options/locale/locale_en-US.ini .github $(filter-out CHANGELOG.md, $(wildcard *.go *.js *.md *.yml *.yaml *.toml)) SPELLCHECK_FILES := $(GO_DIRS) $(WEB_DIRS) templates options/locale/locale_en-US.ini .github $(filter-out CHANGELOG.md, $(wildcard *.go *.js *.md *.yml *.yaml *.toml)) $(filter-out tools/misspellings.csv, $(wildcard tools/*))
EDITORCONFIG_FILES := templates .github/workflows options/locale/locale_en-US.ini EDITORCONFIG_FILES := templates .github/workflows options/locale/locale_en-US.ini
GO_SOURCES := $(wildcard *.go) GO_SOURCES := $(wildcard *.go)
@ -165,10 +164,8 @@ ifdef DEPS_PLAYWRIGHT
endif endif
SWAGGER_SPEC := templates/swagger/v1_json.tmpl SWAGGER_SPEC := templates/swagger/v1_json.tmpl
SWAGGER_SPEC_S_TMPL := s|"basePath": *"/api/v1"|"basePath": "{{AppSubUrl \| JSEscape}}/api/v1"|g SWAGGER_SPEC_INPUT := templates/swagger/v1_input.json
SWAGGER_SPEC_S_JSON := s|"basePath": *"{{AppSubUrl \| JSEscape}}/api/v1"|"basePath": "/api/v1"|g
SWAGGER_EXCLUDE := code.gitea.io/sdk SWAGGER_EXCLUDE := code.gitea.io/sdk
SWAGGER_NEWLINE_COMMAND := -e '$$a\'
TEST_MYSQL_HOST ?= mysql:3306 TEST_MYSQL_HOST ?= mysql:3306
TEST_MYSQL_DBNAME ?= testgitea TEST_MYSQL_DBNAME ?= testgitea
@ -189,67 +186,11 @@ TEST_MSSQL_PASSWORD ?= MwantsaSecurePassword1
all: build all: build
.PHONY: help .PHONY: help
help: help: Makefile ## print Makefile help information.
@echo "Make Routines:" @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m[TARGETS] default target: build\033[0m\n\n\033[35mTargets:\033[0m\n"} /^[0-9A-Za-z._-]+:.*?##/ { printf " \033[36m%-45s\033[0m %s\n", $$1, $$2 }' Makefile #$(MAKEFILE_LIST)
@echo " - \"\" equivalent to \"build\"" @printf " \033[36m%-46s\033[0m %s\n" "test-e2e[#TestSpecificName]" "test end to end using playwright"
@echo " - build build everything" @printf " \033[36m%-46s\033[0m %s\n" "test[#TestSpecificName]" "run unit test"
@echo " - frontend build frontend files" @printf " \033[36m%-46s\033[0m %s\n" "test-sqlite[#TestSpecificName]" "run integration test for sqlite"
@echo " - backend build backend files"
@echo " - watch watch everything and continuously rebuild"
@echo " - watch-frontend watch frontend files and continuously rebuild"
@echo " - watch-backend watch backend files and continuously rebuild"
@echo " - clean delete backend and integration files"
@echo " - clean-all delete backend, frontend and integration files"
@echo " - deps install dependencies"
@echo " - deps-frontend install frontend dependencies"
@echo " - deps-backend install backend dependencies"
@echo " - deps-tools install tool dependencies"
@echo " - deps-py install python dependencies"
@echo " - lint lint everything"
@echo " - lint-fix lint everything and fix issues"
@echo " - lint-actions lint action workflow files"
@echo " - lint-frontend lint frontend files"
@echo " - lint-frontend-fix lint frontend files and fix issues"
@echo " - lint-backend lint backend files"
@echo " - lint-backend-fix lint backend files and fix issues"
@echo " - lint-go lint go files"
@echo " - lint-go-fix lint go files and fix issues"
@echo " - lint-go-vet lint go files with vet"
@echo " - lint-go-gopls lint go files with gopls"
@echo " - lint-js lint js files"
@echo " - lint-js-fix lint js files and fix issues"
@echo " - lint-css lint css files"
@echo " - lint-css-fix lint css files and fix issues"
@echo " - lint-md lint markdown files"
@echo " - lint-swagger lint swagger files"
@echo " - lint-templates lint template files"
@echo " - lint-yaml lint yaml files"
@echo " - lint-spell lint spelling"
@echo " - lint-spell-fix lint spelling and fix issues"
@echo " - checks run various consistency checks"
@echo " - checks-frontend check frontend files"
@echo " - checks-backend check backend files"
@echo " - test test everything"
@echo " - test-frontend test frontend files"
@echo " - test-backend test backend files"
@echo " - test-e2e[\#TestSpecificName] test end to end using playwright"
@echo " - update update js and py dependencies"
@echo " - update-js update js dependencies"
@echo " - update-py update py dependencies"
@echo " - webpack build webpack files"
@echo " - svg build svg files"
@echo " - fomantic build fomantic files"
@echo " - generate run \"go generate\""
@echo " - fmt format the Go code"
@echo " - generate-license update license files"
@echo " - generate-gitignore update gitignore files"
@echo " - generate-manpage generate manpage"
@echo " - generate-swagger generate the swagger spec from code comments"
@echo " - swagger-validate check if the swagger spec is valid"
@echo " - go-licenses regenerate go licenses"
@echo " - tidy run go mod tidy"
@echo " - test[\#TestSpecificName] run unit test"
@echo " - test-sqlite[\#TestSpecificName] run integration test for sqlite"
.PHONY: go-check .PHONY: go-check
go-check: go-check:
@ -280,11 +221,11 @@ node-check:
fi fi
.PHONY: clean-all .PHONY: clean-all
clean-all: clean clean-all: clean ## delete backend, frontend and integration files
rm -rf $(WEBPACK_DEST_ENTRIES) node_modules rm -rf $(WEBPACK_DEST_ENTRIES) node_modules
.PHONY: clean .PHONY: clean
clean: clean: ## delete backend and integration files
rm -rf $(EXECUTABLE) $(DIST) $(BINDATA_DEST) $(BINDATA_HASH) \ rm -rf $(EXECUTABLE) $(DIST) $(BINDATA_DEST) $(BINDATA_HASH) \
integrations*.test \ integrations*.test \
e2e*.test \ e2e*.test \
@ -296,7 +237,7 @@ clean:
tests/e2e/reports/ tests/e2e/test-artifacts/ tests/e2e/test-snapshots/ tests/e2e/reports/ tests/e2e/test-artifacts/ tests/e2e/test-snapshots/
.PHONY: fmt .PHONY: fmt
fmt: fmt: ## format the Go code
@GOFUMPT_PACKAGE=$(GOFUMPT_PACKAGE) $(GO) run build/code-batch-process.go gitea-fmt -w '{file-list}' @GOFUMPT_PACKAGE=$(GOFUMPT_PACKAGE) $(GO) run build/code-batch-process.go gitea-fmt -w '{file-list}'
$(eval TEMPLATES := $(shell find templates -type f -name '*.tmpl')) $(eval TEMPLATES := $(shell find templates -type f -name '*.tmpl'))
@# strip whitespace after '{{' or '(' and before '}}' or ')' unless there is only @# strip whitespace after '{{' or '(' and before '}}' or ')' unless there is only
@ -311,7 +252,7 @@ fmt-check: fmt
@diff=$$(git diff --color=always $(GO_SOURCES) templates $(WEB_DIRS)); \ @diff=$$(git diff --color=always $(GO_SOURCES) templates $(WEB_DIRS)); \
if [ -n "$$diff" ]; then \ if [ -n "$$diff" ]; then \
echo "Please run 'make fmt' and commit the result:"; \ echo "Please run 'make fmt' and commit the result:"; \
echo "$${diff}"; \ printf "%s" "$${diff}"; \
exit 1; \ exit 1; \
fi fi
@ -325,95 +266,95 @@ TAGS_PREREQ := $(TAGS_EVIDENCE)
endif endif
.PHONY: generate-swagger .PHONY: generate-swagger
generate-swagger: $(SWAGGER_SPEC) generate-swagger: $(SWAGGER_SPEC) ## generate the swagger spec from code comments
$(SWAGGER_SPEC): $(GO_SOURCES_NO_BINDATA) $(SWAGGER_SPEC): $(GO_SOURCES_NO_BINDATA) $(SWAGGER_SPEC_INPUT)
$(GO) run $(SWAGGER_PACKAGE) generate spec -x "$(SWAGGER_EXCLUDE)" -o './$(SWAGGER_SPEC)' $(GO) run $(SWAGGER_PACKAGE) generate spec --exclude "$(SWAGGER_EXCLUDE)" --input "$(SWAGGER_SPEC_INPUT)" --output './$(SWAGGER_SPEC)'
$(SED_INPLACE) '$(SWAGGER_SPEC_S_TMPL)' './$(SWAGGER_SPEC)'
$(SED_INPLACE) $(SWAGGER_NEWLINE_COMMAND) './$(SWAGGER_SPEC)'
.PHONY: swagger-check .PHONY: swagger-check
swagger-check: generate-swagger swagger-check: generate-swagger
@diff=$$(git diff --color=always '$(SWAGGER_SPEC)'); \ @diff=$$(git diff --color=always '$(SWAGGER_SPEC)'); \
if [ -n "$$diff" ]; then \ if [ -n "$$diff" ]; then \
echo "Please run 'make generate-swagger' and commit the result:"; \ echo "Please run 'make generate-swagger' and commit the result:"; \
echo "$${diff}"; \ printf "%s" "$${diff}"; \
exit 1; \ exit 1; \
fi fi
.PHONY: swagger-validate .PHONY: swagger-validate
swagger-validate: swagger-validate: ## check if the swagger spec is valid
$(SED_INPLACE) '$(SWAGGER_SPEC_S_JSON)' './$(SWAGGER_SPEC)' @# swagger "validate" requires that the "basePath" must start with a slash, but we are using Golang template "{{...}}"
@$(SED_INPLACE) -E -e 's|"basePath":( *)"(.*)"|"basePath":\1"/\2"|g' './$(SWAGGER_SPEC)' # add a prefix slash to basePath
@# FIXME: there are some warnings
$(GO) run $(SWAGGER_PACKAGE) validate './$(SWAGGER_SPEC)' $(GO) run $(SWAGGER_PACKAGE) validate './$(SWAGGER_SPEC)'
$(SED_INPLACE) '$(SWAGGER_SPEC_S_TMPL)' './$(SWAGGER_SPEC)' @$(SED_INPLACE) -E -e 's|"basePath":( *)"/(.*)"|"basePath":\1"\2"|g' './$(SWAGGER_SPEC)' # remove the prefix slash from basePath
.PHONY: checks .PHONY: checks
checks: checks-frontend checks-backend checks: checks-frontend checks-backend ## run various consistency checks
.PHONY: checks-frontend .PHONY: checks-frontend
checks-frontend: lockfile-check svg-check checks-frontend: lockfile-check svg-check ## check frontend files
.PHONY: checks-backend .PHONY: checks-backend
checks-backend: tidy-check swagger-check fmt-check swagger-validate security-check checks-backend: tidy-check swagger-check fmt-check swagger-validate security-check ## check backend files
.PHONY: lint .PHONY: lint
lint: lint-frontend lint-backend lint-spell lint: lint-frontend lint-backend lint-spell ## lint everything
.PHONY: lint-fix .PHONY: lint-fix
lint-fix: lint-frontend-fix lint-backend-fix lint-spell-fix lint-fix: lint-frontend-fix lint-backend-fix lint-spell-fix ## lint everything and fix issues
.PHONY: lint-frontend .PHONY: lint-frontend
lint-frontend: lint-js lint-css lint-frontend: lint-js lint-css ## lint frontend files
.PHONY: lint-frontend-fix .PHONY: lint-frontend-fix
lint-frontend-fix: lint-js-fix lint-css-fix lint-frontend-fix: lint-js-fix lint-css-fix ## lint frontend files and fix issues
.PHONY: lint-backend .PHONY: lint-backend
lint-backend: lint-go lint-go-vet lint-go-gopls lint-editorconfig lint-backend: lint-go lint-go-gitea-vet lint-go-gopls lint-editorconfig ## lint backend files
.PHONY: lint-backend-fix .PHONY: lint-backend-fix
lint-backend-fix: lint-go-fix lint-go-vet lint-editorconfig lint-backend-fix: lint-go-fix lint-go-gitea-vet lint-editorconfig ## lint backend files and fix issues
.PHONY: lint-js .PHONY: lint-js
lint-js: node_modules lint-js: node_modules ## lint js files
npx eslint --color --max-warnings=0 --ext js,ts,vue $(ESLINT_FILES) npx eslint --color --max-warnings=0 --ext js,ts,vue $(ESLINT_FILES)
# npx tsc npx vue-tsc
.PHONY: lint-js-fix .PHONY: lint-js-fix
lint-js-fix: node_modules lint-js-fix: node_modules ## lint js files and fix issues
npx eslint --color --max-warnings=0 --ext js,ts,vue $(ESLINT_FILES) --fix npx eslint --color --max-warnings=0 --ext js,ts,vue $(ESLINT_FILES) --fix
# npx tsc npx vue-tsc
.PHONY: lint-css .PHONY: lint-css
lint-css: node_modules lint-css: node_modules ## lint css files
npx stylelint --color --max-warnings=0 $(STYLELINT_FILES) npx stylelint --color --max-warnings=0 $(STYLELINT_FILES)
.PHONY: lint-css-fix .PHONY: lint-css-fix
lint-css-fix: node_modules lint-css-fix: node_modules ## lint css files and fix issues
npx stylelint --color --max-warnings=0 $(STYLELINT_FILES) --fix npx stylelint --color --max-warnings=0 $(STYLELINT_FILES) --fix
.PHONY: lint-swagger .PHONY: lint-swagger
lint-swagger: node_modules lint-swagger: node_modules ## lint swagger files
npx spectral lint -q -F hint $(SWAGGER_SPEC) npx spectral lint -q -F hint $(SWAGGER_SPEC)
.PHONY: lint-md .PHONY: lint-md
lint-md: node_modules lint-md: node_modules ## lint markdown files
npx markdownlint *.md npx markdownlint *.md
.PHONY: lint-spell .PHONY: lint-spell
lint-spell: lint-spell: ## lint spelling
@go run $(MISSPELL_PACKAGE) -dict tools/misspellings.csv -error $(SPELLCHECK_FILES) @go run $(MISSPELL_PACKAGE) -dict tools/misspellings.csv -error $(SPELLCHECK_FILES)
.PHONY: lint-spell-fix .PHONY: lint-spell-fix
lint-spell-fix: lint-spell-fix: ## lint spelling and fix issues
@go run $(MISSPELL_PACKAGE) -dict tools/misspellings.csv -w $(SPELLCHECK_FILES) @go run $(MISSPELL_PACKAGE) -dict tools/misspellings.csv -w $(SPELLCHECK_FILES)
.PHONY: lint-go .PHONY: lint-go
lint-go: lint-go: ## lint go files
$(GO) run $(GOLANGCI_LINT_PACKAGE) run $(GO) run $(GOLANGCI_LINT_PACKAGE) run
.PHONY: lint-go-fix .PHONY: lint-go-fix
lint-go-fix: lint-go-fix: ## lint go files and fix issues
$(GO) run $(GOLANGCI_LINT_PACKAGE) run --fix $(GO) run $(GOLANGCI_LINT_PACKAGE) run --fix
# workaround step for the lint-go-windows CI task because 'go run' can not # workaround step for the lint-go-windows CI task because 'go run' can not
@ -423,57 +364,58 @@ lint-go-windows:
@GOOS= GOARCH= $(GO) install $(GOLANGCI_LINT_PACKAGE) @GOOS= GOARCH= $(GO) install $(GOLANGCI_LINT_PACKAGE)
golangci-lint run golangci-lint run
.PHONY: lint-go-vet .PHONY: lint-go-gitea-vet
lint-go-vet: lint-go-gitea-vet: ## lint go files with gitea-vet
@echo "Running go vet..." @echo "Running gitea-vet..."
@GOOS= GOARCH= $(GO) build code.gitea.io/gitea-vet @GOOS= GOARCH= $(GO) build code.gitea.io/gitea-vet
@$(GO) vet -vettool=gitea-vet ./... @$(GO) vet -vettool=gitea-vet ./...
.PHONY: lint-go-gopls .PHONY: lint-go-gopls
lint-go-gopls: lint-go-gopls: ## lint go files with gopls
@echo "Running gopls check..." @echo "Running gopls check..."
@GO=$(GO) GOPLS_PACKAGE=$(GOPLS_PACKAGE) tools/lint-go-gopls.sh $(GO_SOURCES_NO_BINDATA) @GO=$(GO) GOPLS_PACKAGE=$(GOPLS_PACKAGE) tools/lint-go-gopls.sh $(GO_SOURCES_NO_BINDATA)
.PHONY: lint-editorconfig .PHONY: lint-editorconfig
lint-editorconfig: lint-editorconfig:
@echo "Running editorconfig check..."
@$(GO) run $(EDITORCONFIG_CHECKER_PACKAGE) $(EDITORCONFIG_FILES) @$(GO) run $(EDITORCONFIG_CHECKER_PACKAGE) $(EDITORCONFIG_FILES)
.PHONY: lint-actions .PHONY: lint-actions
lint-actions: lint-actions: ## lint action workflow files
$(GO) run $(ACTIONLINT_PACKAGE) $(GO) run $(ACTIONLINT_PACKAGE)
.PHONY: lint-templates .PHONY: lint-templates
lint-templates: .venv node_modules lint-templates: .venv node_modules ## lint template files
@node tools/lint-templates-svg.js @node tools/lint-templates-svg.js
@poetry run djlint $(shell find templates -type f -iname '*.tmpl') @poetry run djlint $(shell find templates -type f -iname '*.tmpl')
.PHONY: lint-yaml .PHONY: lint-yaml
lint-yaml: .venv lint-yaml: .venv ## lint yaml files
@poetry run yamllint . @poetry run yamllint -s .
.PHONY: watch .PHONY: watch
watch: watch: ## watch everything and continuously rebuild
@bash tools/watch.sh @bash tools/watch.sh
.PHONY: watch-frontend .PHONY: watch-frontend
watch-frontend: node-check node_modules watch-frontend: node-check node_modules ## watch frontend files and continuously rebuild
@rm -rf $(WEBPACK_DEST_ENTRIES) @rm -rf $(WEBPACK_DEST_ENTRIES)
NODE_ENV=development npx webpack --watch --progress NODE_ENV=development npx webpack --watch --progress
.PHONY: watch-backend .PHONY: watch-backend
watch-backend: go-check watch-backend: go-check ## watch backend files and continuously rebuild
GITEA_RUN_MODE=dev $(GO) run $(AIR_PACKAGE) -c .air.toml GITEA_RUN_MODE=dev $(GO) run $(AIR_PACKAGE) -c .air.toml
.PHONY: test .PHONY: test
test: test-frontend test-backend test: test-frontend test-backend ## test everything
.PHONY: test-backend .PHONY: test-backend
test-backend: test-backend: ## test frontend files
@echo "Running go test with $(GOTESTFLAGS) -tags '$(TEST_TAGS)'..." @echo "Running go test with $(GOTESTFLAGS) -tags '$(TEST_TAGS)'..."
@$(GO) test $(GOTESTFLAGS) -tags='$(TEST_TAGS)' $(GO_TEST_PACKAGES) @$(GO) test $(GOTESTFLAGS) -tags='$(TEST_TAGS)' $(GO_TEST_PACKAGES)
.PHONY: test-frontend .PHONY: test-frontend
test-frontend: node_modules test-frontend: node_modules ## test backend files
npx vitest npx vitest
.PHONY: test-check .PHONY: test-check
@ -482,7 +424,7 @@ test-check:
@diff=$$(git status -s); \ @diff=$$(git status -s); \
if [ -n "$$diff" ]; then \ if [ -n "$$diff" ]; then \
echo "make test-backend has changed files in the source tree:"; \ echo "make test-backend has changed files in the source tree:"; \
echo "$${diff}"; \ printf "%s" "$${diff}"; \
echo "You should change the tests to create these files in a temporary directory."; \ echo "You should change the tests to create these files in a temporary directory."; \
echo "Do not simply add these files to .gitignore"; \ echo "Do not simply add these files to .gitignore"; \
exit 1; \ exit 1; \
@ -505,7 +447,7 @@ unit-test-coverage:
@$(GO) test $(GOTESTFLAGS) -timeout=20m -tags='$(TEST_TAGS)' -cover -coverprofile coverage.out $(GO_TEST_PACKAGES) && echo "\n==>\033[32m Ok\033[m\n" || exit 1 @$(GO) test $(GOTESTFLAGS) -timeout=20m -tags='$(TEST_TAGS)' -cover -coverprofile coverage.out $(GO_TEST_PACKAGES) && echo "\n==>\033[32m Ok\033[m\n" || exit 1
.PHONY: tidy .PHONY: tidy
tidy: tidy: ## run go mod tidy
$(eval MIN_GO_VERSION := $(shell grep -Eo '^go\s+[0-9]+\.[0-9.]+' go.mod | cut -d' ' -f2)) $(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) $(GO) mod tidy -compat=$(MIN_GO_VERSION)
@$(MAKE) --no-print-directory $(GO_LICENSE_FILE) @$(MAKE) --no-print-directory $(GO_LICENSE_FILE)
@ -519,15 +461,17 @@ tidy-check: tidy
@diff=$$(git diff --color=always go.mod go.sum $(GO_LICENSE_FILE)); \ @diff=$$(git diff --color=always go.mod go.sum $(GO_LICENSE_FILE)); \
if [ -n "$$diff" ]; then \ if [ -n "$$diff" ]; then \
echo "Please run 'make tidy' and commit the result:"; \ echo "Please run 'make tidy' and commit the result:"; \
echo "$${diff}"; \ printf "%s" "$${diff}"; \
exit 1; \ exit 1; \
fi fi
.PHONY: go-licenses .PHONY: go-licenses
go-licenses: $(GO_LICENSE_FILE) go-licenses: $(GO_LICENSE_FILE) ## regenerate go licenses
$(GO_LICENSE_FILE): go.mod go.sum $(GO_LICENSE_FILE): go.mod go.sum
-$(GO) run $(GO_LICENSES_PACKAGE) save . --force --save_path=$(GO_LICENSE_TMP_DIR) 2>/dev/null @rm -rf $(GO_LICENSE_FILE)
$(GO) install $(GO_LICENSES_PACKAGE)
-GOOS=linux CGO_ENABLED=1 go-licenses save . --force --save_path=$(GO_LICENSE_TMP_DIR) 2>/dev/null
$(GO) run build/generate-go-licenses.go $(GO_LICENSE_TMP_DIR) $(GO_LICENSE_FILE) $(GO) run build/generate-go-licenses.go $(GO_LICENSE_TMP_DIR) $(GO_LICENSE_FILE)
@rm -rf $(GO_LICENSE_TMP_DIR) @rm -rf $(GO_LICENSE_TMP_DIR)
@ -771,17 +715,17 @@ install: $(wildcard *.go)
CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) install -v -tags '$(TAGS)' -ldflags '-s -w $(LDFLAGS)' CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) install -v -tags '$(TAGS)' -ldflags '-s -w $(LDFLAGS)'
.PHONY: build .PHONY: build
build: frontend backend build: frontend backend ## build everything
.PHONY: frontend .PHONY: frontend
frontend: $(WEBPACK_DEST) frontend: $(WEBPACK_DEST) ## build frontend files
.PHONY: backend .PHONY: backend
backend: go-check generate-backend $(EXECUTABLE) backend: go-check generate-backend $(EXECUTABLE) ## build backend files
# We generate the backend before the frontend in case we in future we want to generate things in the frontend from generated files in backend # We generate the backend before the frontend in case we in future we want to generate things in the frontend from generated files in backend
.PHONY: generate .PHONY: generate
generate: generate-backend generate: generate-backend ## run "go generate"
.PHONY: generate-backend .PHONY: generate-backend
generate-backend: $(TAGS_PREREQ) generate-go generate-backend: $(TAGS_PREREQ) generate-go
@ -806,22 +750,22 @@ $(DIST_DIRS):
.PHONY: release-windows .PHONY: release-windows
release-windows: | $(DIST_DIRS) 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))) 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 endif
.PHONY: release-linux .PHONY: release-linux
release-linux: | $(DIST_DIRS) 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 .PHONY: release-darwin
release-darwin: | $(DIST_DIRS) 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 .PHONY: release-freebsd
release-freebsd: | $(DIST_DIRS) 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 .PHONY: release-copy
release-copy: | $(DIST_DIRS) release-copy: | $(DIST_DIRS)
@ -846,20 +790,20 @@ release-sources: | $(DIST_DIRS)
rm -f $(STORED_VERSION_FILE) rm -f $(STORED_VERSION_FILE)
.PHONY: deps .PHONY: deps
deps: deps-frontend deps-backend deps-tools deps-py deps: deps-frontend deps-backend deps-tools deps-py ## install dependencies
.PHONY: deps-py .PHONY: deps-py
deps-py: .venv deps-py: .venv ## install python dependencies
.PHONY: deps-frontend .PHONY: deps-frontend
deps-frontend: node_modules deps-frontend: node_modules ## install frontend dependencies
.PHONY: deps-backend .PHONY: deps-backend
deps-backend: deps-backend: ## install backend dependencies
$(GO) mod download $(GO) mod download
.PHONY: deps-tools .PHONY: deps-tools
deps-tools: deps-tools: ## install tool dependencies
$(GO) install $(AIR_PACKAGE) & \ $(GO) install $(AIR_PACKAGE) & \
$(GO) install $(EDITORCONFIG_CHECKER_PACKAGE) & \ $(GO) install $(EDITORCONFIG_CHECKER_PACKAGE) & \
$(GO) install $(GOFUMPT_PACKAGE) & \ $(GO) install $(GOFUMPT_PACKAGE) & \
@ -883,10 +827,10 @@ node_modules: package-lock.json
@touch .venv @touch .venv
.PHONY: update .PHONY: update
update: update-js update-py update: update-js update-py ## update js and py dependencies
.PHONY: update-js .PHONY: update-js
update-js: node-check | node_modules update-js: node-check | node_modules ## update js dependencies
npx updates -u -f package.json npx updates -u -f package.json
rm -rf node_modules package-lock.json rm -rf node_modules package-lock.json
npm install --package-lock npm install --package-lock
@ -895,27 +839,14 @@ update-js: node-check | node_modules
@touch node_modules @touch node_modules
.PHONY: update-py .PHONY: update-py
update-py: node-check | node_modules update-py: node-check | node_modules ## update py dependencies
npx updates -u -f pyproject.toml npx updates -u -f pyproject.toml
rm -rf .venv poetry.lock rm -rf .venv poetry.lock
poetry install poetry install
@touch .venv @touch .venv
.PHONY: fomantic
fomantic:
rm -rf $(FOMANTIC_WORK_DIR)/build
cd $(FOMANTIC_WORK_DIR) && npm install --no-save
cp -f $(FOMANTIC_WORK_DIR)/theme.config.less $(FOMANTIC_WORK_DIR)/node_modules/fomantic-ui/src/theme.config
cp -rf $(FOMANTIC_WORK_DIR)/_site $(FOMANTIC_WORK_DIR)/node_modules/fomantic-ui/src/
$(SED_INPLACE) -e 's/ overrideBrowserslist\r/ overrideBrowserslist: ["defaults"]\r/g' $(FOMANTIC_WORK_DIR)/node_modules/fomantic-ui/tasks/config/tasks.js
cd $(FOMANTIC_WORK_DIR) && npx gulp -f node_modules/fomantic-ui/gulpfile.js build
# fomantic uses "touchstart" as click event for some browsers, it's not ideal, so we force fomantic to always use "click" as click event
$(SED_INPLACE) -e 's/clickEvent[ \t]*=/clickEvent = "click", unstableClickEvent =/g' $(FOMANTIC_WORK_DIR)/build/semantic.js
$(SED_INPLACE) -e 's/\r//g' $(FOMANTIC_WORK_DIR)/build/semantic.css $(FOMANTIC_WORK_DIR)/build/semantic.js
rm -f $(FOMANTIC_WORK_DIR)/build/*.min.*
.PHONY: webpack .PHONY: webpack
webpack: $(WEBPACK_DEST) webpack: $(WEBPACK_DEST) ## build webpack files
$(WEBPACK_DEST): $(WEBPACK_SOURCES) $(WEBPACK_CONFIGS) package-lock.json $(WEBPACK_DEST): $(WEBPACK_SOURCES) $(WEBPACK_CONFIGS) package-lock.json
@$(MAKE) -s node-check node_modules @$(MAKE) -s node-check node_modules
@ -925,7 +856,7 @@ $(WEBPACK_DEST): $(WEBPACK_SOURCES) $(WEBPACK_CONFIGS) package-lock.json
@touch $(WEBPACK_DEST) @touch $(WEBPACK_DEST)
.PHONY: svg .PHONY: svg
svg: node-check | node_modules svg: node-check | node_modules ## build svg files
rm -rf $(SVG_DEST_DIR) rm -rf $(SVG_DEST_DIR)
node tools/generate-svg.js node tools/generate-svg.js
@ -935,7 +866,7 @@ svg-check: svg
@diff=$$(git diff --color=always --cached $(SVG_DEST_DIR)); \ @diff=$$(git diff --color=always --cached $(SVG_DEST_DIR)); \
if [ -n "$$diff" ]; then \ if [ -n "$$diff" ]; then \
echo "Please run 'make svg' and 'git add $(SVG_DEST_DIR)' and commit the result:"; \ echo "Please run 'make svg' and 'git add $(SVG_DEST_DIR)' and commit the result:"; \
echo "$${diff}"; \ printf "%s" "$${diff}"; \
exit 1; \ exit 1; \
fi fi
@ -946,7 +877,7 @@ lockfile-check:
if [ -n "$$diff" ]; then \ if [ -n "$$diff" ]; then \
echo "package-lock.json is inconsistent with package.json"; \ echo "package-lock.json is inconsistent with package.json"; \
echo "Please run 'npm install --package-lock-only' and commit the result:"; \ echo "Please run 'npm install --package-lock-only' and commit the result:"; \
echo "$${diff}"; \ printf "%s" "$${diff}"; \
exit 1; \ exit 1; \
fi fi
@ -960,12 +891,8 @@ update-translations:
mv ./translations/*.ini ./options/locale/ mv ./translations/*.ini ./options/locale/
rmdir ./translations rmdir ./translations
.PHONY: generate-license
generate-license:
$(GO) run build/generate-licenses.go
.PHONY: generate-gitignore .PHONY: generate-gitignore
generate-gitignore: generate-gitignore: ## update gitignore files
$(GO) run build/generate-gitignores.go $(GO) run build/generate-gitignores.go
.PHONY: generate-images .PHONY: generate-images
@ -974,7 +901,7 @@ generate-images: | node_modules
node tools/generate-images.js $(TAGS) node tools/generate-images.js $(TAGS)
.PHONY: generate-manpage .PHONY: generate-manpage
generate-manpage: generate-manpage: ## generate manpage
@[ -f gitea ] || make backend @[ -f gitea ] || make backend
@mkdir -p man/man1/ man/man5 @mkdir -p man/man1/ man/man5
@./gitea docs --man > man/man1/gitea.1 @./gitea docs --man > man/man1/gitea.1

106
README.md
View File

@ -9,7 +9,7 @@
[![](https://opencollective.com/gitea/tiers/backers/badge.svg?label=backers&color=brightgreen)](https://opencollective.com/gitea "Become a backer/sponsor of gitea") [![](https://opencollective.com/gitea/tiers/backers/badge.svg?label=backers&color=brightgreen)](https://opencollective.com/gitea "Become a backer/sponsor of gitea")
[![](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT "License: MIT") [![](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT "License: MIT")
[![Contribute with Gitpod](https://img.shields.io/badge/Contribute%20with-Gitpod-908a85?logo=gitpod&color=green)](https://gitpod.io/#https://github.com/go-gitea/gitea) [![Contribute with Gitpod](https://img.shields.io/badge/Contribute%20with-Gitpod-908a85?logo=gitpod&color=green)](https://gitpod.io/#https://github.com/go-gitea/gitea)
[![](https://badges.crowdin.net/gitea/localized.svg)](https://crowdin.com/project/gitea "Crowdin") [![](https://badges.crowdin.net/gitea/localized.svg)](https://translate.gitea.com "Crowdin")
[View this document in Chinese](./README_ZH.md) [View this document in Chinese](./README_ZH.md)
@ -31,6 +31,14 @@ For accessing free Gitea service (with a limited number of repositories), you ca
To quickly deploy your own dedicated Gitea instance on Gitea Cloud, you can start a free trial at [cloud.gitea.com](https://cloud.gitea.com). To quickly deploy your own dedicated Gitea instance on Gitea Cloud, you can start a free trial at [cloud.gitea.com](https://cloud.gitea.com).
## Documentation
You can find comprehensive documentation on our official [documentation website](https://docs.gitea.com/).
It includes installation, administration, usage, development, contributing guides, and more to help you get started and explore all features effectively.
If you have any suggestions or would like to contribute to it, you can visit the [documentation repository](https://gitea.com/gitea/docs)
## Building ## Building
From the root of the source tree, run: From the root of the source tree, run:
@ -52,6 +60,8 @@ More info: https://docs.gitea.com/installation/install-from-source
## Using ## Using
After building, a binary file named `gitea` will be generated in the root of the source tree by default. To run it, use:
./gitea web ./gitea web
> [!NOTE] > [!NOTE]
@ -68,22 +78,25 @@ Expected workflow is: Fork -> Patch -> Push -> Pull Request
## Translating ## Translating
Translations are done through Crowdin. If you want to translate to a new language ask one of the managers in the Crowdin project to add a new language there. [![Crowdin](https://badges.crowdin.net/gitea/localized.svg)](https://translate.gitea.com)
Translations are done through [Crowdin](https://translate.gitea.com). If you want to translate to a new language ask one of the managers in the Crowdin project to add a new language there.
You can also just create an issue for adding a language or ask on discord on the #translation channel. If you need context or find some translation issues, you can leave a comment on the string or ask on Discord. For general translation questions there is a section in the docs. Currently a bit empty but we hope to fill it as questions pop up. You can also just create an issue for adding a language or ask on discord on the #translation channel. If you need context or find some translation issues, you can leave a comment on the string or ask on Discord. For general translation questions there is a section in the docs. Currently a bit empty but we hope to fill it as questions pop up.
https://docs.gitea.com/contributing/localization Get more information from [documentation](https://docs.gitea.com/contributing/localization).
[![Crowdin](https://badges.crowdin.net/gitea/localized.svg)](https://crowdin.com/project/gitea) ## Official and Third-Party Projects
## Further information We provide an official [go-sdk](https://gitea.com/gitea/go-sdk), a CLI tool called [tea](https://gitea.com/gitea/tea) and an [action runner](https://gitea.com/gitea/act_runner) for Gitea Action.
For more information and instructions about how to install Gitea, please look at our [documentation](https://docs.gitea.com/). We maintain a list of Gitea-related projects at [gitea/awesome-gitea](https://gitea.com/gitea/awesome-gitea), where you can discover more third-party projects, including SDKs, plugins, themes, and more.
If you have questions that are not covered by the documentation, you can get in contact with us on our [Discord server](https://discord.gg/Gitea) or create a post in the [discourse forum](https://forum.gitea.com/).
We maintain a list of Gitea-related projects at [gitea/awesome-gitea](https://gitea.com/gitea/awesome-gitea). ## Communication
The official Gitea CLI is developed at [gitea/tea](https://gitea.com/gitea/tea). [![](https://img.shields.io/discord/322538954119184384.svg?logo=discord&logoColor=white&label=Discord&color=5865F2)](https://discord.gg/Gitea "Join the Discord chat at https://discord.gg/Gitea")
If you have questions that are not covered by the [documentation](https://docs.gitea.com/), you can get in contact with us on our [Discord server](https://discord.gg/Gitea) or create a post in the [discourse forum](https://forum.gitea.com/).
## Authors ## Authors
@ -122,18 +135,79 @@ Gitea is pronounced [/ɡɪti:/](https://youtu.be/EM71-2uDAoY) as in "gi-tea"
We're [working on it](https://github.com/go-gitea/gitea/issues/1029). We're [working on it](https://github.com/go-gitea/gitea/issues/1029).
**Where can I find the security patches?**
In the [release log](https://github.com/go-gitea/gitea/releases) or the [change log](https://github.com/go-gitea/gitea/blob/main/CHANGELOG.md), search for the keyword `SECURITY` to find the security patches.
## License ## License
This project is licensed under the MIT License. This project is licensed under the MIT License.
See the [LICENSE](https://github.com/go-gitea/gitea/blob/main/LICENSE) file See the [LICENSE](https://github.com/go-gitea/gitea/blob/main/LICENSE) file
for the full license text. for the full license text.
## Screenshots ## Further information
Looking for an overview of the interface? Check it out! <details>
<summary>Looking for an overview of the interface? Check it out!</summary>
|![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)| ### Login/Register Page
|:---:|:---:|:---:|
|![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)| ![Login](https://dl.gitea.com/screenshots/login.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)| ![Register](https://dl.gitea.com/screenshots/register.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)|
### User Dashboard
![Home](https://dl.gitea.com/screenshots/home.png)
![Issues](https://dl.gitea.com/screenshots/issues.png)
![Pull Requests](https://dl.gitea.com/screenshots/pull_requests.png)
![Milestones](https://dl.gitea.com/screenshots/milestones.png)
### User Profile
![Profile](https://dl.gitea.com/screenshots/user_profile.png)
### Explore
![Repos](https://dl.gitea.com/screenshots/explore_repos.png)
![Users](https://dl.gitea.com/screenshots/explore_users.png)
![Orgs](https://dl.gitea.com/screenshots/explore_orgs.png)
### Repository
![Home](https://dl.gitea.com/screenshots/repo_home.png)
![Commits](https://dl.gitea.com/screenshots/repo_commits.png)
![Branches](https://dl.gitea.com/screenshots/repo_branches.png)
![Labels](https://dl.gitea.com/screenshots/repo_labels.png)
![Milestones](https://dl.gitea.com/screenshots/repo_milestones.png)
![Releases](https://dl.gitea.com/screenshots/repo_releases.png)
![Tags](https://dl.gitea.com/screenshots/repo_tags.png)
#### Repository Issue
![List](https://dl.gitea.com/screenshots/repo_issues.png)
![Issue](https://dl.gitea.com/screenshots/repo_issue.png)
#### Repository Pull Requests
![List](https://dl.gitea.com/screenshots/repo_pull_requests.png)
![Pull Request](https://dl.gitea.com/screenshots/repo_pull_request.png)
![File](https://dl.gitea.com/screenshots/repo_pull_request_file.png)
![Commits](https://dl.gitea.com/screenshots/repo_pull_request_commits.png)
#### Repository Actions
![List](https://dl.gitea.com/screenshots/repo_actions.png)
![Details](https://dl.gitea.com/screenshots/repo_actions_run.png)
#### Repository Activity
![Activity](https://dl.gitea.com/screenshots/repo_activity.png)
![Contributors](https://dl.gitea.com/screenshots/repo_contributors.png)
![Code Frequency](https://dl.gitea.com/screenshots/repo_code_frequency.png)
![Recent Commits](https://dl.gitea.com/screenshots/repo_recent_commits.png)
### Organization
![Home](https://dl.gitea.com/screenshots/org_home.png)
</details>

View File

@ -9,13 +9,13 @@
[![](https://opencollective.com/gitea/tiers/backers/badge.svg?label=backers&color=brightgreen)](https://opencollective.com/gitea "Become a backer/sponsor of gitea") [![](https://opencollective.com/gitea/tiers/backers/badge.svg?label=backers&color=brightgreen)](https://opencollective.com/gitea "Become a backer/sponsor of gitea")
[![](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT "License: MIT") [![](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT "License: MIT")
[![Contribute with Gitpod](https://img.shields.io/badge/Contribute%20with-Gitpod-908a85?logo=gitpod&color=green)](https://gitpod.io/#https://github.com/go-gitea/gitea) [![Contribute with Gitpod](https://img.shields.io/badge/Contribute%20with-Gitpod-908a85?logo=gitpod&color=green)](https://gitpod.io/#https://github.com/go-gitea/gitea)
[![](https://badges.crowdin.net/gitea/localized.svg)](https://crowdin.com/project/gitea "Crowdin") [![](https://badges.crowdin.net/gitea/localized.svg)](https://translate.gitea.com "Crowdin")
[View this document in English](./README.md) [View this document in English](./README.md)
## 目标 ## 目标
Gitea 的首要目标是创建一个极易安装,运行非常快速,安装和使用体验良好的自建 Git 服务。我们采用 Go 作为后端语言,这使我们只要生成一个可执行程序即可。并且他还支持跨平台,支持 Linux, macOS 和 Windows 以及各种架构,除了 x86amd64还包括 ARM 和 PowerPC。 Gitea 的首要目标是创建一个极易安装,运行非常快速,安装和使用体验良好的自建 Git 服务。我们采用 Go 作为后端语言,这使我们只要生成一个可执行程序即可。并且他还支持跨平台,支持 Linux、macOS 和 Windows 以及各种架构,除了 x86 和 amd64还包括 ARM 和 PowerPC。
如果你想试用在线演示和报告问题,请访问 [demo.gitea.com](https://demo.gitea.com/)。 如果你想试用在线演示和报告问题,请访问 [demo.gitea.com](https://demo.gitea.com/)。
@ -23,39 +23,134 @@ Gitea 的首要目标是创建一个极易安装,运行非常快速,安装
如果你想在 Gitea Cloud 上快速部署你自己独享的 Gitea 实例,请访问 [cloud.gitea.com](https://cloud.gitea.com) 开始免费试用。 如果你想在 Gitea Cloud 上快速部署你自己独享的 Gitea 实例,请访问 [cloud.gitea.com](https://cloud.gitea.com) 开始免费试用。
## 提示
1. **开始贡献代码之前请确保你已经看过了 [贡献者向导(英文)](CONTRIBUTING.md)**.
2. 所有的安全问题,请私下发送邮件给 **security@gitea.io**。谢谢!
3. 如果你要使用API请参见 [API 文档](https://godoc.org/code.gitea.io/sdk/gitea).
## 文档 ## 文档
关于如何安装请访问我们的 [文档站](https://docs.gitea.com/zh-cn/category/installation),如果没有找到对应的文档,你也可以通过 [Discord - 英文](https://discord.gg/gitea) 和 QQ群 328432459 来和我们交流。 关于如何安装请访问我们的 [文档站](https://docs.gitea.com/zh-cn/category/installation),如果没有找到对应的文档,你也可以通过 [Discord - 英文](https://discord.gg/gitea) 和 QQ群 328432459 来和我们交流。
## 贡献流程 ## 编译
Fork -> Patch -> Push -> Pull Request 在源代码的根目录下执行:
TAGS="bindata" make build
或者如果需要SQLite支持
TAGS="bindata sqlite sqlite_unlock_notify" make build
编译过程会分成2个子任务
- `make backend`,需要 [Go Stable](https://go.dev/dl/),最低版本需求可查看 [go.mod](/go.mod)。
- `make frontend`,需要 [Node.js LTS](https://nodejs.org/en/download/) 或更高版本。
你需要连接网络来下载 go 和 npm modules。当从 tar 格式的源文件编译时,其中包含了预编译的前端文件,因此 `make frontend` 将不会被执行。这允许编译时不需要 Node.js。
更多信息: https://docs.gitea.com/installation/install-from-source
## 使用
编译之后,默认会在根目录下生成一个名为 `gitea` 的文件。你可以这样执行它:
./gitea web
> [!注意]
> 如果你要使用API请参见 [API 文档](https://godoc.org/code.gitea.io/sdk/gitea)。
## 贡献
贡献流程Fork -> Patch -> Push -> Pull Request
> [!注意]
>
> 1. **开始贡献代码之前请确保你已经看过了 [贡献者向导(英文)](CONTRIBUTING.md)**
> 2. 所有的安全问题,请私下发送邮件给 **security@gitea.io**。 谢谢!
## 翻译 ## 翻译
多语言翻译是基于Crowdin进行的. [![Crowdin](https://badges.crowdin.net/gitea/localized.svg)](https://translate.gitea.com)
[![Crowdin](https://badges.crowdin.net/gitea/localized.svg)](https://crowdin.com/project/gitea)
多语言翻译是基于Crowdin进行的。
从 [文档](https://docs.gitea.com/contributing/localization) 中获取更多信息。
## 官方和第三方项目
Gitea 提供官方的 [go-sdk](https://gitea.com/gitea/go-sdk),以及名为 [tea](https://gitea.com/gitea/tea) 的 CLI 工具 和 用于 Gitea Action 的 [action runner](https://gitea.com/gitea/act_runner)。
[gitea/awesome-gitea](https://gitea.com/gitea/awesome-gitea) 是一个 Gitea 相关项目的列表,你可以在这里找到更多的第三方项目,包括 SDK、插件、主题等等。
## 作者 ## 作者
* [Maintainers](https://github.com/orgs/go-gitea/people) - [Maintainers](https://github.com/orgs/go-gitea/people)
* [Contributors](https://github.com/go-gitea/gitea/graphs/contributors) - [Contributors](https://github.com/go-gitea/gitea/graphs/contributors)
* [Translators](options/locale/TRANSLATORS) - [Translators](options/locale/TRANSLATORS)
## 授权许可 ## 授权许可
本项目采用 MIT 开源授权许可证,完整的授权说明已放置在 [LICENSE](https://github.com/go-gitea/gitea/blob/main/LICENSE) 文件中。 本项目采用 MIT 开源授权许可证,完整的授权说明已放置在 [LICENSE](https://github.com/go-gitea/gitea/blob/main/LICENSE) 文件中。
## 截图 ## 更多信息
|![Dashboard](https://dl.gitea.com/screenshots/home_timeline.png)|![User Profile](https://dl.gitea.com/screenshots/user_profile.png)|![Global Issues](https://dl.gitea.com/screenshots/global_issues.png)| <details>
|:---:|:---:|:---:| <summary>截图</summary>
|![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)|
![登录](https://dl.gitea.com/screenshots/login.png)
![注册](https://dl.gitea.com/screenshots/register.png)
### 用户首页
![首页](https://dl.gitea.com/screenshots/home.png)
![工单列表](https://dl.gitea.com/screenshots/issues.png)
![合并请求列表](https://dl.gitea.com/screenshots/pull_requests.png)
![里程碑列表](https://dl.gitea.com/screenshots/milestones.png)
### 用户资料
![用户资料](https://dl.gitea.com/screenshots/user_profile.png)
### 探索
![仓库列表](https://dl.gitea.com/screenshots/explore_repos.png)
![用户列表](https://dl.gitea.com/screenshots/explore_users.png)
![组织列表](https://dl.gitea.com/screenshots/explore_orgs.png)
### 仓库
![首页](https://dl.gitea.com/screenshots/repo_home.png)
![提交列表](https://dl.gitea.com/screenshots/repo_commits.png)
![分支列表](https://dl.gitea.com/screenshots/repo_branches.png)
![标签列表](https://dl.gitea.com/screenshots/repo_labels.png)
![里程碑列表](https://dl.gitea.com/screenshots/repo_milestones.png)
![版本发布](https://dl.gitea.com/screenshots/repo_releases.png)
![标签列表](https://dl.gitea.com/screenshots/repo_tags.png)
#### 仓库工单
![列表](https://dl.gitea.com/screenshots/repo_issues.png)
![工单](https://dl.gitea.com/screenshots/repo_issue.png)
#### 仓库合并请求
![列表](https://dl.gitea.com/screenshots/repo_pull_requests.png)
![合并请求](https://dl.gitea.com/screenshots/repo_pull_request.png)
![文件](https://dl.gitea.com/screenshots/repo_pull_request_file.png)
![提交列表](https://dl.gitea.com/screenshots/repo_pull_request_commits.png)
#### 仓库 Actions
![列表](https://dl.gitea.com/screenshots/repo_actions.png)
![Run](https://dl.gitea.com/screenshots/repo_actions_run.png)
#### 仓库动态
![动态](https://dl.gitea.com/screenshots/repo_activity.png)
![贡献者](https://dl.gitea.com/screenshots/repo_contributors.png)
![代码频率](https://dl.gitea.com/screenshots/repo_code_frequency.png)
![最近的提交](https://dl.gitea.com/screenshots/repo_recent_commits.png)
### 组织
![首页](https://dl.gitea.com/screenshots/org_home.png)
</details>

149
assets/go-licenses.json generated

File diff suppressed because one or more lines are too long

View File

@ -53,8 +53,6 @@ func (e Emoji) MarshalJSON() ([]byte, error) {
} }
func main() { func main() {
var err error
flag.Parse() flag.Parse()
// generate data // generate data
@ -83,8 +81,6 @@ var replacer = strings.NewReplacer(
var emojiRE = regexp.MustCompile(`\{Emoji:"([^"]*)"`) var emojiRE = regexp.MustCompile(`\{Emoji:"([^"]*)"`)
func generate() ([]byte, error) { func generate() ([]byte, error) {
var err error
// load gemoji data // load gemoji data
res, err := http.Get(gemojiURL) res, err := http.Get(gemojiURL)
if err != nil { if err != nil {

View File

@ -1,176 +0,0 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build ignore
package main
import (
"archive/tar"
"compress/gzip"
"crypto/md5"
"encoding/hex"
"flag"
"fmt"
"io"
"log"
"net/http"
"os"
"path"
"path/filepath"
"strings"
"code.gitea.io/gitea/build/license"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/util"
)
func main() {
var (
prefix = "gitea-licenses"
url = "https://api.github.com/repos/spdx/license-list-data/tarball"
githubApiToken = ""
githubUsername = ""
destination = ""
)
flag.StringVar(&destination, "dest", "options/license/", "destination for the licenses")
flag.StringVar(&githubUsername, "username", "", "github username")
flag.StringVar(&githubApiToken, "token", "", "github api token")
flag.Parse()
file, err := os.CreateTemp(os.TempDir(), prefix)
if err != nil {
log.Fatalf("Failed to create temp file. %s", err)
}
defer util.Remove(file.Name())
if err := os.RemoveAll(destination); err != nil {
log.Fatalf("Cannot clean destination folder: %v", err)
}
if err := os.MkdirAll(destination, 0o755); err != nil {
log.Fatalf("Cannot create destination: %v", err)
}
req, err := http.NewRequest("GET", url, nil)
if err != nil {
log.Fatalf("Failed to download archive. %s", err)
}
if len(githubApiToken) > 0 && len(githubUsername) > 0 {
req.SetBasicAuth(githubUsername, githubApiToken)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
log.Fatalf("Failed to download archive. %s", err)
}
defer resp.Body.Close()
if _, err := io.Copy(file, resp.Body); err != nil {
log.Fatalf("Failed to copy archive to file. %s", err)
}
if _, err := file.Seek(0, 0); err != nil {
log.Fatalf("Failed to reset seek on archive. %s", err)
}
gz, err := gzip.NewReader(file)
if err != nil {
log.Fatalf("Failed to gunzip the archive. %s", err)
}
tr := tar.NewReader(gz)
aliasesFiles := make(map[string][]string)
for {
hdr, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
log.Fatalf("Failed to iterate archive. %s", err)
}
if !strings.Contains(hdr.Name, "/text/") {
continue
}
if filepath.Ext(hdr.Name) != ".txt" {
continue
}
fileBaseName := filepath.Base(hdr.Name)
licenseName := strings.TrimSuffix(fileBaseName, ".txt")
if strings.HasPrefix(fileBaseName, "README") {
continue
}
if strings.HasPrefix(fileBaseName, "deprecated_") {
continue
}
out, err := os.Create(path.Join(destination, licenseName))
if err != nil {
log.Fatalf("Failed to create new file. %s", err)
}
defer out.Close()
// some license files have same content, so we need to detect these files and create a convert map into a json file
// Later we use this convert map to avoid adding same license content with different license name
h := md5.New()
// calculate md5 and write file in the same time
r := io.TeeReader(tr, h)
if _, err := io.Copy(out, r); err != nil {
log.Fatalf("Failed to write new file. %s", err)
} else {
fmt.Printf("Written %s\n", out.Name())
md5 := hex.EncodeToString(h.Sum(nil))
aliasesFiles[md5] = append(aliasesFiles[md5], licenseName)
}
}
// generate convert license name map
licenseAliases := make(map[string]string)
for _, fileNames := range aliasesFiles {
if len(fileNames) > 1 {
licenseName := license.GetLicenseNameFromAliases(fileNames)
if licenseName == "" {
// license name should not be empty as expected
// if it is empty, we need to rewrite the logic of GetLicenseNameFromAliases
log.Fatalf("GetLicenseNameFromAliases: license name is empty")
}
for _, fileName := range fileNames {
licenseAliases[fileName] = licenseName
}
}
}
// save convert license name map to file
b, err := json.Marshal(licenseAliases)
if err != nil {
log.Fatalf("Failed to create json bytes. %s", err)
}
licenseAliasesDestination := filepath.Join(destination, "etc", "license-aliases.json")
if err := os.MkdirAll(filepath.Dir(licenseAliasesDestination), 0o755); err != nil {
log.Fatalf("Failed to create directory for license aliases json file. %s", err)
}
f, err := os.Create(licenseAliasesDestination)
if err != nil {
log.Fatalf("Failed to create license aliases json file. %s", err)
}
defer f.Close()
if _, err = f.Write(b); err != nil {
log.Fatalf("Failed to write license aliases json file. %s", err)
}
fmt.Println("Done")
}

View File

@ -1,41 +0,0 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package license
import "strings"
func GetLicenseNameFromAliases(fnl []string) string {
if len(fnl) == 0 {
return ""
}
shortestItem := func(list []string) string {
s := list[0]
for _, l := range list[1:] {
if len(l) < len(s) {
s = l
}
}
return s
}
allHasPrefix := func(list []string, s string) bool {
for _, l := range list {
if !strings.HasPrefix(l, s) {
return false
}
}
return true
}
sl := shortestItem(fnl)
slv := strings.Split(sl, "-")
var result string
for i := len(slv); i >= 0; i-- {
result = strings.Join(slv[:i], "-")
if allHasPrefix(fnl, result) {
return result
}
}
return ""
}

View File

@ -1,39 +0,0 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package license
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestGetLicenseNameFromAliases(t *testing.T) {
tests := []struct {
target string
inputs []string
}{
{
// real case which you can find in license-aliases.json
target: "AGPL-1.0",
inputs: []string{
"AGPL-1.0-only",
"AGPL-1.0-or-late",
},
},
{
target: "",
inputs: []string{
"APSL-1.0",
"AGPL-1.0-only",
"AGPL-1.0-or-late",
},
},
}
for _, tt := range tests {
result := GetLicenseNameFromAliases(tt.inputs)
assert.Equal(t, result, tt.target)
}
}

View File

@ -31,6 +31,11 @@ var microcmdUserCreate = &cli.Command{
Name: "username", Name: "username",
Usage: "Username", Usage: "Username",
}, },
&cli.StringFlag{
Name: "user-type",
Usage: "Set user's type: individual or bot",
Value: "individual",
},
&cli.StringFlag{ &cli.StringFlag{
Name: "password", Name: "password",
Usage: "User password", Usage: "User password",
@ -69,10 +74,30 @@ var microcmdUserCreate = &cli.Command{
} }
func runCreateUser(c *cli.Context) error { 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 { if err := argsSet(c, "email"); err != nil {
return err return err
} }
userTypes := map[string]user_model.UserType{
"individual": user_model.UserTypeIndividual,
"bot": user_model.UserTypeBot,
}
userType, ok := userTypes[c.String("user-type")]
if !ok {
return fmt.Errorf("invalid user type: %s", c.String("user-type"))
}
if userType != user_model.UserTypeIndividual {
// Some other commands like "change-password" also only support individual users.
// It needs to clarify the "password" behavior for bot users in the future.
// At the moment, we do not allow setting password for bot users.
if c.IsSet("password") || c.IsSet("random-password") {
return errors.New("password can only be set for individual users")
}
}
if c.IsSet("name") && c.IsSet("username") { if c.IsSet("name") && c.IsSet("username") {
return errors.New("cannot set both --name and --username flags") return errors.New("cannot set both --name and --username flags")
} }
@ -114,16 +139,19 @@ func runCreateUser(c *cli.Context) error {
return err return err
} }
fmt.Printf("generated random password is '%s'\n", password) fmt.Printf("generated random password is '%s'\n", password)
} else { } else if userType == user_model.UserTypeIndividual {
return errors.New("must set either password or random-password flag") return errors.New("must set either password or random-password flag")
} }
isAdmin := c.Bool("admin") isAdmin := c.Bool("admin")
mustChangePassword := true // always default to true mustChangePassword := true // always default to true
if c.IsSet("must-change-password") { if c.IsSet("must-change-password") {
if userType != user_model.UserTypeIndividual {
return errors.New("must-change-password flag can only be set for individual users")
}
// if the flag is set, use the value provided by the user // if the flag is set, use the value provided by the user
mustChangePassword = c.Bool("must-change-password") mustChangePassword = c.Bool("must-change-password")
} else { } else if userType == user_model.UserTypeIndividual {
// check whether there are users in the database // check whether there are users in the database
hasUserRecord, err := db.IsTableNotEmpty(&user_model.User{}) hasUserRecord, err := db.IsTableNotEmpty(&user_model.User{})
if err != nil { if err != nil {
@ -147,8 +175,9 @@ func runCreateUser(c *cli.Context) error {
u := &user_model.User{ u := &user_model.User{
Name: username, Name: username,
Email: c.String("email"), Email: c.String("email"),
Passwd: password,
IsAdmin: isAdmin, IsAdmin: isAdmin,
Type: userType,
Passwd: password,
MustChangePassword: mustChangePassword, MustChangePassword: mustChangePassword,
Visibility: visibility, Visibility: visibility,
} }

View File

@ -13,32 +13,54 @@ import (
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestAdminUserCreate(t *testing.T) { func TestAdminUserCreate(t *testing.T) {
app := NewMainApp(AppVersion{}) app := NewMainApp(AppVersion{})
reset := func() { reset := func() {
assert.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.User{})) require.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.EmailAddress{}))
} }
type createCheck struct{ IsAdmin, MustChangePassword bool } t.Run("MustChangePassword", func(t *testing.T) {
createUser := func(name, args string) createCheck { type check struct {
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)))) IsAdmin bool
u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: name}) MustChangePassword bool
return createCheck{u.IsAdmin, u.MustChangePassword} }
} createCheck := func(name, args string) check {
reset() require.NoError(t, app.Run(strings.Fields(fmt.Sprintf("./gitea admin user create --username %s --email %s@gitea.local %s --password foobar", name, name, args))))
assert.Equal(t, createCheck{IsAdmin: false, MustChangePassword: false}, createUser("u", ""), "first non-admin user doesn't need to change password") u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: name})
return check{IsAdmin: u.IsAdmin, MustChangePassword: u.MustChangePassword}
}
reset()
assert.Equal(t, check{IsAdmin: false, MustChangePassword: false}, createCheck("u", ""), "first non-admin user doesn't need to change password")
reset() reset()
assert.Equal(t, createCheck{IsAdmin: true, MustChangePassword: false}, createUser("u", "--admin"), "first admin user doesn't need to change password") assert.Equal(t, check{IsAdmin: true, MustChangePassword: false}, createCheck("u", "--admin"), "first admin user doesn't need to change password")
reset() reset()
assert.Equal(t, createCheck{IsAdmin: true, MustChangePassword: true}, createUser("u", "--admin --must-change-password")) assert.Equal(t, check{IsAdmin: true, MustChangePassword: true}, createCheck("u", "--admin --must-change-password"))
assert.Equal(t, createCheck{IsAdmin: true, MustChangePassword: true}, createUser("u2", "--admin")) assert.Equal(t, check{IsAdmin: true, MustChangePassword: true}, createCheck("u2", "--admin"))
assert.Equal(t, createCheck{IsAdmin: true, MustChangePassword: false}, createUser("u3", "--admin --must-change-password=false")) assert.Equal(t, check{IsAdmin: true, MustChangePassword: false}, createCheck("u3", "--admin --must-change-password=false"))
assert.Equal(t, createCheck{IsAdmin: false, MustChangePassword: true}, createUser("u4", "")) assert.Equal(t, check{IsAdmin: false, MustChangePassword: true}, createCheck("u4", ""))
assert.Equal(t, createCheck{IsAdmin: false, MustChangePassword: false}, createUser("u5", "--must-change-password=false")) assert.Equal(t, check{IsAdmin: false, MustChangePassword: false}, createCheck("u5", "--must-change-password=false"))
})
t.Run("UserType", func(t *testing.T) {
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.ErrorContains(t, createUser("u", "--user-type invalid"), "invalid user type")
assert.ErrorContains(t, createUser("u", "--user-type bot --password 123"), "can only be set for individual users")
assert.ErrorContains(t, createUser("u", "--user-type bot --must-change-password"), "can only be set for individual users")
assert.NoError(t, createUser("u", "--user-type bot"))
u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "u"})
assert.Equal(t, user_model.UserTypeBot, u.Type)
assert.Equal(t, "", u.Passwd)
})
} }

View File

@ -4,6 +4,7 @@
package cmd package cmd
import ( import (
"context"
"fmt" "fmt"
golog "log" golog "log"
"os" "os"
@ -130,8 +131,8 @@ func runRecreateTable(ctx *cli.Context) error {
} }
recreateTables := migrate_base.RecreateTables(beans...) recreateTables := migrate_base.RecreateTables(beans...)
return db.InitEngineWithMigration(stdCtx, func(x *xorm.Engine) error { return db.InitEngineWithMigration(stdCtx, func(ctx context.Context, x *xorm.Engine) error {
if err := migrations.EnsureUpToDate(x); err != nil { if err := migrations.EnsureUpToDate(ctx, x); err != nil {
return err return err
} }
return recreateTables(x) return recreateTables(x)

View File

@ -316,7 +316,7 @@ func runHookPostReceive(c *cli.Context) error {
setup(ctx, c.Bool("debug")) setup(ctx, c.Bool("debug"))
// First of all run update-server-info no matter what // First of all run update-server-info no matter what
if _, _, err := git.NewCommand(ctx, "update-server-info").RunStdString(nil); err != nil { if _, _, err := git.NewCommand("update-server-info").RunStdString(ctx, nil); err != nil {
return fmt.Errorf("Failed to call 'git update-server-info': %w", err) return fmt.Errorf("Failed to call 'git update-server-info': %w", err)
} }

View File

@ -6,7 +6,6 @@ package cmd
import ( import (
"bufio" "bufio"
"bytes" "bytes"
"context"
"strings" "strings"
"testing" "testing"
@ -15,7 +14,7 @@ import (
func TestPktLine(t *testing.T) { func TestPktLine(t *testing.T) {
// test read // test read
ctx := context.Background() ctx := t.Context()
s := strings.NewReader("0000") s := strings.NewReader("0000")
r := bufio.NewReader(s) r := bufio.NewReader(s)
result, err := readPktLine(ctx, r, pktLineTypeFlush) result, err := readPktLine(ctx, r, pktLineTypeFlush)

View File

@ -165,6 +165,7 @@ func NewMainApp(appVer AppVersion) *cli.App {
app.Commands = append(app.Commands, subCmdWithConfig...) app.Commands = append(app.Commands, subCmdWithConfig...)
app.Commands = append(app.Commands, subCmdStandalone...) app.Commands = append(app.Commands, subCmdStandalone...)
setting.InitGiteaEnvVars()
return app return app
} }

View File

@ -6,7 +6,6 @@ package cmd
import ( import (
"fmt" "fmt"
"io" "io"
"os"
"path/filepath" "path/filepath"
"strings" "strings"
"testing" "testing"
@ -113,37 +112,17 @@ func TestCliCmd(t *testing.T) {
_, _ = fmt.Fprint(ctx.App.Writer, makePathOutput(setting.AppWorkPath, setting.CustomPath, setting.CustomConf)) _, _ = fmt.Fprint(ctx.App.Writer, makePathOutput(setting.AppWorkPath, setting.CustomPath, setting.CustomConf))
return nil return nil
}) })
var envBackup []string
for _, s := range os.Environ() {
if strings.HasPrefix(s, "GITEA_") && strings.Contains(s, "=") {
envBackup = append(envBackup, s)
}
}
clearGiteaEnv := func() {
for _, s := range os.Environ() {
if strings.HasPrefix(s, "GITEA_") {
_ = os.Unsetenv(s)
}
}
}
defer func() {
clearGiteaEnv()
for _, s := range envBackup {
k, v, _ := strings.Cut(s, "=")
_ = os.Setenv(k, v)
}
}()
for _, c := range cases { for _, c := range cases {
clearGiteaEnv() t.Run(c.cmd, func(t *testing.T) {
for k, v := range c.env { for k, v := range c.env {
_ = os.Setenv(k, v) t.Setenv(k, v)
} }
args := strings.Split(c.cmd, " ") // for test only, "split" is good enough args := strings.Split(c.cmd, " ") // for test only, "split" is good enough
r, err := runTestApp(app, args...) r, err := runTestApp(app, args...)
assert.NoError(t, err, c.cmd) assert.NoError(t, err, c.cmd)
assert.NotEmpty(t, c.exp, c.cmd) assert.NotEmpty(t, c.exp, c.cmd)
assert.Contains(t, r.Stdout, c.exp, c.cmd) assert.Contains(t, r.Stdout, c.exp, c.cmd)
})
} }
} }

View File

@ -7,9 +7,9 @@ import (
"context" "context"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/migrations"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/services/versioned_migration"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
@ -18,7 +18,7 @@ import (
var CmdMigrate = &cli.Command{ var CmdMigrate = &cli.Command{
Name: "migrate", Name: "migrate",
Usage: "Migrate the database", 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, Action: runMigrate,
} }
@ -36,7 +36,7 @@ func runMigrate(ctx *cli.Context) error {
log.Info("Log path: %s", setting.Log.RootPath) log.Info("Log path: %s", setting.Log.RootPath)
log.Info("Configuration file: %s", setting.CustomConf) log.Info("Configuration file: %s", setting.CustomConf)
if err := db.InitEngineWithMigration(context.Background(), migrations.Migrate); err != nil { if err := db.InitEngineWithMigration(context.Background(), versioned_migration.Migrate); err != nil {
log.Fatal("Failed to initialize ORM engine: %v", err) log.Fatal("Failed to initialize ORM engine: %v", err)
return err return err
} }

View File

@ -13,7 +13,6 @@ import (
actions_model "code.gitea.io/gitea/models/actions" actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
git_model "code.gitea.io/gitea/models/git" git_model "code.gitea.io/gitea/models/git"
"code.gitea.io/gitea/models/migrations"
packages_model "code.gitea.io/gitea/models/packages" packages_model "code.gitea.io/gitea/models/packages"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
@ -21,6 +20,7 @@ import (
packages_module "code.gitea.io/gitea/modules/packages" packages_module "code.gitea.io/gitea/modules/packages"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/storage" "code.gitea.io/gitea/modules/storage"
"code.gitea.io/gitea/services/versioned_migration"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
@ -196,7 +196,7 @@ func migrateActionsLog(ctx context.Context, dstStorage storage.ObjectStorage) er
func migrateActionsArtifacts(ctx context.Context, dstStorage storage.ObjectStorage) error { func migrateActionsArtifacts(ctx context.Context, dstStorage storage.ObjectStorage) error {
return db.Iterate(ctx, nil, func(ctx context.Context, artifact *actions_model.ActionArtifact) error { return db.Iterate(ctx, nil, func(ctx context.Context, artifact *actions_model.ActionArtifact) error {
if artifact.Status == int64(actions_model.ArtifactStatusExpired) { if artifact.Status == actions_model.ArtifactStatusExpired {
return nil return nil
} }
@ -227,7 +227,7 @@ func runMigrateStorage(ctx *cli.Context) error {
log.Info("Log path: %s", setting.Log.RootPath) log.Info("Log path: %s", setting.Log.RootPath)
log.Info("Configuration file: %s", setting.CustomConf) log.Info("Configuration file: %s", setting.CustomConf)
if err := db.InitEngineWithMigration(context.Background(), migrations.Migrate); err != nil { if err := db.InitEngineWithMigration(context.Background(), versioned_migration.Migrate); err != nil {
log.Fatal("Failed to initialize ORM engine: %v", err) log.Fatal("Failed to initialize ORM engine: %v", err)
return err return err
} }

View File

@ -4,7 +4,6 @@
package cmd package cmd
import ( import (
"context"
"os" "os"
"strings" "strings"
"testing" "testing"
@ -53,7 +52,7 @@ func TestMigratePackages(t *testing.T) {
assert.NotNil(t, v) assert.NotNil(t, v)
assert.NotNil(t, f) assert.NotNil(t, f)
ctx := context.Background() ctx := t.Context()
p := t.TempDir() p := t.TempDir()

View File

@ -104,19 +104,20 @@ func fail(ctx context.Context, userMessage, logMsgFmt string, args ...any) error
// There appears to be a chance to cause a zombie process and failure to read the Exit status // There appears to be a chance to cause a zombie process and failure to read the Exit status
// if nothing is outputted on stdout. // if nothing is outputted on stdout.
_, _ = fmt.Fprintln(os.Stdout, "") _, _ = fmt.Fprintln(os.Stdout, "")
_, _ = fmt.Fprintln(os.Stderr, "Gitea:", userMessage) // add extra empty lines to separate our message from other git errors to get more attention
_, _ = fmt.Fprintln(os.Stderr, "error:")
_, _ = fmt.Fprintln(os.Stderr, "error:", userMessage)
_, _ = fmt.Fprintln(os.Stderr, "error:")
if logMsgFmt != "" { if logMsgFmt != "" {
logMsg := fmt.Sprintf(logMsgFmt, args...) logMsg := fmt.Sprintf(logMsgFmt, args...)
if !setting.IsProd { if !setting.IsProd {
_, _ = fmt.Fprintln(os.Stderr, "Gitea:", logMsg) _, _ = fmt.Fprintln(os.Stderr, "Gitea:", logMsg)
} }
if userMessage != "" { if unicode.IsPunct(rune(userMessage[len(userMessage)-1])) {
if unicode.IsPunct(rune(userMessage[len(userMessage)-1])) { logMsg = userMessage + " " + logMsg
logMsg = userMessage + " " + logMsg } else {
} else { logMsg = userMessage + ". " + logMsg
logMsg = userMessage + ". " + logMsg
}
} }
_ = private.SSHLog(ctx, true, logMsg) _ = private.SSHLog(ctx, true, logMsg)
} }
@ -288,10 +289,10 @@ func runServ(c *cli.Context) error {
if allowedCommands.Contains(verb) { if allowedCommands.Contains(verb) {
if allowedCommandsLfs.Contains(verb) { if allowedCommandsLfs.Contains(verb) {
if !setting.LFS.StartServer { if !setting.LFS.StartServer {
return fail(ctx, "Unknown git command", "LFS authentication request over SSH denied, LFS support is disabled") return fail(ctx, "LFS Server is not enabled", "")
} }
if verb == verbLfsTransfer && !setting.LFS.AllowPureSSH { if verb == verbLfsTransfer && !setting.LFS.AllowPureSSH {
return fail(ctx, "Unknown git command", "LFS SSH transfer connection denied, pure SSH protocol is disabled") return fail(ctx, "LFS SSH transfer is not enabled", "")
} }
if len(words) > 2 { if len(words) > 2 {
lfsVerb = words[2] lfsVerb = words[2]

View File

@ -12,15 +12,18 @@ import (
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings" "strings"
"time"
_ "net/http/pprof" // Used for debugging if enabled and a web server is running _ "net/http/pprof" // Used for debugging if enabled and a web server is running
"code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/gtprof"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/process" "code.gitea.io/gitea/modules/process"
"code.gitea.io/gitea/modules/public" "code.gitea.io/gitea/modules/public"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers" "code.gitea.io/gitea/routers"
"code.gitea.io/gitea/routers/install" "code.gitea.io/gitea/routers/install"
@ -115,6 +118,16 @@ func showWebStartupMessage(msg string) {
log.Info("* CustomPath: %s", setting.CustomPath) log.Info("* CustomPath: %s", setting.CustomPath)
log.Info("* ConfigFile: %s", setting.CustomConf) log.Info("* ConfigFile: %s", setting.CustomConf)
log.Info("%s", msg) // show startup message log.Info("%s", msg) // show startup message
if setting.CORSConfig.Enabled {
log.Info("CORS Service Enabled")
}
if setting.DefaultUILocation != time.Local {
log.Info("Default UI Location is %v", setting.DefaultUILocation.String())
}
if setting.MailService != nil {
log.Info("Mail Service Enabled: RegisterEmailConfirm=%v, Service.EnableNotifyMail=%v", setting.Service.RegisterEmailConfirm, setting.Service.EnableNotifyMail)
}
} }
func serveInstall(ctx *cli.Context) error { func serveInstall(ctx *cli.Context) error {
@ -208,6 +221,8 @@ func serveInstalled(ctx *cli.Context) error {
} }
} }
gtprof.EnableBuiltinTracer(util.Iif(setting.IsProd, 2000*time.Millisecond, 100*time.Millisecond))
// Set up Chi routes // Set up Chi routes
webRoutes := routers.NormalRoutes() webRoutes := routers.NormalRoutes()
err := listen(webRoutes, true) err := listen(webRoutes, true)

View File

@ -16,6 +16,7 @@ import (
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/process" "code.gitea.io/gitea/modules/process"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"github.com/caddyserver/certmagic" "github.com/caddyserver/certmagic"
) )
@ -54,8 +55,6 @@ func runACME(listenAddr string, m http.Handler) error {
altTLSALPNPort = p 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 // Try to use private CA root if provided, otherwise defaults to system's trust
var certPool *x509.CertPool var certPool *x509.CertPool
if setting.AcmeCARoot != "" { 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) log.Warn("Failed to parse CA Root certificate, using default CA trust: %v", err)
} }
} }
myACME := certmagic.NewACMEIssuer(magic, certmagic.ACMEIssuer{ // FIXME: this path is not right, it uses "AppWorkPath" incorrectly, and writes the data into "AppWorkPath/https"
CA: setting.AcmeURL, // 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, TrustedRoots: certPool,
Email: setting.AcmeEmail, Email: setting.AcmeEmail,
Agreed: setting.AcmeTOS, Agreed: setting.AcmeTOS,
@ -75,8 +86,10 @@ func runACME(listenAddr string, m http.Handler) error {
ListenHost: setting.HTTPAddr, ListenHost: setting.HTTPAddr,
AltTLSALPNPort: altTLSALPNPort, AltTLSALPNPort: altTLSALPNPort,
AltHTTPPort: altHTTPPort, AltHTTPPort: altHTTPPort,
}) }
magic := certmagic.NewDefault()
myACME := certmagic.NewACMEIssuer(magic, certmagic.DefaultACME)
magic.Issuers = []certmagic.Issuer{myACME} magic.Issuers = []certmagic.Issuer{myACME}
// this obtains certificates or renews them if necessary // this obtains certificates or renews them if necessary

View File

@ -1,80 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//nolint:forbidigo
package main
import (
"context"
"fmt"
"os"
"path/filepath"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/unittest"
)
// To generate derivative fixtures, execute the following from Gitea's repository base dir:
// go run -tags 'sqlite sqlite_unlock_notify' contrib/fixtures/fixture_generation.go [fixture...]
var (
generators = []struct {
gen func(ctx context.Context) (string, error)
name string
}{
{
models.GetYamlFixturesAccess, "access",
},
}
fixturesDir string
)
func main() {
pathToGiteaRoot := "."
fixturesDir = filepath.Join(pathToGiteaRoot, "models", "fixtures")
if err := unittest.CreateTestEngine(unittest.FixturesOptions{
Dir: fixturesDir,
}); err != nil {
fmt.Printf("CreateTestEngine: %+v", err)
os.Exit(1)
}
if err := unittest.PrepareTestDatabase(); err != nil {
fmt.Printf("PrepareTestDatabase: %+v\n", err)
os.Exit(1)
}
ctx := context.Background()
if len(os.Args) == 0 {
for _, r := range os.Args {
if err := generate(ctx, r); err != nil {
fmt.Printf("generate '%s': %+v\n", r, err)
os.Exit(1)
}
}
} else {
for _, g := range generators {
if err := generate(ctx, g.name); err != nil {
fmt.Printf("generate '%s': %+v\n", g.name, err)
os.Exit(1)
}
}
}
}
func generate(ctx context.Context, name string) error {
for _, g := range generators {
if g.name == name {
data, err := g.gen(ctx)
if err != nil {
return err
}
path := filepath.Join(fixturesDir, name+".yml")
if err := os.WriteFile(path, []byte(data), 0o644); err != nil {
return fmt.Errorf("%s: %+v", path, err)
}
fmt.Printf("%s created.\n", path)
return nil
}
}
return fmt.Errorf("generator not found")
}

View File

@ -78,8 +78,9 @@ RUN_USER = ; git
;; Set the domain for the server ;; Set the domain for the server
;DOMAIN = localhost ;DOMAIN = localhost
;; ;;
;; Overwrite the automatically generated public URL. Necessary for proxies and docker. ;; The AppURL used by Gitea to generate absolute links, defaults to "{PROTOCOL}://{DOMAIN}:{HTTP_PORT}/".
;ROOT_URL = %(PROTOCOL)s://%(DOMAIN)s:%(HTTP_PORT)s/ ;; Most users should set it to the real website URL of their Gitea instance.
;ROOT_URL =
;; ;;
;; For development purpose only. It makes Gitea handle sub-path ("/sub-path/owner/repo/...") directly when debugging without a reverse proxy. ;; For development purpose only. It makes Gitea handle sub-path ("/sub-path/owner/repo/...") directly when debugging without a reverse proxy.
;; DO NOT USE IT IN PRODUCTION!!! ;; DO NOT USE IT IN PRODUCTION!!!
@ -103,8 +104,8 @@ RUN_USER = ; git
;REDIRECT_OTHER_PORT = false ;REDIRECT_OTHER_PORT = false
;PORT_TO_REDIRECT = 80 ;PORT_TO_REDIRECT = 80
;; ;;
;; expect PROXY protocol header on connections to https redirector. ;; expect PROXY protocol header on connections to https redirector, defaults to USE_PROXY_PROTOCOL
;REDIRECTOR_USE_PROXY_PROTOCOL = %(USE_PROXY_PROTOCOL)s ;REDIRECTOR_USE_PROXY_PROTOCOL =
;; Minimum and maximum supported TLS versions ;; Minimum and maximum supported TLS versions
;SSL_MIN_VERSION=TLSv1.2 ;SSL_MIN_VERSION=TLSv1.2
;SSL_MAX_VERSION= ;SSL_MAX_VERSION=
@ -128,13 +129,14 @@ RUN_USER = ; git
;; most cases you do not need to change the default value. Alter it only if ;; most cases you do not need to change the default value. Alter it only if
;; your SSH server node is not the same as HTTP node. For different protocol, the default ;; your SSH server node is not the same as HTTP node. For different protocol, the default
;; values are different. If `PROTOCOL` is `http+unix`, the default value is `http://unix/`. ;; values are different. If `PROTOCOL` is `http+unix`, the default value is `http://unix/`.
;; If `PROTOCOL` is `fcgi` or `fcgi+unix`, the default value is `%(PROTOCOL)s://%(HTTP_ADDR)s:%(HTTP_PORT)s/`. ;; If `PROTOCOL` is `fcgi` or `fcgi+unix`, the default value is `{PROTOCOL}://{HTTP_ADDR}:{HTTP_PORT}/`.
;; If listen on `0.0.0.0`, the default value is `%(PROTOCOL)s://localhost:%(HTTP_PORT)s/`, Otherwise the default ;; If listen on `0.0.0.0`, the default value is `{PROTOCOL}://localhost:{HTTP_PORT}/`.
;; value is `%(PROTOCOL)s://%(HTTP_ADDR)s:%(HTTP_PORT)s/`. ;; Otherwise the default value is `{PROTOCOL}://{HTTP_ADDR}:{HTTP_PORT}/`.
;LOCAL_ROOT_URL = %(PROTOCOL)s://%(HTTP_ADDR)s:%(HTTP_PORT)s/ ;; Most users don't need (and shouldn't) set this value.
;LOCAL_ROOT_URL =
;; ;;
;; When making local connections pass the PROXY protocol header. ;; When making local connections pass the PROXY protocol header, defaults to USE_PROXY_PROTOCOL
;LOCAL_USE_PROXY_PROTOCOL = %(USE_PROXY_PROTOCOL)s ;LOCAL_USE_PROXY_PROTOCOL =
;; ;;
;; Disable SSH feature when not available ;; Disable SSH feature when not available
;DISABLE_SSH = false ;DISABLE_SSH = false
@ -146,13 +148,17 @@ RUN_USER = ; git
;SSH_SERVER_USE_PROXY_PROTOCOL = false ;SSH_SERVER_USE_PROXY_PROTOCOL = false
;; ;;
;; Username to use for the builtin SSH server. If blank, then it is the value of RUN_USER. ;; Username to use for the builtin SSH server. If blank, then it is the value of RUN_USER.
;BUILTIN_SSH_SERVER_USER = %(RUN_USER)s ;BUILTIN_SSH_SERVER_USER =
;; ;;
;; Domain name to be exposed in clone URL ;; Domain name to be exposed in clone URL, defaults to DOMAIN or the domain part of ROOT_URL
;SSH_DOMAIN = %(DOMAIN)s ;SSH_DOMAIN =
;; ;;
;; SSH username displayed in clone URLs. ;; SSH username displayed in clone URLs. It defaults to BUILTIN_SSH_SERVER_USER or RUN_USER.
;SSH_USER = %(BUILTIN_SSH_SERVER_USER)s ;; If it is set to "(DOER_USERNAME)", it will use current signed-in user's username.
;; This option is only for some advanced users who have configured their SSH reverse-proxy
;; and need to use different usernames for git SSH clone.
;; Most users should just leave it blank.
;SSH_USER =
;; ;;
;; The network interface the builtin SSH server should listen on ;; The network interface the builtin SSH server should listen on
;SSH_LISTEN_HOST = ;SSH_LISTEN_HOST =
@ -160,8 +166,8 @@ RUN_USER = ; git
;; Port number to be exposed in clone URL ;; Port number to be exposed in clone URL
;SSH_PORT = 22 ;SSH_PORT = 22
;; ;;
;; The port number the builtin SSH server should listen on ;; The port number the builtin SSH server should listen on, defaults to SSH_PORT
;SSH_LISTEN_PORT = %(SSH_PORT)s ;SSH_LISTEN_PORT =
;; ;;
;; Root path of SSH directory, default is '~/.ssh', but you have to use '/home/git/.ssh'. ;; Root path of SSH directory, default is '~/.ssh', but you have to use '/home/git/.ssh'.
;SSH_ROOT_PATH = ;SSH_ROOT_PATH =
@ -188,7 +194,7 @@ RUN_USER = ; git
;; ;;
;; For the built-in SSH server, choose the keypair to offer as the host key ;; For the built-in SSH server, choose the keypair to offer as the host key
;; The private key should be at SSH_SERVER_HOST_KEY and the public SSH_SERVER_HOST_KEY.pub ;; The private key should be at SSH_SERVER_HOST_KEY and the public SSH_SERVER_HOST_KEY.pub
;; relative paths are made absolute relative to the %(APP_DATA_PATH)s ;; relative paths are made absolute relative to the APP_DATA_PATH
;SSH_SERVER_HOST_KEYS=ssh/gitea.rsa, ssh/gogs.rsa ;SSH_SERVER_HOST_KEYS=ssh/gitea.rsa, ssh/gogs.rsa
;; ;;
;; Directory to create temporary files in when testing public keys using ssh-keygen, ;; Directory to create temporary files in when testing public keys using ssh-keygen,
@ -324,6 +330,10 @@ RUN_USER = ; git
;; Maximum number of locks returned per page ;; Maximum number of locks returned per page
;LFS_LOCKS_PAGING_NUM = 50 ;LFS_LOCKS_PAGING_NUM = 50
;; ;;
;; When clients make lfs batch requests, reject them if there are more pointers than this number
;; zero means 'unlimited'
;LFS_MAX_BATCH_SIZE = 0
;;
;; Allow graceful restarts using SIGHUP to fork ;; Allow graceful restarts using SIGHUP to fork
;ALLOW_GRACEFUL_RESTARTS = true ;ALLOW_GRACEFUL_RESTARTS = true
;; ;;
@ -578,7 +588,7 @@ ENABLED = true
[log] [log]
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Root path for the log files - defaults to %(GITEA_WORK_DIR)/log ;; Root path for the log files - defaults to "{AppWorkPath}/log"
;ROOT_PATH = ;ROOT_PATH =
;; ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@ -678,8 +688,8 @@ LEVEL = Info
;; The path of git executable. If empty, Gitea searches through the PATH environment. ;; The path of git executable. If empty, Gitea searches through the PATH environment.
;PATH = ;PATH =
;; ;;
;; The HOME directory for Git ;; The HOME directory for Git, defaults to "{APP_DATA_PATH}/home"
;HOME_PATH = %(APP_DATA_PATH)s/home ;HOME_PATH =
;; ;;
;; Disables highlight of added and removed changes ;; Disables highlight of added and removed changes
;DISABLE_DIFF_HIGHLIGHT = false ;DISABLE_DIFF_HIGHLIGHT = false
@ -780,6 +790,13 @@ LEVEL = Info
;; Please note that setting this to false will not disable OAuth Basic or Basic authentication using a token ;; Please note that setting this to false will not disable OAuth Basic or Basic authentication using a token
;ENABLE_BASIC_AUTHENTICATION = true ;ENABLE_BASIC_AUTHENTICATION = true
;; ;;
;; Show the password sign-in form (for password-based login), otherwise, only show OAuth2 or passkey login methods if they are enabled.
;; If you set it to false, maybe it also needs to set ENABLE_BASIC_AUTHENTICATION to false to completely disable password-based authentication.
;ENABLE_PASSWORD_SIGNIN_FORM = true
;;
;; Allow users to sign-in with a passkey
;ENABLE_PASSKEY_AUTHENTICATION = true
;;
;; More detail: https://github.com/gogits/gogs/issues/165 ;; More detail: https://github.com/gogits/gogs/issues/165
;ENABLE_REVERSE_PROXY_AUTHENTICATION = false ;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. ; Enable this to allow reverse proxy authentication for API requests, the reverse proxy is responsible for ensuring that no CSRF is possible.
@ -938,8 +955,8 @@ LEVEL = Info
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;[repository] ;[repository]
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Root path for storing all repository data. By default, it is set to %(APP_DATA_PATH)s/gitea-repositories. ;; Root path for storing all repository data. By default, it is set to "{APP_DATA_PATH}/gitea-repositories".
;; A relative path is interpreted as _`AppWorkPath`_/%(ROOT)s ;; A relative path is interpreted as "{AppWorkPath}/{ROOT}" (use AppWorkPath as base path).
;ROOT = ;ROOT =
;; ;;
;; The script type this server supports. Usually this is `bash`, but some users report that only `sh` is available. ;; The script type this server supports. Usually this is `bash`, but some users report that only `sh` is available.
@ -1003,6 +1020,14 @@ LEVEL = Info
;; The set of allowed values and rules are the same as DEFAULT_REPO_UNITS. ;; The set of allowed values and rules are the same as DEFAULT_REPO_UNITS.
;DEFAULT_FORK_REPO_UNITS = repo.code,repo.pulls ;DEFAULT_FORK_REPO_UNITS = repo.code,repo.pulls
;; ;;
;; Comma separated list of default mirror repo units.
;; The set of allowed values and rules are the same as DEFAULT_REPO_UNITS.
;DEFAULT_MIRROR_REPO_UNITS = repo.code,repo.releases,repo.issues,repo.wiki,repo.projects,repo.packages
;;
;; Comma separated list of default template repo units.
;; The set of allowed values and rules are the same as DEFAULT_REPO_UNITS.
;DEFAULT_TEMPLATE_REPO_UNITS = repo.code,repo.releases,repo.issues,repo.pulls,repo.wiki,repo.projects,repo.packages
;;
;; Prefix archive files by placing them in a directory named after the repository ;; Prefix archive files by placing them in a directory named after the repository
;PREFIX_ARCHIVE_FILES = true ;PREFIX_ARCHIVE_FILES = true
;; ;;
@ -1024,9 +1049,13 @@ LEVEL = Info
;; Don't allow download source archive files from UI ;; Don't allow download source archive files from UI
;DISABLE_DOWNLOAD_SOURCE_ARCHIVES = false ;DISABLE_DOWNLOAD_SOURCE_ARCHIVES = false
;; Allow fork repositories without maximum number limit ;; Allow to fork repositories without maximum number limit
;ALLOW_FORK_WITHOUT_MAXIMUM_LIMIT = true ;ALLOW_FORK_WITHOUT_MAXIMUM_LIMIT = true
;; Allow to fork repositories into the same owner (user or organization)
;; This feature is experimental, not fully tested, and may be changed in the future
;ALLOW_FORK_INTO_SAME_OWNER = false
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;[repository.editor] ;[repository.editor]
@ -1100,6 +1129,9 @@ LEVEL = Info
;; In default merge messages only include approvers who are official ;; In default merge messages only include approvers who are official
;DEFAULT_MERGE_MESSAGE_OFFICIAL_APPROVERS_ONLY = true ;DEFAULT_MERGE_MESSAGE_OFFICIAL_APPROVERS_ONLY = true
;; ;;
;; In default squash-merge messages include the commit message of all commits comprising the pull request.
;POPULATE_SQUASH_COMMENT_WITH_COMMIT_MESSAGES = false
;;
;; Add co-authored-by and co-committed-by trailers if committer does not match author ;; Add co-authored-by and co-committed-by trailers if committer does not match author
;ADD_CO_COMMITTER_TRAILERS = true ;ADD_CO_COMMITTER_TRAILERS = true
;; ;;
@ -1262,6 +1294,9 @@ LEVEL = Info
;; Leave it empty to allow users to select any theme from "{CustomPath}/public/assets/css/theme-*.css" ;; Leave it empty to allow users to select any theme from "{CustomPath}/public/assets/css/theme-*.css"
;THEMES = ;THEMES =
;; ;;
;; The icons for file list (basic/material), this is a temporary option which will be replaced by a user setting in the future.
;FILE_ICON_THEME = material
;;
;; All available reactions users can choose on issues/prs and comments. ;; All available reactions users can choose on issues/prs and comments.
;; Values can be emoji alias (:smile:) or a unicode emoji. ;; Values can be emoji alias (:smile:) or a unicode emoji.
;; For custom reactions, add a tightly cropped square image to public/assets/img/emoji/reaction_name.png ;; For custom reactions, add a tightly cropped square image to public/assets/img/emoji/reaction_name.png
@ -1319,6 +1354,9 @@ LEVEL = Info
;; Number of repos that are displayed on one page ;; Number of repos that are displayed on one page
;REPO_PAGING_NUM = 15 ;REPO_PAGING_NUM = 15
;; Number of orgs that are displayed on profile page
;ORG_PAGING_NUM = 15
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;[ui.meta] ;[ui.meta]
@ -1462,6 +1500,10 @@ LEVEL = Info
;REPO_INDEXER_EXCLUDE = ;REPO_INDEXER_EXCLUDE =
;; ;;
;MAX_FILE_SIZE = 1048576 ;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
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@ -1479,7 +1521,8 @@ LEVEL = Info
;TYPE = persistable-channel ;TYPE = persistable-channel
;; ;;
;; data-dir for storing persistable queues and level queues, individual queues will default to `queues/common` meaning the queue is shared. ;; data-dir for storing persistable queues and level queues, individual queues will default to `queues/common` meaning the queue is shared.
;DATADIR = queues/ ; Relative paths will be made absolute against `%(APP_DATA_PATH)s`. ;; Relative paths will be made absolute against "APP_DATA_PATH"
;DATADIR = queues/
;; ;;
;; Default queue length before a channel queue will block ;; Default queue length before a channel queue will block
;LENGTH = 100000 ;LENGTH = 100000
@ -1727,6 +1770,9 @@ LEVEL = Info
;; ;;
;; convert \r\n to \n for Sendmail ;; convert \r\n to \n for Sendmail
;SENDMAIL_CONVERT_CRLF = true ;SENDMAIL_CONVERT_CRLF = true
;;
;; convert links of attached images to inline images. Only for images hosted in this gitea instance.
;EMBED_ATTACHMENT_IMAGES = false
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@ -1900,7 +1946,7 @@ LEVEL = Info
;ENABLED = true ;ENABLED = true
;; ;;
;; Comma-separated list of allowed file extensions (`.zip`), mime types (`text/plain`) or wildcard type (`image/*`, `audio/*`, `video/*`). Empty value or `*/*` allows all types. ;; Comma-separated list of allowed file extensions (`.zip`), mime types (`text/plain`) or wildcard type (`image/*`, `audio/*`, `video/*`). Empty value or `*/*` allows all types.
;ALLOWED_TYPES = .csv,.docx,.fodg,.fodp,.fods,.fodt,.gif,.gz,.jpeg,.jpg,.log,.md,.mov,.mp4,.odf,.odg,.odp,.ods,.odt,.patch,.pdf,.png,.pptx,.svg,.tgz,.txt,.webm,.xls,.xlsx,.zip ;ALLOWED_TYPES = .avif,.cpuprofile,.csv,.dmp,.docx,.fodg,.fodp,.fods,.fodt,.gif,.gz,.jpeg,.jpg,.json,.jsonc,.log,.md,.mov,.mp4,.odf,.odg,.odp,.ods,.odt,.patch,.pdf,.png,.pptx,.svg,.tgz,.txt,.webm,.webp,.xls,.xlsx,.zip
;; ;;
;; Max size of each file. Defaults to 2048MB ;; Max size of each file. Defaults to 2048MB
;MAX_SIZE = 2048 ;MAX_SIZE = 2048
@ -1932,6 +1978,13 @@ LEVEL = Info
;; Minio secretAccessKey to connect only available when STORAGE_TYPE is `minio` ;; Minio secretAccessKey to connect only available when STORAGE_TYPE is `minio`
;MINIO_SECRET_ACCESS_KEY = ;MINIO_SECRET_ACCESS_KEY =
;; ;;
;; Preferred IAM Endpoint to override Minio's default IAM Endpoint resolution only available when STORAGE_TYPE is `minio`.
;; If not provided and STORAGE_TYPE is `minio`, will search for and derive endpoint from known environment variables
;; (AWS_CONTAINER_AUTHORIZATION_TOKEN, AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE, AWS_CONTAINER_CREDENTIALS_RELATIVE_URI,
;; AWS_CONTAINER_CREDENTIALS_FULL_URI, AWS_WEB_IDENTITY_TOKEN_FILE, AWS_ROLE_ARN, AWS_ROLE_SESSION_NAME, AWS_REGION),
;; or the DefaultIAMRoleEndpoint if not provided otherwise.
;MINIO_IAM_ENDPOINT =
;;
;; Minio bucket to store the attachments only available when STORAGE_TYPE is `minio` ;; Minio bucket to store the attachments only available when STORAGE_TYPE is `minio`
;MINIO_BUCKET = gitea ;MINIO_BUCKET = gitea
;; ;;
@ -2638,6 +2691,16 @@ LEVEL = Info
;; override the azure blob base path if storage type is azureblob ;; override the azure blob base path if storage type is azureblob
;AZURE_BLOB_BASE_PATH = lfs/ ;AZURE_BLOB_BASE_PATH = lfs/
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; settings for Gitea's LFS client (eg: mirroring an upstream lfs endpoint)
;;
;[lfs_client]
;; Limit the number of pointers in each batch request to this number
;BATCH_SIZE = 20
;; Limit the number of concurrent upload/download operations within a batch
;BATCH_OPERATION_CONCURRENCY = 8
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; settings for packages, will override storage setting ;; settings for packages, will override storage setting
@ -2666,6 +2729,13 @@ LEVEL = Info
;; Minio secretAccessKey to connect only available when STORAGE_TYPE is `minio` ;; Minio secretAccessKey to connect only available when STORAGE_TYPE is `minio`
;MINIO_SECRET_ACCESS_KEY = ;MINIO_SECRET_ACCESS_KEY =
;; ;;
;; Preferred IAM Endpoint to override Minio's default IAM Endpoint resolution only available when STORAGE_TYPE is `minio`.
;; If not provided and STORAGE_TYPE is `minio`, will search for and derive endpoint from known environment variables
;; (AWS_CONTAINER_AUTHORIZATION_TOKEN, AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE, AWS_CONTAINER_CREDENTIALS_RELATIVE_URI,
;; AWS_CONTAINER_CREDENTIALS_FULL_URI, AWS_WEB_IDENTITY_TOKEN_FILE, AWS_ROLE_ARN, AWS_ROLE_SESSION_NAME, AWS_REGION),
;; or the DefaultIAMRoleEndpoint if not provided otherwise.
;MINIO_IAM_ENDPOINT =
;;
;; Minio bucket to store the attachments only available when STORAGE_TYPE is `minio` ;; Minio bucket to store the attachments only available when STORAGE_TYPE is `minio`
;MINIO_BUCKET = gitea ;MINIO_BUCKET = gitea
;; ;;

View File

@ -37,5 +37,5 @@ done
if [ $# -gt 0 ]; then if [ $# -gt 0 ]; then
exec "$@" exec "$@"
else else
exec /bin/s6-svscan /etc/s6 exec /usr/bin/s6-svscan /etc/s6
fi fi

12
flake.lock generated
View File

@ -5,11 +5,11 @@
"systems": "systems" "systems": "systems"
}, },
"locked": { "locked": {
"lastModified": 1710146030, "lastModified": 1731533236,
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide", "owner": "numtide",
"repo": "flake-utils", "repo": "flake-utils",
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -20,11 +20,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1720542800, "lastModified": 1739214665,
"narHash": "sha256-ZgnNHuKV6h2+fQ5LuqnUaqZey1Lqqt5dTUAiAnqH0QQ=", "narHash": "sha256-26L8VAu3/1YRxS8MHgBOyOM8xALdo6N0I04PgorE7UM=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "feb2849fdeb70028c70d73b848214b00d324a497", "rev": "64e75cd44acf21c7933d61d7721e812eac1b5a0a",
"type": "github" "type": "github"
}, },
"original": { "original": {

View File

@ -22,17 +22,21 @@
gzip gzip
# frontend # frontend
nodejs_20 nodejs_22
# linting # linting
python312 python312
poetry poetry
# backend # backend
go_1_22 go_1_24
gofumpt gofumpt
sqlite sqlite
]; ];
shellHook = ''
export GO="${pkgs.go_1_24}/bin/go"
export GOROOT="${pkgs.go_1_24}/share/go"
'';
}; };
} }
); );

248
go.mod
View File

@ -1,6 +1,6 @@
module code.gitea.io/gitea module code.gitea.io/gitea
go 1.23 go 1.24
// rfc5280 said: "The serial number is an integer assigned by the CA to each certificate." // 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: // But some CAs use negative serial number, just relax the check. related:
@ -8,11 +8,11 @@ go 1.23
godebug x509negativeserial=1 godebug x509negativeserial=1
require ( require (
code.gitea.io/actions-proto-go v0.4.0 code.gitea.io/actions-proto-go v0.4.1
code.gitea.io/gitea-vet v0.2.3 code.gitea.io/gitea-vet v0.2.3
code.gitea.io/sdk/gitea v0.17.1 code.gitea.io/sdk/gitea v0.20.0
codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570 codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570
connectrpc.com/connect v1.15.0 connectrpc.com/connect v1.18.1
gitea.com/go-chi/binding v0.0.0-20240430071103-39a851e106ed gitea.com/go-chi/binding v0.0.0-20240430071103-39a851e106ed
gitea.com/go-chi/cache v0.2.1 gitea.com/go-chi/cache v0.2.1
gitea.com/go-chi/captcha v0.0.0-20240315150714-fb487f629098 gitea.com/go-chi/captcha v0.0.0-20240315150714-fb487f629098
@ -20,21 +20,20 @@ require (
gitea.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96 gitea.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96
gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4 gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4
github.com/42wim/httpsig v1.2.2 github.com/42wim/httpsig v1.2.2
github.com/42wim/sshsig v0.0.0-20211121163825-841cf5bbc121 github.com/42wim/sshsig v0.0.0-20240818000253-e3a6333df815
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.12.0 github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.3.2 github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.0
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 github.com/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.9.2 github.com/PuerkitoBio/goquery v1.10.2
github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.7.2 github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.7.3
github.com/alecthomas/chroma/v2 v2.14.0 github.com/alecthomas/chroma/v2 v2.15.0
github.com/aws/aws-sdk-go v1.43.21 github.com/aws/aws-sdk-go-v2/credentials v1.17.60
github.com/aws/aws-sdk-go-v2/credentials v1.17.30 github.com/aws/aws-sdk-go-v2/service/codecommit v1.27.16
github.com/aws/aws-sdk-go-v2/service/codecommit v1.25.1
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb
github.com/blevesearch/bleve/v2 v2.4.2 github.com/blevesearch/bleve/v2 v2.4.2
github.com/buildkite/terminal-to-html/v3 v3.12.1 github.com/buildkite/terminal-to-html/v3 v3.16.6
github.com/caddyserver/certmagic v0.21.3 github.com/caddyserver/certmagic v0.21.7
github.com/charmbracelet/git-lfs-transfer v0.2.0 github.com/charmbracelet/git-lfs-transfer v0.2.0
github.com/chi-middleware/proxy v1.1.1 github.com/chi-middleware/proxy v1.1.1
github.com/dimiro1/reply v0.0.0-20200315094148-d0136a4c9e21 github.com/dimiro1/reply v0.0.0-20200315094148-d0136a4c9e21
@ -46,53 +45,50 @@ require (
github.com/emersion/go-imap v1.2.1 github.com/emersion/go-imap v1.2.1
github.com/emirpasic/gods v1.18.1 github.com/emirpasic/gods v1.18.1
github.com/ethantkoenig/rupture v1.0.1 github.com/ethantkoenig/rupture v1.0.1
github.com/felixge/fgprof v0.9.4 github.com/felixge/fgprof v0.9.5
github.com/fsnotify/fsnotify v1.7.0 github.com/fsnotify/fsnotify v1.8.0
github.com/gliderlabs/ssh v0.3.7 github.com/gliderlabs/ssh v0.3.8
github.com/go-ap/activitypub v0.0.0-20240408091739-ba76b44c2594 github.com/go-ap/activitypub v0.0.0-20250212090640-aeb6499ba581
github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73 github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73
github.com/go-chi/chi/v5 v5.0.13 github.com/go-chi/chi/v5 v5.2.1
github.com/go-chi/cors v1.2.1 github.com/go-chi/cors v1.2.1
github.com/go-co-op/gocron v1.37.0 github.com/go-co-op/gocron v1.37.0
github.com/go-enry/go-enry/v2 v2.9.1 github.com/go-enry/go-enry/v2 v2.9.2
github.com/go-git/go-billy/v5 v5.5.0 github.com/go-git/go-billy/v5 v5.6.2
github.com/go-git/go-git/v5 v5.12.0 github.com/go-git/go-git/v5 v5.13.2
github.com/go-ldap/ldap/v3 v3.4.6 github.com/go-ldap/ldap/v3 v3.4.10
github.com/go-redsync/redsync/v4 v4.13.0 github.com/go-redsync/redsync/v4 v4.13.0
github.com/go-sql-driver/mysql v1.8.1 github.com/go-sql-driver/mysql v1.9.0
github.com/go-swagger/go-swagger v0.31.0 github.com/go-swagger/go-swagger v0.31.0
github.com/go-testfixtures/testfixtures/v3 v3.11.0 github.com/go-webauthn/webauthn v0.11.2
github.com/go-webauthn/webauthn v0.10.2
github.com/gobwas/glob v0.2.3 github.com/gobwas/glob v0.2.3
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f
github.com/gogs/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85 github.com/gogs/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85
github.com/golang-jwt/jwt/v5 v5.2.1 github.com/golang-jwt/jwt/v5 v5.2.1
github.com/google/go-github/v61 v61.0.0 github.com/google/go-github/v61 v61.0.0
github.com/google/licenseclassifier/v2 v2.0.0 github.com/google/licenseclassifier/v2 v2.0.0
github.com/google/pprof v0.0.0-20240618054019-d3b898a103f8 github.com/google/pprof v0.0.0-20250208200701-d0013a598941
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/gorilla/feeds v1.2.0 github.com/gorilla/feeds v1.2.0
github.com/gorilla/sessions v1.3.0 github.com/gorilla/sessions v1.4.0
github.com/h2non/gock v1.2.0
github.com/hashicorp/go-version v1.7.0 github.com/hashicorp/go-version v1.7.0
github.com/hashicorp/golang-lru/v2 v2.0.7 github.com/hashicorp/golang-lru/v2 v2.0.7
github.com/huandu/xstrings v1.5.0 github.com/huandu/xstrings v1.5.0
github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056
github.com/jhillyerd/enmime v1.2.0 github.com/jhillyerd/enmime v1.3.0
github.com/json-iterator/go v1.1.12 github.com/json-iterator/go v1.1.12
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 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.18.0
github.com/klauspost/compress v1.17.9 github.com/klauspost/cpuid/v2 v2.2.9
github.com/klauspost/cpuid/v2 v2.2.8
github.com/lib/pq v1.10.9 github.com/lib/pq v1.10.9
github.com/markbates/goth v1.80.0 github.com/markbates/goth v1.80.0
github.com/mattn/go-isatty v0.0.20 github.com/mattn/go-isatty v0.0.20
github.com/mattn/go-sqlite3 v1.14.22 github.com/mattn/go-sqlite3 v1.14.24
github.com/meilisearch/meilisearch-go v0.26.3 github.com/meilisearch/meilisearch-go v0.29.1-0.20241106140435-0bf60fad690a
github.com/mholt/archiver/v3 v3.5.1 github.com/mholt/archiver/v3 v3.5.1
github.com/microcosm-cc/bluemonday v1.0.26 github.com/microcosm-cc/bluemonday v1.0.27
github.com/microsoft/go-mssqldb v1.7.2 github.com/microsoft/go-mssqldb v1.8.0
github.com/minio/minio-go/v7 v7.0.77 github.com/minio/minio-go/v7 v7.0.87
github.com/msteinert/pam v1.2.0 github.com/msteinert/pam v1.2.0
github.com/nektos/act v0.2.63 github.com/nektos/act v0.2.63
github.com/niklasfasching/go-org v1.7.0 github.com/niklasfasching/go-org v1.7.0
@ -101,69 +97,68 @@ require (
github.com/opencontainers/image-spec v1.1.0 github.com/opencontainers/image-spec v1.1.0
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/pquerna/otp v1.4.0 github.com/pquerna/otp v1.4.0
github.com/prometheus/client_golang v1.19.1 github.com/prometheus/client_golang v1.21.0
github.com/quasoft/websspi v1.1.2 github.com/quasoft/websspi v1.1.2
github.com/redis/go-redis/v9 v9.6.0 github.com/redis/go-redis/v9 v9.7.0
github.com/robfig/cron/v3 v3.0.1 github.com/robfig/cron/v3 v3.0.1
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 github.com/santhosh-tekuri/jsonschema/v5 v5.3.1
github.com/sassoftware/go-rpmutils v0.4.0 github.com/sassoftware/go-rpmutils v0.4.0
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3
github.com/shurcooL/vfsgen v0.0.0-20230704071429-0000e147ea92 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/syndtr/goleveldb v1.0.0
github.com/tstranex/u2f v1.0.0 github.com/tstranex/u2f v1.0.0
github.com/ulikunitz/xz v0.5.12 github.com/ulikunitz/xz v0.5.12
github.com/urfave/cli/v2 v2.27.2 github.com/urfave/cli/v2 v2.27.5
github.com/xanzy/go-gitlab v0.105.0 github.com/wneessen/go-mail v0.6.2
github.com/xeipuuv/gojsonschema v1.2.0 github.com/xeipuuv/gojsonschema v1.2.0
github.com/yohcop/openid-go v1.0.1 github.com/yohcop/openid-go v1.0.1
github.com/yuin/goldmark v1.7.2 github.com/yuin/goldmark v1.7.8
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
github.com/yuin/goldmark-meta v1.1.0 github.com/yuin/goldmark-meta v1.1.0
golang.org/x/crypto v0.26.0 gitlab.com/gitlab-org/api/client-go v0.123.0
golang.org/x/image v0.18.0 golang.org/x/crypto v0.35.0
golang.org/x/net v0.28.0 golang.org/x/image v0.24.0
golang.org/x/oauth2 v0.21.0 golang.org/x/net v0.36.0
golang.org/x/sys v0.24.0 golang.org/x/oauth2 v0.27.0
golang.org/x/text v0.17.0 golang.org/x/sync v0.11.0
golang.org/x/tools v0.24.0 golang.org/x/sys v0.30.0
google.golang.org/grpc v1.62.1 golang.org/x/text v0.22.0
google.golang.org/protobuf v1.34.2 golang.org/x/tools v0.30.0
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df google.golang.org/grpc v1.70.0
google.golang.org/protobuf v1.36.5
gopkg.in/ini.v1 v1.67.0 gopkg.in/ini.v1 v1.67.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
mvdan.cc/xurls/v2 v2.5.0 mvdan.cc/xurls/v2 v2.6.0
strk.kbt.io/projects/go/libravatar v0.0.0-20191008002943-06d1c002b251 strk.kbt.io/projects/go/libravatar v0.0.0-20191008002943-06d1c002b251
xorm.io/builder v0.3.13 xorm.io/builder v0.3.13
xorm.io/xorm v1.3.9 xorm.io/xorm v1.3.9
) )
require ( require (
cloud.google.com/go/compute/metadata v0.3.0 // indirect cloud.google.com/go/compute/metadata v0.6.0 // indirect
dario.cat/mergo v1.0.0 // indirect dario.cat/mergo v1.0.1 // indirect
filippo.io/edwards25519 v1.1.0 // indirect filippo.io/edwards25519 v1.1.0 // indirect
git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078 // indirect git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.9.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
github.com/ClickHouse/ch-go v0.61.5 // indirect github.com/DataDog/zstd v1.5.6 // indirect
github.com/ClickHouse/clickhouse-go/v2 v2.25.0 // indirect
github.com/DataDog/zstd v1.5.5 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.2.1 // indirect github.com/Masterminds/semver/v3 v3.3.1 // indirect
github.com/Masterminds/sprig/v3 v3.2.3 // indirect github.com/Masterminds/sprig/v3 v3.3.0 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/RoaringBitmap/roaring v1.9.4 // indirect github.com/RoaringBitmap/roaring v1.9.4 // indirect
github.com/andybalholm/brotli v1.1.0 // indirect github.com/andybalholm/brotli v1.1.1 // indirect
github.com/andybalholm/cascadia v1.3.2 // indirect github.com/andybalholm/cascadia v1.3.3 // indirect
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/aws/aws-sdk-go-v2 v1.30.4 // indirect github.com/aws/aws-sdk-go-v2 v1.36.2 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.16 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.33 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.16 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.33 // indirect
github.com/aws/smithy-go v1.20.4 // indirect github.com/aws/smithy-go v1.22.3 // indirect
github.com/aymerick/douceur v0.2.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/bits-and-blooms/bitset v1.13.0 // indirect github.com/bits-and-blooms/bitset v1.20.0 // indirect
github.com/blevesearch/bleve_index_api v1.1.10 // indirect github.com/blevesearch/bleve_index_api v1.1.12 // indirect
github.com/blevesearch/geo v0.1.20 // indirect github.com/blevesearch/geo v0.1.20 // indirect
github.com/blevesearch/go-faiss v1.0.20 // indirect github.com/blevesearch/go-faiss v1.0.20 // indirect
github.com/blevesearch/go-porterstemmer v1.0.3 // indirect github.com/blevesearch/go-porterstemmer v1.0.3 // indirect
@ -180,31 +175,30 @@ require (
github.com/blevesearch/zapx/v14 v14.3.10 // indirect github.com/blevesearch/zapx/v14 v14.3.10 // indirect
github.com/blevesearch/zapx/v15 v15.3.13 // indirect github.com/blevesearch/zapx/v15 v15.3.13 // indirect
github.com/blevesearch/zapx/v16 v16.1.5 // indirect github.com/blevesearch/zapx/v16 v16.1.5 // indirect
github.com/boombuler/barcode v1.0.1 // indirect github.com/bmatcuk/doublestar/v4 v4.8.1 // indirect
github.com/boombuler/barcode v1.0.2 // indirect
github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 // indirect github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 // indirect
github.com/caddyserver/zerossl v0.1.3 // indirect github.com/caddyserver/zerossl v0.1.3 // indirect
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a // indirect github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudflare/circl v1.3.9 // indirect github.com/cloudflare/circl v1.6.0 // indirect
github.com/couchbase/go-couchbase v0.1.1 // indirect github.com/couchbase/go-couchbase v0.1.1 // indirect
github.com/couchbase/gomemcached v0.3.1 // indirect github.com/couchbase/gomemcached v0.3.2 // indirect
github.com/couchbase/goutils v0.1.2 // indirect github.com/couchbase/goutils v0.1.2 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
github.com/cyphar/filepath-securejoin v0.2.5 // indirect github.com/cyphar/filepath-securejoin v0.4.1 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/davidmz/go-pageant v1.0.2 // indirect github.com/davidmz/go-pageant v1.0.2 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dlclark/regexp2 v1.11.0 // indirect github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43 // indirect github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 // indirect
github.com/fatih/color v1.17.0 // indirect github.com/fatih/color v1.18.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fxamacker/cbor/v2 v2.6.0 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/git-lfs/pktline v0.0.0-20230103162542-ca444d533ef1 // indirect github.com/git-lfs/pktline v0.0.0-20230103162542-ca444d533ef1 // indirect
github.com/go-ap/errors v0.0.0-20240304112515-6077fa9c17b0 // indirect github.com/go-ap/errors v0.0.0-20250124135319-3da8adefd4a9 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.7 // 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-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-fed/httpsig v1.1.1-0.20201223112313-55836744818e // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-ini/ini v1.67.0 // indirect github.com/go-ini/ini v1.67.0 // indirect
@ -219,44 +213,44 @@ require (
github.com/go-openapi/strfmt v0.23.0 // indirect github.com/go-openapi/strfmt v0.23.0 // indirect
github.com/go-openapi/swag v0.23.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect
github.com/go-openapi/validate v0.24.0 // indirect github.com/go-openapi/validate v0.24.0 // indirect
github.com/go-webauthn/x v0.1.9 // indirect github.com/go-webauthn/x v0.1.16 // indirect
github.com/goccy/go-json v0.10.3 // indirect github.com/goccy/go-json v0.10.5 // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect github.com/golang-jwt/jwt/v4 v4.5.1 // indirect
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
github.com/golang-sql/sqlexp v0.1.0 // indirect github.com/golang-sql/sqlexp v0.1.0 // indirect
github.com/golang/geo v0.0.0-20230421003525-6adc56603217 // 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/protobuf v1.5.4 // indirect
github.com/golang/snappy v0.0.4 // indirect github.com/golang/snappy v0.0.4 // indirect
github.com/google/btree v1.1.2 // indirect github.com/google/btree v1.1.3 // indirect
github.com/google/go-querystring v1.1.0 // indirect github.com/google/go-querystring v1.1.0 // indirect
github.com/google/go-tpm v0.9.0 // indirect github.com/google/go-tpm v0.9.3 // indirect
github.com/gorilla/css v1.0.1 // indirect github.com/gorilla/css v1.0.1 // indirect
github.com/gorilla/handlers v1.5.2 // indirect github.com/gorilla/handlers v1.5.2 // indirect
github.com/gorilla/mux v1.8.1 // indirect github.com/gorilla/mux v1.8.1 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect github.com/gorilla/securecookie v1.1.2 // indirect
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect
github.com/imdario/mergo v0.3.16 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/jessevdk/go-flags v1.5.0 // indirect github.com/jessevdk/go-flags v1.6.1 // indirect
github.com/josharian/intern v1.0.0 // indirect github.com/josharian/intern v1.0.0 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/klauspost/pgzip v1.2.6 // indirect github.com/klauspost/pgzip v1.2.6 // indirect
github.com/kr/pretty v0.3.1 // indirect github.com/kr/pretty v0.3.1 // indirect
github.com/kr/text v0.2.0 // indirect github.com/kr/text v0.2.0 // indirect
github.com/libdns/libdns v0.2.2 // indirect github.com/libdns/libdns v0.2.3 // indirect
github.com/magiconair/properties v1.8.7 // indirect github.com/magiconair/properties v1.8.9 // indirect
github.com/mailru/easyjson v0.7.7 // indirect github.com/mailru/easyjson v0.9.0 // indirect
github.com/markbates/going v1.0.3 // indirect github.com/markbates/going v1.0.3 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mholt/acmez/v2 v2.0.1 // indirect github.com/mattn/go-shellwords v1.0.12 // indirect
github.com/miekg/dns v1.1.61 // indirect github.com/mholt/acmez/v3 v3.0.1 // indirect
github.com/miekg/dns v1.1.63 // indirect
github.com/minio/crc64nvme v1.0.1 // indirect
github.com/minio/md5-simd v1.1.2 // indirect github.com/minio/md5-simd v1.1.2 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect
@ -270,38 +264,33 @@ require (
github.com/oklog/ulid v1.3.1 // indirect github.com/oklog/ulid v1.3.1 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/onsi/ginkgo v1.16.5 // indirect github.com/onsi/ginkgo v1.16.5 // indirect
github.com/onsi/gomega v1.33.1 // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/paulmach/orb v0.11.1 // indirect github.com/pierrec/lz4/v4 v4.1.22 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/pjbgf/sha1cd v0.3.2 // indirect
github.com/pierrec/lz4/v4 v4.1.21 // indirect
github.com/pjbgf/sha1cd v0.3.0 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.55.0 // indirect github.com/prometheus/common v0.62.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect github.com/prometheus/procfs v0.15.1 // indirect
github.com/rhysd/actionlint v1.7.1 // indirect github.com/rhysd/actionlint v1.7.7 // indirect
github.com/rivo/uniseg v0.4.7 // indirect github.com/rivo/uniseg v0.4.7 // indirect
github.com/rogpeppe/go-internal v1.12.0 // indirect github.com/rogpeppe/go-internal v1.13.2-0.20241226121412-a5dc8ff20d0a // indirect
github.com/rs/xid v1.6.0 // indirect github.com/rs/xid v1.6.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/locafero v0.7.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.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/shopspring/decimal v1.4.0 // indirect
github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c // indirect github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c // indirect
github.com/sirupsen/logrus v1.9.3 // indirect github.com/sirupsen/logrus v1.9.3 // indirect
github.com/skeema/knownhosts v1.2.2 // indirect github.com/skeema/knownhosts v1.3.1 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.11.0 // indirect github.com/spf13/afero v1.12.0 // indirect
github.com/spf13/cast v1.6.0 // indirect github.com/spf13/cast v1.7.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/pflag v1.0.6 // indirect
github.com/spf13/viper v1.18.2 // indirect github.com/spf13/viper v1.19.0 // indirect
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
github.com/subosito/gotenv v1.6.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect
github.com/toqueteos/webbrowser v1.2.0 // indirect github.com/toqueteos/webbrowser v1.2.0 // indirect
github.com/unknwon/com v1.0.1 // indirect github.com/unknwon/com v1.0.1 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.55.0 // indirect
github.com/valyala/fastjson v1.6.4 // indirect github.com/valyala/fastjson v1.6.4 // indirect
github.com/x448/float16 v0.8.4 // indirect github.com/x448/float16 v0.8.4 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect
@ -309,20 +298,18 @@ require (
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
github.com/zeebo/blake3 v0.2.3 // indirect github.com/zeebo/assert v1.3.0 // indirect
go.etcd.io/bbolt v1.3.10 // indirect github.com/zeebo/blake3 v0.2.4 // indirect
go.mongodb.org/mongo-driver v1.14.0 // indirect go.etcd.io/bbolt v1.4.0 // indirect
go.opentelemetry.io/otel v1.27.0 // indirect go.mongodb.org/mongo-driver v1.17.2 // indirect
go.opentelemetry.io/otel/trace v1.27.0 // indirect
go.uber.org/atomic v1.11.0 // indirect go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect go.uber.org/zap v1.27.0 // indirect
golang.org/x/exp v0.0.0-20240314144324-c7f7c6466f7f // indirect go.uber.org/zap/exp v0.3.0 // indirect
golang.org/x/mod v0.20.0 // indirect golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa // indirect
golang.org/x/sync v0.8.0 // indirect golang.org/x/mod v0.23.0 // indirect
golang.org/x/time v0.5.0 // indirect golang.org/x/time v0.10.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240314234333-6e1732d8331c // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250219182151-9fdb1cabc7b2 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
) )
@ -331,8 +318,9 @@ replace github.com/hashicorp/go-version => github.com/6543/go-version v1.3.1
replace github.com/shurcooL/vfsgen => github.com/lunny/vfsgen v0.0.0-20220105142115-2c99e1ffdfa0 replace github.com/shurcooL/vfsgen => github.com/lunny/vfsgen v0.0.0-20220105142115-2c99e1ffdfa0
replace github.com/nektos/act => gitea.com/gitea/act v0.261.3 replace github.com/nektos/act => gitea.com/gitea/act v0.261.4
// TODO: the only difference is in `PutObject`: the fork doesn't use `NewVerifyingReader(r, sha256.New(), oid, expectedSize)`, need to figure out why
replace github.com/charmbracelet/git-lfs-transfer => gitea.com/gitea/git-lfs-transfer v0.2.0 replace github.com/charmbracelet/git-lfs-transfer => gitea.com/gitea/git-lfs-transfer v0.2.0
// TODO: This could be removed after https://github.com/mholt/archiver/pull/396 merged // TODO: This could be removed after https://github.com/mholt/archiver/pull/396 merged

658
go.sum

File diff suppressed because it is too large Load Diff

16
main_timezones.go Normal file
View File

@ -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"

View File

@ -48,7 +48,7 @@ type ActionArtifact struct {
ContentEncoding string // The content encoding of the artifact ContentEncoding string // The content encoding of the artifact
ArtifactPath string `xorm:"index unique(runid_name_path)"` // The path to the artifact when runner uploads it ArtifactPath string `xorm:"index unique(runid_name_path)"` // The path to the artifact when runner uploads it
ArtifactName string `xorm:"index unique(runid_name_path)"` // The name of the artifact when runner uploads it ArtifactName string `xorm:"index unique(runid_name_path)"` // The name of the artifact when runner uploads it
Status int64 `xorm:"index"` // The status of the artifact, uploading, expired or need-delete Status ArtifactStatus `xorm:"index"` // The status of the artifact, uploading, expired or need-delete
CreatedUnix timeutil.TimeStamp `xorm:"created"` CreatedUnix timeutil.TimeStamp `xorm:"created"`
UpdatedUnix timeutil.TimeStamp `xorm:"updated index"` UpdatedUnix timeutil.TimeStamp `xorm:"updated index"`
ExpiredUnix timeutil.TimeStamp `xorm:"index"` // The time when the artifact will be expired ExpiredUnix timeutil.TimeStamp `xorm:"index"` // The time when the artifact will be expired
@ -68,7 +68,7 @@ func CreateArtifact(ctx context.Context, t *ActionTask, artifactName, artifactPa
RepoID: t.RepoID, RepoID: t.RepoID,
OwnerID: t.OwnerID, OwnerID: t.OwnerID,
CommitSHA: t.CommitSHA, CommitSHA: t.CommitSHA,
Status: int64(ArtifactStatusUploadPending), Status: ArtifactStatusUploadPending,
ExpiredUnix: timeutil.TimeStamp(time.Now().Unix() + timeutil.Day*expiredDays), ExpiredUnix: timeutil.TimeStamp(time.Now().Unix() + timeutil.Day*expiredDays),
} }
if _, err := db.GetEngine(ctx).Insert(artifact); err != nil { if _, err := db.GetEngine(ctx).Insert(artifact); err != nil {
@ -108,12 +108,19 @@ func UpdateArtifactByID(ctx context.Context, id int64, art *ActionArtifact) erro
type FindArtifactsOptions struct { type FindArtifactsOptions struct {
db.ListOptions db.ListOptions
RepoID int64 RepoID int64
RunID int64 RunID int64
ArtifactName string ArtifactName string
Status int Status int
FinalizedArtifactsV4 bool
} }
func (opts FindArtifactsOptions) ToOrders() string {
return "id"
}
var _ db.FindOptionsOrder = (*FindArtifactsOptions)(nil)
func (opts FindArtifactsOptions) ToConds() builder.Cond { func (opts FindArtifactsOptions) ToConds() builder.Cond {
cond := builder.NewCond() cond := builder.NewCond()
if opts.RepoID > 0 { if opts.RepoID > 0 {
@ -128,11 +135,15 @@ func (opts FindArtifactsOptions) ToConds() builder.Cond {
if opts.Status > 0 { if opts.Status > 0 {
cond = cond.And(builder.Eq{"status": opts.Status}) cond = cond.And(builder.Eq{"status": opts.Status})
} }
if opts.FinalizedArtifactsV4 {
cond = cond.And(builder.Eq{"status": ArtifactStatusUploadConfirmed}.Or(builder.Eq{"status": ArtifactStatusExpired}))
cond = cond.And(builder.Eq{"content_encoding": "application/zip"})
}
return cond return cond
} }
// ActionArtifactMeta is the meta data of an artifact // ActionArtifactMeta is the meta-data of an artifact
type ActionArtifactMeta struct { type ActionArtifactMeta struct {
ArtifactName string ArtifactName string
FileSize int64 FileSize int64
@ -166,18 +177,18 @@ func ListPendingDeleteArtifacts(ctx context.Context, limit int) ([]*ActionArtifa
// SetArtifactExpired sets an artifact to expired // SetArtifactExpired sets an artifact to expired
func SetArtifactExpired(ctx context.Context, artifactID int64) error { func SetArtifactExpired(ctx context.Context, artifactID int64) error {
_, err := db.GetEngine(ctx).Where("id=? AND status = ?", artifactID, ArtifactStatusUploadConfirmed).Cols("status").Update(&ActionArtifact{Status: int64(ArtifactStatusExpired)}) _, err := db.GetEngine(ctx).Where("id=? AND status = ?", artifactID, ArtifactStatusUploadConfirmed).Cols("status").Update(&ActionArtifact{Status: ArtifactStatusExpired})
return err return err
} }
// SetArtifactNeedDelete sets an artifact to need-delete, cron job will delete it // SetArtifactNeedDelete sets an artifact to need-delete, cron job will delete it
func SetArtifactNeedDelete(ctx context.Context, runID int64, name string) error { func SetArtifactNeedDelete(ctx context.Context, runID int64, name string) error {
_, err := db.GetEngine(ctx).Where("run_id=? AND artifact_name=? AND status = ?", runID, name, ArtifactStatusUploadConfirmed).Cols("status").Update(&ActionArtifact{Status: int64(ArtifactStatusPendingDeletion)}) _, err := db.GetEngine(ctx).Where("run_id=? AND artifact_name=? AND status = ?", runID, name, ArtifactStatusUploadConfirmed).Cols("status").Update(&ActionArtifact{Status: ArtifactStatusPendingDeletion})
return err return err
} }
// SetArtifactDeleted sets an artifact to deleted // SetArtifactDeleted sets an artifact to deleted
func SetArtifactDeleted(ctx context.Context, artifactID int64) error { func SetArtifactDeleted(ctx context.Context, artifactID int64) error {
_, err := db.GetEngine(ctx).ID(artifactID).Cols("status").Update(&ActionArtifact{Status: int64(ArtifactStatusDeleted)}) _, err := db.GetEngine(ctx).ID(artifactID).Cols("status").Update(&ActionArtifact{Status: ArtifactStatusDeleted})
return err return err
} }

View File

@ -37,6 +37,7 @@ type ActionRun struct {
TriggerUser *user_model.User `xorm:"-"` TriggerUser *user_model.User `xorm:"-"`
ScheduleID int64 ScheduleID int64
Ref string `xorm:"index"` // the commit/tag/… that caused the run Ref string `xorm:"index"` // the commit/tag/… that caused the run
IsRefDeleted bool `xorm:"-"`
CommitSHA string CommitSHA string
IsForkPullRequest bool // If this is triggered by a PR from a forked repository or an untrusted user, we need to check if it is approved and limit permissions when running the workflow. IsForkPullRequest bool // If this is triggered by a PR from a forked repository or an untrusted user, we need to check if it is approved and limit permissions when running the workflow.
NeedApproval bool // may need approval if it's a fork pull request NeedApproval bool // may need approval if it's a fork pull request
@ -87,7 +88,7 @@ func (run *ActionRun) RefLink() string {
if refName.IsPull() { if refName.IsPull() {
return run.Repo.Link() + "/pulls/" + refName.ShortName() return run.Repo.Link() + "/pulls/" + refName.ShortName()
} }
return git.RefURL(run.Repo.Link(), run.Ref) return run.Repo.Link() + "/src/" + refName.RefWebLinkPath()
} }
// PrettyRef return #id for pull ref or ShortName for others // PrettyRef return #id for pull ref or ShortName for others
@ -153,7 +154,7 @@ func (run *ActionRun) GetPushEventPayload() (*api.PushPayload, error) {
} }
func (run *ActionRun) GetPullRequestEventPayload() (*api.PullRequestPayload, error) { func (run *ActionRun) GetPullRequestEventPayload() (*api.PullRequestPayload, error) {
if run.Event == webhook_module.HookEventPullRequest || run.Event == webhook_module.HookEventPullRequestSync { if run.Event.IsPullRequest() {
var payload api.PullRequestPayload var payload api.PullRequestPayload
if err := json.Unmarshal([]byte(run.EventPayload), &payload); err != nil { if err := json.Unmarshal([]byte(run.EventPayload), &payload); err != nil {
return nil, err return nil, err
@ -193,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. // 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. // 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 // Find all runs in the specified repository, reference, and workflow with non-final status
runs, total, err := db.FindAndCount[ActionRun](ctx, FindRunOptions{ runs, total, err := db.FindAndCount[ActionRun](ctx, FindRunOptions{
RepoID: repoID, RepoID: repoID,
@ -203,14 +204,16 @@ func CancelPreviousJobs(ctx context.Context, repoID int64, ref, workflowID strin
Status: []Status{StatusRunning, StatusWaiting, StatusBlocked}, Status: []Status{StatusRunning, StatusWaiting, StatusBlocked},
}) })
if err != nil { 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 there are no runs found, there's no need to proceed with cancellation, so return nil.
if total == 0 { if total == 0 {
return nil return nil, nil
} }
cancelledJobs := make([]*ActionRunJob, 0, total)
// Iterate over each found run and cancel its associated jobs. // Iterate over each found run and cancel its associated jobs.
for _, run := range runs { for _, run := range runs {
// Find all jobs associated with the current run. // Find all jobs associated with the current run.
@ -218,7 +221,7 @@ func CancelPreviousJobs(ctx context.Context, repoID int64, ref, workflowID strin
RunID: run.ID, RunID: run.ID,
}) })
if err != nil { if err != nil {
return err return cancelledJobs, err
} }
// Iterate over each job and attempt to cancel it. // Iterate over each job and attempt to cancel it.
@ -237,30 +240,33 @@ func CancelPreviousJobs(ctx context.Context, repoID int64, ref, workflowID strin
// Update the job's status and stopped time in the database. // Update the job's status and stopped time in the database.
n, err := UpdateRunJob(ctx, job, builder.Eq{"task_id": 0}, "status", "stopped") n, err := UpdateRunJob(ctx, job, builder.Eq{"task_id": 0}, "status", "stopped")
if err != nil { 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 the update affected 0 rows, it means the job has changed in the meantime, so we need to try again.
if n == 0 { 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 with the next job.
continue continue
} }
// If the job has an associated task, try to stop the task, effectively cancelling the job. // 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 { 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 to indicate successful cancellation of all running and waiting jobs.
return nil return cancelledJobs, nil
} }
// InsertRun inserts a run // InsertRun inserts a run
// The title will be cut off at 255 characters if it's longer than 255 characters.
func InsertRun(ctx context.Context, run *ActionRun, jobs []*jobparser.SingleWorkflow) error { func InsertRun(ctx context.Context, run *ActionRun, jobs []*jobparser.SingleWorkflow) error {
ctx, committer, err := db.TxContext(ctx) ctx, committer, err := db.TxContext(ctx)
if err != nil { if err != nil {
@ -273,6 +279,7 @@ func InsertRun(ctx context.Context, run *ActionRun, jobs []*jobparser.SingleWork
return err return err
} }
run.Index = index run.Index = index
run.Title = util.EllipsisDisplayString(run.Title, 255)
if err := db.Insert(ctx, run); err != nil { if err := db.Insert(ctx, run); err != nil {
return err return err
@ -305,7 +312,7 @@ func InsertRun(ctx context.Context, run *ActionRun, jobs []*jobparser.SingleWork
} else { } else {
hasWaiting = true hasWaiting = true
} }
job.Name, _ = util.SplitStringAtByteN(job.Name, 255) job.Name = util.EllipsisDisplayString(job.Name, 255)
runJobs = append(runJobs, &ActionRunJob{ runJobs = append(runJobs, &ActionRunJob{
RunID: run.ID, RunID: run.ID,
RepoID: run.RepoID, RepoID: run.RepoID,
@ -399,6 +406,7 @@ func UpdateRun(ctx context.Context, run *ActionRun, cols ...string) error {
if len(cols) > 0 { if len(cols) > 0 {
sess.Cols(cols...) sess.Cols(cols...)
} }
run.Title = util.EllipsisDisplayString(run.Title, 255)
affected, err := sess.Update(run) affected, err := sess.Update(run)
if err != nil { if err != nil {
return err return err

View File

@ -137,7 +137,7 @@ func UpdateRunJob(ctx context.Context, job *ActionRunJob, cond builder.Cond, col
if err != nil { if err != nil {
return 0, err return 0, err
} }
run.Status = aggregateJobStatus(jobs) run.Status = AggregateJobStatus(jobs)
if run.Started.IsZero() && run.Status.IsRunning() { if run.Started.IsZero() && run.Status.IsRunning() {
run.Started = timeutil.TimeStampNow() run.Started = timeutil.TimeStampNow()
} }
@ -152,29 +152,35 @@ func UpdateRunJob(ctx context.Context, job *ActionRunJob, cond builder.Cond, col
return affected, nil return affected, nil
} }
func aggregateJobStatus(jobs []*ActionRunJob) Status { func AggregateJobStatus(jobs []*ActionRunJob) Status {
allDone := true allSuccessOrSkipped := len(jobs) != 0
allWaiting := true allSkipped := len(jobs) != 0
hasFailure := false var hasFailure, hasCancelled, hasWaiting, hasRunning, hasBlocked bool
for _, job := range jobs { for _, job := range jobs {
if !job.Status.IsDone() { allSuccessOrSkipped = allSuccessOrSkipped && (job.Status == StatusSuccess || job.Status == StatusSkipped)
allDone = false allSkipped = allSkipped && job.Status == StatusSkipped
} hasFailure = hasFailure || job.Status == StatusFailure
if job.Status != StatusWaiting && !job.Status.IsDone() { hasCancelled = hasCancelled || job.Status == StatusCancelled
allWaiting = false hasWaiting = hasWaiting || job.Status == StatusWaiting
} hasRunning = hasRunning || job.Status == StatusRunning
if job.Status == StatusFailure || job.Status == StatusCancelled { hasBlocked = hasBlocked || job.Status == StatusBlocked
hasFailure = true
}
} }
if allDone { switch {
if hasFailure { case allSkipped:
return StatusFailure return StatusSkipped
} case allSuccessOrSkipped:
return StatusSuccess return StatusSuccess
} case hasCancelled:
if allWaiting { return StatusCancelled
case hasFailure:
return StatusFailure
case hasRunning:
return StatusRunning
case hasWaiting:
return StatusWaiting return StatusWaiting
case hasBlocked:
return StatusBlocked
default:
return StatusUnknown // it shouldn't happen
} }
return StatusRunning
} }

View File

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

View File

@ -10,6 +10,7 @@ import (
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/translation"
webhook_module "code.gitea.io/gitea/modules/webhook" webhook_module "code.gitea.io/gitea/modules/webhook"
"xorm.io/builder" "xorm.io/builder"
@ -112,14 +113,14 @@ type StatusInfo struct {
} }
// GetStatusInfoList returns a slice of StatusInfo // GetStatusInfoList returns a slice of StatusInfo
func GetStatusInfoList(ctx context.Context) []StatusInfo { func GetStatusInfoList(ctx context.Context, lang translation.Locale) []StatusInfo {
// same as those in aggregateJobStatus // same as those in aggregateJobStatus
allStatus := []Status{StatusSuccess, StatusFailure, StatusWaiting, StatusRunning} allStatus := []Status{StatusSuccess, StatusFailure, StatusWaiting, StatusRunning}
statusInfoList := make([]StatusInfo, 0, 4) statusInfoList := make([]StatusInfo, 0, 4)
for _, s := range allStatus { for _, s := range allStatus {
statusInfoList = append(statusInfoList, StatusInfo{ statusInfoList = append(statusInfoList, StatusInfo{
Status: int(s), Status: int(s),
DisplayedStatus: s.String(), DisplayedStatus: s.LocaleString(lang),
}) })
} }
return statusInfoList return statusInfoList

View File

@ -167,6 +167,7 @@ func init() {
type FindRunnerOptions struct { type FindRunnerOptions struct {
db.ListOptions db.ListOptions
IDs []int64
RepoID int64 RepoID int64
OwnerID int64 // it will be ignored if RepoID is set OwnerID int64 // it will be ignored if RepoID is set
Sort string Sort string
@ -178,6 +179,14 @@ type FindRunnerOptions struct {
func (opts FindRunnerOptions) ToConds() builder.Cond { func (opts FindRunnerOptions) ToConds() builder.Cond {
cond := builder.NewCond() 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 { if opts.RepoID > 0 {
c := builder.NewCond().And(builder.Eq{"repo_id": opts.RepoID}) c := builder.NewCond().And(builder.Eq{"repo_id": opts.RepoID})
if opts.WithAvailable { if opts.WithAvailable {
@ -252,6 +261,7 @@ func GetRunnerByID(ctx context.Context, id int64) (*ActionRunner, error) {
// UpdateRunner updates runner's information. // UpdateRunner updates runner's information.
func UpdateRunner(ctx context.Context, r *ActionRunner, cols ...string) error { func UpdateRunner(ctx context.Context, r *ActionRunner, cols ...string) error {
e := db.GetEngine(ctx) e := db.GetEngine(ctx)
r.Name = util.EllipsisDisplayString(r.Name, 255)
var err error var err error
if len(cols) == 0 { if len(cols) == 0 {
_, err = e.ID(r.ID).AllCols().Update(r) _, err = e.ID(r.ID).AllCols().Update(r)
@ -278,6 +288,7 @@ func CreateRunner(ctx context.Context, t *ActionRunner) error {
// Remove OwnerID to avoid confusion; it's not worth returning an error here. // Remove OwnerID to avoid confusion; it's not worth returning an error here.
t.OwnerID = 0 t.OwnerID = 0
} }
t.Name = util.EllipsisDisplayString(t.Name, 255)
return db.Insert(ctx, t) return db.Insert(ctx, t)
} }
@ -326,3 +337,17 @@ func FixRunnersWithoutBelongingRepo(ctx context.Context) (int64, error) {
} }
return res.RowsAffected() return res.RowsAffected()
} }
func CountWrongRepoLevelRunners(ctx context.Context) (int64, error) {
var result int64
_, err := db.GetEngine(ctx).SQL("SELECT count(`id`) FROM `action_runner` WHERE `repo_id` > 0 AND `owner_id` > 0").Get(&result)
return result, err
}
func UpdateWrongRepoLevelRunners(ctx context.Context) (int64, error) {
result, err := db.GetEngine(ctx).Exec("UPDATE `action_runner` SET `owner_id` = 0 WHERE `repo_id` > 0 AND `owner_id` > 0")
if err != nil {
return 0, err
}
return result.RowsAffected()
}

View File

@ -51,7 +51,7 @@ func GetRunnerToken(ctx context.Context, token string) (*ActionRunnerToken, erro
if err != nil { if err != nil {
return nil, err return nil, err
} else if !has { } else if !has {
return nil, fmt.Errorf("runner token %q: %w", token, util.ErrNotExist) return nil, fmt.Errorf(`runner token "%s...": %w`, util.TruncateRunes(token, 3), util.ErrNotExist)
} }
return &runnerToken, nil return &runnerToken, nil
} }
@ -68,19 +68,15 @@ func UpdateRunnerToken(ctx context.Context, r *ActionRunnerToken, cols ...string
return err return err
} }
// NewRunnerToken creates a new active runner token and invalidate all old tokens // NewRunnerTokenWithValue creates a new active runner token and invalidate all old tokens
// ownerID will be ignored and treated as 0 if repoID is non-zero. // ownerID will be ignored and treated as 0 if repoID is non-zero.
func NewRunnerToken(ctx context.Context, ownerID, repoID int64) (*ActionRunnerToken, error) { func NewRunnerTokenWithValue(ctx context.Context, ownerID, repoID int64, token string) (*ActionRunnerToken, error) {
if ownerID != 0 && repoID != 0 { if ownerID != 0 && repoID != 0 {
// It's trying to create a runner token that belongs to a repository, but OwnerID has been set accidentally. // It's trying to create a runner token that belongs to a repository, but OwnerID has been set accidentally.
// Remove OwnerID to avoid confusion; it's not worth returning an error here. // Remove OwnerID to avoid confusion; it's not worth returning an error here.
ownerID = 0 ownerID = 0
} }
token, err := util.CryptoRandomString(40)
if err != nil {
return nil, err
}
runnerToken := &ActionRunnerToken{ runnerToken := &ActionRunnerToken{
OwnerID: ownerID, OwnerID: ownerID,
RepoID: repoID, RepoID: repoID,
@ -95,11 +91,19 @@ func NewRunnerToken(ctx context.Context, ownerID, repoID int64) (*ActionRunnerTo
return err return err
} }
_, err = db.GetEngine(ctx).Insert(runnerToken) _, err := db.GetEngine(ctx).Insert(runnerToken)
return err return err
}) })
} }
func NewRunnerToken(ctx context.Context, ownerID, repoID int64) (*ActionRunnerToken, error) {
token, err := util.CryptoRandomString(40)
if err != nil {
return nil, err
}
return NewRunnerTokenWithValue(ctx, ownerID, repoID, token)
}
// GetLatestRunnerToken returns the latest runner token // GetLatestRunnerToken returns the latest runner token
func GetLatestRunnerToken(ctx context.Context, ownerID, repoID int64) (*ActionRunnerToken, error) { func GetLatestRunnerToken(ctx context.Context, ownerID, repoID int64) (*ActionRunnerToken, error) {
if ownerID != 0 && repoID != 0 { if ownerID != 0 && repoID != 0 {

View File

@ -17,7 +17,7 @@ func TestGetLatestRunnerToken(t *testing.T) {
token := unittest.AssertExistsAndLoadBean(t, &ActionRunnerToken{ID: 3}) token := unittest.AssertExistsAndLoadBean(t, &ActionRunnerToken{ID: 3})
expectedToken, err := GetLatestRunnerToken(db.DefaultContext, 1, 0) expectedToken, err := GetLatestRunnerToken(db.DefaultContext, 1, 0)
assert.NoError(t, err) assert.NoError(t, err)
assert.EqualValues(t, token, expectedToken) assert.EqualValues(t, expectedToken, token)
} }
func TestNewRunnerToken(t *testing.T) { func TestNewRunnerToken(t *testing.T) {
@ -26,7 +26,7 @@ func TestNewRunnerToken(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
expectedToken, err := GetLatestRunnerToken(db.DefaultContext, 1, 0) expectedToken, err := GetLatestRunnerToken(db.DefaultContext, 1, 0)
assert.NoError(t, err) assert.NoError(t, err)
assert.EqualValues(t, token, expectedToken) assert.EqualValues(t, expectedToken, token)
} }
func TestUpdateRunnerToken(t *testing.T) { func TestUpdateRunnerToken(t *testing.T) {
@ -36,5 +36,5 @@ func TestUpdateRunnerToken(t *testing.T) {
assert.NoError(t, UpdateRunnerToken(db.DefaultContext, token)) assert.NoError(t, UpdateRunnerToken(db.DefaultContext, token))
expectedToken, err := GetLatestRunnerToken(db.DefaultContext, 1, 0) expectedToken, err := GetLatestRunnerToken(db.DefaultContext, 1, 0)
assert.NoError(t, err) assert.NoError(t, err)
assert.EqualValues(t, token, expectedToken) assert.EqualValues(t, expectedToken, token)
} }

View File

@ -12,6 +12,7 @@ import (
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
webhook_module "code.gitea.io/gitea/modules/webhook" webhook_module "code.gitea.io/gitea/modules/webhook"
) )
@ -42,15 +43,12 @@ func init() {
// GetSchedulesMapByIDs returns the schedules by given id slice. // GetSchedulesMapByIDs returns the schedules by given id slice.
func GetSchedulesMapByIDs(ctx context.Context, ids []int64) (map[int64]*ActionSchedule, error) { func GetSchedulesMapByIDs(ctx context.Context, ids []int64) (map[int64]*ActionSchedule, error) {
schedules := make(map[int64]*ActionSchedule, len(ids)) schedules := make(map[int64]*ActionSchedule, len(ids))
if len(ids) == 0 {
return schedules, nil
}
return schedules, db.GetEngine(ctx).In("id", ids).Find(&schedules) return schedules, db.GetEngine(ctx).In("id", ids).Find(&schedules)
} }
// GetReposMapByIDs returns the repos by given id slice.
func GetReposMapByIDs(ctx context.Context, ids []int64) (map[int64]*repo_model.Repository, error) {
repos := make(map[int64]*repo_model.Repository, len(ids))
return repos, db.GetEngine(ctx).In("id", ids).Find(&repos)
}
// CreateScheduleTask creates new schedule task. // CreateScheduleTask creates new schedule task.
func CreateScheduleTask(ctx context.Context, rows []*ActionSchedule) error { func CreateScheduleTask(ctx context.Context, rows []*ActionSchedule) error {
// Return early if there are no rows to insert // Return early if there are no rows to insert
@ -67,6 +65,7 @@ func CreateScheduleTask(ctx context.Context, rows []*ActionSchedule) error {
// Loop through each schedule row // Loop through each schedule row
for _, row := range rows { for _, row := range rows {
row.Title = util.EllipsisDisplayString(row.Title, 255)
// Create new schedule row // Create new schedule row
if err = db.Insert(ctx, row); err != nil { if err = db.Insert(ctx, row); err != nil {
return err return err
@ -118,21 +117,22 @@ func DeleteScheduleTaskByRepo(ctx context.Context, id int64) error {
return committer.Commit() 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 // 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 // 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 { 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 // cancel running cron jobs of this repository and delete old schedules
if err := CancelPreviousJobs( jobs, err := CancelPreviousJobs(
ctx, ctx,
repo.ID, repo.ID,
repo.DefaultBranch, repo.DefaultBranch,
"", "",
webhook_module.HookEventSchedule, 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
} }

View File

@ -32,7 +32,7 @@ func (specs SpecList) LoadSchedules(ctx context.Context) error {
} }
repoIDs := specs.GetRepoIDs() repoIDs := specs.GetRepoIDs()
repos, err := GetReposMapByIDs(ctx, repoIDs) repos, err := repo_model.GetRepositoriesMapByIDs(ctx, repoIDs)
if err != nil { if err != nil {
return err return err
} }

View File

@ -7,19 +7,17 @@ import (
"testing" "testing"
"time" "time"
"code.gitea.io/gitea/modules/test"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestActionScheduleSpec_Parse(t *testing.T) { func TestActionScheduleSpec_Parse(t *testing.T) {
// Mock the local timezone is not UTC // Mock the local timezone is not UTC
local := time.Local
tz, err := time.LoadLocation("Asia/Shanghai") tz, err := time.LoadLocation("Asia/Shanghai")
require.NoError(t, err) require.NoError(t, err)
defer func() { defer test.MockVariableValue(&time.Local, tz)()
time.Local = local
}()
time.Local = tz
now, err := time.Parse(time.RFC3339, "2024-07-31T15:47:55+08:00") now, err := time.Parse(time.RFC3339, "2024-07-31T15:47:55+08:00")
require.NoError(t, err) require.NoError(t, err)

View File

@ -298,7 +298,7 @@ func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask
if len(workflowJob.Steps) > 0 { if len(workflowJob.Steps) > 0 {
steps := make([]*ActionTaskStep, len(workflowJob.Steps)) steps := make([]*ActionTaskStep, len(workflowJob.Steps))
for i, v := range workflowJob.Steps { for i, v := range workflowJob.Steps {
name, _ := util.SplitStringAtByteN(v.String(), 255) name := util.EllipsisDisplayString(v.String(), 255)
steps[i] = &ActionTaskStep{ steps[i] = &ActionTaskStep{
Name: name, Name: name,
TaskID: task.ID, TaskID: task.ID,
@ -341,7 +341,7 @@ func UpdateTask(ctx context.Context, task *ActionTask, cols ...string) error {
// UpdateTaskByState updates the task by the state. // UpdateTaskByState updates the task by the state.
// It will always update the task if the state is not final, even there is no change. // It will always update the task if the state is not final, even there is no change.
// So it will update ActionTask.Updated to avoid the task being judged as a zombie task. // So it will update ActionTask.Updated to avoid the task being judged as a zombie task.
func UpdateTaskByState(ctx context.Context, state *runnerv1.TaskState) (*ActionTask, error) { func UpdateTaskByState(ctx context.Context, runnerID int64, state *runnerv1.TaskState) (*ActionTask, error) {
stepStates := map[int64]*runnerv1.StepState{} stepStates := map[int64]*runnerv1.StepState{}
for _, v := range state.Steps { for _, v := range state.Steps {
stepStates[v.Id] = v stepStates[v.Id] = v
@ -360,6 +360,8 @@ func UpdateTaskByState(ctx context.Context, state *runnerv1.TaskState) (*ActionT
return nil, err return nil, err
} else if !has { } else if !has {
return nil, util.ErrNotExist return nil, util.ErrNotExist
} else if runnerID != task.RunnerID {
return nil, fmt.Errorf("invalid runner for task")
} }
if task.Status.IsDone() { if task.Status.IsDone() {

View File

@ -58,6 +58,7 @@ func InsertVariable(ctx context.Context, ownerID, repoID int64, name, data strin
type FindVariablesOpts struct { type FindVariablesOpts struct {
db.ListOptions db.ListOptions
IDs []int64
RepoID int64 RepoID int64
OwnerID int64 // it will be ignored if RepoID is set OwnerID int64 // it will be ignored if RepoID is set
Name string Name string
@ -65,6 +66,15 @@ type FindVariablesOpts struct {
func (opts FindVariablesOpts) ToConds() builder.Cond { func (opts FindVariablesOpts) ToConds() builder.Cond {
cond := builder.NewCond() 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, // Since we now support instance-level variables,
// there is no need to check for null values for `owner_id` and `repo_id` // there is no need to check for null values for `owner_id` and `repo_id`
cond = cond.And(builder.Eq{"repo_id": opts.RepoID}) 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) return db.Find[ActionVariable](ctx, opts)
} }
func UpdateVariable(ctx context.Context, variable *ActionVariable) (bool, error) { func UpdateVariableCols(ctx context.Context, variable *ActionVariable, cols ...string) (bool, error) {
count, err := db.GetEngine(ctx).ID(variable.ID).Cols("name", "data"). variable.Name = strings.ToUpper(variable.Name)
Update(&ActionVariable{ count, err := db.GetEngine(ctx).
Name: variable.Name, ID(variable.ID).
Data: variable.Data, Cols(cols...).
}) Update(variable)
return count != 0, err return count != 0, err
} }
@ -137,3 +147,17 @@ func GetVariablesOfRun(ctx context.Context, run *ActionRun) (map[string]string,
return variables, nil return variables, nil
} }
func CountWrongRepoLevelVariables(ctx context.Context) (int64, error) {
var result int64
_, err := db.GetEngine(ctx).SQL("SELECT count(`id`) FROM `action_variable` WHERE `repo_id` > 0 AND `owner_id` > 0").Get(&result)
return result, err
}
func UpdateWrongRepoLevelVariables(ctx context.Context) (int64, error) {
result, err := db.GetEngine(ctx).Exec("UPDATE `action_variable` SET `owner_id` = 0 WHERE `repo_id` > 0 AND `owner_id` > 0")
if err != nil {
return 0, err
}
return result.RowsAffected()
}

View File

@ -16,16 +16,14 @@ import (
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues" issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/organization"
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user" 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/git"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
"xorm.io/builder" "xorm.io/builder"
"xorm.io/xorm/schemas" "xorm.io/xorm/schemas"
@ -72,9 +70,9 @@ func (at ActionType) String() string {
case ActionRenameRepo: case ActionRenameRepo:
return "rename_repo" return "rename_repo"
case ActionStarRepo: case ActionStarRepo:
return "star_repo" return "star_repo" // will not displayed in feeds.tmpl
case ActionWatchRepo: case ActionWatchRepo:
return "watch_repo" return "watch_repo" // will not displayed in feeds.tmpl
case ActionCommitRepo: case ActionCommitRepo:
return "commit_repo" return "commit_repo"
case ActionCreateIssue: case ActionCreateIssue:
@ -171,7 +169,10 @@ func (a *Action) TableIndices() []*schemas.Index {
cudIndex := schemas.NewIndex("c_u_d", schemas.IndexType) cudIndex := schemas.NewIndex("c_u_d", schemas.IndexType)
cudIndex.AddColumn("created_unix", "user_id", "is_deleted") cudIndex.AddColumn("created_unix", "user_id", "is_deleted")
indices := []*schemas.Index{actUserIndex, repoIndex, cudIndex} cuIndex := schemas.NewIndex("c_u", schemas.IndexType)
cuIndex.AddColumn("user_id", "is_deleted")
indices := []*schemas.Index{actUserIndex, repoIndex, cudIndex, cuIndex}
return indices return indices
} }
@ -197,15 +198,13 @@ func (a *Action) LoadActUser(ctx context.Context) {
} }
} }
func (a *Action) loadRepo(ctx context.Context) { func (a *Action) LoadRepo(ctx context.Context) error {
if a.Repo != nil { if a.Repo != nil {
return return nil
} }
var err error var err error
a.Repo, err = repo_model.GetRepositoryByID(ctx, a.RepoID) a.Repo, err = repo_model.GetRepositoryByID(ctx, a.RepoID)
if err != nil { return err
log.Error("repo_model.GetRepositoryByID(%d): %v", a.RepoID, err)
}
} }
// GetActFullName gets the action's user full name. // GetActFullName gets the action's user full name.
@ -223,7 +222,7 @@ func (a *Action) GetActUserName(ctx context.Context) string {
// ShortActUserName gets the action's user name trimmed to max 20 // ShortActUserName gets the action's user name trimmed to max 20
// chars. // chars.
func (a *Action) ShortActUserName(ctx context.Context) string { func (a *Action) ShortActUserName(ctx context.Context) string {
return base.EllipsisString(a.GetActUserName(ctx), 20) return util.EllipsisDisplayString(a.GetActUserName(ctx), 20)
} }
// GetActDisplayName gets the action's display name based on DEFAULT_SHOW_FULL_NAME, or falls back to the username if it is blank. // GetActDisplayName gets the action's display name based on DEFAULT_SHOW_FULL_NAME, or falls back to the username if it is blank.
@ -247,26 +246,32 @@ func (a *Action) GetActDisplayNameTitle(ctx context.Context) string {
// GetRepoUserName returns the name of the action repository owner. // GetRepoUserName returns the name of the action repository owner.
func (a *Action) GetRepoUserName(ctx context.Context) string { func (a *Action) GetRepoUserName(ctx context.Context) string {
a.loadRepo(ctx) _ = a.LoadRepo(ctx)
if a.Repo == nil {
return "(non-existing-repo)"
}
return a.Repo.OwnerName return a.Repo.OwnerName
} }
// ShortRepoUserName returns the name of the action repository owner // ShortRepoUserName returns the name of the action repository owner
// trimmed to max 20 chars. // trimmed to max 20 chars.
func (a *Action) ShortRepoUserName(ctx context.Context) string { func (a *Action) ShortRepoUserName(ctx context.Context) string {
return base.EllipsisString(a.GetRepoUserName(ctx), 20) return util.EllipsisDisplayString(a.GetRepoUserName(ctx), 20)
} }
// GetRepoName returns the name of the action repository. // GetRepoName returns the name of the action repository.
func (a *Action) GetRepoName(ctx context.Context) string { func (a *Action) GetRepoName(ctx context.Context) string {
a.loadRepo(ctx) _ = a.LoadRepo(ctx)
if a.Repo == nil {
return "(non-existing-repo)"
}
return a.Repo.Name return a.Repo.Name
} }
// ShortRepoName returns the name of the action repository // ShortRepoName returns the name of the action repository
// trimmed to max 33 chars. // trimmed to max 33 chars.
func (a *Action) ShortRepoName(ctx context.Context) string { func (a *Action) ShortRepoName(ctx context.Context) string {
return base.EllipsisString(a.GetRepoName(ctx), 33) return util.EllipsisDisplayString(a.GetRepoName(ctx), 33)
} }
// GetRepoPath returns the virtual path to the action repository. // GetRepoPath returns the virtual path to the action repository.
@ -346,7 +351,7 @@ func (a *Action) GetBranch() string {
// GetRefLink returns the action's ref link. // GetRefLink returns the action's ref link.
func (a *Action) GetRefLink(ctx context.Context) string { func (a *Action) GetRefLink(ctx context.Context) string {
return git.RefURL(a.GetRepoLink(ctx), a.RefName) return a.GetRepoLink(ctx) + "/src/" + git.RefName(a.RefName).RefWebLinkPath()
} }
// GetTag returns the action's repository tag. // GetTag returns the action's repository tag.
@ -439,65 +444,31 @@ type GetFeedsOptions struct {
Date string // the day we want activity for: YYYY-MM-DD Date string // the day we want activity for: YYYY-MM-DD
} }
// GetFeeds returns actions according to the provided options
func GetFeeds(ctx context.Context, opts GetFeedsOptions) (ActionList, int64, error) {
if opts.RequestedUser == nil && opts.RequestedTeam == nil && opts.RequestedRepo == nil {
return nil, 0, fmt.Errorf("need at least one of these filters: RequestedUser, RequestedTeam, RequestedRepo")
}
cond, err := activityQueryCondition(ctx, opts)
if err != nil {
return nil, 0, err
}
actions := make([]*Action, 0, opts.PageSize)
var count int64
opts.SetDefaultValues()
if opts.Page < 10 { // TODO: why it's 10 but other values? It's an experience value.
sess := db.GetEngine(ctx).Where(cond)
sess = db.SetSessionPagination(sess, &opts)
count, err = sess.Desc("`action`.created_unix").FindAndCount(&actions)
if err != nil {
return nil, 0, fmt.Errorf("FindAndCount: %w", err)
}
} else {
// First, only query which IDs are necessary, and only then query all actions to speed up the overall query
sess := db.GetEngine(ctx).Where(cond).Select("`action`.id")
sess = db.SetSessionPagination(sess, &opts)
actionIDs := make([]int64, 0, opts.PageSize)
if err := sess.Table("action").Desc("`action`.created_unix").Find(&actionIDs); err != nil {
return nil, 0, fmt.Errorf("Find(actionsIDs): %w", err)
}
count, err = db.GetEngine(ctx).Where(cond).
Table("action").
Cols("`action`.id").Count()
if err != nil {
return nil, 0, fmt.Errorf("Count: %w", err)
}
if err := db.GetEngine(ctx).In("`action`.id", actionIDs).Desc("`action`.created_unix").Find(&actions); err != nil {
return nil, 0, fmt.Errorf("Find: %w", err)
}
}
if err := ActionList(actions).LoadAttributes(ctx); err != nil {
return nil, 0, fmt.Errorf("LoadAttributes: %w", err)
}
return actions, count, nil
}
// ActivityReadable return whether doer can read activities of user // ActivityReadable return whether doer can read activities of user
func ActivityReadable(user, doer *user_model.User) bool { func ActivityReadable(user, doer *user_model.User) bool {
return !user.KeepActivityPrivate || return !user.KeepActivityPrivate ||
doer != nil && (doer.IsAdmin || user.ID == doer.ID) doer != nil && (doer.IsAdmin || user.ID == doer.ID)
} }
func activityQueryCondition(ctx context.Context, opts GetFeedsOptions) (builder.Cond, error) { 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() cond := builder.NewCond()
if opts.RequestedTeam != nil && opts.RequestedUser == nil { if opts.RequestedTeam != nil && opts.RequestedUser == nil {
@ -554,8 +525,8 @@ func activityQueryCondition(ctx context.Context, opts GetFeedsOptions) (builder.
} }
if opts.RequestedTeam != nil { if opts.RequestedTeam != nil {
env := organization.OrgFromUser(opts.RequestedUser).AccessibleTeamReposEnv(ctx, opts.RequestedTeam) env := repo_model.AccessibleTeamReposEnv(organization.OrgFromUser(opts.RequestedUser), opts.RequestedTeam)
teamRepoIDs, err := env.RepoIDs(1, opts.RequestedUser.NumRepos) teamRepoIDs, err := env.RepoIDs(ctx, 1, opts.RequestedUser.NumRepos)
if err != nil { if err != nil {
return nil, fmt.Errorf("GetTeamRepositories: %w", err) return nil, fmt.Errorf("GetTeamRepositories: %w", err)
} }
@ -577,17 +548,7 @@ func activityQueryCondition(ctx context.Context, opts GetFeedsOptions) (builder.
cond = cond.And(builder.Eq{"is_deleted": false}) cond = cond.And(builder.Eq{"is_deleted": false})
} }
if opts.Date != "" { cond = cond.And(FeedDateCond(opts))
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, nil return cond, nil
} }
@ -602,130 +563,6 @@ func DeleteOldActions(ctx context.Context, olderThan time.Duration) (err error)
return err return err
} }
// NotifyWatchers creates batch of actions for every watcher.
// It could insert duplicate actions for a repository action, like this:
// * Original action: UserID=1 (the real actor), ActUserID=1
// * Organization action: UserID=100 (the repo's org), ActUserID=1
// * Watcher action: UserID=20 (a user who is watching a repo), ActUserID=1
func NotifyWatchers(ctx context.Context, actions ...*Action) error {
var watchers []*repo_model.Watch
var repo *repo_model.Repository
var err error
var permCode []bool
var permIssue []bool
var permPR []bool
e := db.GetEngine(ctx)
for _, act := range actions {
repoChanged := repo == nil || repo.ID != act.RepoID
if repoChanged {
// Add feeds for user self and all watchers.
watchers, err = repo_model.GetWatchers(ctx, act.RepoID)
if err != nil {
return fmt.Errorf("get watchers: %w", err)
}
}
// Add feed for actioner.
act.UserID = act.ActUserID
if _, err = e.Insert(act); err != nil {
return fmt.Errorf("insert new actioner: %w", err)
}
if repoChanged {
act.loadRepo(ctx)
repo = act.Repo
// check repo owner exist.
if err := act.Repo.LoadOwner(ctx); err != nil {
return fmt.Errorf("can't get repo owner: %w", err)
}
} else if act.Repo == nil {
act.Repo = repo
}
// Add feed for organization
if act.Repo.Owner.IsOrganization() && act.ActUserID != act.Repo.Owner.ID {
act.ID = 0
act.UserID = act.Repo.Owner.ID
if err = db.Insert(ctx, act); err != nil {
return fmt.Errorf("insert new actioner: %w", err)
}
}
if repoChanged {
permCode = make([]bool, len(watchers))
permIssue = make([]bool, len(watchers))
permPR = make([]bool, len(watchers))
for i, watcher := range watchers {
user, err := user_model.GetUserByID(ctx, watcher.UserID)
if err != nil {
permCode[i] = false
permIssue[i] = false
permPR[i] = false
continue
}
perm, err := access_model.GetUserRepoPermission(ctx, repo, user)
if err != nil {
permCode[i] = false
permIssue[i] = false
permPR[i] = false
continue
}
permCode[i] = perm.CanRead(unit.TypeCode)
permIssue[i] = perm.CanRead(unit.TypeIssues)
permPR[i] = perm.CanRead(unit.TypePullRequests)
}
}
for i, watcher := range watchers {
if act.ActUserID == watcher.UserID {
continue
}
act.ID = 0
act.UserID = watcher.UserID
act.Repo.Units = nil
switch act.OpType {
case ActionCommitRepo, ActionPushTag, ActionDeleteTag, ActionPublishRelease, ActionDeleteBranch:
if !permCode[i] {
continue
}
case ActionCreateIssue, ActionCommentIssue, ActionCloseIssue, ActionReopenIssue:
if !permIssue[i] {
continue
}
case ActionCreatePullRequest, ActionCommentPull, ActionMergePullRequest, ActionClosePullRequest, ActionReopenPullRequest, ActionAutoMergePullRequest:
if !permPR[i] {
continue
}
}
if err = db.Insert(ctx, act); err != nil {
return fmt.Errorf("insert new action: %w", err)
}
}
}
return nil
}
// NotifyWatchersActions creates batch of actions for every watcher.
func NotifyWatchersActions(ctx context.Context, acts []*Action) error {
ctx, committer, err := db.TxContext(ctx)
if err != nil {
return err
}
defer committer.Close()
for _, act := range acts {
if err := NotifyWatchers(ctx, act); err != nil {
return err
}
}
return committer.Commit()
}
// DeleteIssueActions delete all actions related with issueID // DeleteIssueActions delete all actions related with issueID
func DeleteIssueActions(ctx context.Context, repoID, issueID, issueIndex int64) error { func DeleteIssueActions(ctx context.Context, repoID, issueID, issueIndex int64) error {
// delete actions assigned to this issue // delete actions assigned to this issue
@ -761,7 +598,7 @@ func DeleteIssueActions(ctx context.Context, repoID, issueID, issueIndex int64)
// CountActionCreatedUnixString count actions where created_unix is an empty string // CountActionCreatedUnixString count actions where created_unix is an empty string
func CountActionCreatedUnixString(ctx context.Context) (int64, error) { func CountActionCreatedUnixString(ctx context.Context) (int64, error) {
if setting.Database.Type.IsSQLite3() { if setting.Database.Type.IsSQLite3() {
return db.GetEngine(ctx).Where(`created_unix = ""`).Count(new(Action)) return db.GetEngine(ctx).Where(`created_unix = ''`).Count(new(Action))
} }
return 0, nil return 0, nil
} }
@ -769,7 +606,7 @@ func CountActionCreatedUnixString(ctx context.Context) (int64, error) {
// FixActionCreatedUnixString set created_unix to zero if it is an empty string // FixActionCreatedUnixString set created_unix to zero if it is an empty string
func FixActionCreatedUnixString(ctx context.Context) (int64, error) { func FixActionCreatedUnixString(ctx context.Context) (int64, error) {
if setting.Database.Type.IsSQLite3() { if setting.Database.Type.IsSQLite3() {
res, err := db.GetEngine(ctx).Exec(`UPDATE action SET created_unix = 0 WHERE created_unix = ""`) res, err := db.GetEngine(ctx).Exec(`UPDATE action SET created_unix = 0 WHERE created_unix = ''`)
if err != nil { if err != nil {
return 0, err return 0, err
} }

View File

@ -201,3 +201,77 @@ func (actions ActionList) LoadIssues(ctx context.Context) error {
} }
return nil return nil
} }
// GetFeeds returns actions according to the provided options
func GetFeeds(ctx context.Context, opts GetFeedsOptions) (ActionList, int64, error) {
if opts.RequestedUser == nil && opts.RequestedTeam == nil && opts.RequestedRepo == nil {
return nil, 0, fmt.Errorf("need at least one of these filters: RequestedUser, RequestedTeam, RequestedRepo")
}
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)
var count int64
opts.SetDefaultValues()
if opts.Page < 10 { // TODO: why it's 10 but other values? It's an experience value.
sess := db.GetEngine(ctx).Where(cond)
sess = db.SetSessionPagination(sess, &opts)
count, err = sess.Desc("`action`.created_unix").FindAndCount(&actions)
if err != nil {
return nil, 0, fmt.Errorf("FindAndCount: %w", err)
}
} else {
// First, only query which IDs are necessary, and only then query all actions to speed up the overall query
sess := db.GetEngine(ctx).Where(cond).Select("`action`.id")
sess = db.SetSessionPagination(sess, &opts)
actionIDs := make([]int64, 0, opts.PageSize)
if err := sess.Table("action").Desc("`action`.created_unix").Find(&actionIDs); err != nil {
return nil, 0, fmt.Errorf("Find(actionsIDs): %w", err)
}
count, err = db.GetEngine(ctx).Where(cond).
Table("action").
Cols("`action`.id").Count()
if err != nil {
return nil, 0, fmt.Errorf("Count: %w", err)
}
if err := db.GetEngine(ctx).In("`action`.id", actionIDs).Desc("`action`.created_unix").Find(&actions); err != nil {
return nil, 0, fmt.Errorf("Find: %w", err)
}
}
if err := ActionList(actions).LoadAttributes(ctx); err != nil {
return nil, 0, fmt.Errorf("LoadAttributes: %w", err)
}
return actions, count, nil
}

View File

@ -42,114 +42,6 @@ func TestAction_GetRepoLink(t *testing.T) {
assert.Equal(t, comment.HTMLURL(db.DefaultContext), action.GetCommentHTMLURL(db.DefaultContext)) assert.Equal(t, comment.HTMLURL(db.DefaultContext), action.GetCommentHTMLURL(db.DefaultContext))
} }
func TestGetFeeds(t *testing.T) {
// test with an individual user
assert.NoError(t, unittest.PrepareTestDatabase())
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
actions, count, err := activities_model.GetFeeds(db.DefaultContext, activities_model.GetFeedsOptions{
RequestedUser: user,
Actor: user,
IncludePrivate: true,
OnlyPerformedBy: false,
IncludeDeleted: true,
})
assert.NoError(t, err)
if assert.Len(t, actions, 1) {
assert.EqualValues(t, 1, actions[0].ID)
assert.EqualValues(t, user.ID, actions[0].UserID)
}
assert.Equal(t, int64(1), count)
actions, count, err = activities_model.GetFeeds(db.DefaultContext, activities_model.GetFeedsOptions{
RequestedUser: user,
Actor: user,
IncludePrivate: false,
OnlyPerformedBy: false,
})
assert.NoError(t, err)
assert.Len(t, actions, 0)
assert.Equal(t, int64(0), count)
}
func TestGetFeedsForRepos(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
privRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
pubRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 8})
// private repo & no login
actions, count, err := activities_model.GetFeeds(db.DefaultContext, activities_model.GetFeedsOptions{
RequestedRepo: privRepo,
IncludePrivate: true,
})
assert.NoError(t, err)
assert.Len(t, actions, 0)
assert.Equal(t, int64(0), count)
// public repo & no login
actions, count, err = activities_model.GetFeeds(db.DefaultContext, activities_model.GetFeedsOptions{
RequestedRepo: pubRepo,
IncludePrivate: true,
})
assert.NoError(t, err)
assert.Len(t, actions, 1)
assert.Equal(t, int64(1), count)
// private repo and login
actions, count, err = activities_model.GetFeeds(db.DefaultContext, activities_model.GetFeedsOptions{
RequestedRepo: privRepo,
IncludePrivate: true,
Actor: user,
})
assert.NoError(t, err)
assert.Len(t, actions, 1)
assert.Equal(t, int64(1), count)
// public repo & login
actions, count, err = activities_model.GetFeeds(db.DefaultContext, activities_model.GetFeedsOptions{
RequestedRepo: pubRepo,
IncludePrivate: true,
Actor: user,
})
assert.NoError(t, err)
assert.Len(t, actions, 1)
assert.Equal(t, int64(1), count)
}
func TestGetFeeds2(t *testing.T) {
// test with an organization user
assert.NoError(t, unittest.PrepareTestDatabase())
org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3})
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
actions, count, err := activities_model.GetFeeds(db.DefaultContext, activities_model.GetFeedsOptions{
RequestedUser: org,
Actor: user,
IncludePrivate: true,
OnlyPerformedBy: false,
IncludeDeleted: true,
})
assert.NoError(t, err)
assert.Len(t, actions, 1)
if assert.Len(t, actions, 1) {
assert.EqualValues(t, 2, actions[0].ID)
assert.EqualValues(t, org.ID, actions[0].UserID)
}
assert.Equal(t, int64(1), count)
actions, count, err = activities_model.GetFeeds(db.DefaultContext, activities_model.GetFeedsOptions{
RequestedUser: org,
Actor: user,
IncludePrivate: false,
OnlyPerformedBy: false,
IncludeDeleted: true,
})
assert.NoError(t, err)
assert.Len(t, actions, 0)
assert.Equal(t, int64(0), count)
}
func TestActivityReadable(t *testing.T) { func TestActivityReadable(t *testing.T) {
tt := []struct { tt := []struct {
desc string desc string
@ -190,63 +82,6 @@ func TestActivityReadable(t *testing.T) {
} }
} }
func TestNotifyWatchers(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
action := &activities_model.Action{
ActUserID: 8,
RepoID: 1,
OpType: activities_model.ActionStarRepo,
}
assert.NoError(t, activities_model.NotifyWatchers(db.DefaultContext, action))
// One watchers are inactive, thus action is only created for user 8, 1, 4, 11
unittest.AssertExistsAndLoadBean(t, &activities_model.Action{
ActUserID: action.ActUserID,
UserID: 8,
RepoID: action.RepoID,
OpType: action.OpType,
})
unittest.AssertExistsAndLoadBean(t, &activities_model.Action{
ActUserID: action.ActUserID,
UserID: 1,
RepoID: action.RepoID,
OpType: action.OpType,
})
unittest.AssertExistsAndLoadBean(t, &activities_model.Action{
ActUserID: action.ActUserID,
UserID: 4,
RepoID: action.RepoID,
OpType: action.OpType,
})
unittest.AssertExistsAndLoadBean(t, &activities_model.Action{
ActUserID: action.ActUserID,
UserID: 11,
RepoID: action.RepoID,
OpType: action.OpType,
})
}
func TestGetFeedsCorrupted(t *testing.T) {
// Now we will not check for corrupted data in the feeds
// users should run doctor to fix their data
assert.NoError(t, unittest.PrepareTestDatabase())
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
unittest.AssertExistsAndLoadBean(t, &activities_model.Action{
ID: 8,
RepoID: 1700,
})
actions, count, err := activities_model.GetFeeds(db.DefaultContext, activities_model.GetFeedsOptions{
RequestedUser: user,
Actor: user,
IncludePrivate: true,
})
assert.NoError(t, err)
assert.Len(t, actions, 1)
assert.Equal(t, int64(1), count)
}
func TestConsistencyUpdateAction(t *testing.T) { func TestConsistencyUpdateAction(t *testing.T) {
if !setting.Database.Type.IsSQLite3() { if !setting.Database.Type.IsSQLite3() {
t.Skip("Test is only for SQLite database.") t.Skip("Test is only for SQLite database.")
@ -256,7 +91,7 @@ func TestConsistencyUpdateAction(t *testing.T) {
unittest.AssertExistsAndLoadBean(t, &activities_model.Action{ unittest.AssertExistsAndLoadBean(t, &activities_model.Action{
ID: int64(id), ID: int64(id),
}) })
_, err := db.GetEngine(db.DefaultContext).Exec(`UPDATE action SET created_unix = "" WHERE id = ?`, id) _, err := db.GetEngine(db.DefaultContext).Exec(`UPDATE action SET created_unix = '' WHERE id = ?`, id)
assert.NoError(t, err) assert.NoError(t, err)
actions := make([]*activities_model.Action, 0, 1) actions := make([]*activities_model.Action, 0, 1)
// //
@ -322,24 +157,3 @@ func TestDeleteIssueActions(t *testing.T) {
assert.NoError(t, activities_model.DeleteIssueActions(db.DefaultContext, issue.RepoID, issue.ID, issue.Index)) assert.NoError(t, activities_model.DeleteIssueActions(db.DefaultContext, issue.RepoID, issue.ID, issue.Index))
unittest.AssertCount(t, &activities_model.Action{}, 0) unittest.AssertCount(t, &activities_model.Action{}, 0)
} }
func TestRepoActions(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
_ = db.TruncateBeans(db.DefaultContext, &activities_model.Action{})
for i := 0; i < 3; i++ {
_ = db.Insert(db.DefaultContext, &activities_model.Action{
UserID: 2 + int64(i),
ActUserID: 2,
RepoID: repo.ID,
OpType: activities_model.ActionCommentIssue,
})
}
count, _ := db.Count[activities_model.Action](db.DefaultContext, &db.ListOptions{})
assert.EqualValues(t, 3, count)
actions, _, err := activities_model.GetFeeds(db.DefaultContext, activities_model.GetFeedsOptions{
RequestedRepo: repo,
})
assert.NoError(t, err)
assert.Len(t, actions, 1)
}

View File

@ -18,6 +18,7 @@ import (
"code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/timeutil"
"xorm.io/builder" "xorm.io/builder"
"xorm.io/xorm/schemas"
) )
type ( type (
@ -50,25 +51,64 @@ const (
// Notification represents a notification // Notification represents a notification
type Notification struct { type Notification struct {
ID int64 `xorm:"pk autoincr"` ID int64 `xorm:"pk autoincr"`
UserID int64 `xorm:"INDEX NOT NULL"` UserID int64 `xorm:"NOT NULL"`
RepoID int64 `xorm:"INDEX NOT NULL"` RepoID int64 `xorm:"NOT NULL"`
Status NotificationStatus `xorm:"SMALLINT INDEX NOT NULL"` Status NotificationStatus `xorm:"SMALLINT NOT NULL"`
Source NotificationSource `xorm:"SMALLINT INDEX NOT NULL"` Source NotificationSource `xorm:"SMALLINT NOT NULL"`
IssueID int64 `xorm:"INDEX NOT NULL"` IssueID int64 `xorm:"NOT NULL"`
CommitID string `xorm:"INDEX"` CommitID string
CommentID int64 CommentID int64
UpdatedBy int64 `xorm:"INDEX NOT NULL"` UpdatedBy int64 `xorm:"NOT NULL"`
Issue *issues_model.Issue `xorm:"-"` Issue *issues_model.Issue `xorm:"-"`
Repository *repo_model.Repository `xorm:"-"` Repository *repo_model.Repository `xorm:"-"`
Comment *issues_model.Comment `xorm:"-"` Comment *issues_model.Comment `xorm:"-"`
User *user_model.User `xorm:"-"` User *user_model.User `xorm:"-"`
CreatedUnix timeutil.TimeStamp `xorm:"created INDEX NOT NULL"` CreatedUnix timeutil.TimeStamp `xorm:"created NOT NULL"`
UpdatedUnix timeutil.TimeStamp `xorm:"updated INDEX NOT NULL"` UpdatedUnix timeutil.TimeStamp `xorm:"updated NOT NULL"`
}
// TableIndices implements xorm's TableIndices interface
func (n *Notification) TableIndices() []*schemas.Index {
indices := make([]*schemas.Index, 0, 8)
usuuIndex := schemas.NewIndex("u_s_uu", schemas.IndexType)
usuuIndex.AddColumn("user_id", "status", "updated_unix")
indices = append(indices, usuuIndex)
// Add the individual indices that were previously defined in struct tags
userIDIndex := schemas.NewIndex("idx_notification_user_id", schemas.IndexType)
userIDIndex.AddColumn("user_id")
indices = append(indices, userIDIndex)
repoIDIndex := schemas.NewIndex("idx_notification_repo_id", schemas.IndexType)
repoIDIndex.AddColumn("repo_id")
indices = append(indices, repoIDIndex)
statusIndex := schemas.NewIndex("idx_notification_status", schemas.IndexType)
statusIndex.AddColumn("status")
indices = append(indices, statusIndex)
sourceIndex := schemas.NewIndex("idx_notification_source", schemas.IndexType)
sourceIndex.AddColumn("source")
indices = append(indices, sourceIndex)
issueIDIndex := schemas.NewIndex("idx_notification_issue_id", schemas.IndexType)
issueIDIndex.AddColumn("issue_id")
indices = append(indices, issueIDIndex)
commitIDIndex := schemas.NewIndex("idx_notification_commit_id", schemas.IndexType)
commitIDIndex.AddColumn("commit_id")
indices = append(indices, commitIDIndex)
updatedByIndex := schemas.NewIndex("idx_notification_updated_by", schemas.IndexType)
updatedByIndex.AddColumn("updated_by")
indices = append(indices, updatedByIndex)
return indices
} }
func init() { func init() {

View File

@ -16,6 +16,7 @@ import (
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/gitrepo"
"xorm.io/builder"
"xorm.io/xorm" "xorm.io/xorm"
) )
@ -337,8 +338,10 @@ func newlyCreatedIssues(ctx context.Context, repoID int64, fromTime time.Time) *
func activeIssues(ctx context.Context, repoID int64, fromTime time.Time) *xorm.Session { func activeIssues(ctx context.Context, repoID int64, fromTime time.Time) *xorm.Session {
sess := db.GetEngine(ctx).Where("issue.repo_id = ?", repoID). sess := db.GetEngine(ctx).Where("issue.repo_id = ?", repoID).
And("issue.is_pull = ?", false). And("issue.is_pull = ?", false).
And("issue.created_unix >= ?", fromTime.Unix()). And(builder.Or(
Or("issue.closed_unix >= ?", fromTime.Unix()) builder.Gte{"issue.created_unix": fromTime.Unix()},
builder.Gte{"issue.closed_unix": fromTime.Unix()},
))
return sess return sess
} }

View File

@ -47,7 +47,7 @@ func getUserHeatmapData(ctx context.Context, user *user_model.User, team *organi
groupByName = groupBy groupByName = groupBy
} }
cond, err := activityQueryCondition(ctx, GetFeedsOptions{ cond, err := ActivityQueryCondition(ctx, GetFeedsOptions{
RequestedUser: user, RequestedUser: user,
RequestedTeam: team, RequestedTeam: team,
Actor: doer, Actor: doer,

View File

@ -4,7 +4,6 @@
package activities_test package activities_test
import ( import (
"fmt"
"testing" "testing"
"time" "time"
@ -65,11 +64,9 @@ func TestGetUserHeatmapDataByUser(t *testing.T) {
for _, tc := range testCases { for _, tc := range testCases {
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: tc.userID}) user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: tc.userID})
doer := &user_model.User{ID: tc.doerID} var doer *user_model.User
_, err := unittest.LoadBeanIfExists(doer) if tc.doerID != 0 {
assert.NoError(t, err) doer = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: tc.doerID})
if tc.doerID == 0 {
doer = nil
} }
// get the action for comparison // get the action for comparison
@ -91,11 +88,11 @@ func TestGetUserHeatmapDataByUser(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, actions, contributions, "invalid action count: did the test data became too old?") assert.Len(t, actions, contributions, "invalid action count: did the test data became too old?")
assert.Equal(t, count, int64(contributions)) assert.Equal(t, count, int64(contributions))
assert.Equal(t, tc.CountResult, contributions, fmt.Sprintf("testcase '%s'", tc.desc)) assert.Equal(t, tc.CountResult, contributions, "testcase '%s'", tc.desc)
// Test JSON rendering // Test JSON rendering
jsonData, err := json.Marshal(heatmap) jsonData, err := json.Marshal(heatmap)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, tc.JSONResult, string(jsonData)) assert.JSONEq(t, tc.JSONResult, string(jsonData))
} }
} }

View File

@ -44,7 +44,7 @@ func init() {
// TranslatableMessage represents JSON struct that can be translated with a Locale // TranslatableMessage represents JSON struct that can be translated with a Locale
type TranslatableMessage struct { type TranslatableMessage struct {
Format string Format string
Args []any `json:"omitempty"` Args []any `json:",omitempty"`
} }
// LoadRepo loads repository of the task // LoadRepo loads repository of the task

View File

@ -25,7 +25,7 @@ func (err ErrKeyUnableVerify) Error() string {
} }
// ErrKeyIsPrivate is returned when the provided key is a private key not a public key // ErrKeyIsPrivate is returned when the provided key is a private key not a public key
var ErrKeyIsPrivate = util.NewSilentWrapErrorf(util.ErrInvalidArgument, "the provided key is a private key") var ErrKeyIsPrivate = util.ErrorWrap(util.ErrInvalidArgument, "the provided key is a private key")
// ErrKeyNotExist represents a "KeyNotExist" kind of error. // ErrKeyNotExist represents a "KeyNotExist" kind of error.
type ErrKeyNotExist struct { type ErrKeyNotExist struct {
@ -217,6 +217,7 @@ func (err ErrGPGKeyAccessDenied) Unwrap() error {
// ErrKeyAccessDenied represents a "KeyAccessDenied" kind of error. // ErrKeyAccessDenied represents a "KeyAccessDenied" kind of error.
type ErrKeyAccessDenied struct { type ErrKeyAccessDenied struct {
UserID int64 UserID int64
RepoID int64
KeyID int64 KeyID int64
Note string Note string
} }
@ -228,8 +229,8 @@ func IsErrKeyAccessDenied(err error) bool {
} }
func (err ErrKeyAccessDenied) Error() string { func (err ErrKeyAccessDenied) Error() string {
return fmt.Sprintf("user does not have access to the key [user_id: %d, key_id: %d, note: %s]", return fmt.Sprintf("user does not have access to the key [user_id: %d, repo_id: %d, key_id: %d, note: %s]",
err.UserID, err.KeyID, err.Note) err.UserID, err.RepoID, err.KeyID, err.Note)
} }
func (err ErrKeyAccessDenied) Unwrap() error { func (err ErrKeyAccessDenied) Unwrap() error {

View File

@ -13,8 +13,8 @@ import (
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/timeutil"
"github.com/keybase/go-crypto/openpgp" "github.com/ProtonMail/go-crypto/openpgp"
"github.com/keybase/go-crypto/openpgp/packet" "github.com/ProtonMail/go-crypto/openpgp/packet"
"xorm.io/builder" "xorm.io/builder"
) )
@ -106,7 +106,7 @@ func GPGKeyToEntity(ctx context.Context, k *GPGKey) (*openpgp.Entity, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
keys, err := checkArmoredGPGKeyString(impKey.Content) keys, err := CheckArmoredGPGKeyString(impKey.Content)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -115,7 +115,7 @@ func GPGKeyToEntity(ctx context.Context, k *GPGKey) (*openpgp.Entity, error) {
// parseSubGPGKey parse a sub Key // parseSubGPGKey parse a sub Key
func parseSubGPGKey(ownerID int64, primaryID string, pubkey *packet.PublicKey, expiry time.Time) (*GPGKey, error) { func parseSubGPGKey(ownerID int64, primaryID string, pubkey *packet.PublicKey, expiry time.Time) (*GPGKey, error) {
content, err := base64EncPubKey(pubkey) content, err := Base64EncPubKey(pubkey)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -141,7 +141,11 @@ func parseGPGKey(ctx context.Context, ownerID int64, e *openpgp.Entity, verified
// Parse Subkeys // Parse Subkeys
subkeys := make([]*GPGKey, len(e.Subkeys)) subkeys := make([]*GPGKey, len(e.Subkeys))
for i, k := range 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 { if err != nil {
return nil, ErrGPGKeyParsing{ParseError: err} 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)) emails := make([]*user_model.EmailAddress, 0, len(e.Identities))
for _, ident := range e.Identities { for _, ident := range e.Identities {
if ident.Revocation != nil { if ident.Revoked(time.Now()) {
continue continue
} }
email := strings.ToLower(strings.TrimSpace(ident.UserId.Email)) email := strings.ToLower(strings.TrimSpace(ident.UserId.Email))
@ -179,7 +183,7 @@ func parseGPGKey(ctx context.Context, ownerID int64, e *openpgp.Entity, verified
} }
} }
content, err := base64EncPubKey(pubkey) content, err := Base64EncPubKey(pubkey)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -235,33 +239,3 @@ func DeleteGPGKey(ctx context.Context, doer *user_model.User, id int64) (err err
return committer.Commit() return committer.Commit()
} }
func checkKeyEmails(ctx context.Context, email string, keys ...*GPGKey) (bool, string) {
uid := int64(0)
var userEmails []*user_model.EmailAddress
var user *user_model.User
for _, key := range keys {
for _, e := range key.Emails {
if e.IsActivated && (email == "" || strings.EqualFold(e.Email, email)) {
return true, e.Email
}
}
if key.Verified && key.OwnerID != 0 {
if uid != key.OwnerID {
userEmails, _ = user_model.GetEmailAddresses(ctx, key.OwnerID)
uid = key.OwnerID
user = &user_model.User{ID: uid}
_, _ = user_model.GetUser(ctx, user)
}
for _, e := range userEmails {
if e.IsActivated && (email == "" || strings.EqualFold(e.Email, email)) {
return true, e.Email
}
}
if user.KeepEmailPrivate && strings.EqualFold(email, user.GetEmail()) {
return true, user.GetEmail()
}
}
}
return false, email
}

View File

@ -10,7 +10,7 @@ import (
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"github.com/keybase/go-crypto/openpgp" "github.com/ProtonMail/go-crypto/openpgp"
) )
// __________________ ________ ____ __. // __________________ ________ ____ __.
@ -67,7 +67,7 @@ func addGPGSubKey(ctx context.Context, key *GPGKey) (err error) {
// AddGPGKey adds new public key to database. // AddGPGKey adds new public key to database.
func AddGPGKey(ctx context.Context, ownerID int64, content, token, signature string) ([]*GPGKey, error) { func AddGPGKey(ctx context.Context, ownerID int64, content, token, signature string) ([]*GPGKey, error) {
ekeys, err := checkArmoredGPGKeyString(content) ekeys, err := CheckArmoredGPGKeyString(content)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -83,12 +83,12 @@ func AddGPGKey(ctx context.Context, ownerID int64, content, token, signature str
verified := false verified := false
// Handle provided signature // Handle provided signature
if 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 { 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 { 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 { if err != nil {
log.Error("Unable to validate token signature. Error: %v", err) log.Error("Unable to validate token signature. Error: %v", err)

View File

@ -4,19 +4,14 @@
package asymkey package asymkey
import ( import (
"context"
"fmt" "fmt"
"hash" "hash"
"strings"
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log" "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"
) )
// __________________ ________ ____ __. // __________________ ________ ____ __.
@ -70,263 +65,6 @@ const (
NoKeyFound = "gpg.error.no_gpg_keys_found" NoKeyFound = "gpg.error.no_gpg_keys_found"
) )
// ParseCommitsWithSignature checks if signaute of commits are corresponding to users gpg keys.
func ParseCommitsWithSignature(ctx context.Context, oldCommits []*user_model.UserCommit, repoTrustModel repo_model.TrustModelType, isOwnerMemberCollaborator func(*user_model.User) (bool, error)) []*SignCommit {
newCommits := make([]*SignCommit, 0, len(oldCommits))
keyMap := map[string]bool{}
for _, c := range oldCommits {
signCommit := &SignCommit{
UserCommit: c,
Verification: ParseCommitWithSignature(ctx, c.Commit),
}
_ = CalculateTrustStatus(signCommit.Verification, repoTrustModel, isOwnerMemberCollaborator, &keyMap)
newCommits = append(newCommits, signCommit)
}
return newCommits
}
// ParseCommitWithSignature check if signature is good against keystore.
func ParseCommitWithSignature(ctx context.Context, c *git.Commit) *CommitVerification {
var committer *user_model.User
if c.Committer != nil {
var err error
// Find Committer account
committer, err = user_model.GetUserByEmail(ctx, c.Committer.Email) // This finds the user by primary email or activated email so commit will not be valid if email is not
if err != nil { // Skipping not user for committer
committer = &user_model.User{
Name: c.Committer.Name,
Email: c.Committer.Email,
}
// We can expect this to often be an ErrUserNotExist. in the case
// it is not, however, it is important to log it.
if !user_model.IsErrUserNotExist(err) {
log.Error("GetUserByEmail: %v", err)
return &CommitVerification{
CommittingUser: committer,
Verified: false,
Reason: "gpg.error.no_committer_account",
}
}
}
}
// If no signature just report the committer
if c.Signature == nil {
return &CommitVerification{
CommittingUser: committer,
Verified: false, // Default value
Reason: "gpg.error.not_signed_commit", // Default value
}
}
// If this a SSH signature handle it differently
if strings.HasPrefix(c.Signature.Signature, "-----BEGIN SSH SIGNATURE-----") {
return ParseCommitWithSSHSignature(ctx, c, committer)
}
// Parsing signature
sig, err := extractSignature(c.Signature.Signature)
if err != nil { // Skipping failed to extract sign
log.Error("SignatureRead err: %v", err)
return &CommitVerification{
CommittingUser: committer,
Verified: false,
Reason: "gpg.error.extract_sign",
}
}
keyID := tryGetKeyIDFromSignature(sig)
defaultReason := NoKeyFound
// First check if the sig has a keyID and if so just look at that
if commitVerification := hashAndVerifyForKeyID(
ctx,
sig,
c.Signature.Payload,
committer,
keyID,
setting.AppName,
""); commitVerification != nil {
if commitVerification.Reason == BadSignature {
defaultReason = BadSignature
} else {
return commitVerification
}
}
// Now try to associate the signature with the committer, if present
if committer.ID != 0 {
keys, err := db.Find[GPGKey](ctx, FindGPGKeyOptions{
OwnerID: committer.ID,
})
if err != nil { // Skipping failed to get gpg keys of user
log.Error("ListGPGKeys: %v", err)
return &CommitVerification{
CommittingUser: committer,
Verified: false,
Reason: "gpg.error.failed_retrieval_gpg_keys",
}
}
if err := GPGKeyList(keys).LoadSubKeys(ctx); err != nil {
log.Error("LoadSubKeys: %v", err)
return &CommitVerification{
CommittingUser: committer,
Verified: false,
Reason: "gpg.error.failed_retrieval_gpg_keys",
}
}
committerEmailAddresses, _ := user_model.GetEmailAddresses(ctx, committer.ID)
activated := false
for _, e := range committerEmailAddresses {
if e.IsActivated && strings.EqualFold(e.Email, c.Committer.Email) {
activated = true
break
}
}
for _, k := range keys {
// Pre-check (& optimization) that emails attached to key can be attached to the committer email and can validate
canValidate := false
email := ""
if k.Verified && activated {
canValidate = true
email = c.Committer.Email
}
if !canValidate {
for _, e := range k.Emails {
if e.IsActivated && strings.EqualFold(e.Email, c.Committer.Email) {
canValidate = true
email = e.Email
break
}
}
}
if !canValidate {
continue // Skip this key
}
commitVerification := hashAndVerifyWithSubKeysCommitVerification(sig, c.Signature.Payload, k, committer, committer, email)
if commitVerification != nil {
return commitVerification
}
}
}
if setting.Repository.Signing.SigningKey != "" && setting.Repository.Signing.SigningKey != "default" && setting.Repository.Signing.SigningKey != "none" {
// OK we should try the default key
gpgSettings := git.GPGSettings{
Sign: true,
KeyID: setting.Repository.Signing.SigningKey,
Name: setting.Repository.Signing.SigningName,
Email: setting.Repository.Signing.SigningEmail,
}
if err := gpgSettings.LoadPublicKeyContent(); err != nil {
log.Error("Error getting default signing key: %s %v", gpgSettings.KeyID, err)
} else if commitVerification := verifyWithGPGSettings(ctx, &gpgSettings, sig, c.Signature.Payload, committer, keyID); commitVerification != nil {
if commitVerification.Reason == BadSignature {
defaultReason = BadSignature
} else {
return commitVerification
}
}
}
defaultGPGSettings, err := c.GetRepositoryDefaultPublicGPGKey(false)
if err != nil {
log.Error("Error getting default public gpg key: %v", err)
} else if defaultGPGSettings == nil {
log.Warn("Unable to get defaultGPGSettings for unattached commit: %s", c.ID.String())
} else if defaultGPGSettings.Sign {
if commitVerification := verifyWithGPGSettings(ctx, defaultGPGSettings, sig, c.Signature.Payload, committer, keyID); commitVerification != nil {
if commitVerification.Reason == BadSignature {
defaultReason = BadSignature
} else {
return commitVerification
}
}
}
return &CommitVerification{ // Default at this stage
CommittingUser: committer,
Verified: false,
Warning: defaultReason != NoKeyFound,
Reason: defaultReason,
SigningKey: &GPGKey{
KeyID: keyID,
},
}
}
func verifyWithGPGSettings(ctx context.Context, gpgSettings *git.GPGSettings, sig *packet.Signature, payload string, committer *user_model.User, keyID string) *CommitVerification {
// First try to find the key in the db
if commitVerification := hashAndVerifyForKeyID(ctx, sig, payload, committer, gpgSettings.KeyID, gpgSettings.Name, gpgSettings.Email); commitVerification != nil {
return commitVerification
}
// Otherwise we have to parse the key
ekeys, err := checkArmoredGPGKeyString(gpgSettings.PublicKeyContent)
if err != nil {
log.Error("Unable to get default signing key: %v", err)
return &CommitVerification{
CommittingUser: committer,
Verified: false,
Reason: "gpg.error.generate_hash",
}
}
for _, ekey := range ekeys {
pubkey := ekey.PrimaryKey
content, err := base64EncPubKey(pubkey)
if err != nil {
return &CommitVerification{
CommittingUser: committer,
Verified: false,
Reason: "gpg.error.generate_hash",
}
}
k := &GPGKey{
Content: content,
CanSign: pubkey.CanSign(),
KeyID: pubkey.KeyIdString(),
}
for _, subKey := range ekey.Subkeys {
content, err := base64EncPubKey(subKey.PublicKey)
if err != nil {
return &CommitVerification{
CommittingUser: committer,
Verified: false,
Reason: "gpg.error.generate_hash",
}
}
k.SubsKey = append(k.SubsKey, &GPGKey{
Content: content,
CanSign: subKey.PublicKey.CanSign(),
KeyID: subKey.PublicKey.KeyIdString(),
})
}
if commitVerification := hashAndVerifyWithSubKeysCommitVerification(sig, payload, k, committer, &user_model.User{
Name: gpgSettings.Name,
Email: gpgSettings.Email,
}, gpgSettings.Email); commitVerification != nil {
return commitVerification
}
if keyID == k.KeyID {
// This is a bad situation ... We have a key id that matches our default key but the signature doesn't match.
return &CommitVerification{
CommittingUser: committer,
Verified: false,
Warning: true,
Reason: BadSignature,
}
}
}
return nil
}
func verifySign(s *packet.Signature, h hash.Hash, k *GPGKey) error { func verifySign(s *packet.Signature, h hash.Hash, k *GPGKey) error {
// Check if key can sign // Check if key can sign
if !k.CanSign { if !k.CanSign {
@ -369,7 +107,7 @@ func hashAndVerifyWithSubKeys(sig *packet.Signature, payload string, k *GPGKey)
return nil, nil return nil, nil
} }
func hashAndVerifyWithSubKeysCommitVerification(sig *packet.Signature, payload string, k *GPGKey, committer, signer *user_model.User, email string) *CommitVerification { func HashAndVerifyWithSubKeysCommitVerification(sig *packet.Signature, payload string, k *GPGKey, committer, signer *user_model.User, email string) *CommitVerification {
key, err := hashAndVerifyWithSubKeys(sig, payload, k) key, err := hashAndVerifyWithSubKeys(sig, payload, k)
if err != nil { // Skipping failed to generate hash if err != nil { // Skipping failed to generate hash
return &CommitVerification{ return &CommitVerification{
@ -392,78 +130,6 @@ func hashAndVerifyWithSubKeysCommitVerification(sig *packet.Signature, payload s
return nil return nil
} }
func hashAndVerifyForKeyID(ctx context.Context, sig *packet.Signature, payload string, committer *user_model.User, keyID, name, email string) *CommitVerification {
if keyID == "" {
return nil
}
keys, err := db.Find[GPGKey](ctx, FindGPGKeyOptions{
KeyID: keyID,
IncludeSubKeys: true,
})
if err != nil {
log.Error("GetGPGKeysByKeyID: %v", err)
return &CommitVerification{
CommittingUser: committer,
Verified: false,
Reason: "gpg.error.failed_retrieval_gpg_keys",
}
}
if len(keys) == 0 {
return nil
}
for _, key := range keys {
var primaryKeys []*GPGKey
if key.PrimaryKeyID != "" {
primaryKeys, err = db.Find[GPGKey](ctx, FindGPGKeyOptions{
KeyID: key.PrimaryKeyID,
IncludeSubKeys: true,
})
if err != nil {
log.Error("GetGPGKeysByKeyID: %v", err)
return &CommitVerification{
CommittingUser: committer,
Verified: false,
Reason: "gpg.error.failed_retrieval_gpg_keys",
}
}
}
activated, email := checkKeyEmails(ctx, email, append([]*GPGKey{key}, primaryKeys...)...)
if !activated {
continue
}
signer := &user_model.User{
Name: name,
Email: email,
}
if key.OwnerID != 0 {
owner, err := user_model.GetUserByID(ctx, key.OwnerID)
if err == nil {
signer = owner
} else if !user_model.IsErrUserNotExist(err) {
log.Error("Failed to user_model.GetUserByID: %d for key ID: %d (%s) %v", key.OwnerID, key.ID, key.KeyID, err)
return &CommitVerification{
CommittingUser: committer,
Verified: false,
Reason: "gpg.error.no_committer_account",
}
}
}
commitVerification := hashAndVerifyWithSubKeysCommitVerification(sig, payload, key, committer, signer, email)
if commitVerification != nil {
return commitVerification
}
}
// This is a bad situation ... We have a key id that is in our database but the signature doesn't match.
return &CommitVerification{
CommittingUser: committer,
Verified: false,
Warning: true,
Reason: BadSignature,
}
}
// CalculateTrustStatus will calculate the TrustStatus for a commit verification within a repository // CalculateTrustStatus will calculate the TrustStatus for a commit verification within a repository
// There are several trust models in Gitea // There are several trust models in Gitea
func CalculateTrustStatus(verification *CommitVerification, repoTrustModel repo_model.TrustModelType, isOwnerMemberCollaborator func(*user_model.User) (bool, error), keyMap *map[string]bool) error { func CalculateTrustStatus(verification *CommitVerification, repoTrustModel repo_model.TrustModelType, isOwnerMemberCollaborator func(*user_model.User) (bool, error), keyMap *map[string]bool) error {

View File

@ -13,9 +13,9 @@ import (
"strings" "strings"
"time" "time"
"github.com/keybase/go-crypto/openpgp" "github.com/ProtonMail/go-crypto/openpgp"
"github.com/keybase/go-crypto/openpgp/armor" "github.com/ProtonMail/go-crypto/openpgp/armor"
"github.com/keybase/go-crypto/openpgp/packet" "github.com/ProtonMail/go-crypto/openpgp/packet"
) )
// __________________ ________ ____ __. // __________________ ________ ____ __.
@ -33,9 +33,9 @@ import (
// This file provides common functions relating to GPG Keys // This file provides common functions relating to GPG Keys
// checkArmoredGPGKeyString checks if the given key string is a valid GPG armored key. // CheckArmoredGPGKeyString checks if the given key string is a valid GPG armored key.
// The function returns the actual public key on success // The function returns the actual public key on success
func checkArmoredGPGKeyString(content string) (openpgp.EntityList, error) { func CheckArmoredGPGKeyString(content string) (openpgp.EntityList, error) {
list, err := openpgp.ReadArmoredKeyRing(strings.NewReader(content)) list, err := openpgp.ReadArmoredKeyRing(strings.NewReader(content))
if err != nil { if err != nil {
return nil, ErrGPGKeyParsing{err} return nil, ErrGPGKeyParsing{err}
@ -43,8 +43,8 @@ func checkArmoredGPGKeyString(content string) (openpgp.EntityList, error) {
return list, nil return list, nil
} }
// base64EncPubKey encode public key content to base 64 // Base64EncPubKey encode public key content to base 64
func base64EncPubKey(pubkey *packet.PublicKey) (string, error) { func Base64EncPubKey(pubkey *packet.PublicKey) (string, error) {
var w bytes.Buffer var w bytes.Buffer
err := pubkey.Serialize(&w) err := pubkey.Serialize(&w)
if err != nil { if err != nil {
@ -80,7 +80,7 @@ func base64DecPubKey(content string) (*packet.PublicKey, error) {
return pkey, nil 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 { func getExpiryTime(e *openpgp.Entity) time.Time {
expiry := time.Time{} expiry := time.Time{}
// Extract self-sign for expire date based on : https://github.com/golang/crypto/blob/master/openpgp/keys.go#L165 // 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 { for _, ident := range e.Identities {
if selfSig == nil { if selfSig == nil {
selfSig = ident.SelfSignature 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 selfSig = ident.SelfSignature
break break
} }
} }
if selfSig.KeyLifetimeSecs != nil { if selfSig != nil && selfSig.KeyLifetimeSecs != nil {
expiry = e.PrimaryKey.CreationTime.Add(time.Duration(*selfSig.KeyLifetimeSecs) * time.Second) expiry = e.PrimaryKey.CreationTime.Add(time.Duration(*selfSig.KeyLifetimeSecs) * time.Second)
} }
return expiry return expiry
@ -119,7 +119,7 @@ func readArmoredSign(r io.Reader) (body io.Reader, err error) {
return block.Body, nil return block.Body, nil
} }
func extractSignature(s string) (*packet.Signature, error) { func ExtractSignature(s string) (*packet.Signature, error) {
r, err := readArmoredSign(strings.NewReader(s)) r, err := readArmoredSign(strings.NewReader(s))
if err != nil { if err != nil {
return nil, fmt.Errorf("Failed to read signature armor") return nil, fmt.Errorf("Failed to read signature armor")
@ -135,7 +135,7 @@ func extractSignature(s string) (*packet.Signature, error) {
return sig, nil return sig, nil
} }
func tryGetKeyIDFromSignature(sig *packet.Signature) string { func TryGetKeyIDFromSignature(sig *packet.Signature) string {
if sig.IssuerKeyId != nil && (*sig.IssuerKeyId) != 0 { if sig.IssuerKeyId != nil && (*sig.IssuerKeyId) != 0 {
return fmt.Sprintf("%016X", *sig.IssuerKeyId) return fmt.Sprintf("%016X", *sig.IssuerKeyId)
} }

View File

@ -13,8 +13,10 @@ import (
"code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
"github.com/keybase/go-crypto/openpgp/packet" "github.com/ProtonMail/go-crypto/openpgp"
"github.com/ProtonMail/go-crypto/openpgp/packet"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestCheckArmoredGPGKeyString(t *testing.T) { func TestCheckArmoredGPGKeyString(t *testing.T) {
@ -49,7 +51,7 @@ MkM/fdpyc2hY7Dl/+qFmN5MG5yGmMpQcX+RNNR222ibNC1D3wg==
=i9b7 =i9b7
-----END PGP PUBLIC KEY BLOCK-----` -----END PGP PUBLIC KEY BLOCK-----`
key, err := checkArmoredGPGKeyString(testGPGArmor) key, err := CheckArmoredGPGKeyString(testGPGArmor)
assert.NoError(t, err, "Could not parse a valid GPG public armored rsa key", key) assert.NoError(t, err, "Could not parse a valid GPG public armored rsa key", key)
// TODO verify value of key // TODO verify value of key
} }
@ -70,7 +72,7 @@ OyjLLnFQiVmq7kEA/0z0CQe3ZQiQIq5zrs7Nh1XRkFAo8GlU/SGC9XFFi722
=ZiSe =ZiSe
-----END PGP PUBLIC KEY BLOCK-----` -----END PGP PUBLIC KEY BLOCK-----`
key, err := checkArmoredGPGKeyString(testGPGArmor) key, err := CheckArmoredGPGKeyString(testGPGArmor)
assert.NoError(t, err, "Could not parse a valid GPG public armored brainpoolP256r1 key", key) assert.NoError(t, err, "Could not parse a valid GPG public armored brainpoolP256r1 key", key)
// TODO verify value of key // TODO verify value of key
} }
@ -106,15 +108,14 @@ Av844q/BfRuVsJsK1NDNG09LC30B0l3LKBqlrRmRTUMHtgchdX2dY+p7GPOoSzlR
MkM/fdpyc2hY7Dl/+qFmN5MG5yGmMpQcX+RNNR222ibNC1D3wg== MkM/fdpyc2hY7Dl/+qFmN5MG5yGmMpQcX+RNNR222ibNC1D3wg==
=i9b7 =i9b7
-----END PGP PUBLIC KEY BLOCK-----` -----END PGP PUBLIC KEY BLOCK-----`
keys, err := checkArmoredGPGKeyString(testGPGArmor) keys, err := CheckArmoredGPGKeyString(testGPGArmor)
if !assert.NotEmpty(t, keys) { require.NotEmpty(t, keys)
return
}
ekey := keys[0] ekey := keys[0]
assert.NoError(t, err, "Could not parse a valid GPG armored key", ekey) assert.NoError(t, err, "Could not parse a valid GPG armored key", ekey)
pubkey := ekey.PrimaryKey pubkey := ekey.PrimaryKey
content, err := base64EncPubKey(pubkey) content, err := Base64EncPubKey(pubkey)
assert.NoError(t, err, "Could not base64 encode a valid PublicKey content", ekey) assert.NoError(t, err, "Could not base64 encode a valid PublicKey content", ekey)
key := &GPGKey{ key := &GPGKey{
@ -175,9 +176,9 @@ committer Antoine GIRARD <sapk@sapk.fr> 1489013107 +0100
Unknown GPG key with good email Unknown GPG key with good email
` `
// Reading Sign // Reading Sign
goodSig, err := extractSignature(testGoodSigArmor) goodSig, err := ExtractSignature(testGoodSigArmor)
assert.NoError(t, err, "Could not parse a valid GPG armored signature", testGoodSigArmor) assert.NoError(t, err, "Could not parse a valid GPG armored signature", testGoodSigArmor)
badSig, err := extractSignature(testBadSigArmor) badSig, err := ExtractSignature(testBadSigArmor)
assert.NoError(t, err, "Could not parse a valid GPG armored signature", testBadSigArmor) assert.NoError(t, err, "Could not parse a valid GPG armored signature", testBadSigArmor)
// Generating hash of commit // Generating hash of commit
@ -385,7 +386,7 @@ epiDVQ==
=VSKJ =VSKJ
-----END PGP PUBLIC KEY BLOCK----- -----END PGP PUBLIC KEY BLOCK-----
` `
keys, err := checkArmoredGPGKeyString(testIssue6599) keys, err := CheckArmoredGPGKeyString(testIssue6599)
assert.NoError(t, err) assert.NoError(t, err)
if assert.NotEmpty(t, keys) { if assert.NotEmpty(t, keys) {
ekey := keys[0] ekey := keys[0]
@ -395,11 +396,33 @@ epiDVQ==
} }
func TestTryGetKeyIDFromSignature(t *testing.T) { func TestTryGetKeyIDFromSignature(t *testing.T) {
assert.Empty(t, tryGetKeyIDFromSignature(&packet.Signature{})) assert.Empty(t, TryGetKeyIDFromSignature(&packet.Signature{}))
assert.Equal(t, "038D1A3EADDBEA9C", tryGetKeyIDFromSignature(&packet.Signature{ assert.Equal(t, "038D1A3EADDBEA9C", TryGetKeyIDFromSignature(&packet.Signature{
IssuerKeyId: util.ToPointer(uint64(0x38D1A3EADDBEA9C)), IssuerKeyId: util.ToPointer(uint64(0x38D1A3EADDBEA9C)),
})) }))
assert.Equal(t, "038D1A3EADDBEA9C", tryGetKeyIDFromSignature(&packet.Signature{ assert.Equal(t, "038D1A3EADDBEA9C", TryGetKeyIDFromSignature(&packet.Signature{
IssuerFingerprint: []uint8{0xb, 0x23, 0x24, 0xc7, 0xe6, 0xfe, 0x4f, 0x3a, 0x6, 0x26, 0xc1, 0x21, 0x3, 0x8d, 0x1a, 0x3e, 0xad, 0xdb, 0xea, 0x9c}, 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)
require.NoError(t, err)
k, err := parseGPGKey(db.DefaultContext, 1, e, true)
require.NoError(t, err)
assert.NotEmpty(t, k.KeyID)
assert.NotEmpty(t, k.Emails) // the key is valid, matches the email
// then revoke the key
for _, id := range e.Identities {
id.Revocations = append(id.Revocations, &packet.Signature{RevocationReason: util.ToPointer(packet.KeyCompromised)})
}
k, err = parseGPGKey(db.DefaultContext, 1, e, true)
require.NoError(t, err)
assert.NotEmpty(t, k.KeyID)
assert.Empty(t, k.Emails) // the key is revoked, matches no email
}

View File

@ -50,7 +50,7 @@ func VerifyGPGKey(ctx context.Context, ownerID int64, keyID, token, signature st
return "", err return "", err
} }
sig, err := extractSignature(signature) sig, err := ExtractSignature(signature)
if err != nil { if err != nil {
return "", ErrGPGInvalidTokenSignature{ return "", ErrGPGInvalidTokenSignature{
ID: key.KeyID, ID: key.KeyID,

View File

@ -5,6 +5,7 @@ package auth
import ( import (
"fmt" "fmt"
"slices"
"strings" "strings"
"code.gitea.io/gitea/models/perm" "code.gitea.io/gitea/models/perm"
@ -14,7 +15,7 @@ import (
type AccessTokenScopeCategory int type AccessTokenScopeCategory int
const ( const (
AccessTokenScopeCategoryActivityPub = iota AccessTokenScopeCategoryActivityPub AccessTokenScopeCategory = iota
AccessTokenScopeCategoryAdmin AccessTokenScopeCategoryAdmin
AccessTokenScopeCategoryMisc // WARN: this is now just a placeholder, don't remove it which will change the following values AccessTokenScopeCategoryMisc // WARN: this is now just a placeholder, don't remove it which will change the following values
AccessTokenScopeCategoryNotification AccessTokenScopeCategoryNotification
@ -193,6 +194,14 @@ var accessTokenScopes = map[AccessTokenScopeLevel]map[AccessTokenScopeCategory]A
}, },
} }
func GetAccessTokenCategories() (res []string) {
for _, cat := range accessTokenScopes[Read] {
res = append(res, strings.TrimPrefix(string(cat), "read:"))
}
slices.Sort(res)
return res
}
// GetRequiredScopes gets the specific scopes for a given level and categories // GetRequiredScopes gets the specific scopes for a given level and categories
func GetRequiredScopes(level AccessTokenScopeLevel, scopeCategories ...AccessTokenScopeCategory) []AccessTokenScope { func GetRequiredScopes(level AccessTokenScopeLevel, scopeCategories ...AccessTokenScopeCategory) []AccessTokenScope {
scopes := make([]AccessTokenScope, 0, len(scopeCategories)) scopes := make([]AccessTokenScope, 0, len(scopeCategories))
@ -270,6 +279,9 @@ func (s AccessTokenScope) parse() (accessTokenScopeBitmap, error) {
// StringSlice returns the AccessTokenScope as a []string // StringSlice returns the AccessTokenScope as a []string
func (s AccessTokenScope) StringSlice() []string { func (s AccessTokenScope) StringSlice() []string {
if s == "" {
return nil
}
return strings.Split(string(s), ",") return strings.Split(string(s), ",")
} }

View File

@ -17,6 +17,7 @@ type scopeTestNormalize struct {
} }
func TestAccessTokenScope_Normalize(t *testing.T) { func TestAccessTokenScope_Normalize(t *testing.T) {
assert.Equal(t, []string{"activitypub", "admin", "issue", "misc", "notification", "organization", "package", "repository", "user"}, GetAccessTokenCategories())
tests := []scopeTestNormalize{ tests := []scopeTestNormalize{
{"", "", nil}, {"", "", nil},
{"write:misc,write:notification,read:package,write:notification,public-only", "public-only,write:misc,write:notification,read:package", nil}, {"write:misc,write:notification,read:package,write:notification,public-only", "public-only,write:misc,write:notification,read:package", nil},
@ -25,7 +26,7 @@ func TestAccessTokenScope_Normalize(t *testing.T) {
{"write:activitypub,write:admin,write:misc,write:notification,write:organization,write:package,write:issue,write:repository,write:user,public-only", "public-only,all", nil}, {"write:activitypub,write:admin,write:misc,write:notification,write:organization,write:package,write:issue,write:repository,write:user,public-only", "public-only,all", nil},
} }
for _, scope := range []string{"activitypub", "admin", "misc", "notification", "organization", "package", "issue", "repository", "user"} { for _, scope := range GetAccessTokenCategories() {
tests = append(tests, tests = append(tests,
scopeTestNormalize{AccessTokenScope(fmt.Sprintf("read:%s", scope)), AccessTokenScope(fmt.Sprintf("read:%s", scope)), nil}, scopeTestNormalize{AccessTokenScope(fmt.Sprintf("read:%s", scope)), AccessTokenScope(fmt.Sprintf("read:%s", scope)), nil},
scopeTestNormalize{AccessTokenScope(fmt.Sprintf("write:%s", scope)), AccessTokenScope(fmt.Sprintf("write:%s", scope)), nil}, scopeTestNormalize{AccessTokenScope(fmt.Sprintf("write:%s", scope)), AccessTokenScope(fmt.Sprintf("write:%s", scope)), nil},
@ -59,7 +60,7 @@ func TestAccessTokenScope_HasScope(t *testing.T) {
{"public-only", "read:issue", false, nil}, {"public-only", "read:issue", false, nil},
} }
for _, scope := range []string{"activitypub", "admin", "misc", "notification", "organization", "package", "issue", "repository", "user"} { for _, scope := range GetAccessTokenCategories() {
tests = append(tests, tests = append(tests,
scopeTestHasScope{ scopeTestHasScope{
AccessTokenScope(fmt.Sprintf("read:%s", scope)), AccessTokenScope(fmt.Sprintf("read:%s", scope)),

View File

@ -18,14 +18,14 @@ func TestOAuth2Application_GenerateClientSecret(t *testing.T) {
app := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Application{ID: 1}) app := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Application{ID: 1})
secret, err := app.GenerateClientSecret(db.DefaultContext) secret, err := app.GenerateClientSecret(db.DefaultContext)
assert.NoError(t, err) assert.NoError(t, err)
assert.True(t, len(secret) > 0) assert.NotEmpty(t, secret)
unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Application{ID: 1, ClientSecret: app.ClientSecret}) unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Application{ID: 1, ClientSecret: app.ClientSecret})
} }
func BenchmarkOAuth2Application_GenerateClientSecret(b *testing.B) { func BenchmarkOAuth2Application_GenerateClientSecret(b *testing.B) {
assert.NoError(b, unittest.PrepareTestDatabase()) assert.NoError(b, unittest.PrepareTestDatabase())
app := unittest.AssertExistsAndLoadBean(b, &auth_model.OAuth2Application{ID: 1}) app := unittest.AssertExistsAndLoadBean(b, &auth_model.OAuth2Application{ID: 1})
for i := 0; i < b.N; i++ { for b.Loop() {
_, _ = app.GenerateClientSecret(db.DefaultContext) _, _ = app.GenerateClientSecret(db.DefaultContext)
} }
} }
@ -165,7 +165,7 @@ func TestOAuth2Grant_GenerateNewAuthorizationCode(t *testing.T) {
code, err := grant.GenerateNewAuthorizationCode(db.DefaultContext, "https://example2.com/callback", "CjvyTLSdR47G5zYenDA-eDWW4lRrO8yvjcWwbD_deOg", "S256") code, err := grant.GenerateNewAuthorizationCode(db.DefaultContext, "https://example2.com/callback", "CjvyTLSdR47G5zYenDA-eDWW4lRrO8yvjcWwbD_deOg", "S256")
assert.NoError(t, err) assert.NoError(t, err)
assert.NotNil(t, code) assert.NotNil(t, code)
assert.True(t, len(code.Code) > 32) // secret length > 32 assert.Greater(t, len(code.Code), 32) // secret length > 32
} }
func TestOAuth2Grant_TableName(t *testing.T) { func TestOAuth2Grant_TableName(t *testing.T) {

View File

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

View File

@ -12,6 +12,7 @@ import (
"code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
"github.com/go-webauthn/webauthn/protocol"
"github.com/go-webauthn/webauthn/webauthn" "github.com/go-webauthn/webauthn/webauthn"
) )
@ -89,14 +90,33 @@ func (cred *WebAuthnCredential) AfterLoad() {
// WebAuthnCredentialList is a list of *WebAuthnCredential // WebAuthnCredentialList is a list of *WebAuthnCredential
type WebAuthnCredentialList []*WebAuthnCredential type WebAuthnCredentialList []*WebAuthnCredential
// newCredentialFlagsFromAuthenticatorFlags is copied from https://github.com/go-webauthn/webauthn/pull/337
// to convert protocol.AuthenticatorFlags to webauthn.CredentialFlags
func newCredentialFlagsFromAuthenticatorFlags(flags protocol.AuthenticatorFlags) webauthn.CredentialFlags {
return webauthn.CredentialFlags{
UserPresent: flags.HasUserPresent(),
UserVerified: flags.HasUserVerified(),
BackupEligible: flags.HasBackupEligible(),
BackupState: flags.HasBackupState(),
}
}
// ToCredentials will convert all WebAuthnCredentials to webauthn.Credentials // ToCredentials will convert all WebAuthnCredentials to webauthn.Credentials
func (list WebAuthnCredentialList) ToCredentials() []webauthn.Credential { func (list WebAuthnCredentialList) ToCredentials(defaultAuthFlags ...protocol.AuthenticatorFlags) []webauthn.Credential {
// TODO: at the moment, Gitea doesn't store or check the flags
// so we need to use the default flags from the authenticator to make the login validation pass
// In the future, we should:
// 1. store the flags when registering the credential
// 2. provide the stored flags when converting the credentials (for login)
// 3. for old users, still use this fallback to the default flags
defAuthFlags := util.OptionalArg(defaultAuthFlags)
creds := make([]webauthn.Credential, 0, len(list)) creds := make([]webauthn.Credential, 0, len(list))
for _, cred := range list { for _, cred := range list {
creds = append(creds, webauthn.Credential{ creds = append(creds, webauthn.Credential{
ID: cred.CredentialID, ID: cred.CredentialID,
PublicKey: cred.PublicKey, PublicKey: cred.PublicKey,
AttestationType: cred.AttestationType, AttestationType: cred.AttestationType,
Flags: newCredentialFlagsFromAuthenticatorFlags(defAuthFlags),
Authenticator: webauthn.Authenticator{ Authenticator: webauthn.Authenticator{
AAGUID: cred.AAGUID, AAGUID: cred.AAGUID,
SignCount: cred.SignCount, SignCount: cred.SignCount,

View File

@ -44,7 +44,7 @@ func TestWebAuthnCredential_UpdateSignCount(t *testing.T) {
cred := unittest.AssertExistsAndLoadBean(t, &auth_model.WebAuthnCredential{ID: 1}) cred := unittest.AssertExistsAndLoadBean(t, &auth_model.WebAuthnCredential{ID: 1})
cred.SignCount = 1 cred.SignCount = 1
assert.NoError(t, cred.UpdateSignCount(db.DefaultContext)) assert.NoError(t, cred.UpdateSignCount(db.DefaultContext))
unittest.AssertExistsIf(t, true, &auth_model.WebAuthnCredential{ID: 1, SignCount: 1}) unittest.AssertExistsAndLoadBean(t, &auth_model.WebAuthnCredential{ID: 1, SignCount: 1})
} }
func TestWebAuthnCredential_UpdateLargeCounter(t *testing.T) { 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 := unittest.AssertExistsAndLoadBean(t, &auth_model.WebAuthnCredential{ID: 1})
cred.SignCount = 0xffffffff cred.SignCount = 0xffffffff
assert.NoError(t, cred.UpdateSignCount(db.DefaultContext)) assert.NoError(t, cred.UpdateSignCount(db.DefaultContext))
unittest.AssertExistsIf(t, true, &auth_model.WebAuthnCredential{ID: 1, SignCount: 0xffffffff}) unittest.AssertExistsAndLoadBean(t, &auth_model.WebAuthnCredential{ID: 1, SignCount: 0xffffffff})
} }
func TestCreateCredential(t *testing.T) { 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, "WebAuthn Created Credential", res.Name)
assert.Equal(t, []byte("Test"), res.CredentialID) assert.Equal(t, []byte("Test"), res.CredentialID)
unittest.AssertExistsIf(t, true, &auth_model.WebAuthnCredential{Name: "WebAuthn Created Credential", UserID: 1}) unittest.AssertExistsAndLoadBean(t, &auth_model.WebAuthnCredential{Name: "WebAuthn Created Credential", UserID: 1})
} }

View File

@ -68,7 +68,8 @@ func CheckCollations(x *xorm.Engine) (*CheckCollationsResult, error) {
var candidateCollations []string var candidateCollations []string
if x.Dialect().URI().DBType == schemas.MYSQL { if x.Dialect().URI().DBType == schemas.MYSQL {
if _, err = x.SQL("SELECT @@collation_database").Get(&res.DatabaseCollation); err != nil { _, err = x.SQL("SELECT DEFAULT_COLLATION_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = ?", setting.Database.Name).Get(&res.DatabaseCollation)
if err != nil {
return nil, err return nil, err
} }
res.IsCollationCaseSensitive = func(s string) bool { res.IsCollationCaseSensitive = func(s string) bool {
@ -139,7 +140,7 @@ func CheckCollations(x *xorm.Engine) (*CheckCollationsResult, error) {
} }
func CheckCollationsDefaultEngine() (*CheckCollationsResult, error) { func CheckCollationsDefaultEngine() (*CheckCollationsResult, error) {
return CheckCollations(x) return CheckCollations(xormEngine)
} }
func alterDatabaseCollation(x *xorm.Engine, collation string) error { func alterDatabaseCollation(x *xorm.Engine, collation string) error {

View File

@ -6,6 +6,12 @@ package db
import ( import (
"context" "context"
"database/sql" "database/sql"
"errors"
"runtime"
"slices"
"sync"
"code.gitea.io/gitea/modules/setting"
"xorm.io/builder" "xorm.io/builder"
"xorm.io/xorm" "xorm.io/xorm"
@ -15,45 +21,23 @@ import (
// will be overwritten by Init with HammerContext // will be overwritten by Init with HammerContext
var DefaultContext context.Context var DefaultContext context.Context
// contextKey is a value for use with context.WithValue. type engineContextKeyType struct{}
type contextKey struct {
name string
}
// enginedContextKey is a context key. It is used with context.Value() to get the current Engined for the context var engineContextKey = engineContextKeyType{}
var (
enginedContextKey = &contextKey{"engined"}
_ Engined = &Context{}
)
// Context represents a db context // Context represents a db context
type Context struct { type Context struct {
context.Context context.Context
e Engine engine Engine
transaction bool
} }
func newContext(ctx context.Context, e Engine, transaction bool) *Context { func newContext(ctx context.Context, e Engine) *Context {
return &Context{ return &Context{Context: ctx, engine: e}
Context: ctx,
e: e,
transaction: transaction,
}
}
// InTransaction if context is in a transaction
func (ctx *Context) InTransaction() bool {
return ctx.transaction
}
// Engine returns db engine
func (ctx *Context) Engine() Engine {
return ctx.e
} }
// Value shadows Value for context.Context but allows us to get ourselves and an Engined object // Value shadows Value for context.Context but allows us to get ourselves and an Engined object
func (ctx *Context) Value(key any) any { func (ctx *Context) Value(key any) any {
if key == enginedContextKey { if key == engineContextKey {
return ctx return ctx
} }
return ctx.Context.Value(key) return ctx.Context.Value(key)
@ -61,30 +45,66 @@ func (ctx *Context) Value(key any) any {
// WithContext returns this engine tied to this context // WithContext returns this engine tied to this context
func (ctx *Context) WithContext(other context.Context) *Context { func (ctx *Context) WithContext(other context.Context) *Context {
return newContext(ctx, ctx.e.Context(other), ctx.transaction) return newContext(ctx, ctx.engine.Context(other))
} }
// Engined structs provide an Engine var (
type Engined interface { contextSafetyOnce sync.Once
Engine() Engine contextSafetyDeniedFuncPCs []uintptr
)
func contextSafetyCheck(e Engine) {
if setting.IsProd && !setting.IsInTesting {
return
}
if e == nil {
return
}
// Only do this check for non-end-users. If the problem could be fixed in the future, this code could be removed.
contextSafetyOnce.Do(func() {
// try to figure out the bad functions to deny
type m struct{}
_ = e.SQL("SELECT 1").Iterate(&m{}, func(int, any) error {
callers := make([]uintptr, 32)
callerNum := runtime.Callers(1, callers)
for i := 0; i < callerNum; i++ {
if funcName := runtime.FuncForPC(callers[i]).Name(); funcName == "xorm.io/xorm.(*Session).Iterate" {
contextSafetyDeniedFuncPCs = append(contextSafetyDeniedFuncPCs, callers[i])
}
}
return nil
})
if len(contextSafetyDeniedFuncPCs) != 1 {
panic(errors.New("unable to determine the functions to deny"))
}
})
// it should be very fast: xxxx ns/op
callers := make([]uintptr, 32)
callerNum := runtime.Callers(3, callers) // skip 3: runtime.Callers, contextSafetyCheck, GetEngine
for i := 0; i < callerNum; i++ {
if slices.Contains(contextSafetyDeniedFuncPCs, callers[i]) {
panic(errors.New("using database context in an iterator would cause corrupted results"))
}
}
} }
// GetEngine will get a db Engine from this context or return an Engine restricted to this context // GetEngine gets an existing db Engine/Statement or creates a new Session
func GetEngine(ctx context.Context) Engine { func GetEngine(ctx context.Context) Engine {
if e := getEngine(ctx); e != nil { if e := getExistingEngine(ctx); e != nil {
return e return e
} }
return x.Context(ctx) return xormEngine.Context(ctx)
} }
// getEngine will get a db Engine from this context or return nil // getExistingEngine gets an existing db Engine/Statement from this context or returns nil
func getEngine(ctx context.Context) Engine { func getExistingEngine(ctx context.Context) (e Engine) {
if engined, ok := ctx.(Engined); ok { defer func() { contextSafetyCheck(e) }()
return engined.Engine() if engined, ok := ctx.(*Context); ok {
return engined.engine
} }
enginedInterface := ctx.Value(enginedContextKey) if engined, ok := ctx.Value(engineContextKey).(*Context); ok {
if enginedInterface != nil { return engined.engine
return enginedInterface.(Engined).Engine()
} }
return nil return nil
} }
@ -132,23 +152,23 @@ func (c *halfCommitter) Close() error {
// d. It doesn't mean rollback is forbidden, but always do it only when there is an error, and you do want to rollback. // d. It doesn't mean rollback is forbidden, but always do it only when there is an error, and you do want to rollback.
func TxContext(parentCtx context.Context) (*Context, Committer, error) { func TxContext(parentCtx context.Context) (*Context, Committer, error) {
if sess, ok := inTransaction(parentCtx); ok { if sess, ok := inTransaction(parentCtx); ok {
return newContext(parentCtx, sess, true), &halfCommitter{committer: sess}, nil return newContext(parentCtx, sess), &halfCommitter{committer: sess}, nil
} }
sess := x.NewSession() sess := xormEngine.NewSession()
if err := sess.Begin(); err != nil { if err := sess.Begin(); err != nil {
sess.Close() _ = sess.Close()
return nil, nil, err return nil, nil, err
} }
return newContext(DefaultContext, sess, true), sess, nil return newContext(DefaultContext, sess), sess, nil
} }
// WithTx represents executing database operations on a transaction, if the transaction exist, // WithTx represents executing database operations on a transaction, if the transaction exist,
// this function will reuse it otherwise will create a new one and close it when finished. // this function will reuse it otherwise will create a new one and close it when finished.
func WithTx(parentCtx context.Context, f func(ctx context.Context) error) error { func WithTx(parentCtx context.Context, f func(ctx context.Context) error) error {
if sess, ok := inTransaction(parentCtx); ok { if sess, ok := inTransaction(parentCtx); ok {
err := f(newContext(parentCtx, sess, true)) err := f(newContext(parentCtx, sess))
if err != nil { if err != nil {
// rollback immediately, in case the caller ignores returned error and tries to commit the transaction. // rollback immediately, in case the caller ignores returned error and tries to commit the transaction.
_ = sess.Close() _ = sess.Close()
@ -159,13 +179,13 @@ func WithTx(parentCtx context.Context, f func(ctx context.Context) error) error
} }
func txWithNoCheck(parentCtx context.Context, f func(ctx context.Context) error) error { func txWithNoCheck(parentCtx context.Context, f func(ctx context.Context) error) error {
sess := x.NewSession() sess := xormEngine.NewSession()
defer sess.Close() defer sess.Close()
if err := sess.Begin(); err != nil { if err := sess.Begin(); err != nil {
return err return err
} }
if err := f(newContext(parentCtx, sess, true)); err != nil { if err := f(newContext(parentCtx, sess)); err != nil {
return err return err
} }
@ -269,6 +289,9 @@ func FindIDs(ctx context.Context, tableName, idCol string, cond builder.Cond) ([
// DecrByIDs decreases the given column for entities of the "bean" type with one of the given ids by one // DecrByIDs decreases the given column for entities of the "bean" type with one of the given ids by one
// Timestamps of the entities won't be updated // Timestamps of the entities won't be updated
func DecrByIDs(ctx context.Context, ids []int64, decrCol string, bean any) error { func DecrByIDs(ctx context.Context, ids []int64, decrCol string, bean any) error {
if len(ids) == 0 {
return nil
}
_, err := GetEngine(ctx).Decr(decrCol).In("id", ids).NoAutoCondition().NoAutoTime().Update(bean) _, err := GetEngine(ctx).Decr(decrCol).In("id", ids).NoAutoCondition().NoAutoTime().Update(bean)
return err return err
} }
@ -302,7 +325,7 @@ func CountByBean(ctx context.Context, bean any) (int64, error) {
// TableName returns the table name according a bean object // TableName returns the table name according a bean object
func TableName(bean any) string { func TableName(bean any) string {
return x.TableName(bean) return xormEngine.TableName(bean)
} }
// InTransaction returns true if the engine is in a transaction otherwise return false // InTransaction returns true if the engine is in a transaction otherwise return false
@ -312,7 +335,7 @@ func InTransaction(ctx context.Context) bool {
} }
func inTransaction(ctx context.Context) (*xorm.Session, bool) { func inTransaction(ctx context.Context) (*xorm.Session, bool) {
e := getEngine(ctx) e := getExistingEngine(ctx)
if e == nil { if e == nil {
return nil, false return nil, false
} }

View File

@ -4,7 +4,7 @@
package db // it's not db_test, because this file is for testing the private type halfCommitter package db // it's not db_test, because this file is for testing the private type halfCommitter
import ( import (
"fmt" "errors"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -80,7 +80,7 @@ func Test_halfCommitter(t *testing.T) {
testWithCommitter(mockCommitter, func(committer Committer) error { testWithCommitter(mockCommitter, func(committer Committer) error {
defer committer.Close() defer committer.Close()
if true { if true {
return fmt.Errorf("error") return errors.New("error")
} }
return committer.Commit() return committer.Commit()
}) })
@ -94,7 +94,7 @@ func Test_halfCommitter(t *testing.T) {
testWithCommitter(mockCommitter, func(committer Committer) error { testWithCommitter(mockCommitter, func(committer Committer) error {
committer.Close() committer.Close()
committer.Commit() committer.Commit()
return fmt.Errorf("error") return errors.New("error")
}) })
mockCommitter.Assert(t) mockCommitter.Assert(t)

View File

@ -84,3 +84,47 @@ func TestTxContext(t *testing.T) {
})) }))
} }
} }
func TestContextSafety(t *testing.T) {
type TestModel1 struct {
ID int64
}
type TestModel2 struct {
ID int64
}
assert.NoError(t, unittest.GetXORMEngine().Sync(&TestModel1{}, &TestModel2{}))
assert.NoError(t, db.TruncateBeans(db.DefaultContext, &TestModel1{}, &TestModel2{}))
testCount := 10
for i := 1; i <= testCount; i++ {
assert.NoError(t, db.Insert(db.DefaultContext, &TestModel1{ID: int64(i)}))
assert.NoError(t, db.Insert(db.DefaultContext, &TestModel2{ID: int64(-i)}))
}
actualCount := 0
// here: db.GetEngine(db.DefaultContext) is a new *Session created from *Engine
_ = db.WithTx(db.DefaultContext, func(ctx context.Context) error {
_ = db.GetEngine(ctx).Iterate(&TestModel1{}, func(i int, bean any) error {
// here: db.GetEngine(ctx) is always the unclosed "Iterate" *Session with autoResetStatement=false,
// and the internal states (including "cond" and others) are always there and not be reset in this callback.
m1 := bean.(*TestModel1)
assert.EqualValues(t, i+1, m1.ID)
// here: XORM bug, it fails because the SQL becomes "WHERE id=-1", "WHERE id=-1 AND id=-2", "WHERE id=-1 AND id=-2 AND id=-3" ...
// and it conflicts with the "Iterate"'s internal states.
// has, err := db.GetEngine(ctx).Get(&TestModel2{ID: -m1.ID})
actualCount++
return nil
})
return nil
})
assert.EqualValues(t, testCount, actualCount)
// deny the bad usages
assert.PanicsWithError(t, "using database context in an iterator would cause corrupted results", func() {
_ = unittest.GetXORMEngine().Iterate(&TestModel1{}, func(i int, bean any) error {
_ = db.GetEngine(db.DefaultContext)
return nil
})
})
}

View File

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

View File

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

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

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

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

@ -0,0 +1,47 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package db
import (
"context"
"time"
"code.gitea.io/gitea/modules/gtprof"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"xorm.io/xorm/contexts"
)
type EngineHook struct {
Threshold time.Duration
Logger log.Logger
}
var _ contexts.Hook = (*EngineHook)(nil)
func (*EngineHook) BeforeProcess(c *contexts.ContextHook) (context.Context, error) {
ctx, _ := gtprof.GetTracer().Start(c.Ctx, gtprof.TraceSpanDatabase)
return ctx, nil
}
func (h *EngineHook) AfterProcess(c *contexts.ContextHook) error {
span := gtprof.GetContextSpan(c.Ctx)
if span != nil {
// Do not record SQL parameters here:
// * It shouldn't expose the parameters because they contain sensitive information, end users need to report the trace details safely.
// * Some parameters contain quite long texts, waste memory and are difficult to display.
span.SetAttributeString(gtprof.TraceAttrDbSQL, c.SQL)
span.End()
} else {
setting.PanicInDevOrTesting("span in database engine hook is nil")
}
if c.ExecuteTime >= h.Threshold {
// 8 is the amount of skips passed to runtime.Caller, so that in the log the correct function
// is being displayed (the function that ultimately wants to execute the query in the code)
// instead of the function of the slow query hook being called.
h.Logger.Log(8, &log.Event{Level: log.WARN}, "[Slow SQL Query] %s %v - %v", c.SQL, c.Args, c.ExecuteTime)
}
return nil
}

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

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

View File

@ -15,6 +15,7 @@ import (
_ "code.gitea.io/gitea/cmd" // for TestPrimaryKeys _ "code.gitea.io/gitea/cmd" // for TestPrimaryKeys
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestDumpDatabase(t *testing.T) { func TestDumpDatabase(t *testing.T) {
@ -62,9 +63,7 @@ func TestPrimaryKeys(t *testing.T) {
// Import "code.gitea.io/gitea/cmd" to make sure each db.RegisterModel in init functions has been called. // Import "code.gitea.io/gitea/cmd" to make sure each db.RegisterModel in init functions has been called.
beans, err := db.NamesToBean() beans, err := db.NamesToBean()
if err != nil { require.NoError(t, err)
t.Fatal(err)
}
whitelist := map[string]string{ whitelist := map[string]string{
"the_table_name_to_skip_checking": "Write a note here to explain why", "the_table_name_to_skip_checking": "Write a note here to explain why",
@ -79,8 +78,6 @@ func TestPrimaryKeys(t *testing.T) {
t.Logf("ignore %q because %q", table.Name, why) t.Logf("ignore %q because %q", table.Name, why)
continue continue
} }
if len(table.PrimaryKeys) == 0 { assert.NotEmpty(t, table.PrimaryKeys, "table %q has no primary key", table.Name)
t.Errorf("table %q has no primary key", table.Name)
}
} }
} }

View File

@ -11,7 +11,7 @@ import (
) )
func getXORMEngine() *xorm.Engine { func getXORMEngine() *xorm.Engine {
return db.DefaultContext.(*db.Context).Engine().(*xorm.Engine) return db.GetEngine(db.DefaultContext).(*xorm.Engine)
} }
// CheckDatabaseConnection checks the database connection // CheckDatabaseConnection checks the database connection

View File

@ -11,7 +11,7 @@ import (
"xorm.io/builder" "xorm.io/builder"
) )
// Iterate iterate all the Bean object // Iterate iterates all the Bean object
func Iterate[Bean any](ctx context.Context, cond builder.Cond, f func(ctx context.Context, bean *Bean) error) error { func Iterate[Bean any](ctx context.Context, cond builder.Cond, f func(ctx context.Context, bean *Bean) error) error {
var start int var start int
batchSize := setting.Database.IterateBufferSize batchSize := setting.Database.IterateBufferSize

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