mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-11-03 08:02:36 +09:00 
			
		
		
		
	Compare commits
	
		
			120 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					de7026528b | ||
| 
						 | 
					ee3f5e8fac | ||
| 
						 | 
					b2707bcd18 | ||
| 
						 | 
					0512b02b01 | ||
| 
						 | 
					99545ae2fd | ||
| 
						 | 
					7697df9f93 | ||
| 
						 | 
					d17f8ffcc1 | ||
| 
						 | 
					5e9cc919cf | ||
| 
						 | 
					cc6ec56738 | ||
| 
						 | 
					76bd60fc1d | ||
| 
						 | 
					744f7c8200 | ||
| 
						 | 
					da33b708af | ||
| 
						 | 
					8fa3925874 | ||
| 
						 | 
					7794ff0874 | ||
| 
						 | 
					7c17d0a73e | ||
| 
						 | 
					a014d071e4 | ||
| 
						 | 
					312565e3c2 | ||
| 
						 | 
					58daaf66e8 | ||
| 
						 | 
					f076ada601 | ||
| 
						 | 
					92436b8b2a | ||
| 
						 | 
					2df7d0835a | ||
| 
						 | 
					200cb6140d | ||
| 
						 | 
					2746c6f1aa | ||
| 
						 | 
					23971a77a0 | ||
| 
						 | 
					ebac324ff2 | ||
| 
						 | 
					7df1204795 | ||
| 
						 | 
					159544a950 | ||
| 
						 | 
					9780da583d | ||
| 
						 | 
					a8eaf43f97 | ||
| 
						 | 
					b6fd8741ee | ||
| 
						 | 
					2674d27fb8 | ||
| 
						 | 
					6f3837284d | ||
| 
						 | 
					c30f4f4be5 | ||
| 
						 | 
					4578288ea3 | ||
| 
						 | 
					826fffb59e | ||
| 
						 | 
					2196ba5e42 | ||
| 
						 | 
					12347f07ae | ||
| 
						 | 
					a3c5358d35 | ||
| 
						 | 
					987d014468 | ||
| 
						 | 
					e08eed9040 | ||
| 
						 | 
					4ffa49aa04 | ||
| 
						 | 
					72837530bf | ||
| 
						 | 
					eef635523a | ||
| 
						 | 
					8f45a11919 | ||
| 
						 | 
					e72d001708 | ||
| 
						 | 
					8d9ea68f19 | ||
| 
						 | 
					bf664c2e85 | ||
| 
						 | 
					52d298890b | ||
| 
						 | 
					c09e43acf5 | ||
| 
						 | 
					3b4af01633 | ||
| 
						 | 
					b4e2d5e8ee | ||
| 
						 | 
					2984a7c121 | ||
| 
						 | 
					80cc87b3d8 | ||
| 
						 | 
					10b6047498 | ||
| 
						 | 
					2c47b06869 | ||
| 
						 | 
					31f2a325dc | ||
| 
						 | 
					fcbbc24cc4 | ||
| 
						 | 
					1454e1b6eb | ||
| 
						 | 
					d70348836b | ||
| 
						 | 
					940a930d13 | ||
| 
						 | 
					45d21a0d5c | ||
| 
						 | 
					15ad001aef | ||
| 
						 | 
					ed1828ca92 | ||
| 
						 | 
					3cfff5af0d | ||
| 
						 | 
					6f6c66a07d | ||
| 
						 | 
					d65af69c2b | ||
| 
						 | 
					12c24c2189 | ||
| 
						 | 
					a330f42f01 | ||
| 
						 | 
					531f36ea4a | ||
| 
						 | 
					b4f0eed969 | ||
| 
						 | 
					63b3a33bf2 | ||
| 
						 | 
					9899989ece | ||
| 
						 | 
					0fad40dd8c | ||
| 
						 | 
					e637008fe3 | ||
| 
						 | 
					fd281518ae | ||
| 
						 | 
					e10d222434 | ||
| 
						 | 
					7a35f90b29 | ||
| 
						 | 
					d371aa3031 | ||
| 
						 | 
					81768675d4 | ||
| 
						 | 
					39cc72562b | ||
| 
						 | 
					bc83fb26ef | ||
| 
						 | 
					68736ec292 | ||
| 
						 | 
					3df11c07a8 | ||
| 
						 | 
					96fff862dc | ||
| 
						 | 
					968c04c7da | ||
| 
						 | 
					27de60381d | ||
| 
						 | 
					d2d763318c | ||
| 
						 | 
					610b2fb88d | ||
| 
						 | 
					0858a36016 | ||
| 
						 | 
					fef364e7d6 | ||
| 
						 | 
					ce6a60a38b | ||
| 
						 | 
					74159a8855 | ||
| 
						 | 
					ce6464123f | ||
| 
						 | 
					7f0050cf39 | ||
| 
						 | 
					c102e344f3 | ||
| 
						 | 
					f27128bf94 | ||
| 
						 | 
					f35ab5cd52 | ||
| 
						 | 
					0137bc4e5c | ||
| 
						 | 
					eed0968c37 | ||
| 
						 | 
					a0b65ed17f | ||
| 
						 | 
					ad1b76540e | ||
| 
						 | 
					6636b37a9c | ||
| 
						 | 
					af5e5e8f00 | ||
| 
						 | 
					0e0ebf68d7 | ||
| 
						 | 
					90bd08ceef | ||
| 
						 | 
					0c581106d2 | ||
| 
						 | 
					e18e31d557 | ||
| 
						 | 
					e1026feddc | ||
| 
						 | 
					d670820722 | ||
| 
						 | 
					a8f98fd3be | ||
| 
						 | 
					c442c682ef | ||
| 
						 | 
					57868c2315 | ||
| 
						 | 
					b1c21880c1 | ||
| 
						 | 
					1e71ad89ce | ||
| 
						 | 
					c20642fa99 | ||
| 
						 | 
					a4291fd553 | ||
| 
						 | 
					fa5a064559 | ||
| 
						 | 
					cb42232080 | ||
| 
						 | 
					c8ffe777cf | ||
| 
						 | 
					e98dd6ee5b | 
@@ -22,25 +22,20 @@ groups:
 | 
			
		||||
    name: FEATURES
 | 
			
		||||
    labels:
 | 
			
		||||
      - type/feature
 | 
			
		||||
  -
 | 
			
		||||
    name: API
 | 
			
		||||
    labels:
 | 
			
		||||
      - modifies/api
 | 
			
		||||
  -
 | 
			
		||||
    name: ENHANCEMENTS
 | 
			
		||||
    labels:
 | 
			
		||||
      - type/enhancement
 | 
			
		||||
  -
 | 
			
		||||
    name: PERFORMANCE
 | 
			
		||||
    labels:
 | 
			
		||||
      - performance/memory
 | 
			
		||||
      - performance/speed
 | 
			
		||||
      - performance/bigrepo
 | 
			
		||||
      - performance/cpu
 | 
			
		||||
      - type/refactoring
 | 
			
		||||
      - topic/ui
 | 
			
		||||
  -
 | 
			
		||||
    name: BUGFIXES
 | 
			
		||||
    labels:
 | 
			
		||||
      - type/bug
 | 
			
		||||
  -
 | 
			
		||||
    name: API
 | 
			
		||||
    labels:
 | 
			
		||||
      - modifies/api
 | 
			
		||||
  -
 | 
			
		||||
    name: TESTING
 | 
			
		||||
    labels:
 | 
			
		||||
 
 | 
			
		||||
@@ -1,19 +1,15 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "Gitea DevContainer",
 | 
			
		||||
  "image": "mcr.microsoft.com/devcontainers/go:1.25-trixie",
 | 
			
		||||
  "containerEnv": {
 | 
			
		||||
    // override "local" from packaged version
 | 
			
		||||
    "GOTOOLCHAIN": "auto"
 | 
			
		||||
  },
 | 
			
		||||
  "image": "mcr.microsoft.com/devcontainers/go:1.23-bookworm",
 | 
			
		||||
  "features": {
 | 
			
		||||
    // installs nodejs into container
 | 
			
		||||
    "ghcr.io/devcontainers/features/node:1": {
 | 
			
		||||
      "version": "latest"
 | 
			
		||||
      "version": "20"
 | 
			
		||||
    },
 | 
			
		||||
    "ghcr.io/devcontainers/features/git-lfs:1.2.5": {},
 | 
			
		||||
    "ghcr.io/jsburckhardt/devcontainer-features/uv:1": {},
 | 
			
		||||
    "ghcr.io/devcontainers/features/git-lfs:1.2.2": {},
 | 
			
		||||
    "ghcr.io/devcontainers-contrib/features/poetry:2": {},
 | 
			
		||||
    "ghcr.io/devcontainers/features/python:1": {
 | 
			
		||||
      "version": "3.13"
 | 
			
		||||
      "version": "3.12"
 | 
			
		||||
    },
 | 
			
		||||
    "ghcr.io/warrenbuckley/codespace-features/sqlite:1": {}
 | 
			
		||||
  },
 | 
			
		||||
 
 | 
			
		||||
@@ -36,6 +36,15 @@ _testmain.go
 | 
			
		||||
coverage.all
 | 
			
		||||
cpu.out
 | 
			
		||||
 | 
			
		||||
/modules/migration/bindata.go
 | 
			
		||||
/modules/migration/bindata.go.hash
 | 
			
		||||
/modules/options/bindata.go
 | 
			
		||||
/modules/options/bindata.go.hash
 | 
			
		||||
/modules/public/bindata.go
 | 
			
		||||
/modules/public/bindata.go.hash
 | 
			
		||||
/modules/templates/bindata.go
 | 
			
		||||
/modules/templates/bindata.go.hash
 | 
			
		||||
 | 
			
		||||
*.db
 | 
			
		||||
*.log
 | 
			
		||||
 | 
			
		||||
@@ -65,18 +74,26 @@ cpu.out
 | 
			
		||||
/yarn.lock
 | 
			
		||||
/yarn-error.log
 | 
			
		||||
/npm-debug.log*
 | 
			
		||||
/pnpm-debug.log*
 | 
			
		||||
/public/assets/js
 | 
			
		||||
/public/assets/css
 | 
			
		||||
/public/assets/fonts
 | 
			
		||||
/public/assets/img/avatar
 | 
			
		||||
/vendor
 | 
			
		||||
/web_src/fomantic/node_modules
 | 
			
		||||
/web_src/fomantic/build/*
 | 
			
		||||
!/web_src/fomantic/build/semantic.js
 | 
			
		||||
!/web_src/fomantic/build/semantic.css
 | 
			
		||||
!/web_src/fomantic/build/themes
 | 
			
		||||
/web_src/fomantic/build/themes/*
 | 
			
		||||
!/web_src/fomantic/build/themes/default
 | 
			
		||||
/web_src/fomantic/build/themes/default/assets/*
 | 
			
		||||
!/web_src/fomantic/build/themes/default/assets/fonts
 | 
			
		||||
/web_src/fomantic/build/themes/default/assets/fonts/*
 | 
			
		||||
!/web_src/fomantic/build/themes/default/assets/fonts/icons.woff2
 | 
			
		||||
!/web_src/fomantic/build/themes/default/assets/fonts/outline-icons.woff2
 | 
			
		||||
/VERSION
 | 
			
		||||
/.air
 | 
			
		||||
/.go-licenses
 | 
			
		||||
/Dockerfile
 | 
			
		||||
/Dockerfile.rootless
 | 
			
		||||
/.venv
 | 
			
		||||
 | 
			
		||||
# Files and folders that were previously generated
 | 
			
		||||
/public/assets/img/webpack
 | 
			
		||||
 
 | 
			
		||||
@@ -12,15 +12,11 @@ insert_final_newline = true
 | 
			
		||||
[*.{go,tmpl,html}]
 | 
			
		||||
indent_style = tab
 | 
			
		||||
 | 
			
		||||
[go.*]
 | 
			
		||||
indent_style = tab
 | 
			
		||||
 | 
			
		||||
[templates/custom/*.tmpl]
 | 
			
		||||
insert_final_newline = false
 | 
			
		||||
 | 
			
		||||
[templates/swagger/v1_json.tmpl]
 | 
			
		||||
indent_style = space
 | 
			
		||||
insert_final_newline = false
 | 
			
		||||
 | 
			
		||||
[templates/user/auth/oidc_wellknown.tmpl]
 | 
			
		||||
indent_style = space
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										967
									
								
								.eslintrc.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										967
									
								
								.eslintrc.yaml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,967 @@
 | 
			
		||||
root: true
 | 
			
		||||
reportUnusedDisableDirectives: true
 | 
			
		||||
 | 
			
		||||
ignorePatterns:
 | 
			
		||||
  - /web_src/js/vendor
 | 
			
		||||
  - /web_src/fomantic
 | 
			
		||||
  - /public/assets/js
 | 
			
		||||
 | 
			
		||||
parser: "@typescript-eslint/parser"
 | 
			
		||||
 | 
			
		||||
parserOptions:
 | 
			
		||||
  sourceType: module
 | 
			
		||||
  ecmaVersion: latest
 | 
			
		||||
  project: true
 | 
			
		||||
  extraFileExtensions: [".vue"]
 | 
			
		||||
  parser: "@typescript-eslint/parser" # for vue plugin - https://eslint.vuejs.org/user-guide/#how-to-use-a-custom-parser
 | 
			
		||||
 | 
			
		||||
settings:
 | 
			
		||||
  import-x/extensions: [".js", ".ts"]
 | 
			
		||||
  import-x/parsers:
 | 
			
		||||
    "@typescript-eslint/parser": [".js", ".ts"]
 | 
			
		||||
  import-x/resolver:
 | 
			
		||||
    typescript: true
 | 
			
		||||
 | 
			
		||||
plugins:
 | 
			
		||||
  - "@eslint-community/eslint-plugin-eslint-comments"
 | 
			
		||||
  - "@stylistic/eslint-plugin-js"
 | 
			
		||||
  - "@typescript-eslint/eslint-plugin"
 | 
			
		||||
  - eslint-plugin-array-func
 | 
			
		||||
  - eslint-plugin-github
 | 
			
		||||
  - eslint-plugin-import-x
 | 
			
		||||
  - eslint-plugin-no-jquery
 | 
			
		||||
  - eslint-plugin-no-use-extend-native
 | 
			
		||||
  - eslint-plugin-regexp
 | 
			
		||||
  - eslint-plugin-sonarjs
 | 
			
		||||
  - eslint-plugin-unicorn
 | 
			
		||||
  - eslint-plugin-vitest
 | 
			
		||||
  - eslint-plugin-vitest-globals
 | 
			
		||||
  - eslint-plugin-wc
 | 
			
		||||
 | 
			
		||||
env:
 | 
			
		||||
  es2024: true
 | 
			
		||||
  node: true
 | 
			
		||||
 | 
			
		||||
overrides:
 | 
			
		||||
  - files: ["web_src/**/*"]
 | 
			
		||||
    globals:
 | 
			
		||||
      __webpack_public_path__: true
 | 
			
		||||
      process: false # https://github.com/webpack/webpack/issues/15833
 | 
			
		||||
  - files: ["web_src/**/*", "docs/**/*"]
 | 
			
		||||
    env:
 | 
			
		||||
      browser: true
 | 
			
		||||
      node: false
 | 
			
		||||
  - files: ["web_src/**/*worker.*"]
 | 
			
		||||
    env:
 | 
			
		||||
      worker: true
 | 
			
		||||
    rules:
 | 
			
		||||
      no-restricted-globals: [2, addEventListener, blur, close, closed, confirm, defaultStatus, defaultstatus, error, event, external, find, focus, frameElement, frames, history, innerHeight, innerWidth, isFinite, isNaN, length, locationbar, menubar, moveBy, moveTo, name, onblur, onerror, onfocus, onload, onresize, onunload, open, opener, opera, outerHeight, outerWidth, pageXOffset, pageYOffset, parent, print, removeEventListener, resizeBy, resizeTo, screen, screenLeft, screenTop, screenX, screenY, scroll, scrollbars, scrollBy, scrollTo, scrollX, scrollY, status, statusbar, stop, toolbar, top]
 | 
			
		||||
  - files: ["*.config.*"]
 | 
			
		||||
    rules:
 | 
			
		||||
      import-x/no-unused-modules: [0]
 | 
			
		||||
  - files: ["**/*.d.ts"]
 | 
			
		||||
    rules:
 | 
			
		||||
      import-x/no-unused-modules: [0]
 | 
			
		||||
      "@typescript-eslint/consistent-type-definitions": [0]
 | 
			
		||||
      "@typescript-eslint/consistent-type-imports": [0]
 | 
			
		||||
  - files: ["web_src/js/types.ts"]
 | 
			
		||||
    rules:
 | 
			
		||||
      import-x/no-unused-modules: [0]
 | 
			
		||||
  - files: ["**/*.test.*", "web_src/js/test/setup.ts"]
 | 
			
		||||
    env:
 | 
			
		||||
      vitest-globals/env: true
 | 
			
		||||
    rules:
 | 
			
		||||
      vitest/consistent-test-filename: [0]
 | 
			
		||||
      vitest/consistent-test-it: [0]
 | 
			
		||||
      vitest/expect-expect: [0]
 | 
			
		||||
      vitest/max-expects: [0]
 | 
			
		||||
      vitest/max-nested-describe: [0]
 | 
			
		||||
      vitest/no-alias-methods: [0]
 | 
			
		||||
      vitest/no-commented-out-tests: [0]
 | 
			
		||||
      vitest/no-conditional-expect: [0]
 | 
			
		||||
      vitest/no-conditional-in-test: [0]
 | 
			
		||||
      vitest/no-conditional-tests: [0]
 | 
			
		||||
      vitest/no-disabled-tests: [0]
 | 
			
		||||
      vitest/no-done-callback: [0]
 | 
			
		||||
      vitest/no-duplicate-hooks: [0]
 | 
			
		||||
      vitest/no-focused-tests: [0]
 | 
			
		||||
      vitest/no-hooks: [0]
 | 
			
		||||
      vitest/no-identical-title: [2]
 | 
			
		||||
      vitest/no-interpolation-in-snapshots: [0]
 | 
			
		||||
      vitest/no-large-snapshots: [0]
 | 
			
		||||
      vitest/no-mocks-import: [0]
 | 
			
		||||
      vitest/no-restricted-matchers: [0]
 | 
			
		||||
      vitest/no-restricted-vi-methods: [0]
 | 
			
		||||
      vitest/no-standalone-expect: [0]
 | 
			
		||||
      vitest/no-test-prefixes: [0]
 | 
			
		||||
      vitest/no-test-return-statement: [0]
 | 
			
		||||
      vitest/prefer-called-with: [0]
 | 
			
		||||
      vitest/prefer-comparison-matcher: [0]
 | 
			
		||||
      vitest/prefer-each: [0]
 | 
			
		||||
      vitest/prefer-equality-matcher: [0]
 | 
			
		||||
      vitest/prefer-expect-resolves: [0]
 | 
			
		||||
      vitest/prefer-hooks-in-order: [0]
 | 
			
		||||
      vitest/prefer-hooks-on-top: [2]
 | 
			
		||||
      vitest/prefer-lowercase-title: [0]
 | 
			
		||||
      vitest/prefer-mock-promise-shorthand: [0]
 | 
			
		||||
      vitest/prefer-snapshot-hint: [0]
 | 
			
		||||
      vitest/prefer-spy-on: [0]
 | 
			
		||||
      vitest/prefer-strict-equal: [0]
 | 
			
		||||
      vitest/prefer-to-be: [0]
 | 
			
		||||
      vitest/prefer-to-be-falsy: [0]
 | 
			
		||||
      vitest/prefer-to-be-object: [0]
 | 
			
		||||
      vitest/prefer-to-be-truthy: [0]
 | 
			
		||||
      vitest/prefer-to-contain: [0]
 | 
			
		||||
      vitest/prefer-to-have-length: [0]
 | 
			
		||||
      vitest/prefer-todo: [0]
 | 
			
		||||
      vitest/require-hook: [0]
 | 
			
		||||
      vitest/require-to-throw-message: [0]
 | 
			
		||||
      vitest/require-top-level-describe: [0]
 | 
			
		||||
      vitest/valid-describe-callback: [2]
 | 
			
		||||
      vitest/valid-expect: [2]
 | 
			
		||||
      vitest/valid-title: [2]
 | 
			
		||||
  - files: ["web_src/js/modules/fetch.ts", "web_src/js/standalone/**/*"]
 | 
			
		||||
    rules:
 | 
			
		||||
      no-restricted-syntax: [2, WithStatement, ForInStatement, LabeledStatement, SequenceExpression]
 | 
			
		||||
  - files: ["**/*.vue"]
 | 
			
		||||
    plugins:
 | 
			
		||||
      - eslint-plugin-vue
 | 
			
		||||
      - eslint-plugin-vue-scoped-css
 | 
			
		||||
    extends:
 | 
			
		||||
      - plugin:vue/vue3-recommended
 | 
			
		||||
      - plugin:vue-scoped-css/vue3-recommended
 | 
			
		||||
    rules:
 | 
			
		||||
      vue/attributes-order: [0]
 | 
			
		||||
      vue/html-closing-bracket-spacing: [2, {startTag: never, endTag: never, selfClosingTag: never}]
 | 
			
		||||
      vue/max-attributes-per-line: [0]
 | 
			
		||||
      vue/singleline-html-element-content-newline: [0]
 | 
			
		||||
  - files: ["tests/e2e/**"]
 | 
			
		||||
    plugins:
 | 
			
		||||
      - eslint-plugin-playwright
 | 
			
		||||
    extends: plugin:playwright/recommended
 | 
			
		||||
 | 
			
		||||
rules:
 | 
			
		||||
  "@eslint-community/eslint-comments/disable-enable-pair": [2]
 | 
			
		||||
  "@eslint-community/eslint-comments/no-aggregating-enable": [2]
 | 
			
		||||
  "@eslint-community/eslint-comments/no-duplicate-disable": [2]
 | 
			
		||||
  "@eslint-community/eslint-comments/no-restricted-disable": [0]
 | 
			
		||||
  "@eslint-community/eslint-comments/no-unlimited-disable": [2]
 | 
			
		||||
  "@eslint-community/eslint-comments/no-unused-disable": [2]
 | 
			
		||||
  "@eslint-community/eslint-comments/no-unused-enable": [2]
 | 
			
		||||
  "@eslint-community/eslint-comments/no-use": [0]
 | 
			
		||||
  "@eslint-community/eslint-comments/require-description": [0]
 | 
			
		||||
  "@stylistic/js/array-bracket-newline": [0]
 | 
			
		||||
  "@stylistic/js/array-bracket-spacing": [2, never]
 | 
			
		||||
  "@stylistic/js/array-element-newline": [0]
 | 
			
		||||
  "@stylistic/js/arrow-parens": [2, always]
 | 
			
		||||
  "@stylistic/js/arrow-spacing": [2, {before: true, after: true}]
 | 
			
		||||
  "@stylistic/js/block-spacing": [0]
 | 
			
		||||
  "@stylistic/js/brace-style": [2, 1tbs, {allowSingleLine: true}]
 | 
			
		||||
  "@stylistic/js/comma-dangle": [2, always-multiline]
 | 
			
		||||
  "@stylistic/js/comma-spacing": [2, {before: false, after: true}]
 | 
			
		||||
  "@stylistic/js/comma-style": [2, last]
 | 
			
		||||
  "@stylistic/js/computed-property-spacing": [2, never]
 | 
			
		||||
  "@stylistic/js/dot-location": [2, property]
 | 
			
		||||
  "@stylistic/js/eol-last": [2]
 | 
			
		||||
  "@stylistic/js/function-call-argument-newline": [0]
 | 
			
		||||
  "@stylistic/js/function-call-spacing": [2, never]
 | 
			
		||||
  "@stylistic/js/function-paren-newline": [0]
 | 
			
		||||
  "@stylistic/js/generator-star-spacing": [0]
 | 
			
		||||
  "@stylistic/js/implicit-arrow-linebreak": [0]
 | 
			
		||||
  "@stylistic/js/indent": [2, 2, {ignoreComments: true, SwitchCase: 1}]
 | 
			
		||||
  "@stylistic/js/key-spacing": [2]
 | 
			
		||||
  "@stylistic/js/keyword-spacing": [2]
 | 
			
		||||
  "@stylistic/js/line-comment-position": [0]
 | 
			
		||||
  "@stylistic/js/linebreak-style": [2, unix]
 | 
			
		||||
  "@stylistic/js/lines-around-comment": [0]
 | 
			
		||||
  "@stylistic/js/lines-between-class-members": [0]
 | 
			
		||||
  "@stylistic/js/max-len": [0]
 | 
			
		||||
  "@stylistic/js/max-statements-per-line": [0]
 | 
			
		||||
  "@stylistic/js/multiline-comment-style": [0]
 | 
			
		||||
  "@stylistic/js/multiline-ternary": [0]
 | 
			
		||||
  "@stylistic/js/new-parens": [2]
 | 
			
		||||
  "@stylistic/js/newline-per-chained-call": [0]
 | 
			
		||||
  "@stylistic/js/no-confusing-arrow": [0]
 | 
			
		||||
  "@stylistic/js/no-extra-parens": [0]
 | 
			
		||||
  "@stylistic/js/no-extra-semi": [2]
 | 
			
		||||
  "@stylistic/js/no-floating-decimal": [0]
 | 
			
		||||
  "@stylistic/js/no-mixed-operators": [0]
 | 
			
		||||
  "@stylistic/js/no-mixed-spaces-and-tabs": [2]
 | 
			
		||||
  "@stylistic/js/no-multi-spaces": [2, {ignoreEOLComments: true, exceptions: {Property: true}}]
 | 
			
		||||
  "@stylistic/js/no-multiple-empty-lines": [2, {max: 1, maxEOF: 0, maxBOF: 0}]
 | 
			
		||||
  "@stylistic/js/no-tabs": [2]
 | 
			
		||||
  "@stylistic/js/no-trailing-spaces": [2]
 | 
			
		||||
  "@stylistic/js/no-whitespace-before-property": [2]
 | 
			
		||||
  "@stylistic/js/nonblock-statement-body-position": [2]
 | 
			
		||||
  "@stylistic/js/object-curly-newline": [0]
 | 
			
		||||
  "@stylistic/js/object-curly-spacing": [2, never]
 | 
			
		||||
  "@stylistic/js/object-property-newline": [0]
 | 
			
		||||
  "@stylistic/js/one-var-declaration-per-line": [0]
 | 
			
		||||
  "@stylistic/js/operator-linebreak": [2, after]
 | 
			
		||||
  "@stylistic/js/padded-blocks": [2, never]
 | 
			
		||||
  "@stylistic/js/padding-line-between-statements": [0]
 | 
			
		||||
  "@stylistic/js/quote-props": [0]
 | 
			
		||||
  "@stylistic/js/quotes": [2, single, {avoidEscape: true, allowTemplateLiterals: true}]
 | 
			
		||||
  "@stylistic/js/rest-spread-spacing": [2, never]
 | 
			
		||||
  "@stylistic/js/semi": [2, always, {omitLastInOneLineBlock: true}]
 | 
			
		||||
  "@stylistic/js/semi-spacing": [2, {before: false, after: true}]
 | 
			
		||||
  "@stylistic/js/semi-style": [2, last]
 | 
			
		||||
  "@stylistic/js/space-before-blocks": [2, always]
 | 
			
		||||
  "@stylistic/js/space-before-function-paren": [2, {anonymous: ignore, named: never, asyncArrow: always}]
 | 
			
		||||
  "@stylistic/js/space-in-parens": [2, never]
 | 
			
		||||
  "@stylistic/js/space-infix-ops": [2]
 | 
			
		||||
  "@stylistic/js/space-unary-ops": [2]
 | 
			
		||||
  "@stylistic/js/spaced-comment": [2, always]
 | 
			
		||||
  "@stylistic/js/switch-colon-spacing": [2]
 | 
			
		||||
  "@stylistic/js/template-curly-spacing": [2, never]
 | 
			
		||||
  "@stylistic/js/template-tag-spacing": [2, never]
 | 
			
		||||
  "@stylistic/js/wrap-iife": [2, inside]
 | 
			
		||||
  "@stylistic/js/wrap-regex": [0]
 | 
			
		||||
  "@stylistic/js/yield-star-spacing": [2, after]
 | 
			
		||||
  "@typescript-eslint/adjacent-overload-signatures": [0]
 | 
			
		||||
  "@typescript-eslint/array-type": [0]
 | 
			
		||||
  "@typescript-eslint/await-thenable": [2]
 | 
			
		||||
  "@typescript-eslint/ban-ts-comment": [2, {'ts-expect-error': false, 'ts-ignore': true, 'ts-nocheck': false, 'ts-check': false}]
 | 
			
		||||
  "@typescript-eslint/ban-tslint-comment": [0]
 | 
			
		||||
  "@typescript-eslint/class-literal-property-style": [0]
 | 
			
		||||
  "@typescript-eslint/class-methods-use-this": [0]
 | 
			
		||||
  "@typescript-eslint/consistent-generic-constructors": [0]
 | 
			
		||||
  "@typescript-eslint/consistent-indexed-object-style": [0]
 | 
			
		||||
  "@typescript-eslint/consistent-return": [0]
 | 
			
		||||
  "@typescript-eslint/consistent-type-assertions": [2, {assertionStyle: as, objectLiteralTypeAssertions: allow}]
 | 
			
		||||
  "@typescript-eslint/consistent-type-definitions": [2, type]
 | 
			
		||||
  "@typescript-eslint/consistent-type-exports": [2, {fixMixedExportsWithInlineTypeSpecifier: false}]
 | 
			
		||||
  "@typescript-eslint/consistent-type-imports": [2, {prefer: type-imports, fixStyle: separate-type-imports, disallowTypeAnnotations: true}]
 | 
			
		||||
  "@typescript-eslint/default-param-last": [0]
 | 
			
		||||
  "@typescript-eslint/dot-notation": [0]
 | 
			
		||||
  "@typescript-eslint/explicit-function-return-type": [0]
 | 
			
		||||
  "@typescript-eslint/explicit-member-accessibility": [0]
 | 
			
		||||
  "@typescript-eslint/explicit-module-boundary-types": [0]
 | 
			
		||||
  "@typescript-eslint/init-declarations": [0]
 | 
			
		||||
  "@typescript-eslint/max-params": [0]
 | 
			
		||||
  "@typescript-eslint/member-ordering": [0]
 | 
			
		||||
  "@typescript-eslint/method-signature-style": [0]
 | 
			
		||||
  "@typescript-eslint/naming-convention": [0]
 | 
			
		||||
  "@typescript-eslint/no-array-constructor": [2]
 | 
			
		||||
  "@typescript-eslint/no-array-delete": [2]
 | 
			
		||||
  "@typescript-eslint/no-base-to-string": [0]
 | 
			
		||||
  "@typescript-eslint/no-confusing-non-null-assertion": [2]
 | 
			
		||||
  "@typescript-eslint/no-confusing-void-expression": [0]
 | 
			
		||||
  "@typescript-eslint/no-deprecated": [2]
 | 
			
		||||
  "@typescript-eslint/no-dupe-class-members": [0]
 | 
			
		||||
  "@typescript-eslint/no-duplicate-enum-values": [2]
 | 
			
		||||
  "@typescript-eslint/no-duplicate-type-constituents": [2, {ignoreUnions: true}]
 | 
			
		||||
  "@typescript-eslint/no-dynamic-delete": [0]
 | 
			
		||||
  "@typescript-eslint/no-empty-function": [0]
 | 
			
		||||
  "@typescript-eslint/no-empty-interface": [0]
 | 
			
		||||
  "@typescript-eslint/no-empty-object-type": [2]
 | 
			
		||||
  "@typescript-eslint/no-explicit-any": [0]
 | 
			
		||||
  "@typescript-eslint/no-extra-non-null-assertion": [2]
 | 
			
		||||
  "@typescript-eslint/no-extraneous-class": [0]
 | 
			
		||||
  "@typescript-eslint/no-floating-promises": [0]
 | 
			
		||||
  "@typescript-eslint/no-for-in-array": [2]
 | 
			
		||||
  "@typescript-eslint/no-implied-eval": [2]
 | 
			
		||||
  "@typescript-eslint/no-import-type-side-effects": [0] # dupe with consistent-type-imports
 | 
			
		||||
  "@typescript-eslint/no-inferrable-types": [0]
 | 
			
		||||
  "@typescript-eslint/no-invalid-this": [0]
 | 
			
		||||
  "@typescript-eslint/no-invalid-void-type": [0]
 | 
			
		||||
  "@typescript-eslint/no-loop-func": [0]
 | 
			
		||||
  "@typescript-eslint/no-loss-of-precision": [0]
 | 
			
		||||
  "@typescript-eslint/no-magic-numbers": [0]
 | 
			
		||||
  "@typescript-eslint/no-meaningless-void-operator": [0]
 | 
			
		||||
  "@typescript-eslint/no-misused-new": [2]
 | 
			
		||||
  "@typescript-eslint/no-misused-promises": [2, {checksVoidReturn: {attributes: false, arguments: false}}]
 | 
			
		||||
  "@typescript-eslint/no-mixed-enums": [0]
 | 
			
		||||
  "@typescript-eslint/no-namespace": [2]
 | 
			
		||||
  "@typescript-eslint/no-non-null-asserted-nullish-coalescing": [0]
 | 
			
		||||
  "@typescript-eslint/no-non-null-asserted-optional-chain": [2]
 | 
			
		||||
  "@typescript-eslint/no-non-null-assertion": [0]
 | 
			
		||||
  "@typescript-eslint/no-redeclare": [0]
 | 
			
		||||
  "@typescript-eslint/no-redundant-type-constituents": [2]
 | 
			
		||||
  "@typescript-eslint/no-require-imports": [2]
 | 
			
		||||
  "@typescript-eslint/no-restricted-imports": [0]
 | 
			
		||||
  "@typescript-eslint/no-restricted-types": [0]
 | 
			
		||||
  "@typescript-eslint/no-shadow": [0]
 | 
			
		||||
  "@typescript-eslint/no-this-alias": [0] # handled by unicorn/no-this-assignment
 | 
			
		||||
  "@typescript-eslint/no-unnecessary-boolean-literal-compare": [0]
 | 
			
		||||
  "@typescript-eslint/no-unnecessary-condition": [0]
 | 
			
		||||
  "@typescript-eslint/no-unnecessary-qualifier": [0]
 | 
			
		||||
  "@typescript-eslint/no-unnecessary-template-expression": [0]
 | 
			
		||||
  "@typescript-eslint/no-unnecessary-type-arguments": [0]
 | 
			
		||||
  "@typescript-eslint/no-unnecessary-type-assertion": [2]
 | 
			
		||||
  "@typescript-eslint/no-unnecessary-type-constraint": [2]
 | 
			
		||||
  "@typescript-eslint/no-unsafe-argument": [0]
 | 
			
		||||
  "@typescript-eslint/no-unsafe-assignment": [0]
 | 
			
		||||
  "@typescript-eslint/no-unsafe-call": [0]
 | 
			
		||||
  "@typescript-eslint/no-unsafe-declaration-merging": [2]
 | 
			
		||||
  "@typescript-eslint/no-unsafe-enum-comparison": [2]
 | 
			
		||||
  "@typescript-eslint/no-unsafe-function-type": [2]
 | 
			
		||||
  "@typescript-eslint/no-unsafe-member-access": [0]
 | 
			
		||||
  "@typescript-eslint/no-unsafe-return": [0]
 | 
			
		||||
  "@typescript-eslint/no-unsafe-unary-minus": [2]
 | 
			
		||||
  "@typescript-eslint/no-unused-expressions": [0]
 | 
			
		||||
  "@typescript-eslint/no-unused-vars": [2, {vars: all, args: all, caughtErrors: all, ignoreRestSiblings: false, argsIgnorePattern: ^_, varsIgnorePattern: ^_, caughtErrorsIgnorePattern: ^_, destructuredArrayIgnorePattern: ^_}]
 | 
			
		||||
  "@typescript-eslint/no-use-before-define": [0]
 | 
			
		||||
  "@typescript-eslint/no-useless-constructor": [0]
 | 
			
		||||
  "@typescript-eslint/no-useless-empty-export": [0]
 | 
			
		||||
  "@typescript-eslint/no-wrapper-object-types": [2]
 | 
			
		||||
  "@typescript-eslint/non-nullable-type-assertion-style": [0]
 | 
			
		||||
  "@typescript-eslint/only-throw-error": [2]
 | 
			
		||||
  "@typescript-eslint/parameter-properties": [0]
 | 
			
		||||
  "@typescript-eslint/prefer-as-const": [2]
 | 
			
		||||
  "@typescript-eslint/prefer-destructuring": [0]
 | 
			
		||||
  "@typescript-eslint/prefer-enum-initializers": [0]
 | 
			
		||||
  "@typescript-eslint/prefer-find": [2]
 | 
			
		||||
  "@typescript-eslint/prefer-for-of": [2]
 | 
			
		||||
  "@typescript-eslint/prefer-function-type": [2]
 | 
			
		||||
  "@typescript-eslint/prefer-includes": [2]
 | 
			
		||||
  "@typescript-eslint/prefer-literal-enum-member": [0]
 | 
			
		||||
  "@typescript-eslint/prefer-namespace-keyword": [0]
 | 
			
		||||
  "@typescript-eslint/prefer-nullish-coalescing": [0]
 | 
			
		||||
  "@typescript-eslint/prefer-optional-chain": [2, {requireNullish: true}]
 | 
			
		||||
  "@typescript-eslint/prefer-promise-reject-errors": [0]
 | 
			
		||||
  "@typescript-eslint/prefer-readonly": [0]
 | 
			
		||||
  "@typescript-eslint/prefer-readonly-parameter-types": [0]
 | 
			
		||||
  "@typescript-eslint/prefer-reduce-type-parameter": [0]
 | 
			
		||||
  "@typescript-eslint/prefer-regexp-exec": [0]
 | 
			
		||||
  "@typescript-eslint/prefer-return-this-type": [0]
 | 
			
		||||
  "@typescript-eslint/prefer-string-starts-ends-with": [2, {allowSingleElementEquality: always}]
 | 
			
		||||
  "@typescript-eslint/promise-function-async": [0]
 | 
			
		||||
  "@typescript-eslint/require-array-sort-compare": [0]
 | 
			
		||||
  "@typescript-eslint/require-await": [0]
 | 
			
		||||
  "@typescript-eslint/restrict-plus-operands": [2]
 | 
			
		||||
  "@typescript-eslint/restrict-template-expressions": [0]
 | 
			
		||||
  "@typescript-eslint/return-await": [0]
 | 
			
		||||
  "@typescript-eslint/strict-boolean-expressions": [0]
 | 
			
		||||
  "@typescript-eslint/switch-exhaustiveness-check": [0]
 | 
			
		||||
  "@typescript-eslint/triple-slash-reference": [2]
 | 
			
		||||
  "@typescript-eslint/typedef": [0]
 | 
			
		||||
  "@typescript-eslint/unbound-method": [0] # too many false-positives
 | 
			
		||||
  "@typescript-eslint/unified-signatures": [2]
 | 
			
		||||
  accessor-pairs: [2]
 | 
			
		||||
  array-callback-return: [2, {checkForEach: true}]
 | 
			
		||||
  array-func/avoid-reverse: [2]
 | 
			
		||||
  array-func/from-map: [2]
 | 
			
		||||
  array-func/no-unnecessary-this-arg: [2]
 | 
			
		||||
  array-func/prefer-array-from: [2]
 | 
			
		||||
  array-func/prefer-flat-map: [0] # handled by unicorn/prefer-array-flat-map
 | 
			
		||||
  array-func/prefer-flat: [0] # handled by unicorn/prefer-array-flat
 | 
			
		||||
  arrow-body-style: [0]
 | 
			
		||||
  block-scoped-var: [2]
 | 
			
		||||
  camelcase: [0]
 | 
			
		||||
  capitalized-comments: [0]
 | 
			
		||||
  class-methods-use-this: [0]
 | 
			
		||||
  complexity: [0]
 | 
			
		||||
  consistent-return: [0]
 | 
			
		||||
  consistent-this: [0]
 | 
			
		||||
  constructor-super: [2]
 | 
			
		||||
  curly: [0]
 | 
			
		||||
  default-case-last: [2]
 | 
			
		||||
  default-case: [0]
 | 
			
		||||
  default-param-last: [0]
 | 
			
		||||
  dot-notation: [0]
 | 
			
		||||
  eqeqeq: [2]
 | 
			
		||||
  for-direction: [2]
 | 
			
		||||
  func-name-matching: [2]
 | 
			
		||||
  func-names: [0]
 | 
			
		||||
  func-style: [0]
 | 
			
		||||
  getter-return: [2]
 | 
			
		||||
  github/a11y-aria-label-is-well-formatted: [0]
 | 
			
		||||
  github/a11y-no-title-attribute: [0]
 | 
			
		||||
  github/a11y-no-visually-hidden-interactive-element: [0]
 | 
			
		||||
  github/a11y-role-supports-aria-props: [0]
 | 
			
		||||
  github/a11y-svg-has-accessible-name: [0]
 | 
			
		||||
  github/array-foreach: [0]
 | 
			
		||||
  github/async-currenttarget: [2]
 | 
			
		||||
  github/async-preventdefault: [2]
 | 
			
		||||
  github/authenticity-token: [0]
 | 
			
		||||
  github/get-attribute: [0]
 | 
			
		||||
  github/js-class-name: [0]
 | 
			
		||||
  github/no-blur: [0]
 | 
			
		||||
  github/no-d-none: [0]
 | 
			
		||||
  github/no-dataset: [2]
 | 
			
		||||
  github/no-dynamic-script-tag: [2]
 | 
			
		||||
  github/no-implicit-buggy-globals: [2]
 | 
			
		||||
  github/no-inner-html: [0]
 | 
			
		||||
  github/no-innerText: [2]
 | 
			
		||||
  github/no-then: [2]
 | 
			
		||||
  github/no-useless-passive: [2]
 | 
			
		||||
  github/prefer-observers: [2]
 | 
			
		||||
  github/require-passive-events: [2]
 | 
			
		||||
  github/unescaped-html-literal: [0]
 | 
			
		||||
  grouped-accessor-pairs: [2]
 | 
			
		||||
  guard-for-in: [0]
 | 
			
		||||
  id-blacklist: [0]
 | 
			
		||||
  id-length: [0]
 | 
			
		||||
  id-match: [0]
 | 
			
		||||
  import-x/consistent-type-specifier-style: [0]
 | 
			
		||||
  import-x/default: [0]
 | 
			
		||||
  import-x/dynamic-import-chunkname: [0]
 | 
			
		||||
  import-x/export: [2]
 | 
			
		||||
  import-x/exports-last: [0]
 | 
			
		||||
  import-x/extensions: [2, always, {ignorePackages: true}]
 | 
			
		||||
  import-x/first: [2]
 | 
			
		||||
  import-x/group-exports: [0]
 | 
			
		||||
  import-x/max-dependencies: [0]
 | 
			
		||||
  import-x/named: [2]
 | 
			
		||||
  import-x/namespace: [0]
 | 
			
		||||
  import-x/newline-after-import: [0]
 | 
			
		||||
  import-x/no-absolute-path: [0]
 | 
			
		||||
  import-x/no-amd: [2]
 | 
			
		||||
  import-x/no-anonymous-default-export: [0]
 | 
			
		||||
  import-x/no-commonjs: [2]
 | 
			
		||||
  import-x/no-cycle: [2, {ignoreExternal: true, maxDepth: 1}]
 | 
			
		||||
  import-x/no-default-export: [0]
 | 
			
		||||
  import-x/no-deprecated: [0]
 | 
			
		||||
  import-x/no-dynamic-require: [0]
 | 
			
		||||
  import-x/no-empty-named-blocks: [2]
 | 
			
		||||
  import-x/no-extraneous-dependencies: [2]
 | 
			
		||||
  import-x/no-import-module-exports: [0]
 | 
			
		||||
  import-x/no-internal-modules: [0]
 | 
			
		||||
  import-x/no-mutable-exports: [0]
 | 
			
		||||
  import-x/no-named-as-default-member: [0]
 | 
			
		||||
  import-x/no-named-as-default: [0]
 | 
			
		||||
  import-x/no-named-default: [0]
 | 
			
		||||
  import-x/no-named-export: [0]
 | 
			
		||||
  import-x/no-namespace: [0]
 | 
			
		||||
  import-x/no-nodejs-modules: [0]
 | 
			
		||||
  import-x/no-relative-packages: [0]
 | 
			
		||||
  import-x/no-relative-parent-imports: [0]
 | 
			
		||||
  import-x/no-restricted-paths: [0]
 | 
			
		||||
  import-x/no-self-import: [2]
 | 
			
		||||
  import-x/no-unassigned-import: [0]
 | 
			
		||||
  import-x/no-unresolved: [2, {commonjs: true, ignore: ["\\?.+$"]}]
 | 
			
		||||
  import-x/no-unused-modules: [2, {unusedExports: true}]
 | 
			
		||||
  import-x/no-useless-path-segments: [2, {commonjs: true}]
 | 
			
		||||
  import-x/no-webpack-loader-syntax: [2]
 | 
			
		||||
  import-x/order: [0]
 | 
			
		||||
  import-x/prefer-default-export: [0]
 | 
			
		||||
  import-x/unambiguous: [0]
 | 
			
		||||
  init-declarations: [0]
 | 
			
		||||
  line-comment-position: [0]
 | 
			
		||||
  logical-assignment-operators: [0]
 | 
			
		||||
  max-classes-per-file: [0]
 | 
			
		||||
  max-depth: [0]
 | 
			
		||||
  max-lines-per-function: [0]
 | 
			
		||||
  max-lines: [0]
 | 
			
		||||
  max-nested-callbacks: [0]
 | 
			
		||||
  max-params: [0]
 | 
			
		||||
  max-statements: [0]
 | 
			
		||||
  multiline-comment-style: [2, separate-lines]
 | 
			
		||||
  new-cap: [0]
 | 
			
		||||
  no-alert: [0]
 | 
			
		||||
  no-array-constructor: [0] # handled by @typescript-eslint/no-array-constructor
 | 
			
		||||
  no-async-promise-executor: [0]
 | 
			
		||||
  no-await-in-loop: [0]
 | 
			
		||||
  no-bitwise: [0]
 | 
			
		||||
  no-buffer-constructor: [0]
 | 
			
		||||
  no-caller: [2]
 | 
			
		||||
  no-case-declarations: [2]
 | 
			
		||||
  no-class-assign: [2]
 | 
			
		||||
  no-compare-neg-zero: [2]
 | 
			
		||||
  no-cond-assign: [2, except-parens]
 | 
			
		||||
  no-console: [1, {allow: [debug, info, warn, error]}]
 | 
			
		||||
  no-const-assign: [2]
 | 
			
		||||
  no-constant-binary-expression: [2]
 | 
			
		||||
  no-constant-condition: [0]
 | 
			
		||||
  no-constructor-return: [2]
 | 
			
		||||
  no-continue: [0]
 | 
			
		||||
  no-control-regex: [0]
 | 
			
		||||
  no-debugger: [1]
 | 
			
		||||
  no-delete-var: [2]
 | 
			
		||||
  no-div-regex: [0]
 | 
			
		||||
  no-dupe-args: [2]
 | 
			
		||||
  no-dupe-class-members: [2]
 | 
			
		||||
  no-dupe-else-if: [2]
 | 
			
		||||
  no-dupe-keys: [2]
 | 
			
		||||
  no-duplicate-case: [2]
 | 
			
		||||
  no-duplicate-imports: [0]
 | 
			
		||||
  no-else-return: [2]
 | 
			
		||||
  no-empty-character-class: [2]
 | 
			
		||||
  no-empty-function: [0]
 | 
			
		||||
  no-empty-pattern: [2]
 | 
			
		||||
  no-empty-static-block: [2]
 | 
			
		||||
  no-empty: [2, {allowEmptyCatch: true}]
 | 
			
		||||
  no-eq-null: [2]
 | 
			
		||||
  no-eval: [2]
 | 
			
		||||
  no-ex-assign: [2]
 | 
			
		||||
  no-extend-native: [2]
 | 
			
		||||
  no-extra-bind: [2]
 | 
			
		||||
  no-extra-boolean-cast: [2]
 | 
			
		||||
  no-extra-label: [0]
 | 
			
		||||
  no-fallthrough: [2]
 | 
			
		||||
  no-func-assign: [2]
 | 
			
		||||
  no-global-assign: [2]
 | 
			
		||||
  no-implicit-coercion: [2]
 | 
			
		||||
  no-implicit-globals: [0]
 | 
			
		||||
  no-implied-eval: [0] # handled by @typescript-eslint/no-implied-eval
 | 
			
		||||
  no-import-assign: [2]
 | 
			
		||||
  no-inline-comments: [0]
 | 
			
		||||
  no-inner-declarations: [2]
 | 
			
		||||
  no-invalid-regexp: [2]
 | 
			
		||||
  no-invalid-this: [0]
 | 
			
		||||
  no-irregular-whitespace: [2]
 | 
			
		||||
  no-iterator: [2]
 | 
			
		||||
  no-jquery/no-ajax-events: [2]
 | 
			
		||||
  no-jquery/no-ajax: [2]
 | 
			
		||||
  no-jquery/no-and-self: [2]
 | 
			
		||||
  no-jquery/no-animate-toggle: [2]
 | 
			
		||||
  no-jquery/no-animate: [2]
 | 
			
		||||
  no-jquery/no-append-html: [2]
 | 
			
		||||
  no-jquery/no-attr: [2]
 | 
			
		||||
  no-jquery/no-bind: [2]
 | 
			
		||||
  no-jquery/no-box-model: [2]
 | 
			
		||||
  no-jquery/no-browser: [2]
 | 
			
		||||
  no-jquery/no-camel-case: [2]
 | 
			
		||||
  no-jquery/no-class-state: [2]
 | 
			
		||||
  no-jquery/no-class: [0]
 | 
			
		||||
  no-jquery/no-clone: [2]
 | 
			
		||||
  no-jquery/no-closest: [0]
 | 
			
		||||
  no-jquery/no-constructor-attributes: [2]
 | 
			
		||||
  no-jquery/no-contains: [2]
 | 
			
		||||
  no-jquery/no-context-prop: [2]
 | 
			
		||||
  no-jquery/no-css: [2]
 | 
			
		||||
  no-jquery/no-data: [0]
 | 
			
		||||
  no-jquery/no-deferred: [2]
 | 
			
		||||
  no-jquery/no-delegate: [2]
 | 
			
		||||
  no-jquery/no-done-fail: [2]
 | 
			
		||||
  no-jquery/no-each-collection: [0]
 | 
			
		||||
  no-jquery/no-each-util: [0]
 | 
			
		||||
  no-jquery/no-each: [0]
 | 
			
		||||
  no-jquery/no-error-shorthand: [2]
 | 
			
		||||
  no-jquery/no-error: [2]
 | 
			
		||||
  no-jquery/no-escape-selector: [2]
 | 
			
		||||
  no-jquery/no-event-shorthand: [2]
 | 
			
		||||
  no-jquery/no-extend: [2]
 | 
			
		||||
  no-jquery/no-fade: [2]
 | 
			
		||||
  no-jquery/no-filter: [0]
 | 
			
		||||
  no-jquery/no-find-collection: [0]
 | 
			
		||||
  no-jquery/no-find-util: [2]
 | 
			
		||||
  no-jquery/no-find: [0]
 | 
			
		||||
  no-jquery/no-fx-interval: [2]
 | 
			
		||||
  no-jquery/no-fx: [2]
 | 
			
		||||
  no-jquery/no-global-eval: [2]
 | 
			
		||||
  no-jquery/no-global-selector: [0]
 | 
			
		||||
  no-jquery/no-grep: [2]
 | 
			
		||||
  no-jquery/no-has: [2]
 | 
			
		||||
  no-jquery/no-hold-ready: [2]
 | 
			
		||||
  no-jquery/no-html: [0]
 | 
			
		||||
  no-jquery/no-in-array: [2]
 | 
			
		||||
  no-jquery/no-is-array: [2]
 | 
			
		||||
  no-jquery/no-is-empty-object: [2]
 | 
			
		||||
  no-jquery/no-is-function: [2]
 | 
			
		||||
  no-jquery/no-is-numeric: [2]
 | 
			
		||||
  no-jquery/no-is-plain-object: [2]
 | 
			
		||||
  no-jquery/no-is-window: [2]
 | 
			
		||||
  no-jquery/no-is: [2]
 | 
			
		||||
  no-jquery/no-jquery-constructor: [0]
 | 
			
		||||
  no-jquery/no-live: [2]
 | 
			
		||||
  no-jquery/no-load-shorthand: [2]
 | 
			
		||||
  no-jquery/no-load: [2]
 | 
			
		||||
  no-jquery/no-map-collection: [0]
 | 
			
		||||
  no-jquery/no-map-util: [2]
 | 
			
		||||
  no-jquery/no-map: [2]
 | 
			
		||||
  no-jquery/no-merge: [2]
 | 
			
		||||
  no-jquery/no-node-name: [2]
 | 
			
		||||
  no-jquery/no-noop: [2]
 | 
			
		||||
  no-jquery/no-now: [2]
 | 
			
		||||
  no-jquery/no-on-ready: [2]
 | 
			
		||||
  no-jquery/no-other-methods: [0]
 | 
			
		||||
  no-jquery/no-other-utils: [2]
 | 
			
		||||
  no-jquery/no-param: [2]
 | 
			
		||||
  no-jquery/no-parent: [0]
 | 
			
		||||
  no-jquery/no-parents: [2]
 | 
			
		||||
  no-jquery/no-parse-html-literal: [2]
 | 
			
		||||
  no-jquery/no-parse-html: [2]
 | 
			
		||||
  no-jquery/no-parse-json: [2]
 | 
			
		||||
  no-jquery/no-parse-xml: [2]
 | 
			
		||||
  no-jquery/no-prop: [2]
 | 
			
		||||
  no-jquery/no-proxy: [2]
 | 
			
		||||
  no-jquery/no-ready-shorthand: [2]
 | 
			
		||||
  no-jquery/no-ready: [2]
 | 
			
		||||
  no-jquery/no-selector-prop: [2]
 | 
			
		||||
  no-jquery/no-serialize: [2]
 | 
			
		||||
  no-jquery/no-size: [2]
 | 
			
		||||
  no-jquery/no-sizzle: [2]
 | 
			
		||||
  no-jquery/no-slide: [2]
 | 
			
		||||
  no-jquery/no-sub: [2]
 | 
			
		||||
  no-jquery/no-support: [2]
 | 
			
		||||
  no-jquery/no-text: [2]
 | 
			
		||||
  no-jquery/no-trigger: [0]
 | 
			
		||||
  no-jquery/no-trim: [2]
 | 
			
		||||
  no-jquery/no-type: [2]
 | 
			
		||||
  no-jquery/no-unique: [2]
 | 
			
		||||
  no-jquery/no-unload-shorthand: [2]
 | 
			
		||||
  no-jquery/no-val: [0]
 | 
			
		||||
  no-jquery/no-visibility: [2]
 | 
			
		||||
  no-jquery/no-when: [2]
 | 
			
		||||
  no-jquery/no-wrap: [2]
 | 
			
		||||
  no-jquery/variable-pattern: [2]
 | 
			
		||||
  no-label-var: [2]
 | 
			
		||||
  no-labels: [0] # handled by no-restricted-syntax
 | 
			
		||||
  no-lone-blocks: [2]
 | 
			
		||||
  no-lonely-if: [0]
 | 
			
		||||
  no-loop-func: [0]
 | 
			
		||||
  no-loss-of-precision: [2]
 | 
			
		||||
  no-magic-numbers: [0]
 | 
			
		||||
  no-misleading-character-class: [2]
 | 
			
		||||
  no-multi-assign: [0]
 | 
			
		||||
  no-multi-str: [2]
 | 
			
		||||
  no-negated-condition: [0]
 | 
			
		||||
  no-nested-ternary: [0]
 | 
			
		||||
  no-new-func: [2]
 | 
			
		||||
  no-new-native-nonconstructor: [2]
 | 
			
		||||
  no-new-object: [2]
 | 
			
		||||
  no-new-symbol: [2]
 | 
			
		||||
  no-new-wrappers: [2]
 | 
			
		||||
  no-new: [0]
 | 
			
		||||
  no-nonoctal-decimal-escape: [2]
 | 
			
		||||
  no-obj-calls: [2]
 | 
			
		||||
  no-octal-escape: [2]
 | 
			
		||||
  no-octal: [2]
 | 
			
		||||
  no-param-reassign: [0]
 | 
			
		||||
  no-plusplus: [0]
 | 
			
		||||
  no-promise-executor-return: [0]
 | 
			
		||||
  no-proto: [2]
 | 
			
		||||
  no-prototype-builtins: [2]
 | 
			
		||||
  no-redeclare: [0] # must be disabled for typescript overloads
 | 
			
		||||
  no-regex-spaces: [2]
 | 
			
		||||
  no-restricted-exports: [0]
 | 
			
		||||
  no-restricted-globals: [2, addEventListener, blur, close, closed, confirm, defaultStatus, defaultstatus, error, event, external, find, focus, frameElement, frames, history, innerHeight, innerWidth, isFinite, isNaN, length, location, locationbar, menubar, moveBy, moveTo, name, onblur, onerror, onfocus, onload, onresize, onunload, open, opener, opera, outerHeight, outerWidth, pageXOffset, pageYOffset, parent, print, removeEventListener, resizeBy, resizeTo, screen, screenLeft, screenTop, screenX, screenY, scroll, scrollbars, scrollBy, scrollTo, scrollX, scrollY, self, status, statusbar, stop, toolbar, top, __dirname, __filename]
 | 
			
		||||
  no-restricted-imports: [0]
 | 
			
		||||
  no-restricted-syntax: [2, WithStatement, ForInStatement, LabeledStatement, SequenceExpression, {selector: "CallExpression[callee.name='fetch']", message: "use modules/fetch.ts instead"}]
 | 
			
		||||
  no-return-assign: [0]
 | 
			
		||||
  no-script-url: [2]
 | 
			
		||||
  no-self-assign: [2, {props: true}]
 | 
			
		||||
  no-self-compare: [2]
 | 
			
		||||
  no-sequences: [2]
 | 
			
		||||
  no-setter-return: [2]
 | 
			
		||||
  no-shadow-restricted-names: [2]
 | 
			
		||||
  no-shadow: [0]
 | 
			
		||||
  no-sparse-arrays: [2]
 | 
			
		||||
  no-template-curly-in-string: [2]
 | 
			
		||||
  no-ternary: [0]
 | 
			
		||||
  no-this-before-super: [2]
 | 
			
		||||
  no-throw-literal: [2]
 | 
			
		||||
  no-undef-init: [2]
 | 
			
		||||
  no-undef: [2, {typeof: true}] # TODO: disable this rule after tsc passes
 | 
			
		||||
  no-undefined: [0]
 | 
			
		||||
  no-underscore-dangle: [0]
 | 
			
		||||
  no-unexpected-multiline: [2]
 | 
			
		||||
  no-unmodified-loop-condition: [2]
 | 
			
		||||
  no-unneeded-ternary: [2]
 | 
			
		||||
  no-unreachable-loop: [2]
 | 
			
		||||
  no-unreachable: [2]
 | 
			
		||||
  no-unsafe-finally: [2]
 | 
			
		||||
  no-unsafe-negation: [2]
 | 
			
		||||
  no-unused-expressions: [2]
 | 
			
		||||
  no-unused-labels: [2]
 | 
			
		||||
  no-unused-private-class-members: [2]
 | 
			
		||||
  no-unused-vars: [0] # handled by @typescript-eslint/no-unused-vars
 | 
			
		||||
  no-use-before-define: [2, {functions: false, classes: true, variables: true, allowNamedExports: true}]
 | 
			
		||||
  no-use-extend-native/no-use-extend-native: [2]
 | 
			
		||||
  no-useless-backreference: [2]
 | 
			
		||||
  no-useless-call: [2]
 | 
			
		||||
  no-useless-catch: [2]
 | 
			
		||||
  no-useless-computed-key: [2]
 | 
			
		||||
  no-useless-concat: [2]
 | 
			
		||||
  no-useless-constructor: [2]
 | 
			
		||||
  no-useless-escape: [2]
 | 
			
		||||
  no-useless-rename: [2]
 | 
			
		||||
  no-useless-return: [2]
 | 
			
		||||
  no-var: [2]
 | 
			
		||||
  no-void: [2]
 | 
			
		||||
  no-warning-comments: [0]
 | 
			
		||||
  no-with: [0] # handled by no-restricted-syntax
 | 
			
		||||
  object-shorthand: [2, always]
 | 
			
		||||
  one-var-declaration-per-line: [0]
 | 
			
		||||
  one-var: [0]
 | 
			
		||||
  operator-assignment: [2, always]
 | 
			
		||||
  operator-linebreak: [2, after]
 | 
			
		||||
  prefer-arrow-callback: [2, {allowNamedFunctions: true, allowUnboundThis: true}]
 | 
			
		||||
  prefer-const: [2, {destructuring: all, ignoreReadBeforeAssign: true}]
 | 
			
		||||
  prefer-destructuring: [0]
 | 
			
		||||
  prefer-exponentiation-operator: [2]
 | 
			
		||||
  prefer-named-capture-group: [0]
 | 
			
		||||
  prefer-numeric-literals: [2]
 | 
			
		||||
  prefer-object-has-own: [2]
 | 
			
		||||
  prefer-object-spread: [2]
 | 
			
		||||
  prefer-promise-reject-errors: [2, {allowEmptyReject: false}]
 | 
			
		||||
  prefer-regex-literals: [2]
 | 
			
		||||
  prefer-rest-params: [2]
 | 
			
		||||
  prefer-spread: [2]
 | 
			
		||||
  prefer-template: [2]
 | 
			
		||||
  radix: [2, as-needed]
 | 
			
		||||
  regexp/confusing-quantifier: [2]
 | 
			
		||||
  regexp/control-character-escape: [2]
 | 
			
		||||
  regexp/hexadecimal-escape: [0]
 | 
			
		||||
  regexp/letter-case: [0]
 | 
			
		||||
  regexp/match-any: [2]
 | 
			
		||||
  regexp/negation: [2]
 | 
			
		||||
  regexp/no-contradiction-with-assertion: [0]
 | 
			
		||||
  regexp/no-control-character: [0]
 | 
			
		||||
  regexp/no-dupe-characters-character-class: [2]
 | 
			
		||||
  regexp/no-dupe-disjunctions: [2]
 | 
			
		||||
  regexp/no-empty-alternative: [2]
 | 
			
		||||
  regexp/no-empty-capturing-group: [2]
 | 
			
		||||
  regexp/no-empty-character-class: [0]
 | 
			
		||||
  regexp/no-empty-group: [2]
 | 
			
		||||
  regexp/no-empty-lookarounds-assertion: [2]
 | 
			
		||||
  regexp/no-empty-string-literal: [2]
 | 
			
		||||
  regexp/no-escape-backspace: [2]
 | 
			
		||||
  regexp/no-extra-lookaround-assertions: [0]
 | 
			
		||||
  regexp/no-invalid-regexp: [2]
 | 
			
		||||
  regexp/no-invisible-character: [2]
 | 
			
		||||
  regexp/no-lazy-ends: [2]
 | 
			
		||||
  regexp/no-legacy-features: [2]
 | 
			
		||||
  regexp/no-misleading-capturing-group: [0]
 | 
			
		||||
  regexp/no-misleading-unicode-character: [0]
 | 
			
		||||
  regexp/no-missing-g-flag: [2]
 | 
			
		||||
  regexp/no-non-standard-flag: [2]
 | 
			
		||||
  regexp/no-obscure-range: [2]
 | 
			
		||||
  regexp/no-octal: [2]
 | 
			
		||||
  regexp/no-optional-assertion: [2]
 | 
			
		||||
  regexp/no-potentially-useless-backreference: [2]
 | 
			
		||||
  regexp/no-standalone-backslash: [2]
 | 
			
		||||
  regexp/no-super-linear-backtracking: [0]
 | 
			
		||||
  regexp/no-super-linear-move: [0]
 | 
			
		||||
  regexp/no-trivially-nested-assertion: [2]
 | 
			
		||||
  regexp/no-trivially-nested-quantifier: [2]
 | 
			
		||||
  regexp/no-unused-capturing-group: [0]
 | 
			
		||||
  regexp/no-useless-assertions: [2]
 | 
			
		||||
  regexp/no-useless-backreference: [2]
 | 
			
		||||
  regexp/no-useless-character-class: [2]
 | 
			
		||||
  regexp/no-useless-dollar-replacements: [2]
 | 
			
		||||
  regexp/no-useless-escape: [2]
 | 
			
		||||
  regexp/no-useless-flag: [2]
 | 
			
		||||
  regexp/no-useless-lazy: [2]
 | 
			
		||||
  regexp/no-useless-non-capturing-group: [2]
 | 
			
		||||
  regexp/no-useless-quantifier: [2]
 | 
			
		||||
  regexp/no-useless-range: [2]
 | 
			
		||||
  regexp/no-useless-set-operand: [2]
 | 
			
		||||
  regexp/no-useless-string-literal: [2]
 | 
			
		||||
  regexp/no-useless-two-nums-quantifier: [2]
 | 
			
		||||
  regexp/no-zero-quantifier: [2]
 | 
			
		||||
  regexp/optimal-lookaround-quantifier: [2]
 | 
			
		||||
  regexp/optimal-quantifier-concatenation: [0]
 | 
			
		||||
  regexp/prefer-character-class: [0]
 | 
			
		||||
  regexp/prefer-d: [0]
 | 
			
		||||
  regexp/prefer-escape-replacement-dollar-char: [0]
 | 
			
		||||
  regexp/prefer-lookaround: [0]
 | 
			
		||||
  regexp/prefer-named-backreference: [0]
 | 
			
		||||
  regexp/prefer-named-capture-group: [0]
 | 
			
		||||
  regexp/prefer-named-replacement: [0]
 | 
			
		||||
  regexp/prefer-plus-quantifier: [2]
 | 
			
		||||
  regexp/prefer-predefined-assertion: [2]
 | 
			
		||||
  regexp/prefer-quantifier: [0]
 | 
			
		||||
  regexp/prefer-question-quantifier: [2]
 | 
			
		||||
  regexp/prefer-range: [2]
 | 
			
		||||
  regexp/prefer-regexp-exec: [2]
 | 
			
		||||
  regexp/prefer-regexp-test: [2]
 | 
			
		||||
  regexp/prefer-result-array-groups: [0]
 | 
			
		||||
  regexp/prefer-set-operation: [2]
 | 
			
		||||
  regexp/prefer-star-quantifier: [2]
 | 
			
		||||
  regexp/prefer-unicode-codepoint-escapes: [2]
 | 
			
		||||
  regexp/prefer-w: [0]
 | 
			
		||||
  regexp/require-unicode-regexp: [0]
 | 
			
		||||
  regexp/simplify-set-operations: [2]
 | 
			
		||||
  regexp/sort-alternatives: [0]
 | 
			
		||||
  regexp/sort-character-class-elements: [0]
 | 
			
		||||
  regexp/sort-flags: [0]
 | 
			
		||||
  regexp/strict: [2]
 | 
			
		||||
  regexp/unicode-escape: [0]
 | 
			
		||||
  regexp/use-ignore-case: [0]
 | 
			
		||||
  require-atomic-updates: [0]
 | 
			
		||||
  require-await: [0] # handled by @typescript-eslint/require-await
 | 
			
		||||
  require-unicode-regexp: [0]
 | 
			
		||||
  require-yield: [2]
 | 
			
		||||
  sonarjs/cognitive-complexity: [0]
 | 
			
		||||
  sonarjs/elseif-without-else: [0]
 | 
			
		||||
  sonarjs/max-switch-cases: [0]
 | 
			
		||||
  sonarjs/no-all-duplicated-branches: [2]
 | 
			
		||||
  sonarjs/no-collapsible-if: [0]
 | 
			
		||||
  sonarjs/no-collection-size-mischeck: [2]
 | 
			
		||||
  sonarjs/no-duplicate-string: [0]
 | 
			
		||||
  sonarjs/no-duplicated-branches: [0]
 | 
			
		||||
  sonarjs/no-element-overwrite: [2]
 | 
			
		||||
  sonarjs/no-empty-collection: [2]
 | 
			
		||||
  sonarjs/no-extra-arguments: [2]
 | 
			
		||||
  sonarjs/no-gratuitous-expressions: [2]
 | 
			
		||||
  sonarjs/no-identical-conditions: [2]
 | 
			
		||||
  sonarjs/no-identical-expressions: [2]
 | 
			
		||||
  sonarjs/no-identical-functions: [2, 5]
 | 
			
		||||
  sonarjs/no-ignored-return: [2]
 | 
			
		||||
  sonarjs/no-inverted-boolean-check: [2]
 | 
			
		||||
  sonarjs/no-nested-switch: [0]
 | 
			
		||||
  sonarjs/no-nested-template-literals: [0]
 | 
			
		||||
  sonarjs/no-one-iteration-loop: [2]
 | 
			
		||||
  sonarjs/no-redundant-boolean: [2]
 | 
			
		||||
  sonarjs/no-redundant-jump: [2]
 | 
			
		||||
  sonarjs/no-same-line-conditional: [2]
 | 
			
		||||
  sonarjs/no-small-switch: [0]
 | 
			
		||||
  sonarjs/no-unused-collection: [2]
 | 
			
		||||
  sonarjs/no-use-of-empty-return-value: [2]
 | 
			
		||||
  sonarjs/no-useless-catch: [2]
 | 
			
		||||
  sonarjs/non-existent-operator: [2]
 | 
			
		||||
  sonarjs/prefer-immediate-return: [0]
 | 
			
		||||
  sonarjs/prefer-object-literal: [0]
 | 
			
		||||
  sonarjs/prefer-single-boolean-return: [0]
 | 
			
		||||
  sonarjs/prefer-while: [2]
 | 
			
		||||
  sort-imports: [0]
 | 
			
		||||
  sort-keys: [0]
 | 
			
		||||
  sort-vars: [0]
 | 
			
		||||
  strict: [0]
 | 
			
		||||
  symbol-description: [2]
 | 
			
		||||
  unicode-bom: [2, never]
 | 
			
		||||
  unicorn/better-regex: [0]
 | 
			
		||||
  unicorn/catch-error-name: [0]
 | 
			
		||||
  unicorn/consistent-destructuring: [2]
 | 
			
		||||
  unicorn/consistent-empty-array-spread: [2]
 | 
			
		||||
  unicorn/consistent-existence-index-check: [0]
 | 
			
		||||
  unicorn/consistent-function-scoping: [0]
 | 
			
		||||
  unicorn/custom-error-definition: [0]
 | 
			
		||||
  unicorn/empty-brace-spaces: [2]
 | 
			
		||||
  unicorn/error-message: [0]
 | 
			
		||||
  unicorn/escape-case: [0]
 | 
			
		||||
  unicorn/expiring-todo-comments: [0]
 | 
			
		||||
  unicorn/explicit-length-check: [0]
 | 
			
		||||
  unicorn/filename-case: [0]
 | 
			
		||||
  unicorn/import-index: [0]
 | 
			
		||||
  unicorn/import-style: [0]
 | 
			
		||||
  unicorn/new-for-builtins: [2]
 | 
			
		||||
  unicorn/no-abusive-eslint-disable: [0]
 | 
			
		||||
  unicorn/no-anonymous-default-export: [0]
 | 
			
		||||
  unicorn/no-array-callback-reference: [0]
 | 
			
		||||
  unicorn/no-array-for-each: [2]
 | 
			
		||||
  unicorn/no-array-method-this-argument: [2]
 | 
			
		||||
  unicorn/no-array-push-push: [2]
 | 
			
		||||
  unicorn/no-array-reduce: [2]
 | 
			
		||||
  unicorn/no-await-expression-member: [0]
 | 
			
		||||
  unicorn/no-await-in-promise-methods: [2]
 | 
			
		||||
  unicorn/no-console-spaces: [0]
 | 
			
		||||
  unicorn/no-document-cookie: [2]
 | 
			
		||||
  unicorn/no-empty-file: [2]
 | 
			
		||||
  unicorn/no-for-loop: [0]
 | 
			
		||||
  unicorn/no-hex-escape: [0]
 | 
			
		||||
  unicorn/no-instanceof-array: [0]
 | 
			
		||||
  unicorn/no-invalid-fetch-options: [2]
 | 
			
		||||
  unicorn/no-invalid-remove-event-listener: [2]
 | 
			
		||||
  unicorn/no-keyword-prefix: [0]
 | 
			
		||||
  unicorn/no-length-as-slice-end: [2]
 | 
			
		||||
  unicorn/no-lonely-if: [2]
 | 
			
		||||
  unicorn/no-magic-array-flat-depth: [0]
 | 
			
		||||
  unicorn/no-negated-condition: [0]
 | 
			
		||||
  unicorn/no-negation-in-equality-check: [2]
 | 
			
		||||
  unicorn/no-nested-ternary: [0]
 | 
			
		||||
  unicorn/no-new-array: [0]
 | 
			
		||||
  unicorn/no-new-buffer: [0]
 | 
			
		||||
  unicorn/no-null: [0]
 | 
			
		||||
  unicorn/no-object-as-default-parameter: [0]
 | 
			
		||||
  unicorn/no-process-exit: [0]
 | 
			
		||||
  unicorn/no-single-promise-in-promise-methods: [2]
 | 
			
		||||
  unicorn/no-static-only-class: [2]
 | 
			
		||||
  unicorn/no-thenable: [2]
 | 
			
		||||
  unicorn/no-this-assignment: [2]
 | 
			
		||||
  unicorn/no-typeof-undefined: [2]
 | 
			
		||||
  unicorn/no-unnecessary-await: [2]
 | 
			
		||||
  unicorn/no-unnecessary-polyfills: [2]
 | 
			
		||||
  unicorn/no-unreadable-array-destructuring: [0]
 | 
			
		||||
  unicorn/no-unreadable-iife: [2]
 | 
			
		||||
  unicorn/no-unused-properties: [2]
 | 
			
		||||
  unicorn/no-useless-fallback-in-spread: [2]
 | 
			
		||||
  unicorn/no-useless-length-check: [2]
 | 
			
		||||
  unicorn/no-useless-promise-resolve-reject: [2]
 | 
			
		||||
  unicorn/no-useless-spread: [2]
 | 
			
		||||
  unicorn/no-useless-switch-case: [2]
 | 
			
		||||
  unicorn/no-useless-undefined: [0]
 | 
			
		||||
  unicorn/no-zero-fractions: [2]
 | 
			
		||||
  unicorn/number-literal-case: [0]
 | 
			
		||||
  unicorn/numeric-separators-style: [0]
 | 
			
		||||
  unicorn/prefer-add-event-listener: [2]
 | 
			
		||||
  unicorn/prefer-array-find: [2]
 | 
			
		||||
  unicorn/prefer-array-flat-map: [2]
 | 
			
		||||
  unicorn/prefer-array-flat: [2]
 | 
			
		||||
  unicorn/prefer-array-index-of: [2]
 | 
			
		||||
  unicorn/prefer-array-some: [2]
 | 
			
		||||
  unicorn/prefer-at: [0]
 | 
			
		||||
  unicorn/prefer-blob-reading-methods: [2]
 | 
			
		||||
  unicorn/prefer-code-point: [0]
 | 
			
		||||
  unicorn/prefer-date-now: [2]
 | 
			
		||||
  unicorn/prefer-default-parameters: [0]
 | 
			
		||||
  unicorn/prefer-dom-node-append: [2]
 | 
			
		||||
  unicorn/prefer-dom-node-dataset: [0]
 | 
			
		||||
  unicorn/prefer-dom-node-remove: [2]
 | 
			
		||||
  unicorn/prefer-dom-node-text-content: [2]
 | 
			
		||||
  unicorn/prefer-event-target: [2]
 | 
			
		||||
  unicorn/prefer-export-from: [0]
 | 
			
		||||
  unicorn/prefer-global-this: [0]
 | 
			
		||||
  unicorn/prefer-includes: [2]
 | 
			
		||||
  unicorn/prefer-json-parse-buffer: [0]
 | 
			
		||||
  unicorn/prefer-keyboard-event-key: [2]
 | 
			
		||||
  unicorn/prefer-logical-operator-over-ternary: [2]
 | 
			
		||||
  unicorn/prefer-math-min-max: [2]
 | 
			
		||||
  unicorn/prefer-math-trunc: [2]
 | 
			
		||||
  unicorn/prefer-modern-dom-apis: [0]
 | 
			
		||||
  unicorn/prefer-modern-math-apis: [2]
 | 
			
		||||
  unicorn/prefer-module: [2]
 | 
			
		||||
  unicorn/prefer-native-coercion-functions: [2]
 | 
			
		||||
  unicorn/prefer-negative-index: [2]
 | 
			
		||||
  unicorn/prefer-node-protocol: [2]
 | 
			
		||||
  unicorn/prefer-number-properties: [0]
 | 
			
		||||
  unicorn/prefer-object-from-entries: [2]
 | 
			
		||||
  unicorn/prefer-object-has-own: [0]
 | 
			
		||||
  unicorn/prefer-optional-catch-binding: [2]
 | 
			
		||||
  unicorn/prefer-prototype-methods: [0]
 | 
			
		||||
  unicorn/prefer-query-selector: [2]
 | 
			
		||||
  unicorn/prefer-reflect-apply: [0]
 | 
			
		||||
  unicorn/prefer-regexp-test: [2]
 | 
			
		||||
  unicorn/prefer-set-has: [0]
 | 
			
		||||
  unicorn/prefer-set-size: [2]
 | 
			
		||||
  unicorn/prefer-spread: [0]
 | 
			
		||||
  unicorn/prefer-string-raw: [0]
 | 
			
		||||
  unicorn/prefer-string-replace-all: [0]
 | 
			
		||||
  unicorn/prefer-string-slice: [0]
 | 
			
		||||
  unicorn/prefer-string-starts-ends-with: [2]
 | 
			
		||||
  unicorn/prefer-string-trim-start-end: [2]
 | 
			
		||||
  unicorn/prefer-structured-clone: [2]
 | 
			
		||||
  unicorn/prefer-switch: [0]
 | 
			
		||||
  unicorn/prefer-ternary: [0]
 | 
			
		||||
  unicorn/prefer-text-content: [2]
 | 
			
		||||
  unicorn/prefer-top-level-await: [0]
 | 
			
		||||
  unicorn/prefer-type-error: [0]
 | 
			
		||||
  unicorn/prevent-abbreviations: [0]
 | 
			
		||||
  unicorn/relative-url-style: [2]
 | 
			
		||||
  unicorn/require-array-join-separator: [2]
 | 
			
		||||
  unicorn/require-number-to-fixed-digits-argument: [2]
 | 
			
		||||
  unicorn/require-post-message-target-origin: [0]
 | 
			
		||||
  unicorn/string-content: [0]
 | 
			
		||||
  unicorn/switch-case-braces: [0]
 | 
			
		||||
  unicorn/template-indent: [2]
 | 
			
		||||
  unicorn/text-encoding-identifier-case: [0]
 | 
			
		||||
  unicorn/throw-new-error: [2]
 | 
			
		||||
  use-isnan: [2]
 | 
			
		||||
  valid-typeof: [2, {requireStringLiterals: true}]
 | 
			
		||||
  vars-on-top: [0]
 | 
			
		||||
  wc/attach-shadow-constructor: [2]
 | 
			
		||||
  wc/define-tag-after-class-definition: [0]
 | 
			
		||||
  wc/expose-class-on-global: [0]
 | 
			
		||||
  wc/file-name-matches-element: [2]
 | 
			
		||||
  wc/guard-define-call: [0]
 | 
			
		||||
  wc/guard-super-call: [2]
 | 
			
		||||
  wc/max-elements-per-file: [0]
 | 
			
		||||
  wc/no-child-traversal-in-attributechangedcallback: [2]
 | 
			
		||||
  wc/no-child-traversal-in-connectedcallback: [2]
 | 
			
		||||
  wc/no-closed-shadow-root: [2]
 | 
			
		||||
  wc/no-constructor-attributes: [2]
 | 
			
		||||
  wc/no-constructor-params: [2]
 | 
			
		||||
  wc/no-constructor: [2]
 | 
			
		||||
  wc/no-customized-built-in-elements: [2]
 | 
			
		||||
  wc/no-exports-with-element: [0]
 | 
			
		||||
  wc/no-invalid-element-name: [2]
 | 
			
		||||
  wc/no-invalid-extends: [2]
 | 
			
		||||
  wc/no-method-prefixed-with-on: [2]
 | 
			
		||||
  wc/no-self-class: [2]
 | 
			
		||||
  wc/no-typos: [2]
 | 
			
		||||
  wc/require-listener-teardown: [2]
 | 
			
		||||
  wc/tag-name-matches-class: [2]
 | 
			
		||||
  yoda: [2, never]
 | 
			
		||||
							
								
								
									
										3
									
								
								.gitattributes
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitattributes
									
									
									
									
										vendored
									
									
								
							@@ -4,7 +4,8 @@
 | 
			
		||||
/assets/*.json linguist-generated
 | 
			
		||||
/public/assets/img/svg/*.svg linguist-generated
 | 
			
		||||
/templates/swagger/v1_json.tmpl linguist-generated
 | 
			
		||||
/options/fileicon/** linguist-generated
 | 
			
		||||
/vendor/** -text -eol linguist-vendored
 | 
			
		||||
/web_src/fomantic/build/** linguist-generated
 | 
			
		||||
/web_src/fomantic/_site/globals/site.variables linguist-language=Less
 | 
			
		||||
/web_src/js/vendor/** -text -eol linguist-vendored
 | 
			
		||||
Dockerfile.* linguist-language=Dockerfile
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								.github/ISSUE_TEMPLATE/config.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/ISSUE_TEMPLATE/config.yml
									
									
									
									
										vendored
									
									
								
							@@ -13,5 +13,5 @@ contact_links:
 | 
			
		||||
    url: https://docs.gitea.com/help/faq
 | 
			
		||||
    about: Please check if your question isn't mentioned here.
 | 
			
		||||
  - name: Crowdin Translations
 | 
			
		||||
    url: https://translate.gitea.com
 | 
			
		||||
    url: https://crowdin.com/project/gitea
 | 
			
		||||
    about: Translations are managed here.
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										24
									
								
								.github/labeler.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										24
									
								
								.github/labeler.yml
									
									
									
									
										vendored
									
									
								
							@@ -41,7 +41,7 @@ modifies/internal:
 | 
			
		||||
          - ".dockerignore"
 | 
			
		||||
          - "docker/**"
 | 
			
		||||
          - ".editorconfig"
 | 
			
		||||
          - ".eslintrc.cjs"
 | 
			
		||||
          - ".eslintrc.yaml"
 | 
			
		||||
          - ".golangci.yml"
 | 
			
		||||
          - ".gitpod.yml"
 | 
			
		||||
          - ".markdownlint.yaml"
 | 
			
		||||
@@ -49,7 +49,7 @@ modifies/internal:
 | 
			
		||||
          - "stylelint.config.js"
 | 
			
		||||
          - ".yamllint.yaml"
 | 
			
		||||
          - ".github/**"
 | 
			
		||||
          - ".gitea/**"
 | 
			
		||||
          - ".gitea/"
 | 
			
		||||
          - ".devcontainer/**"
 | 
			
		||||
          - "build.go"
 | 
			
		||||
          - "build/**"
 | 
			
		||||
@@ -59,9 +59,9 @@ modifies/dependencies:
 | 
			
		||||
  - changed-files:
 | 
			
		||||
      - any-glob-to-any-file:
 | 
			
		||||
          - "package.json"
 | 
			
		||||
          - "pnpm-lock.yaml"
 | 
			
		||||
          - "package-lock.json"
 | 
			
		||||
          - "pyproject.toml"
 | 
			
		||||
          - "uv.lock"
 | 
			
		||||
          - "poetry.lock"
 | 
			
		||||
          - "go.mod"
 | 
			
		||||
          - "go.sum"
 | 
			
		||||
 | 
			
		||||
@@ -73,21 +73,11 @@ modifies/go:
 | 
			
		||||
modifies/frontend:
 | 
			
		||||
  - changed-files:
 | 
			
		||||
      - any-glob-to-any-file:
 | 
			
		||||
          - "*.js"
 | 
			
		||||
          - "*.ts"
 | 
			
		||||
          - "web_src/**"
 | 
			
		||||
          - "**/*.js"
 | 
			
		||||
          - "**/*.ts"
 | 
			
		||||
          - "**/*.vue"
 | 
			
		||||
 | 
			
		||||
docs-update-needed:
 | 
			
		||||
  - changed-files:
 | 
			
		||||
      - any-glob-to-any-file:
 | 
			
		||||
          - "custom/conf/app.example.ini"
 | 
			
		||||
 | 
			
		||||
topic/code-linting:
 | 
			
		||||
  - changed-files:
 | 
			
		||||
      - any-glob-to-any-file:
 | 
			
		||||
          - ".eslintrc.cjs"
 | 
			
		||||
          - ".golangci.yml"
 | 
			
		||||
          - ".markdownlint.yaml"
 | 
			
		||||
          - ".spectral.yaml"
 | 
			
		||||
          - ".yamllint.yaml"
 | 
			
		||||
          - "stylelint.config.js"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										10
									
								
								.github/workflows/cron-licenses.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										10
									
								
								.github/workflows/cron-licenses.yml
									
									
									
									
										vendored
									
									
								
							@@ -1,8 +1,8 @@
 | 
			
		||||
name: cron-licenses
 | 
			
		||||
 | 
			
		||||
on:
 | 
			
		||||
  # schedule:
 | 
			
		||||
  #   - cron: "7 0 * * 1" # every Monday at 00:07 UTC
 | 
			
		||||
  schedule:
 | 
			
		||||
    - cron: "7 0 * * 1" # every Monday at 00:07 UTC
 | 
			
		||||
  workflow_dispatch:
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
@@ -10,12 +10,12 @@ jobs:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    if: github.repository == 'go-gitea/gitea'
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v5
 | 
			
		||||
      - uses: actions/setup-go@v6
 | 
			
		||||
      - uses: actions/checkout@v4
 | 
			
		||||
      - uses: actions/setup-go@v5
 | 
			
		||||
        with:
 | 
			
		||||
          go-version-file: go.mod
 | 
			
		||||
          check-latest: true
 | 
			
		||||
      - run: make generate-gitignore
 | 
			
		||||
      - run: make generate-license generate-gitignore
 | 
			
		||||
        timeout-minutes: 40
 | 
			
		||||
      - name: push translations to repo
 | 
			
		||||
        uses: appleboy/git-push-action@v0.0.3
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								.github/workflows/cron-translations.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/cron-translations.yml
									
									
									
									
										vendored
									
									
								
							@@ -10,7 +10,7 @@ jobs:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    if: github.repository == 'go-gitea/gitea'
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v5
 | 
			
		||||
      - uses: actions/checkout@v4
 | 
			
		||||
      - uses: crowdin/github-action@v1
 | 
			
		||||
        with:
 | 
			
		||||
          upload_sources: true
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										20
									
								
								.github/workflows/files-changed.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										20
									
								
								.github/workflows/files-changed.yml
									
									
									
									
										vendored
									
									
								
							@@ -34,7 +34,7 @@ jobs:
 | 
			
		||||
      swagger: ${{ steps.changes.outputs.swagger }}
 | 
			
		||||
      yaml: ${{ steps.changes.outputs.yaml }}
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v5
 | 
			
		||||
      - uses: actions/checkout@v4
 | 
			
		||||
      - uses: dorny/paths-filter@v3
 | 
			
		||||
        id: changes
 | 
			
		||||
        with:
 | 
			
		||||
@@ -51,23 +51,21 @@ jobs:
 | 
			
		||||
              - "options/locale/locale_en-US.ini"
 | 
			
		||||
 | 
			
		||||
            frontend:
 | 
			
		||||
              - "*.js"
 | 
			
		||||
              - "*.ts"
 | 
			
		||||
              - "**/*.js"
 | 
			
		||||
              - "web_src/**"
 | 
			
		||||
              - "tools/*.js"
 | 
			
		||||
              - "tools/*.ts"
 | 
			
		||||
              - "assets/emoji.json"
 | 
			
		||||
              - "package.json"
 | 
			
		||||
              - "pnpm-lock.yaml"
 | 
			
		||||
              - "package-lock.json"
 | 
			
		||||
              - "Makefile"
 | 
			
		||||
              - ".eslintrc.cjs"
 | 
			
		||||
              - ".eslintrc.yaml"
 | 
			
		||||
              - "stylelint.config.js"
 | 
			
		||||
              - ".npmrc"
 | 
			
		||||
 | 
			
		||||
            docs:
 | 
			
		||||
              - "**/*.md"
 | 
			
		||||
              - ".markdownlint.yaml"
 | 
			
		||||
              - "package.json"
 | 
			
		||||
              - "pnpm-lock.yaml"
 | 
			
		||||
              - "package-lock.json"
 | 
			
		||||
 | 
			
		||||
            actions:
 | 
			
		||||
              - ".github/workflows/*"
 | 
			
		||||
@@ -77,7 +75,7 @@ jobs:
 | 
			
		||||
              - "tools/lint-templates-*.js"
 | 
			
		||||
              - "templates/**/*.tmpl"
 | 
			
		||||
              - "pyproject.toml"
 | 
			
		||||
              - "uv.lock"
 | 
			
		||||
              - "poetry.lock"
 | 
			
		||||
 | 
			
		||||
            docker:
 | 
			
		||||
              - "Dockerfile"
 | 
			
		||||
@@ -87,10 +85,9 @@ jobs:
 | 
			
		||||
 | 
			
		||||
            swagger:
 | 
			
		||||
              - "templates/swagger/v1_json.tmpl"
 | 
			
		||||
              - "templates/swagger/v1_input.json"
 | 
			
		||||
              - "Makefile"
 | 
			
		||||
              - "package.json"
 | 
			
		||||
              - "pnpm-lock.yaml"
 | 
			
		||||
              - "package-lock.json"
 | 
			
		||||
              - ".spectral.yaml"
 | 
			
		||||
 | 
			
		||||
            yaml:
 | 
			
		||||
@@ -98,3 +95,4 @@ jobs:
 | 
			
		||||
              - "**/*.yaml"
 | 
			
		||||
              - ".yamllint.yaml"
 | 
			
		||||
              - "pyproject.toml"
 | 
			
		||||
              - "poetry.lock"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										80
									
								
								.github/workflows/pull-compliance.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										80
									
								
								.github/workflows/pull-compliance.yml
									
									
									
									
										vendored
									
									
								
							@@ -16,8 +16,8 @@ jobs:
 | 
			
		||||
    needs: files-changed
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v5
 | 
			
		||||
      - uses: actions/setup-go@v6
 | 
			
		||||
      - uses: actions/checkout@v4
 | 
			
		||||
      - uses: actions/setup-go@v5
 | 
			
		||||
        with:
 | 
			
		||||
          go-version-file: go.mod
 | 
			
		||||
          check-latest: true
 | 
			
		||||
@@ -31,13 +31,16 @@ jobs:
 | 
			
		||||
    needs: files-changed
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v5
 | 
			
		||||
      - uses: astral-sh/setup-uv@v6
 | 
			
		||||
      - run: uv python install 3.12
 | 
			
		||||
      - uses: pnpm/action-setup@v4
 | 
			
		||||
      - uses: actions/setup-node@v5
 | 
			
		||||
      - uses: actions/checkout@v4
 | 
			
		||||
      - uses: actions/setup-python@v5
 | 
			
		||||
        with:
 | 
			
		||||
          node-version: 24
 | 
			
		||||
          python-version: "3.12"
 | 
			
		||||
      - uses: actions/setup-node@v4
 | 
			
		||||
        with:
 | 
			
		||||
          node-version: 22
 | 
			
		||||
          cache: npm
 | 
			
		||||
          cache-dependency-path: package-lock.json
 | 
			
		||||
      - run: pip install poetry
 | 
			
		||||
      - run: make deps-py
 | 
			
		||||
      - run: make deps-frontend
 | 
			
		||||
      - run: make lint-templates
 | 
			
		||||
@@ -47,9 +50,11 @@ jobs:
 | 
			
		||||
    needs: files-changed
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v5
 | 
			
		||||
      - uses: astral-sh/setup-uv@v6
 | 
			
		||||
      - run: uv python install 3.12
 | 
			
		||||
      - uses: actions/checkout@v4
 | 
			
		||||
      - uses: actions/setup-python@v5
 | 
			
		||||
        with:
 | 
			
		||||
          python-version: "3.12"
 | 
			
		||||
      - run: pip install poetry
 | 
			
		||||
      - run: make deps-py
 | 
			
		||||
      - run: make lint-yaml
 | 
			
		||||
 | 
			
		||||
@@ -58,11 +63,12 @@ jobs:
 | 
			
		||||
    needs: files-changed
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v5
 | 
			
		||||
      - uses: pnpm/action-setup@v4
 | 
			
		||||
      - uses: actions/setup-node@v5
 | 
			
		||||
      - uses: actions/checkout@v4
 | 
			
		||||
      - uses: actions/setup-node@v4
 | 
			
		||||
        with:
 | 
			
		||||
          node-version: 24
 | 
			
		||||
          node-version: 22
 | 
			
		||||
          cache: npm
 | 
			
		||||
          cache-dependency-path: package-lock.json
 | 
			
		||||
      - run: make deps-frontend
 | 
			
		||||
      - run: make lint-swagger
 | 
			
		||||
 | 
			
		||||
@@ -71,8 +77,8 @@ jobs:
 | 
			
		||||
    needs: files-changed
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v5
 | 
			
		||||
      - uses: actions/setup-go@v6
 | 
			
		||||
      - uses: actions/checkout@v4
 | 
			
		||||
      - uses: actions/setup-go@v5
 | 
			
		||||
        with:
 | 
			
		||||
          go-version-file: go.mod
 | 
			
		||||
          check-latest: true
 | 
			
		||||
@@ -83,13 +89,13 @@ jobs:
 | 
			
		||||
    needs: files-changed
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v5
 | 
			
		||||
      - uses: actions/setup-go@v6
 | 
			
		||||
      - uses: actions/checkout@v4
 | 
			
		||||
      - uses: actions/setup-go@v5
 | 
			
		||||
        with:
 | 
			
		||||
          go-version-file: go.mod
 | 
			
		||||
          check-latest: true
 | 
			
		||||
      - run: make deps-backend deps-tools
 | 
			
		||||
      - run: make lint-go-windows lint-go-gitea-vet
 | 
			
		||||
      - run: make lint-go-windows lint-go-vet
 | 
			
		||||
        env:
 | 
			
		||||
          TAGS: bindata sqlite sqlite_unlock_notify
 | 
			
		||||
          GOOS: windows
 | 
			
		||||
@@ -100,8 +106,8 @@ jobs:
 | 
			
		||||
    needs: files-changed
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v5
 | 
			
		||||
      - uses: actions/setup-go@v6
 | 
			
		||||
      - uses: actions/checkout@v4
 | 
			
		||||
      - uses: actions/setup-go@v5
 | 
			
		||||
        with:
 | 
			
		||||
          go-version-file: go.mod
 | 
			
		||||
          check-latest: true
 | 
			
		||||
@@ -115,8 +121,8 @@ jobs:
 | 
			
		||||
    needs: files-changed
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v5
 | 
			
		||||
      - uses: actions/setup-go@v6
 | 
			
		||||
      - uses: actions/checkout@v4
 | 
			
		||||
      - uses: actions/setup-go@v5
 | 
			
		||||
        with:
 | 
			
		||||
          go-version-file: go.mod
 | 
			
		||||
          check-latest: true
 | 
			
		||||
@@ -128,11 +134,12 @@ jobs:
 | 
			
		||||
    needs: files-changed
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v5
 | 
			
		||||
      - uses: pnpm/action-setup@v4
 | 
			
		||||
      - uses: actions/setup-node@v5
 | 
			
		||||
      - uses: actions/checkout@v4
 | 
			
		||||
      - uses: actions/setup-node@v4
 | 
			
		||||
        with:
 | 
			
		||||
          node-version: 24
 | 
			
		||||
          node-version: 22
 | 
			
		||||
          cache: npm
 | 
			
		||||
          cache-dependency-path: package-lock.json
 | 
			
		||||
      - run: make deps-frontend
 | 
			
		||||
      - run: make lint-frontend
 | 
			
		||||
      - run: make checks-frontend
 | 
			
		||||
@@ -144,8 +151,8 @@ jobs:
 | 
			
		||||
    needs: files-changed
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v5
 | 
			
		||||
      - uses: actions/setup-go@v6
 | 
			
		||||
      - uses: actions/checkout@v4
 | 
			
		||||
      - uses: actions/setup-go@v5
 | 
			
		||||
        with:
 | 
			
		||||
          go-version-file: go.mod
 | 
			
		||||
          check-latest: true
 | 
			
		||||
@@ -176,11 +183,12 @@ jobs:
 | 
			
		||||
    needs: files-changed
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v5
 | 
			
		||||
      - uses: pnpm/action-setup@v4
 | 
			
		||||
      - uses: actions/setup-node@v5
 | 
			
		||||
      - uses: actions/checkout@v4
 | 
			
		||||
      - uses: actions/setup-node@v4
 | 
			
		||||
        with:
 | 
			
		||||
          node-version: 24
 | 
			
		||||
          node-version: 22
 | 
			
		||||
          cache: npm
 | 
			
		||||
          cache-dependency-path: package-lock.json
 | 
			
		||||
      - run: make deps-frontend
 | 
			
		||||
      - run: make lint-md
 | 
			
		||||
 | 
			
		||||
@@ -189,8 +197,8 @@ jobs:
 | 
			
		||||
    needs: files-changed
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v5
 | 
			
		||||
      - uses: actions/setup-go@v6
 | 
			
		||||
      - uses: actions/checkout@v4
 | 
			
		||||
      - uses: actions/setup-go@v5
 | 
			
		||||
        with:
 | 
			
		||||
          go-version-file: go.mod
 | 
			
		||||
          check-latest: true
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										42
									
								
								.github/workflows/pull-db-tests.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										42
									
								
								.github/workflows/pull-db-tests.yml
									
									
									
									
										vendored
									
									
								
							@@ -17,7 +17,7 @@ jobs:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    services:
 | 
			
		||||
      pgsql:
 | 
			
		||||
        image: postgres:14
 | 
			
		||||
        image: postgres:12
 | 
			
		||||
        env:
 | 
			
		||||
          POSTGRES_DB: test
 | 
			
		||||
          POSTGRES_PASSWORD: postgres
 | 
			
		||||
@@ -31,15 +31,15 @@ jobs:
 | 
			
		||||
      minio:
 | 
			
		||||
        # as github actions doesn't support "entrypoint", we need to use a non-official image
 | 
			
		||||
        # that has a custom entrypoint set to "minio server /data"
 | 
			
		||||
        image: bitnamilegacy/minio:2023.8.31
 | 
			
		||||
        image: bitnami/minio:2023.8.31
 | 
			
		||||
        env:
 | 
			
		||||
          MINIO_ROOT_USER: 123456
 | 
			
		||||
          MINIO_ROOT_PASSWORD: 12345678
 | 
			
		||||
        ports:
 | 
			
		||||
          - "9000:9000"
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v5
 | 
			
		||||
      - uses: actions/setup-go@v6
 | 
			
		||||
      - uses: actions/checkout@v4
 | 
			
		||||
      - uses: actions/setup-go@v5
 | 
			
		||||
        with:
 | 
			
		||||
          go-version-file: go.mod
 | 
			
		||||
          check-latest: true
 | 
			
		||||
@@ -66,19 +66,19 @@ jobs:
 | 
			
		||||
    needs: files-changed
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v5
 | 
			
		||||
      - uses: actions/setup-go@v6
 | 
			
		||||
      - uses: actions/checkout@v4
 | 
			
		||||
      - uses: actions/setup-go@v5
 | 
			
		||||
        with:
 | 
			
		||||
          go-version-file: go.mod
 | 
			
		||||
          check-latest: true
 | 
			
		||||
      - run: make deps-backend
 | 
			
		||||
      - run: GOEXPERIMENT='' make backend
 | 
			
		||||
      - run: make backend
 | 
			
		||||
        env:
 | 
			
		||||
          TAGS: bindata gogit sqlite sqlite_unlock_notify
 | 
			
		||||
      - name: run migration tests
 | 
			
		||||
        run: make test-sqlite-migration
 | 
			
		||||
      - name: run tests
 | 
			
		||||
        run: GOEXPERIMENT='' make test-sqlite
 | 
			
		||||
        run: make test-sqlite
 | 
			
		||||
        timeout-minutes: 50
 | 
			
		||||
        env:
 | 
			
		||||
          TAGS: bindata gogit sqlite sqlite_unlock_notify
 | 
			
		||||
@@ -98,7 +98,7 @@ jobs:
 | 
			
		||||
        ports:
 | 
			
		||||
          - "9200:9200"
 | 
			
		||||
      meilisearch:
 | 
			
		||||
        image: getmeili/meilisearch:v1
 | 
			
		||||
        image: getmeili/meilisearch:v1.2.0
 | 
			
		||||
        env:
 | 
			
		||||
          MEILI_ENV: development # disable auth
 | 
			
		||||
        ports:
 | 
			
		||||
@@ -113,7 +113,7 @@ jobs:
 | 
			
		||||
        ports:
 | 
			
		||||
          - 6379:6379
 | 
			
		||||
      minio:
 | 
			
		||||
        image: bitnamilegacy/minio:2021.3.17
 | 
			
		||||
        image: bitnami/minio:2021.3.17
 | 
			
		||||
        env:
 | 
			
		||||
          MINIO_ACCESS_KEY: 123456
 | 
			
		||||
          MINIO_SECRET_KEY: 12345678
 | 
			
		||||
@@ -124,8 +124,8 @@ jobs:
 | 
			
		||||
        ports:
 | 
			
		||||
          - 10000:10000
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v5
 | 
			
		||||
      - uses: actions/setup-go@v6
 | 
			
		||||
      - uses: actions/checkout@v4
 | 
			
		||||
      - uses: actions/setup-go@v5
 | 
			
		||||
        with:
 | 
			
		||||
          go-version-file: go.mod
 | 
			
		||||
          check-latest: true
 | 
			
		||||
@@ -142,7 +142,7 @@ jobs:
 | 
			
		||||
          RACE_ENABLED: true
 | 
			
		||||
          GITHUB_READ_TOKEN: ${{ secrets.GITHUB_READ_TOKEN }}
 | 
			
		||||
      - name: unit-tests-gogit
 | 
			
		||||
        run: GOEXPERIMENT='' make unit-test-coverage test-check
 | 
			
		||||
        run: make unit-test-coverage test-check
 | 
			
		||||
        env:
 | 
			
		||||
          TAGS: bindata gogit
 | 
			
		||||
          RACE_ENABLED: true
 | 
			
		||||
@@ -155,7 +155,7 @@ jobs:
 | 
			
		||||
    services:
 | 
			
		||||
      mysql:
 | 
			
		||||
        # the bitnami mysql image has more options than the official one, it's easier to customize
 | 
			
		||||
        image: bitnamilegacy/mysql:8.0
 | 
			
		||||
        image: bitnami/mysql:8.0
 | 
			
		||||
        env:
 | 
			
		||||
          ALLOW_EMPTY_PASSWORD: true
 | 
			
		||||
          MYSQL_DATABASE: testgitea
 | 
			
		||||
@@ -177,8 +177,8 @@ jobs:
 | 
			
		||||
          - "587:587"
 | 
			
		||||
          - "993:993"
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v5
 | 
			
		||||
      - uses: actions/setup-go@v6
 | 
			
		||||
      - uses: actions/checkout@v4
 | 
			
		||||
      - uses: actions/setup-go@v5
 | 
			
		||||
        with:
 | 
			
		||||
          go-version-file: go.mod
 | 
			
		||||
          check-latest: true
 | 
			
		||||
@@ -202,10 +202,12 @@ jobs:
 | 
			
		||||
  test-mssql:
 | 
			
		||||
    if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
 | 
			
		||||
    needs: files-changed
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    # specifying the version of ubuntu in use as mssql fails on newer kernels
 | 
			
		||||
    # pending resolution from vendor
 | 
			
		||||
    runs-on: ubuntu-20.04
 | 
			
		||||
    services:
 | 
			
		||||
      mssql:
 | 
			
		||||
        image: mcr.microsoft.com/mssql/server:2019-latest
 | 
			
		||||
        image: mcr.microsoft.com/mssql/server:2017-latest
 | 
			
		||||
        env:
 | 
			
		||||
          ACCEPT_EULA: Y
 | 
			
		||||
          MSSQL_PID: Standard
 | 
			
		||||
@@ -217,8 +219,8 @@ jobs:
 | 
			
		||||
        ports:
 | 
			
		||||
          - 10000:10000
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v5
 | 
			
		||||
      - uses: actions/setup-go@v6
 | 
			
		||||
      - uses: actions/checkout@v4
 | 
			
		||||
      - uses: actions/setup-go@v5
 | 
			
		||||
        with:
 | 
			
		||||
          go-version-file: go.mod
 | 
			
		||||
          check-latest: true
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										18
									
								
								.github/workflows/pull-docker-dryrun.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										18
									
								
								.github/workflows/pull-docker-dryrun.yml
									
									
									
									
										vendored
									
									
								
							@@ -11,23 +11,25 @@ jobs:
 | 
			
		||||
  files-changed:
 | 
			
		||||
    uses: ./.github/workflows/files-changed.yml
 | 
			
		||||
 | 
			
		||||
  container:
 | 
			
		||||
  regular:
 | 
			
		||||
    if: needs.files-changed.outputs.docker == 'true' || needs.files-changed.outputs.actions == 'true'
 | 
			
		||||
    needs: files-changed
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v5
 | 
			
		||||
      - uses: docker/setup-buildx-action@v3
 | 
			
		||||
      - name: Build regular container image
 | 
			
		||||
        uses: docker/build-push-action@v5
 | 
			
		||||
      - uses: docker/build-push-action@v5
 | 
			
		||||
        with:
 | 
			
		||||
          context: .
 | 
			
		||||
          push: false
 | 
			
		||||
          tags: gitea/gitea:linux-amd64
 | 
			
		||||
      - name: Build rootless container image
 | 
			
		||||
        uses: docker/build-push-action@v5
 | 
			
		||||
 | 
			
		||||
  rootless:
 | 
			
		||||
    if: needs.files-changed.outputs.docker == 'true' || needs.files-changed.outputs.actions == 'true'
 | 
			
		||||
    needs: files-changed
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: docker/setup-buildx-action@v3
 | 
			
		||||
      - uses: docker/build-push-action@v5
 | 
			
		||||
        with:
 | 
			
		||||
          context: .
 | 
			
		||||
          push: false
 | 
			
		||||
          file: Dockerfile.rootless
 | 
			
		||||
          tags: gitea/gitea:linux-amd64
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										17
									
								
								.github/workflows/pull-e2e-tests.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										17
									
								
								.github/workflows/pull-e2e-tests.yml
									
									
									
									
										vendored
									
									
								
							@@ -12,23 +12,22 @@ jobs:
 | 
			
		||||
    uses: ./.github/workflows/files-changed.yml
 | 
			
		||||
 | 
			
		||||
  test-e2e:
 | 
			
		||||
    # the "test-e2e" won't pass, and it seems that there is no useful test, so skip
 | 
			
		||||
    # if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.frontend == 'true' || needs.files-changed.outputs.actions == 'true'
 | 
			
		||||
    if: false
 | 
			
		||||
    if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.frontend == 'true' || needs.files-changed.outputs.actions == 'true'
 | 
			
		||||
    needs: files-changed
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v5
 | 
			
		||||
      - uses: actions/setup-go@v6
 | 
			
		||||
      - uses: actions/checkout@v4
 | 
			
		||||
      - uses: actions/setup-go@v5
 | 
			
		||||
        with:
 | 
			
		||||
          go-version-file: go.mod
 | 
			
		||||
          check-latest: true
 | 
			
		||||
      - uses: pnpm/action-setup@v4
 | 
			
		||||
      - uses: actions/setup-node@v5
 | 
			
		||||
      - uses: actions/setup-node@v4
 | 
			
		||||
        with:
 | 
			
		||||
          node-version: 24
 | 
			
		||||
          node-version: 22
 | 
			
		||||
          cache: npm
 | 
			
		||||
          cache-dependency-path: package-lock.json
 | 
			
		||||
      - run: make deps-frontend frontend deps-backend
 | 
			
		||||
      - run: pnpm exec playwright install --with-deps
 | 
			
		||||
      - run: npx playwright install --with-deps
 | 
			
		||||
      - run: make test-e2e-sqlite
 | 
			
		||||
        timeout-minutes: 40
 | 
			
		||||
        env:
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								.github/workflows/pull-labeler.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/pull-labeler.yml
									
									
									
									
										vendored
									
									
								
							@@ -15,6 +15,6 @@ jobs:
 | 
			
		||||
      contents: read
 | 
			
		||||
      pull-requests: write
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/labeler@v6
 | 
			
		||||
      - uses: actions/labeler@v5
 | 
			
		||||
        with:
 | 
			
		||||
          sync-labels: true
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										99
									
								
								.github/workflows/release-nightly.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										99
									
								
								.github/workflows/release-nightly.yml
									
									
									
									
										vendored
									
									
								
							@@ -12,18 +12,19 @@ jobs:
 | 
			
		||||
  nightly-binary:
 | 
			
		||||
    runs-on: namespace-profile-gitea-release-binary
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v5
 | 
			
		||||
      - 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 tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
 | 
			
		||||
      - run: git fetch --unshallow --quiet --tags --force
 | 
			
		||||
      - uses: actions/setup-go@v6
 | 
			
		||||
      - uses: actions/setup-go@v5
 | 
			
		||||
        with:
 | 
			
		||||
          go-version-file: go.mod
 | 
			
		||||
          check-latest: true
 | 
			
		||||
      - uses: pnpm/action-setup@v4
 | 
			
		||||
      - uses: actions/setup-node@v5
 | 
			
		||||
      - uses: actions/setup-node@v4
 | 
			
		||||
        with:
 | 
			
		||||
          node-version: 24
 | 
			
		||||
          node-version: 22
 | 
			
		||||
          cache: npm
 | 
			
		||||
          cache-dependency-path: package-lock.json
 | 
			
		||||
      - run: make deps-frontend deps-backend
 | 
			
		||||
      # xgo build
 | 
			
		||||
      - run: make release
 | 
			
		||||
@@ -56,70 +57,78 @@ jobs:
 | 
			
		||||
      - name: upload binaries to s3
 | 
			
		||||
        run: |
 | 
			
		||||
          aws s3 sync dist/release s3://${{ secrets.AWS_S3_BUCKET }}/gitea/${{ steps.clean_name.outputs.branch }} --no-progress
 | 
			
		||||
  nightly-container:
 | 
			
		||||
  nightly-docker-rootful:
 | 
			
		||||
    runs-on: namespace-profile-gitea-release-docker
 | 
			
		||||
    permissions:
 | 
			
		||||
      packages: write # to publish to ghcr.io
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v5
 | 
			
		||||
      - 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 tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
 | 
			
		||||
      - run: git fetch --unshallow --quiet --tags --force
 | 
			
		||||
      - uses: actions/setup-go@v5
 | 
			
		||||
        with:
 | 
			
		||||
          go-version-file: go.mod
 | 
			
		||||
          check-latest: true
 | 
			
		||||
      - uses: docker/setup-qemu-action@v3
 | 
			
		||||
      - uses: docker/setup-buildx-action@v3
 | 
			
		||||
      - name: Get cleaned branch name
 | 
			
		||||
        id: clean_name
 | 
			
		||||
        run: |
 | 
			
		||||
          # if main then say nightly otherwise cleanup name
 | 
			
		||||
          if [ "${{ github.ref }}" = "refs/heads/main" ]; then
 | 
			
		||||
            echo "branch=nightly" >> "$GITHUB_OUTPUT"
 | 
			
		||||
            exit 0
 | 
			
		||||
          fi
 | 
			
		||||
          REF_NAME=$(echo "${{ github.ref }}" | sed -e 's/refs\/heads\///' -e 's/refs\/tags\///' -e 's/release\/v//')
 | 
			
		||||
          echo "branch=${REF_NAME}-nightly" >> "$GITHUB_OUTPUT"
 | 
			
		||||
      - uses: docker/metadata-action@v5
 | 
			
		||||
        id: meta
 | 
			
		||||
        with:
 | 
			
		||||
          images: |-
 | 
			
		||||
            gitea/gitea
 | 
			
		||||
            ghcr.io/go-gitea/gitea
 | 
			
		||||
          tags: |
 | 
			
		||||
            type=raw,value=${{ steps.clean_name.outputs.branch }}
 | 
			
		||||
          annotations: |
 | 
			
		||||
            org.opencontainers.image.authors="maintainers@gitea.io"
 | 
			
		||||
      - uses: docker/metadata-action@v5
 | 
			
		||||
        id: meta_rootless
 | 
			
		||||
        with:
 | 
			
		||||
          images: |-
 | 
			
		||||
            gitea/gitea
 | 
			
		||||
            ghcr.io/go-gitea/gitea
 | 
			
		||||
          # each tag below will have the suffix of -rootless
 | 
			
		||||
          flavor: |
 | 
			
		||||
            suffix=-rootless
 | 
			
		||||
          tags: |
 | 
			
		||||
            type=raw,value=${{ steps.clean_name.outputs.branch }}
 | 
			
		||||
          annotations: |
 | 
			
		||||
            org.opencontainers.image.authors="maintainers@gitea.io"
 | 
			
		||||
      - name: Login to Docker Hub
 | 
			
		||||
        uses: docker/login-action@v3
 | 
			
		||||
        with:
 | 
			
		||||
          username: ${{ secrets.DOCKERHUB_USERNAME }}
 | 
			
		||||
          password: ${{ secrets.DOCKERHUB_TOKEN }}
 | 
			
		||||
      - name: Login to GHCR using PAT
 | 
			
		||||
        uses: docker/login-action@v3
 | 
			
		||||
        with:
 | 
			
		||||
          registry: ghcr.io
 | 
			
		||||
          username: ${{ github.repository_owner }}
 | 
			
		||||
          password: ${{ secrets.GITHUB_TOKEN }}
 | 
			
		||||
      - name: build regular docker image
 | 
			
		||||
      - name: fetch go modules
 | 
			
		||||
        run: make vendor
 | 
			
		||||
      - name: build rootful docker image
 | 
			
		||||
        uses: docker/build-push-action@v5
 | 
			
		||||
        with:
 | 
			
		||||
          context: .
 | 
			
		||||
          platforms: linux/amd64,linux/arm64,linux/riscv64
 | 
			
		||||
          platforms: linux/amd64,linux/arm64
 | 
			
		||||
          push: true
 | 
			
		||||
          tags: ${{ steps.meta.outputs.tags }}
 | 
			
		||||
          annotations: ${{ steps.meta.outputs.annotations }}
 | 
			
		||||
          tags: gitea/gitea:${{ steps.clean_name.outputs.branch }}
 | 
			
		||||
  nightly-docker-rootless:
 | 
			
		||||
    runs-on: namespace-profile-gitea-release-docker
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v4
 | 
			
		||||
      # fetch all commits instead of only the last as some branches are long lived and could have many between versions
 | 
			
		||||
      # fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
 | 
			
		||||
      - run: git fetch --unshallow --quiet --tags --force
 | 
			
		||||
      - uses: actions/setup-go@v5
 | 
			
		||||
        with:
 | 
			
		||||
          go-version-file: go.mod
 | 
			
		||||
          check-latest: true
 | 
			
		||||
      - uses: docker/setup-qemu-action@v3
 | 
			
		||||
      - uses: docker/setup-buildx-action@v3
 | 
			
		||||
      - name: Get cleaned branch name
 | 
			
		||||
        id: clean_name
 | 
			
		||||
        run: |
 | 
			
		||||
          # if main then say nightly otherwise cleanup name
 | 
			
		||||
          if [ "${{ github.ref }}" = "refs/heads/main" ]; then
 | 
			
		||||
            echo "branch=nightly" >> "$GITHUB_OUTPUT"
 | 
			
		||||
            exit 0
 | 
			
		||||
          fi
 | 
			
		||||
          REF_NAME=$(echo "${{ github.ref }}" | sed -e 's/refs\/heads\///' -e 's/refs\/tags\///' -e 's/release\/v//')
 | 
			
		||||
          echo "branch=${REF_NAME}-nightly" >> "$GITHUB_OUTPUT"
 | 
			
		||||
      - name: Login to Docker Hub
 | 
			
		||||
        uses: docker/login-action@v3
 | 
			
		||||
        with:
 | 
			
		||||
          username: ${{ secrets.DOCKERHUB_USERNAME }}
 | 
			
		||||
          password: ${{ secrets.DOCKERHUB_TOKEN }}
 | 
			
		||||
      - name: fetch go modules
 | 
			
		||||
        run: make vendor
 | 
			
		||||
      - name: build rootless docker image
 | 
			
		||||
        uses: docker/build-push-action@v5
 | 
			
		||||
        with:
 | 
			
		||||
          context: .
 | 
			
		||||
          platforms: linux/amd64,linux/arm64,linux/riscv64
 | 
			
		||||
          platforms: linux/amd64,linux/arm64
 | 
			
		||||
          push: true
 | 
			
		||||
          file: Dockerfile.rootless
 | 
			
		||||
          tags: ${{ steps.meta_rootless.outputs.tags }}
 | 
			
		||||
          annotations: ${{ steps.meta_rootless.outputs.annotations }}
 | 
			
		||||
          tags: gitea/gitea:${{ steps.clean_name.outputs.branch }}-rootless
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										77
									
								
								.github/workflows/release-tag-rc.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										77
									
								
								.github/workflows/release-tag-rc.yml
									
									
									
									
										vendored
									
									
								
							@@ -13,18 +13,19 @@ jobs:
 | 
			
		||||
  binary:
 | 
			
		||||
    runs-on: namespace-profile-gitea-release-binary
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v5
 | 
			
		||||
      - 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 tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
 | 
			
		||||
      - run: git fetch --unshallow --quiet --tags --force
 | 
			
		||||
      - uses: actions/setup-go@v6
 | 
			
		||||
      - uses: actions/setup-go@v5
 | 
			
		||||
        with:
 | 
			
		||||
          go-version-file: go.mod
 | 
			
		||||
          check-latest: true
 | 
			
		||||
      - uses: pnpm/action-setup@v4
 | 
			
		||||
      - uses: actions/setup-node@v5
 | 
			
		||||
      - uses: actions/setup-node@v4
 | 
			
		||||
        with:
 | 
			
		||||
          node-version: 24
 | 
			
		||||
          node-version: 22
 | 
			
		||||
          cache: npm
 | 
			
		||||
          cache-dependency-path: package-lock.json
 | 
			
		||||
      - run: make deps-frontend deps-backend
 | 
			
		||||
      # xgo build
 | 
			
		||||
      - run: make release
 | 
			
		||||
@@ -66,12 +67,10 @@ jobs:
 | 
			
		||||
          gh release create ${{ github.ref_name }} --title ${{ github.ref_name }} --draft --notes-from-tag dist/release/*
 | 
			
		||||
        env:
 | 
			
		||||
          GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
 | 
			
		||||
  container:
 | 
			
		||||
  docker-rootful:
 | 
			
		||||
    runs-on: namespace-profile-gitea-release-docker
 | 
			
		||||
    permissions:
 | 
			
		||||
      packages: write # to publish to ghcr.io
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v5
 | 
			
		||||
      - 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 tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
 | 
			
		||||
      - run: git fetch --unshallow --quiet --tags --force
 | 
			
		||||
@@ -80,22 +79,38 @@ jobs:
 | 
			
		||||
      - uses: docker/metadata-action@v5
 | 
			
		||||
        id: meta
 | 
			
		||||
        with:
 | 
			
		||||
          images: |-
 | 
			
		||||
            gitea/gitea
 | 
			
		||||
            ghcr.io/go-gitea/gitea
 | 
			
		||||
          images: gitea/gitea
 | 
			
		||||
          flavor: |
 | 
			
		||||
            latest=false
 | 
			
		||||
          # 1.2.3-rc0
 | 
			
		||||
          tags: |
 | 
			
		||||
            type=semver,pattern={{version}}
 | 
			
		||||
          annotations: |
 | 
			
		||||
            org.opencontainers.image.authors="maintainers@gitea.io"
 | 
			
		||||
      - uses: docker/metadata-action@v5
 | 
			
		||||
        id: meta_rootless
 | 
			
		||||
      - name: Login to Docker Hub
 | 
			
		||||
        uses: docker/login-action@v3
 | 
			
		||||
        with:
 | 
			
		||||
          images: |-
 | 
			
		||||
            gitea/gitea
 | 
			
		||||
            ghcr.io/go-gitea/gitea
 | 
			
		||||
          username: ${{ secrets.DOCKERHUB_USERNAME }}
 | 
			
		||||
          password: ${{ secrets.DOCKERHUB_TOKEN }}
 | 
			
		||||
      - name: build rootful docker image
 | 
			
		||||
        uses: docker/build-push-action@v5
 | 
			
		||||
        with:
 | 
			
		||||
          context: .
 | 
			
		||||
          platforms: linux/amd64,linux/arm64
 | 
			
		||||
          push: true
 | 
			
		||||
          tags: ${{ steps.meta.outputs.tags }}
 | 
			
		||||
          labels: ${{ steps.meta.outputs.labels }}
 | 
			
		||||
  docker-rootless:
 | 
			
		||||
    runs-on: namespace-profile-gitea-release-docker
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v4
 | 
			
		||||
      # fetch all commits instead of only the last as some branches are long lived and could have many between versions
 | 
			
		||||
      # fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
 | 
			
		||||
      - run: git fetch --unshallow --quiet --tags --force
 | 
			
		||||
      - uses: docker/setup-qemu-action@v3
 | 
			
		||||
      - uses: docker/setup-buildx-action@v3
 | 
			
		||||
      - uses: docker/metadata-action@v5
 | 
			
		||||
        id: meta
 | 
			
		||||
        with:
 | 
			
		||||
          images: gitea/gitea
 | 
			
		||||
          # each tag below will have the suffix of -rootless
 | 
			
		||||
          flavor: |
 | 
			
		||||
            latest=false
 | 
			
		||||
@@ -103,33 +118,17 @@ jobs:
 | 
			
		||||
          # 1.2.3-rc0
 | 
			
		||||
          tags: |
 | 
			
		||||
            type=semver,pattern={{version}}
 | 
			
		||||
          annotations: |
 | 
			
		||||
            org.opencontainers.image.authors="maintainers@gitea.io"
 | 
			
		||||
      - name: Login to Docker Hub
 | 
			
		||||
        uses: docker/login-action@v3
 | 
			
		||||
        with:
 | 
			
		||||
          username: ${{ secrets.DOCKERHUB_USERNAME }}
 | 
			
		||||
          password: ${{ secrets.DOCKERHUB_TOKEN }}
 | 
			
		||||
      - name: Login to GHCR using PAT
 | 
			
		||||
        uses: docker/login-action@v3
 | 
			
		||||
        with:
 | 
			
		||||
          registry: ghcr.io
 | 
			
		||||
          username: ${{ github.repository_owner }}
 | 
			
		||||
          password: ${{ secrets.GITHUB_TOKEN }}
 | 
			
		||||
      - name: build regular container image
 | 
			
		||||
      - name: build rootless docker image
 | 
			
		||||
        uses: docker/build-push-action@v5
 | 
			
		||||
        with:
 | 
			
		||||
          context: .
 | 
			
		||||
          platforms: linux/amd64,linux/arm64,linux/riscv64
 | 
			
		||||
          push: true
 | 
			
		||||
          tags: ${{ steps.meta.outputs.tags }}
 | 
			
		||||
          annotations: ${{ steps.meta.outputs.annotations }}
 | 
			
		||||
      - name: build rootless container image
 | 
			
		||||
        uses: docker/build-push-action@v5
 | 
			
		||||
        with:
 | 
			
		||||
          context: .
 | 
			
		||||
          platforms: linux/amd64,linux/arm64,linux/riscv64
 | 
			
		||||
          platforms: linux/amd64,linux/arm64
 | 
			
		||||
          push: true
 | 
			
		||||
          file: Dockerfile.rootless
 | 
			
		||||
          tags: ${{ steps.meta_rootless.outputs.tags }}
 | 
			
		||||
          annotations: ${{ steps.meta_rootless.outputs.annotations }}
 | 
			
		||||
          tags: ${{ steps.meta.outputs.tags }}
 | 
			
		||||
          labels: ${{ steps.meta.outputs.labels }}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										83
									
								
								.github/workflows/release-tag-version.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										83
									
								
								.github/workflows/release-tag-version.yml
									
									
									
									
										vendored
									
									
								
							@@ -14,21 +14,20 @@ concurrency:
 | 
			
		||||
jobs:
 | 
			
		||||
  binary:
 | 
			
		||||
    runs-on: namespace-profile-gitea-release-binary
 | 
			
		||||
    permissions:
 | 
			
		||||
      packages: write # to publish to ghcr.io
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v5
 | 
			
		||||
      - 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 tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
 | 
			
		||||
      - run: git fetch --unshallow --quiet --tags --force
 | 
			
		||||
      - uses: actions/setup-go@v6
 | 
			
		||||
      - uses: actions/setup-go@v5
 | 
			
		||||
        with:
 | 
			
		||||
          go-version-file: go.mod
 | 
			
		||||
          check-latest: true
 | 
			
		||||
      - uses: pnpm/action-setup@v4
 | 
			
		||||
      - uses: actions/setup-node@v5
 | 
			
		||||
      - uses: actions/setup-node@v4
 | 
			
		||||
        with:
 | 
			
		||||
          node-version: 24
 | 
			
		||||
          node-version: 22
 | 
			
		||||
          cache: npm
 | 
			
		||||
          cache-dependency-path: package-lock.json
 | 
			
		||||
      - run: make deps-frontend deps-backend
 | 
			
		||||
      # xgo build
 | 
			
		||||
      - run: make release
 | 
			
		||||
@@ -70,12 +69,10 @@ jobs:
 | 
			
		||||
          gh release create ${{ github.ref_name }} --title ${{ github.ref_name }} --notes-from-tag dist/release/*
 | 
			
		||||
        env:
 | 
			
		||||
          GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
 | 
			
		||||
  container:
 | 
			
		||||
  docker-rootful:
 | 
			
		||||
    runs-on: namespace-profile-gitea-release-docker
 | 
			
		||||
    permissions:
 | 
			
		||||
      packages: write # to publish to ghcr.io
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v5
 | 
			
		||||
      - 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 tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
 | 
			
		||||
      - run: git fetch --unshallow --quiet --tags --force
 | 
			
		||||
@@ -84,26 +81,42 @@ jobs:
 | 
			
		||||
      - uses: docker/metadata-action@v5
 | 
			
		||||
        id: meta
 | 
			
		||||
        with:
 | 
			
		||||
          images: |-
 | 
			
		||||
            gitea/gitea
 | 
			
		||||
            ghcr.io/go-gitea/gitea
 | 
			
		||||
          images: gitea/gitea
 | 
			
		||||
          # this will generate tags in the following format:
 | 
			
		||||
          # latest
 | 
			
		||||
          # 1
 | 
			
		||||
          # 1.2
 | 
			
		||||
          # 1.2.3
 | 
			
		||||
          tags: |
 | 
			
		||||
            type=semver,pattern={{version}}
 | 
			
		||||
            type=semver,pattern={{major}}
 | 
			
		||||
            type=semver,pattern={{major}}.{{minor}}
 | 
			
		||||
          annotations: |
 | 
			
		||||
            org.opencontainers.image.authors="maintainers@gitea.io"
 | 
			
		||||
      - uses: docker/metadata-action@v5
 | 
			
		||||
        id: meta_rootless
 | 
			
		||||
            type=semver,pattern={{version}}
 | 
			
		||||
      - name: Login to Docker Hub
 | 
			
		||||
        uses: docker/login-action@v3
 | 
			
		||||
        with:
 | 
			
		||||
          images: |-
 | 
			
		||||
            gitea/gitea
 | 
			
		||||
            ghcr.io/go-gitea/gitea
 | 
			
		||||
          username: ${{ secrets.DOCKERHUB_USERNAME }}
 | 
			
		||||
          password: ${{ secrets.DOCKERHUB_TOKEN }}
 | 
			
		||||
      - name: build rootful docker image
 | 
			
		||||
        uses: docker/build-push-action@v5
 | 
			
		||||
        with:
 | 
			
		||||
          context: .
 | 
			
		||||
          platforms: linux/amd64,linux/arm64
 | 
			
		||||
          push: true
 | 
			
		||||
          tags: ${{ steps.meta.outputs.tags }}
 | 
			
		||||
          labels: ${{ steps.meta.outputs.labels }}
 | 
			
		||||
  docker-rootless:
 | 
			
		||||
    runs-on: namespace-profile-gitea-release-docker
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v4
 | 
			
		||||
      # fetch all commits instead of only the last as some branches are long lived and could have many between versions
 | 
			
		||||
      # fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
 | 
			
		||||
      - run: git fetch --unshallow --quiet --tags --force
 | 
			
		||||
      - uses: docker/setup-qemu-action@v3
 | 
			
		||||
      - uses: docker/setup-buildx-action@v3
 | 
			
		||||
      - uses: docker/metadata-action@v5
 | 
			
		||||
        id: meta
 | 
			
		||||
        with:
 | 
			
		||||
          images: gitea/gitea
 | 
			
		||||
          # each tag below will have the suffix of -rootless
 | 
			
		||||
          flavor: |
 | 
			
		||||
            suffix=-rootless,onlatest=true
 | 
			
		||||
@@ -113,36 +126,20 @@ jobs:
 | 
			
		||||
          # 1.2
 | 
			
		||||
          # 1.2.3
 | 
			
		||||
          tags: |
 | 
			
		||||
            type=semver,pattern={{version}}
 | 
			
		||||
            type=semver,pattern={{major}}
 | 
			
		||||
            type=semver,pattern={{major}}.{{minor}}
 | 
			
		||||
          annotations: |
 | 
			
		||||
            org.opencontainers.image.authors="maintainers@gitea.io"
 | 
			
		||||
            type=semver,pattern={{version}}
 | 
			
		||||
      - name: Login to Docker Hub
 | 
			
		||||
        uses: docker/login-action@v3
 | 
			
		||||
        with:
 | 
			
		||||
          username: ${{ secrets.DOCKERHUB_USERNAME }}
 | 
			
		||||
          password: ${{ secrets.DOCKERHUB_TOKEN }}
 | 
			
		||||
      - name: Login to GHCR using PAT
 | 
			
		||||
        uses: docker/login-action@v3
 | 
			
		||||
        with:
 | 
			
		||||
          registry: ghcr.io
 | 
			
		||||
          username: ${{ github.repository_owner }}
 | 
			
		||||
          password: ${{ secrets.GITHUB_TOKEN }}
 | 
			
		||||
      - name: build regular container image
 | 
			
		||||
      - name: build rootless docker image
 | 
			
		||||
        uses: docker/build-push-action@v5
 | 
			
		||||
        with:
 | 
			
		||||
          context: .
 | 
			
		||||
          platforms: linux/amd64,linux/arm64,linux/riscv64
 | 
			
		||||
          push: true
 | 
			
		||||
          tags: ${{ steps.meta.outputs.tags }}
 | 
			
		||||
          annotations: ${{ steps.meta.outputs.annotations }}
 | 
			
		||||
      - name: build rootless container image
 | 
			
		||||
        uses: docker/build-push-action@v5
 | 
			
		||||
        with:
 | 
			
		||||
          context: .
 | 
			
		||||
          platforms: linux/amd64,linux/arm64,linux/riscv64
 | 
			
		||||
          platforms: linux/amd64,linux/arm64
 | 
			
		||||
          push: true
 | 
			
		||||
          file: Dockerfile.rootless
 | 
			
		||||
          tags: ${{ steps.meta_rootless.outputs.tags }}
 | 
			
		||||
          annotations: ${{ steps.meta_rootless.outputs.annotations }}
 | 
			
		||||
          tags: ${{ steps.meta.outputs.tags }}
 | 
			
		||||
          labels: ${{ steps.meta.outputs.labels }}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										47
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										47
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -9,11 +9,6 @@ _test
 | 
			
		||||
 | 
			
		||||
# IntelliJ
 | 
			
		||||
.idea
 | 
			
		||||
.run
 | 
			
		||||
 | 
			
		||||
# IntelliJ Gateway
 | 
			
		||||
.uuid
 | 
			
		||||
 | 
			
		||||
# Goland's output filename can not be set manually
 | 
			
		||||
/go_build_*
 | 
			
		||||
/gitea_*
 | 
			
		||||
@@ -22,12 +17,6 @@ _test
 | 
			
		||||
.vscode
 | 
			
		||||
__debug_bin*
 | 
			
		||||
 | 
			
		||||
# Visual Studio
 | 
			
		||||
/.vs/
 | 
			
		||||
 | 
			
		||||
# mise version managment tool
 | 
			
		||||
mise.toml
 | 
			
		||||
 | 
			
		||||
*.cgo1.go
 | 
			
		||||
*.cgo2.c
 | 
			
		||||
_cgo_defun.c
 | 
			
		||||
@@ -45,10 +34,14 @@ _testmain.go
 | 
			
		||||
coverage.all
 | 
			
		||||
cpu.out
 | 
			
		||||
 | 
			
		||||
/modules/migration/bindata.*
 | 
			
		||||
/modules/options/bindata.*
 | 
			
		||||
/modules/public/bindata.*
 | 
			
		||||
/modules/templates/bindata.*
 | 
			
		||||
/modules/migration/bindata.go
 | 
			
		||||
/modules/migration/bindata.go.hash
 | 
			
		||||
/modules/options/bindata.go
 | 
			
		||||
/modules/options/bindata.go.hash
 | 
			
		||||
/modules/public/bindata.go
 | 
			
		||||
/modules/public/bindata.go.hash
 | 
			
		||||
/modules/templates/bindata.go
 | 
			
		||||
/modules/templates/bindata.go.hash
 | 
			
		||||
 | 
			
		||||
*.db
 | 
			
		||||
*.log
 | 
			
		||||
@@ -81,12 +74,23 @@ cpu.out
 | 
			
		||||
/yarn.lock
 | 
			
		||||
/yarn-error.log
 | 
			
		||||
/npm-debug.log*
 | 
			
		||||
/.pnpm-store
 | 
			
		||||
/public/assets/js
 | 
			
		||||
/public/assets/css
 | 
			
		||||
/public/assets/fonts
 | 
			
		||||
/public/assets/licenses.txt
 | 
			
		||||
/vendor
 | 
			
		||||
/web_src/fomantic/node_modules
 | 
			
		||||
/web_src/fomantic/build/*
 | 
			
		||||
!/web_src/fomantic/build/semantic.js
 | 
			
		||||
!/web_src/fomantic/build/semantic.css
 | 
			
		||||
!/web_src/fomantic/build/themes
 | 
			
		||||
/web_src/fomantic/build/themes/*
 | 
			
		||||
!/web_src/fomantic/build/themes/default
 | 
			
		||||
/web_src/fomantic/build/themes/default/assets/*
 | 
			
		||||
!/web_src/fomantic/build/themes/default/assets/fonts
 | 
			
		||||
/web_src/fomantic/build/themes/default/assets/fonts/*
 | 
			
		||||
!/web_src/fomantic/build/themes/default/assets/fonts/icons.woff2
 | 
			
		||||
!/web_src/fomantic/build/themes/default/assets/fonts/outline-icons.woff2
 | 
			
		||||
/VERSION
 | 
			
		||||
/.air
 | 
			
		||||
/.go-licenses
 | 
			
		||||
@@ -113,14 +117,3 @@ prime/
 | 
			
		||||
 | 
			
		||||
# Manpage
 | 
			
		||||
/man
 | 
			
		||||
 | 
			
		||||
# Ignore AI/LLM instruction files
 | 
			
		||||
/.claude/
 | 
			
		||||
/.cursorrules
 | 
			
		||||
/.cursor/
 | 
			
		||||
/.goosehints
 | 
			
		||||
/.windsurfrules
 | 
			
		||||
/.github/copilot-instructions.md
 | 
			
		||||
/AGENT.md
 | 
			
		||||
/CLAUDE.md
 | 
			
		||||
/llms.txt
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										211
									
								
								.golangci.yml
									
									
									
									
									
								
							
							
						
						
									
										211
									
								
								.golangci.yml
									
									
									
									
									
								
							@@ -1,9 +1,7 @@
 | 
			
		||||
version: "2"
 | 
			
		||||
output:
 | 
			
		||||
  sort-order:
 | 
			
		||||
    - file
 | 
			
		||||
linters:
 | 
			
		||||
  default: none
 | 
			
		||||
  enable-all: false
 | 
			
		||||
  disable-all: true
 | 
			
		||||
  fast: false
 | 
			
		||||
  enable:
 | 
			
		||||
    - bidichk
 | 
			
		||||
    - depguard
 | 
			
		||||
@@ -11,47 +9,42 @@ linters:
 | 
			
		||||
    - errcheck
 | 
			
		||||
    - forbidigo
 | 
			
		||||
    - gocritic
 | 
			
		||||
    - gofmt
 | 
			
		||||
    - gofumpt
 | 
			
		||||
    - gosimple
 | 
			
		||||
    - govet
 | 
			
		||||
    - ineffassign
 | 
			
		||||
    - mirror
 | 
			
		||||
    - nakedret
 | 
			
		||||
    - nolintlint
 | 
			
		||||
    - perfsprint
 | 
			
		||||
    - revive
 | 
			
		||||
    - staticcheck
 | 
			
		||||
    - stylecheck
 | 
			
		||||
    - tenv
 | 
			
		||||
    - testifylint
 | 
			
		||||
    - typecheck
 | 
			
		||||
    - unconvert
 | 
			
		||||
    - unparam
 | 
			
		||||
    - unused
 | 
			
		||||
    - usestdlibvars
 | 
			
		||||
    - usetesting
 | 
			
		||||
    - unparam
 | 
			
		||||
    - wastedassign
 | 
			
		||||
  settings:
 | 
			
		||||
    depguard:
 | 
			
		||||
      rules:
 | 
			
		||||
        main:
 | 
			
		||||
          deny:
 | 
			
		||||
            - pkg: encoding/json
 | 
			
		||||
              desc: use gitea's modules/json instead of encoding/json
 | 
			
		||||
            - pkg: github.com/unknwon/com
 | 
			
		||||
              desc: use gitea's util and replacements
 | 
			
		||||
            - pkg: io/ioutil
 | 
			
		||||
              desc: use os or io instead
 | 
			
		||||
            - pkg: golang.org/x/exp
 | 
			
		||||
              desc: it's experimental and unreliable
 | 
			
		||||
            - pkg: code.gitea.io/gitea/modules/git/internal
 | 
			
		||||
              desc: do not use the internal package, use AddXxx function instead
 | 
			
		||||
            - pkg: gopkg.in/ini.v1
 | 
			
		||||
              desc: do not use the ini package, use gitea's config system instead
 | 
			
		||||
            - pkg: gitea.com/go-chi/cache
 | 
			
		||||
              desc: do not use the go-chi cache package, use gitea's cache system
 | 
			
		||||
    nolintlint:
 | 
			
		||||
      allow-unused: false
 | 
			
		||||
      require-explanation: true
 | 
			
		||||
      require-specific: true
 | 
			
		||||
 | 
			
		||||
run:
 | 
			
		||||
  timeout: 10m
 | 
			
		||||
 | 
			
		||||
output:
 | 
			
		||||
  sort-results: true
 | 
			
		||||
  sort-order: [file]
 | 
			
		||||
  show-stats: true
 | 
			
		||||
 | 
			
		||||
linters-settings:
 | 
			
		||||
  testifylint:
 | 
			
		||||
    disable:
 | 
			
		||||
      - go-require
 | 
			
		||||
      - require-error
 | 
			
		||||
  stylecheck:
 | 
			
		||||
    checks: ["all", "-ST1005", "-ST1003"]
 | 
			
		||||
  nakedret:
 | 
			
		||||
    max-func-lines: 0
 | 
			
		||||
  gocritic:
 | 
			
		||||
      enabled-checks:
 | 
			
		||||
        - equalFold
 | 
			
		||||
    disabled-checks:
 | 
			
		||||
      - ifElseChain
 | 
			
		||||
      - singleCaseSwitch # Every time this occurred in the code, there  was no other way.
 | 
			
		||||
@@ -89,96 +82,66 @@ linters:
 | 
			
		||||
      - name: unreachable-code
 | 
			
		||||
      - name: var-declaration
 | 
			
		||||
      - name: var-naming
 | 
			
		||||
          arguments:
 | 
			
		||||
            - [] # AllowList - do not remove as args for the rule are positional and won't work without lists first
 | 
			
		||||
            - [] # DenyList
 | 
			
		||||
            - - skip-package-name-checks: true # supress errors from underscore in migration packages
 | 
			
		||||
    staticcheck:
 | 
			
		||||
      checks:
 | 
			
		||||
        - all
 | 
			
		||||
        - -ST1003
 | 
			
		||||
        - -ST1005
 | 
			
		||||
        - -QF1001
 | 
			
		||||
        - -QF1006
 | 
			
		||||
        - -QF1008
 | 
			
		||||
    testifylint:
 | 
			
		||||
      disable:
 | 
			
		||||
        - go-require
 | 
			
		||||
        - require-error
 | 
			
		||||
    usetesting:
 | 
			
		||||
      os-temp-dir: true
 | 
			
		||||
  exclusions:
 | 
			
		||||
    generated: lax
 | 
			
		||||
    presets:
 | 
			
		||||
      - comments
 | 
			
		||||
      - common-false-positives
 | 
			
		||||
      - legacy
 | 
			
		||||
      - std-error-handling
 | 
			
		||||
  gofumpt:
 | 
			
		||||
    extra-rules: true
 | 
			
		||||
  depguard:
 | 
			
		||||
    rules:
 | 
			
		||||
      - linters:
 | 
			
		||||
          - dupl
 | 
			
		||||
          - errcheck
 | 
			
		||||
          - gocyclo
 | 
			
		||||
          - gosec
 | 
			
		||||
          - staticcheck
 | 
			
		||||
          - unparam
 | 
			
		||||
        path: _test\.go
 | 
			
		||||
      - linters:
 | 
			
		||||
          - dupl
 | 
			
		||||
          - errcheck
 | 
			
		||||
          - gocyclo
 | 
			
		||||
          - gosec
 | 
			
		||||
        path: models/migrations/v
 | 
			
		||||
      - linters:
 | 
			
		||||
          - forbidigo
 | 
			
		||||
        path: cmd
 | 
			
		||||
      - linters:
 | 
			
		||||
          - dupl
 | 
			
		||||
        text: (?i)webhook
 | 
			
		||||
      - linters:
 | 
			
		||||
          - gocritic
 | 
			
		||||
        text: (?i)`ID' should not be capitalized
 | 
			
		||||
      - linters:
 | 
			
		||||
          - deadcode
 | 
			
		||||
          - unused
 | 
			
		||||
        text: (?i)swagger
 | 
			
		||||
      - linters:
 | 
			
		||||
          - staticcheck
 | 
			
		||||
        text: (?i)argument x is overwritten before first use
 | 
			
		||||
      - linters:
 | 
			
		||||
          - gocritic
 | 
			
		||||
        text: '(?i)commentFormatting: put a space between `//` and comment text'
 | 
			
		||||
      - linters:
 | 
			
		||||
          - gocritic
 | 
			
		||||
        text: '(?i)exitAfterDefer:'
 | 
			
		||||
    paths:
 | 
			
		||||
      - node_modules
 | 
			
		||||
      - .venv
 | 
			
		||||
      - public
 | 
			
		||||
      - web_src
 | 
			
		||||
      - third_party$
 | 
			
		||||
      - builtin$
 | 
			
		||||
      - examples$
 | 
			
		||||
      main:
 | 
			
		||||
        deny:
 | 
			
		||||
          - pkg: encoding/json
 | 
			
		||||
            desc: use gitea's modules/json instead of encoding/json
 | 
			
		||||
          - pkg: github.com/unknwon/com
 | 
			
		||||
            desc: use gitea's util and replacements
 | 
			
		||||
          - pkg: io/ioutil
 | 
			
		||||
            desc: use os or io instead
 | 
			
		||||
          - pkg: golang.org/x/exp
 | 
			
		||||
            desc: it's experimental and unreliable
 | 
			
		||||
          - pkg: code.gitea.io/gitea/modules/git/internal
 | 
			
		||||
            desc: do not use the internal package, use AddXxx function instead
 | 
			
		||||
          - pkg: gopkg.in/ini.v1
 | 
			
		||||
            desc: do not use the ini package, use gitea's config system instead
 | 
			
		||||
          - pkg: gitea.com/go-chi/cache
 | 
			
		||||
            desc: do not use the go-chi cache package, use gitea's cache system
 | 
			
		||||
 | 
			
		||||
issues:
 | 
			
		||||
  max-issues-per-linter: 0
 | 
			
		||||
  max-same-issues: 0
 | 
			
		||||
formatters:
 | 
			
		||||
  enable:
 | 
			
		||||
    - gofmt
 | 
			
		||||
    - gofumpt
 | 
			
		||||
  settings:
 | 
			
		||||
    gofumpt:
 | 
			
		||||
      extra-rules: true
 | 
			
		||||
  exclusions:
 | 
			
		||||
    generated: lax
 | 
			
		||||
    paths:
 | 
			
		||||
      - node_modules
 | 
			
		||||
      - .venv
 | 
			
		||||
      - public
 | 
			
		||||
      - web_src
 | 
			
		||||
      - third_party$
 | 
			
		||||
      - builtin$
 | 
			
		||||
      - examples$
 | 
			
		||||
 | 
			
		||||
run:
 | 
			
		||||
  timeout: 10m
 | 
			
		||||
  exclude-dirs: [node_modules, public, web_src]
 | 
			
		||||
  exclude-case-sensitive: true
 | 
			
		||||
  exclude-rules:
 | 
			
		||||
    - path: _test\.go
 | 
			
		||||
      linters:
 | 
			
		||||
        - gocyclo
 | 
			
		||||
        - errcheck
 | 
			
		||||
        - dupl
 | 
			
		||||
        - gosec
 | 
			
		||||
        - unparam
 | 
			
		||||
        - staticcheck
 | 
			
		||||
    - path: models/migrations/v
 | 
			
		||||
      linters:
 | 
			
		||||
        - gocyclo
 | 
			
		||||
        - errcheck
 | 
			
		||||
        - dupl
 | 
			
		||||
        - gosec
 | 
			
		||||
    - path: cmd
 | 
			
		||||
      linters:
 | 
			
		||||
        - forbidigo
 | 
			
		||||
    - text: "webhook"
 | 
			
		||||
      linters:
 | 
			
		||||
        - dupl
 | 
			
		||||
    - text: "`ID' should not be capitalized"
 | 
			
		||||
      linters:
 | 
			
		||||
        - gocritic
 | 
			
		||||
    - text: "swagger"
 | 
			
		||||
      linters:
 | 
			
		||||
        - unused
 | 
			
		||||
        - deadcode
 | 
			
		||||
    - text: "argument x is overwritten before first use"
 | 
			
		||||
      linters:
 | 
			
		||||
        - staticcheck
 | 
			
		||||
    - text: "commentFormatting: put a space between `//` and comment text"
 | 
			
		||||
      linters:
 | 
			
		||||
        - gocritic
 | 
			
		||||
    - text: "exitAfterDefer:"
 | 
			
		||||
      linters:
 | 
			
		||||
        - gocritic
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										3
									
								
								.ignore
									
									
									
									
									
								
							
							
						
						
									
										3
									
								
								.ignore
									
									
									
									
									
								
							@@ -1,6 +1,9 @@
 | 
			
		||||
*.min.css
 | 
			
		||||
*.min.js
 | 
			
		||||
/assets/*.json
 | 
			
		||||
/modules/options/bindata.go
 | 
			
		||||
/modules/public/bindata.go
 | 
			
		||||
/modules/templates/bindata.go
 | 
			
		||||
/options/gitignore
 | 
			
		||||
/options/license
 | 
			
		||||
/public/assets
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								.mailmap
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								.mailmap
									
									
									
									
									
								
							@@ -1,2 +0,0 @@
 | 
			
		||||
Unknwon <u@gogs.io> <joe2010xtmf@163.com>
 | 
			
		||||
Unknwon <u@gogs.io> 无闻 <u@gogs.io>
 | 
			
		||||
							
								
								
									
										5
									
								
								.npmrc
									
									
									
									
									
								
							
							
						
						
									
										5
									
								
								.npmrc
									
									
									
									
									
								
							@@ -1,7 +1,6 @@
 | 
			
		||||
audit=false
 | 
			
		||||
fund=false
 | 
			
		||||
update-notifier=false
 | 
			
		||||
package-lock=true
 | 
			
		||||
save-exact=true
 | 
			
		||||
auto-install-peers=true
 | 
			
		||||
dedupe-peer-dependents=false
 | 
			
		||||
enable-pre-post-scripts=true
 | 
			
		||||
lockfile-version=3
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										497
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										497
									
								
								CHANGELOG.md
									
									
									
									
									
								
							@@ -4,473 +4,6 @@ This changelog goes through the changes that have been made in each release
 | 
			
		||||
without substantial changes to our git log; to see the highlights of what has
 | 
			
		||||
been added to each release, please refer to the [blog](https://blog.gitea.com).
 | 
			
		||||
 | 
			
		||||
## [1.24.0](https://github.com/go-gitea/gitea/releases/tag/1.24.0) - 2025-05-26
 | 
			
		||||
 | 
			
		||||
* BREAKING
 | 
			
		||||
  * Make Gitea always use its internal config, ignore `/etc/gitconfig` (#33076)
 | 
			
		||||
  * Improve log format (#33814)
 | 
			
		||||
  * Fix markdown render behaviors (#34122)
 | 
			
		||||
  * Add package version api endpoints (#34173)
 | 
			
		||||
 | 
			
		||||
* FEATURES
 | 
			
		||||
  * Enforce two-factor auth (2FA: TOTP or WebAuthn) (#34187)
 | 
			
		||||
  * Add fullscreen mode as a more efficient operation way to view projects (#34081)
 | 
			
		||||
  * Add anonymous access support for private/unlisted repositories (#34051)
 | 
			
		||||
  * Support public code/issue access for private repositories (#33127)
 | 
			
		||||
  * Add middleware for request prioritization (#33951)
 | 
			
		||||
  * Add cli flags LDAP group configuration (#33933)
 | 
			
		||||
  * Add file tree to file view page (#32721)
 | 
			
		||||
  * Add material icons for file list (#33837)
 | 
			
		||||
  * Artifacts download api for artifact actions v4 (#33510)
 | 
			
		||||
  * Support choose email when creating a commit via web UI (#33432)
 | 
			
		||||
  * Add basic auth support to rss/atom feeds (#33371)
 | 
			
		||||
  * Add sorting by exclusive labels (issue priority) (#33206)
 | 
			
		||||
  * Add sub issue list support (#32940)
 | 
			
		||||
  * Private README.md for organization (#32872)
 | 
			
		||||
  * Email option to embed images as base64 instead of link (#32061)
 | 
			
		||||
  * Option to delay conflict checking of old pull requests until page view (#27779)
 | 
			
		||||
  * Worktime tracking for the organization level (#19808)
 | 
			
		||||
 | 
			
		||||
* PERFORMANCE
 | 
			
		||||
  * Add cache for common package queries (#22491)
 | 
			
		||||
  * Move issue pin to an standalone table for querying performance (#33452)
 | 
			
		||||
  * Improve commits list performance to reduce unnecessary database queries (#33528)
 | 
			
		||||
  * Optimize total count of feed when loading activities in user dashboard. (#33841)
 | 
			
		||||
  * Optimize heatmap query (#33853)
 | 
			
		||||
  * Only use prev and next buttons for pagination on user dashboard (#33981)
 | 
			
		||||
  * Improve pull request list API performance (#34052)
 | 
			
		||||
  * Cache GPG keys, emails and users when list commits (#34086)
 | 
			
		||||
  * Refactor Git Attribute & performance optimization (#34154)
 | 
			
		||||
  * Performance optimization for tags synchronization (#34355) #34522
 | 
			
		||||
 | 
			
		||||
* ENHANCEMENTS
 | 
			
		||||
  * Code
 | 
			
		||||
    * Display when a release attachment was uploaded (#34261)
 | 
			
		||||
    * Support creating relative link to raw path in markdown (#34105)
 | 
			
		||||
    * Improve code block readability and isolate copy button (#34009)
 | 
			
		||||
    * Improve repository commit view (#33877)
 | 
			
		||||
    * Full-file syntax highlighting for diff pages (#33766)
 | 
			
		||||
    * Clone repository with Tea CLI (#33725)
 | 
			
		||||
    * Improve sync fork behavior (#33319)
 | 
			
		||||
    * Make git clone URL could use current signed-in user (#33091)
 | 
			
		||||
    * Add submodule diff links (#33097)
 | 
			
		||||
    * Link to tree views of submodules if possible (#33424)
 | 
			
		||||
    * Only keep popular licenses (#33832)
 | 
			
		||||
    * De-emphasize signed commits (#31160)
 | 
			
		||||
 | 
			
		||||
  * Actions
 | 
			
		||||
    * Add flat-square action badge style (#34062)
 | 
			
		||||
    * Update action status badge layout (#34018)
 | 
			
		||||
    * Download actions job logs from API (#33858)
 | 
			
		||||
    * Always show the "rerun" button for action jobs (#33692)
 | 
			
		||||
    * Add auto-expanding running actions step (#30058)
 | 
			
		||||
    * Update status check for all supported on.pull_request.types in Gitea (#33117)
 | 
			
		||||
    * Workflow_dispatch use workflow from trigger branch (#33098)
 | 
			
		||||
    * Add action auto-scroll (#30057)
 | 
			
		||||
    * Add workflow_job webhook (#33694)
 | 
			
		||||
    * Add a button editing action secret (#34462)
 | 
			
		||||
 | 
			
		||||
  * Pull Request
 | 
			
		||||
    * Auto expand "New PR" form (#33971)
 | 
			
		||||
    * Mark parent directory as viewed when all files are viewed (#33958)
 | 
			
		||||
    * Show info about maintainers are allowed to edit a PR (#33738)
 | 
			
		||||
    * Automerge supports deleting branch automatically after merging (#32343)
 | 
			
		||||
    * Add additional command hints for PowerShell & CMD (#33548)
 | 
			
		||||
 | 
			
		||||
  * Issues
 | 
			
		||||
    * Allow filtering issues by any assignee (#33343)
 | 
			
		||||
    * Show warning on navigation if currently editing comment or title (#32920)
 | 
			
		||||
    * Make tracked time representation display as hours (#33315)
 | 
			
		||||
    * Add No Results Prompt Message on Issue List Page (#33699)
 | 
			
		||||
    * Add sort option recentclose for issues and pulls (#34525) #34539
 | 
			
		||||
 | 
			
		||||
  * Packages
 | 
			
		||||
    * Link to nuget dependencies (#26554)
 | 
			
		||||
    * Add composor source field (#33502)
 | 
			
		||||
 | 
			
		||||
  * Administration
 | 
			
		||||
    * Improve navbar: add "admin" tip, add "active" style (#32927)
 | 
			
		||||
    * Add a option "--user-type bot" to admin user create, improve role display (#27885)
 | 
			
		||||
    * Improve admin user view page (#33735)
 | 
			
		||||
    * Support performance trace (#32973)
 | 
			
		||||
    * Change pprof labels to be prometheus compatible (#32865)
 | 
			
		||||
    * Allow admins and org owners to change org member public status (#28294)
 | 
			
		||||
    * Optimize the installation page (#32994)
 | 
			
		||||
    * Make public URL generation configurable (#34250)
 | 
			
		||||
    * Add a --fullname arg to gitea admin user create. (#34241)
 | 
			
		||||
 | 
			
		||||
  * Others
 | 
			
		||||
    * Improve oauth2 error handling (#33969)
 | 
			
		||||
    * Fail mirroring more gracefully (#34002)
 | 
			
		||||
    * Align User Details Page Header Layout with Design Specifications (#34192)
 | 
			
		||||
    * Webhook add X-Gitea-Hook-Installation-Target-Type Header (#33752)
 | 
			
		||||
    * Optimize the dashboard (#32990)
 | 
			
		||||
    * Improve button layout on small screens (#33633)
 | 
			
		||||
    * Add cropping support when modifying the user/org/repo avatar (#33498)
 | 
			
		||||
    * Make ROOT_URL support using request Host header (#32564)
 | 
			
		||||
    * Add `show more` organizations icon in user's profile (#32986)
 | 
			
		||||
    * Introduce `--page-space-bottom` at 64px (#30692)
 | 
			
		||||
    * Improve theme display (#30671)
 | 
			
		||||
    * Add alphabetical project sorting (#33504)
 | 
			
		||||
    * Add global lock for migrations to make upgrade more safe with multiple replications (#33706)
 | 
			
		||||
    * Add descriptions for private repo public access settings and improve the UI (#34057)
 | 
			
		||||
 | 
			
		||||
* API
 | 
			
		||||
  * Actions Runner rest api (#33873)
 | 
			
		||||
  * Inclusion of rename organization api (#33303)
 | 
			
		||||
  * Add API to support link package to repository and unlink it (#33481)
 | 
			
		||||
  * Add API endpoint to request contents of multiple files simultaniously (#34139)
 | 
			
		||||
  * Actions artifacts API list/download check status upload confirmed (#34273)
 | 
			
		||||
  * Add API routes to lock and unlock issues (#34165)
 | 
			
		||||
  * Fix some user name usages (#33689)
 | 
			
		||||
  * Allow filtering /repos/{owner}/{repo}/pulls by target base branch queryparam (#33684)
 | 
			
		||||
  * Improve swagger generation (#33664)
 | 
			
		||||
  * Support Ephemeral action runners (#33570)
 | 
			
		||||
  * Support workflow event dispatch via API (#33545)
 | 
			
		||||
  * Support workflow event dispatch via API (#32059)
 | 
			
		||||
  * Added Description Field for Secrets and Variables  (#33526)
 | 
			
		||||
  * Reject star-related requests if stars are disabled (#33208)
 | 
			
		||||
  * Let API create and edit system webhooks, attempt 2 (#33180)
 | 
			
		||||
  * Use `Project-URL` metadata field to get a PyPI package's homepage URL (#33089)
 | 
			
		||||
  * Add `last_committer_date` and `last_author_date` for file contents API (#32921)
 | 
			
		||||
 | 
			
		||||
* REFACTORS
 | 
			
		||||
  * Remove context from git struct (#33793)
 | 
			
		||||
  * Refactor admin/common.ts (#33788)
 | 
			
		||||
  * Refactor repo-settings.ts (#33785)
 | 
			
		||||
  * Refactor repo-issue.ts (#33784)
 | 
			
		||||
  * Small refactor to reduce unnecessary database queries and remove duplicated functions (#33779)
 | 
			
		||||
  * Refactor initRepoBranchTagSelector to use new init framework (#33776)
 | 
			
		||||
  * Refactor buttons to use new init framework (#33774)
 | 
			
		||||
  * Refactor markup and pdf-viewer to use new init framework (#33772)
 | 
			
		||||
  * Refactor error system (#33771)
 | 
			
		||||
  * Refactor mail code (#33768)
 | 
			
		||||
  * Update TypeScript types (#33799)
 | 
			
		||||
  * Refactor older tests to use testify (#33140)
 | 
			
		||||
  * Move notifywatch to service layer (#33825)
 | 
			
		||||
  * Decouple context from repository related structs (#33823)
 | 
			
		||||
  * Remove context from mail struct (#33811)
 | 
			
		||||
  * Refactor dropdown ellipsis (#34123)
 | 
			
		||||
  * Refactor functions to reduce repopath expose (#33892)
 | 
			
		||||
  * Refactor repo-diff.ts (#33746)
 | 
			
		||||
  * Refactor web route handler (#33488)
 | 
			
		||||
  * Refactor user & avatar (#33433)
 | 
			
		||||
  * Refactor user package (#33423)
 | 
			
		||||
  * Refactor decouple context from migration structs (#33399)
 | 
			
		||||
  * Refactor context flash msg and global variables (#33375)
 | 
			
		||||
  * Refactor response writer & access logger (#33323)
 | 
			
		||||
  * Refactor ref type (#33242)
 | 
			
		||||
  * Refactor context repository (#33202)
 | 
			
		||||
  * Refactor legacy JS (#33115)
 | 
			
		||||
  * Refactor legacy line-number and scroll code (#33094)
 | 
			
		||||
  * Refactor env var related code (#33075)
 | 
			
		||||
  * Move SetMerged to service layer (#33045)
 | 
			
		||||
  * Merge updatecommentattachment functions (#33044)
 | 
			
		||||
  * Refactor pull-request compare&create page (#33071)
 | 
			
		||||
  * Refactor repo-new.ts (#33070)
 | 
			
		||||
  * Refactor pagination (#33037)
 | 
			
		||||
  * Refactor tests (#33021)
 | 
			
		||||
  * Refactor markup render to fix various path problems (#34114)
 | 
			
		||||
  * Refactor Branch struct in package modules/git (#33980)
 | 
			
		||||
  * Don't create duplicated functions for code repositories and wiki repositories (#33924)
 | 
			
		||||
  * Move git references checking to gitrepo packages to reduce expose of repository path (#33891)
 | 
			
		||||
  * Refactor cache-control (#33861)
 | 
			
		||||
  * Decouple diff stats query from actual diffing (#33810)
 | 
			
		||||
  * Move part of updating protected branch logic to service layer (#33742)
 | 
			
		||||
  * Decouple Batch from git.Repository to simplify usage without requiring the creation of a Repository struct. (#34001)
 | 
			
		||||
  * Refactor tmpl and blob_excerpt (#32967)
 | 
			
		||||
  * Refactor template & test related code (#32938)
 | 
			
		||||
  * Refactor db package and remove unnecessary `DumpTables` (#32930)
 | 
			
		||||
  * Refactor pprof labels and process desc (#32909)
 | 
			
		||||
  * Refactor repo-projects.ts (#32892)
 | 
			
		||||
  * Refactor getpatch/getdiff functions and remove unnecessary fallback (#32817)
 | 
			
		||||
  * Uniform all temporary directories and allow customizing temp path (#32352)
 | 
			
		||||
  * Remove context from retry downloader (#33871)
 | 
			
		||||
  * Refactor global init code and add more comments (#33755)
 | 
			
		||||
  * Remove some unnecessary template helpers (#33069)
 | 
			
		||||
  * Move and rename UpdateRepository (#34136)
 | 
			
		||||
  * Move hooks function to gitrepo and reduce expose repopath (#33890)
 | 
			
		||||
  * Add abstraction layer to delete repository from disk (#33879)
 | 
			
		||||
  * Add abstraction layer to check if the repository exists on disk (#33874)
 | 
			
		||||
  * Move ParseCommitWithSSHSignature to service layer (#34087)
 | 
			
		||||
  * Move duplicated functions (#33977)
 | 
			
		||||
  * Extract code to their own functions for push update (#33944)
 | 
			
		||||
  * Move gitgraph from modules to services layer (#33527)
 | 
			
		||||
  * Move commits signature and verify functions to service layers (#33605)
 | 
			
		||||
  * Use `CloseIssue` and `ReopenIssue` instead of `ChangeStatus` (#32467)
 | 
			
		||||
  * Refactor arch route handlers (#32993)
 | 
			
		||||
  * Refactor "string truncate" (#32984)
 | 
			
		||||
  * Refactor arch route handlers (#32972)
 | 
			
		||||
  * Clarify path param naming (#32969)
 | 
			
		||||
  * Refactor request context (#32956)
 | 
			
		||||
  * Move some errors to their own sub packages (#32880)
 | 
			
		||||
  * Move RepoTransfer from models to models/repo sub package (#32506)
 | 
			
		||||
  * Move delete deploy keys into service layer (#32201)
 | 
			
		||||
  * Refactor webhook events (#33337)
 | 
			
		||||
  * Move some Actions related functions from `routers` to `services` (#33280)
 | 
			
		||||
  * Refactor RefName (#33234)
 | 
			
		||||
  * Refactor context RefName and RepoAssignment (#33226)
 | 
			
		||||
  * Refactor repository transfer (#33211)
 | 
			
		||||
  * Refactor error system (#33626)
 | 
			
		||||
  * Refactor error system (#33610)
 | 
			
		||||
  * Refactor package (routes and error handling, npm peer dependency) (#33111)
 | 
			
		||||
  * Use test context in tests and new loop system in benchmarks (#33648)
 | 
			
		||||
  * Some small refactors (#33144)
 | 
			
		||||
  * Simplify context ref name (#33267)
 | 
			
		||||
 | 
			
		||||
* BUGFIXES
 | 
			
		||||
  * Fix some dropdown problems on the issue sidebar (#34308) #34327
 | 
			
		||||
  * Do not return archive download URLs in API if downloads are disabled (#34324) #34338
 | 
			
		||||
  * Fix LFS files being editable in web UI (#34356) #34362
 | 
			
		||||
  * Fix only text/* being viewable in web UI (#34374) #34378
 | 
			
		||||
  * Fix LFS file not stored in LFS when uploaded/edited via API or web UI (#34367)
 | 
			
		||||
  * Grey out expired artifact on Artifacts list (#34314) #34404
 | 
			
		||||
  * Fix incorrect divergence cache after switching default branch (#34370) #34406
 | 
			
		||||
  * Refactor commit message rendering and fix bugs (#34412) #34414
 | 
			
		||||
  * Merge and tweak markup editor expander CSS (#34409) #34415
 | 
			
		||||
  * Fix GetUsersByEmails (#34423) #34425
 | 
			
		||||
  * Only git operations should update last changed of a repository (#34388) #34427
 | 
			
		||||
  * Fix comment textarea scroll issue in Firefox (#34438) #34446
 | 
			
		||||
  * Fix repo broken check (#34444) #34452
 | 
			
		||||
  * Fix remove org user failure on mssql (#34449) #34453
 | 
			
		||||
  * Fix Workflow run Not Found page (#34459) #34466
 | 
			
		||||
  * When updating comment, if the content is the same, just return and not update the database (#34422) #34464
 | 
			
		||||
  * Fix project board view (#34470) #34475
 | 
			
		||||
  * Fix get / delete runner to use consistent http 404 and 500 status (#34480) #34488
 | 
			
		||||
  * Fix url validation in webhook add/edit API (#34492) #34496
 | 
			
		||||
  * Fix edithook api can not update package, status and workflow_job events (#34495) #34499
 | 
			
		||||
  * Fix ephemeral runner deletion (#34447) #34513
 | 
			
		||||
  * Don't display error log when .git-blame-ignore-revs doesn't exist (#34457)
 | 
			
		||||
  * Only allow admins to rename default/protected branches (#33276)
 | 
			
		||||
  * Improve "lock conversation" UI (#34207)
 | 
			
		||||
  * Fix incorrect file links (#34189)
 | 
			
		||||
  * Optimize Overflow Menu (#34183)
 | 
			
		||||
  * Check user/org repo limit instead of doer (#34147)
 | 
			
		||||
  * Make markdown render match GitHub's behavior (#34129)
 | 
			
		||||
  * Fix team permission (#34128)
 | 
			
		||||
  * Correctly handle submodule view and avoid throwing 500 error (#34121)
 | 
			
		||||
  * Fix users being able bypass limits with repo transfers (#34031)
 | 
			
		||||
  * Avoid creating unnecessary temporary cat file sub process (#33942)
 | 
			
		||||
  * Refactor organization menu (#33928)
 | 
			
		||||
  * Fix various Fomantic UI and htmx problems (#33851)
 | 
			
		||||
  * Fix 500 error when error occurred in migration page (#33256)
 | 
			
		||||
  * Validate that the tag doesn't exist when creating a tag via the web (#33241)
 | 
			
		||||
  * Add missed transaction on setmerged (#33079)
 | 
			
		||||
  * Rework create/fork/adopt/generate repository to make sure resources will be cleanup once failed (#31035)
 | 
			
		||||
  * Valid email address should only start with alphanumeric (#28174)
 | 
			
		||||
  * Fix webhook url (#34186)
 | 
			
		||||
  * Fix "toAbsoluteLocaleDate" test when system locale is not en-US (#33939)
 | 
			
		||||
  * Fix file name could not be searched if the file was not a text file when using the Bleve indexer (#33959)
 | 
			
		||||
  * Fix cannot delete runners via the modal dialog (#33895)
 | 
			
		||||
  * Fix unpin hint on the pinned pull requests (#33207)
 | 
			
		||||
  * Fix parentCommit invalid memory address or nil pointer dereference. (#33204)
 | 
			
		||||
  * Fix comment header padding (#33377)
 | 
			
		||||
  * Fix some migration and repo name problems (#33986)
 | 
			
		||||
  * Fix various trivial frontend problems (#34263)
 | 
			
		||||
  * Fix Set Email Preference dropdown and button placement (#34255)
 | 
			
		||||
  * Fix quoted replies incorrectly render user input as part of the quote (#34216)
 | 
			
		||||
  * Fix button alignments and remove unnecessary styles (#34206)
 | 
			
		||||
  * Restore form inputs on organization create error (#34201)
 | 
			
		||||
  * Try to fix ACME (3rd) (#33807)
 | 
			
		||||
  * Fix incorrect ref "blob" (#33240)
 | 
			
		||||
  * Fix dynamic content loading init problem (#33748)
 | 
			
		||||
  * Fix git empty check and HEAD request (#33690)
 | 
			
		||||
  * Fix Untranslated Text on Actions Page (#33635)
 | 
			
		||||
  * Fix issue label delete incorrect labels webhook payload (#34575)
 | 
			
		||||
  * Fix incorrect page navigation with up and down arrow on last item of dashboard repos (#34570)
 | 
			
		||||
  * Fix/improve avatar sync from LDAP (#34573)
 | 
			
		||||
  * Fix some trivial problems (#34579)
 | 
			
		||||
  * Retain issue sort type when a keyword search is introduced (#34559)
 | 
			
		||||
  * Always use an empty line to separate the commit message and trailer (#34512)
 | 
			
		||||
  * Fix line-button issue after file selection in file tree (#34574)
 | 
			
		||||
  * Fix doctor deleting orphaned issues attachments (#34142)
 | 
			
		||||
  * Add webhook assigning test and fix possible bug (#34420)
 | 
			
		||||
  * Fix possible nil description of pull request when migrating from CodeCommit (#34541)
 | 
			
		||||
  * Refactor commit reader (#34542)
 | 
			
		||||
  * Fix possible pull request broken when leave the page immediately after clicking the update button #34509
 | 
			
		||||
  * Ignore "Close" error when uploading container blob (#34620)
 | 
			
		||||
  * Fix missed merge commit sha and time when migrating from codecommit (#34645)
 | 
			
		||||
  * Fix GetUsersByEmails (#34643)
 | 
			
		||||
  * Misc CSS fixes (#34638)
 | 
			
		||||
  * Add codecommit to supported services in api docs (#34626)
 | 
			
		||||
  * Validate hex colors when creating/editing labels (#34623)
 | 
			
		||||
  * Fix possible pull request broken when leave the page immediately after clicking the update button (#34509)
 | 
			
		||||
  * Fix margin issue in markup paragraph rendering (#34599)
 | 
			
		||||
  * Fix migration pull request title too long (#34577)
 | 
			
		||||
  * Fix footnote jump behavior on the issue page. (#34621)
 | 
			
		||||
  * Fix "oras" OCI client compatibility (#34666)
 | 
			
		||||
  * Fix last admin check when syncing users (#34649)
 | 
			
		||||
  * Fix skip paths check on tag push events in workflows (#34602) #34670
 | 
			
		||||
 | 
			
		||||
* MISC
 | 
			
		||||
 | 
			
		||||
  * Bump to alpine 3.22 (#34613)
 | 
			
		||||
  * Make pull request and issue history more compact (#34588)
 | 
			
		||||
  * Run integration tests against postgres 14 (#34514) #34536
 | 
			
		||||
  * Enable addtional linters (#34085)
 | 
			
		||||
  * Enable testifylint rules (#34075)
 | 
			
		||||
  * Enable staticcheck QFxxxx rules (#34064)
 | 
			
		||||
  * Improve Actions test (#32883)
 | 
			
		||||
  * Drop fomantic build (#33845)
 | 
			
		||||
  * Go1.24 (#33562)
 | 
			
		||||
  * Run yamllint with strict mode, fix issue (#33551)
 | 
			
		||||
  * Disable cron task to update license (#33486)
 | 
			
		||||
  * Optimize makefile help information generation (#33390)
 | 
			
		||||
  * Convert github.com/xanzy/go-gitlab into gitlab.com/gitlab-org/api/client-go (#33126)
 | 
			
		||||
  * Add missed changelogs (#33649)
 | 
			
		||||
  * Update .changelog file to add performance label group (#33472)
 | 
			
		||||
  * Add missing POPULATE_SQUASH_COMMENT_WITH_COMMIT_MESSAGES in app.example.ini (#33363)
 | 
			
		||||
  * Update README screenshots (#33347)
 | 
			
		||||
  * Update unrs-resolver (#34279)
 | 
			
		||||
  * Update go&js dependencies (#34262)
 | 
			
		||||
  * Optimize the calling code of queryElems (#34235)
 | 
			
		||||
  * Update protected_branch.tmpl (#34193)
 | 
			
		||||
  * Feat/optimize span svg layout (#34185)
 | 
			
		||||
  * Set MERMAID_MAX_SOURCE_CHARACTERS to 50000 (#34152)
 | 
			
		||||
  * Update JS and PY deps (#34143)
 | 
			
		||||
  * Add Chinese translations for README files (#34132)
 | 
			
		||||
  * Use `overflow-wrap: anywhere` to replace `word-break: break-all` (#34126)
 | 
			
		||||
  * Clarify ownership in password change error messages (#34092)
 | 
			
		||||
  * Add toggleClass function in dom.ts (#34063)
 | 
			
		||||
  * Update to golangci-lint v2 (#34054)
 | 
			
		||||
  * Update Makefile test comments (#34013)
 | 
			
		||||
  * Update go mod dependencies (#33988)
 | 
			
		||||
  * Use filepath.Join instead of path.Join for file system file operations (#33978)
 | 
			
		||||
  * Prepare common tmpl functions in a middleware (#33957)
 | 
			
		||||
  * Remove unused or abused styles (#33918)
 | 
			
		||||
  * Update JS and PY deps, misc tweaks (#33903)
 | 
			
		||||
  * Try to figure out attribute checker problem (#33901)
 | 
			
		||||
  * Add lock for a repository pull mirror (#33876)
 | 
			
		||||
  * Fine tune push mirror UI (#33866)
 | 
			
		||||
  * Improve issue & code search (#33860)
 | 
			
		||||
  * Use pullrequestlist instead of []*pullrequest (#33765)
 | 
			
		||||
  * Upgrade act to 0.261.4 and actions-proto-go to v0.4.1 (#33760)
 | 
			
		||||
  * Align sidebar gears to the right (#33721)
 | 
			
		||||
  * Update Go dependencies (skip blevesearch, meilisearch) (#33655)
 | 
			
		||||
  * Add migrations and doctor fixes (#33556)
 | 
			
		||||
  * Remove "class-name" from svg icon (#33540)
 | 
			
		||||
  * Update MAINTAINERS (#33529)
 | 
			
		||||
  * Add "No data available" display when list is empty (#33517)
 | 
			
		||||
  * Use `git diff-tree` for `DiffFileTree` on diff pages (#33514)
 | 
			
		||||
  * Give organisation members access to organisation feeds (#33508)
 | 
			
		||||
  * Update feishu icon (#33470)
 | 
			
		||||
  * Hide/disable unusable UI elements when a repository is archived (#33459)
 | 
			
		||||
  * Update `@github/text-expander-element` to 2.9.0 (#33435)
 | 
			
		||||
  * Do not access GitRepo when a repo is being created (#33380)
 | 
			
		||||
  * Fix incorrect ref usages (#33301)
 | 
			
		||||
  * Prepare for support performance trace (#33286)
 | 
			
		||||
  * Enable Typescript `noImplicitThis` (#33250)
 | 
			
		||||
  * Remove unused CSS styles and move some styles to proper files (#33217)
 | 
			
		||||
  * Add .run to gitignore (#33175)
 | 
			
		||||
  * Fix typo in gitea downloader test and add missing codebase in `ToGitServiceType` (#33146)
 | 
			
		||||
  * Remove extended glob pattern from branch protection UI (#33125)
 | 
			
		||||
  * Clean up legacy form CSS styles (#33081)
 | 
			
		||||
  * Unset XDG_HOME_CONFIG as gitea manages configuration locations (#33067)
 | 
			
		||||
  * Add IntelliJ Gateway's .uuid to gitignore (#33052)
 | 
			
		||||
  * User facing messages for AGit errors (#33012)
 | 
			
		||||
  * Always show assignees on right (#33006)
 | 
			
		||||
  * Fix eslint (#33002)
 | 
			
		||||
  * Update JS dependencies (#32914)
 | 
			
		||||
  * Bump x/net (#32896) (#32900)
 | 
			
		||||
  * Only activity tab needs heatmap data loading (#34652)
 | 
			
		||||
 | 
			
		||||
## [1.23.8](https://github.com/go-gitea/gitea/releases/tag/1.23.8) - 2025-05-11
 | 
			
		||||
 | 
			
		||||
* SECURITY
 | 
			
		||||
  * Fix a bug when uploading file via lfs ssh command (#34408) (#34411)
 | 
			
		||||
  * Update net package (#34228) (#34232)
 | 
			
		||||
* BUGFIXES
 | 
			
		||||
  * Fix releases sidebar navigation link (#34436) #34439
 | 
			
		||||
  * Fix bug webhook milestone is not right. (#34419) #34429
 | 
			
		||||
  * Fix two missed null value checks on the wiki page. (#34205) (#34215)
 | 
			
		||||
  * Swift files can be passed either as file or as form value (#34068) (#34236)
 | 
			
		||||
  * Fix bug when API get pull changed files for deleted head repository (#34333) (#34368)
 | 
			
		||||
  * Upgrade github v61 -> v71 to fix migrating bug (#34389)
 | 
			
		||||
  * Fix bug when visiting comparation page (#34334) (#34364)
 | 
			
		||||
  * Fix wrong review requests when updating the pull request (#34286) (#34304)
 | 
			
		||||
  * Fix github migration error when using multiple tokens (#34144) (#34302)
 | 
			
		||||
  * Explicitly not update indexes when sync database schemas (#34281) (#34295)
 | 
			
		||||
  * Fix panic when comment is nil (#34257) (#34277)
 | 
			
		||||
  * Fix project board links to related Pull Requests (#34213) (#34222)
 | 
			
		||||
  * Don't assume the default wiki branch is master in the wiki API (#34244) (#34245)
 | 
			
		||||
* DOCUMENTATION
 | 
			
		||||
  * Update token creation API swagger documentation (#34288) (#34296)
 | 
			
		||||
* MISC
 | 
			
		||||
  * Fix CI Build (#34315)
 | 
			
		||||
  * Add riscv64 support (#34199) (#34204)
 | 
			
		||||
  * Bump go version in go.mod (#34160)
 | 
			
		||||
  * remove hardcoded 'code' string in clone_panel.tmpl (#34153) (#34158)
 | 
			
		||||
 | 
			
		||||
## [1.23.7](https://github.com/go-gitea/gitea/releases/tag/1.23.7) - 2025-04-07
 | 
			
		||||
 | 
			
		||||
* Enhancements
 | 
			
		||||
  * Add a config option to block "expensive" pages for anonymous users (#34024) (#34071)
 | 
			
		||||
  * Also check default ssh-cert location for host (#34099) (#34100) (#34116)
 | 
			
		||||
* BUGFIXES
 | 
			
		||||
  * Fix discord webhook 400 status code when description limit is exceeded (#34084) (#34124)
 | 
			
		||||
  * Get changed files based on merge base when checking `pull_request` actions trigger (#34106) (#34120)
 | 
			
		||||
  * Fix invalid version in RPM package path (#34112) (#34115)
 | 
			
		||||
  * Return default avatar url when user id is zero rather than updating database (#34094) (#34095)
 | 
			
		||||
  * Add additional ReplaceAll in pathsep to cater for different pathsep (#34061) (#34070)
 | 
			
		||||
  * Try to fix check-attr bug (#34029) (#34033)
 | 
			
		||||
  * Git client will follow 301 but 307 (#34005) (#34010)
 | 
			
		||||
  * Fix block expensive for 1.23 (#34127)
 | 
			
		||||
  * Fix markdown frontmatter rendering (#34102) (#34107)
 | 
			
		||||
  * Add new CLI flags to set name and scopes when creating a user with access token (#34080) (#34103)
 | 
			
		||||
  * Do not show 500 error when default branch doesn't exist (#34096) (#34097)
 | 
			
		||||
  * Hide activity contributors, recent commits and code frequrency left tabs if there is no code permission (#34053) (#34065)
 | 
			
		||||
  * Simplify emoji rendering (#34048) (#34049)
 | 
			
		||||
  * Adjust the layout of the toolbar on the Issues/Projects page (#33667) (#34047)
 | 
			
		||||
  * Pull request updates will also trigger code owners review requests (#33744) (#34045)
 | 
			
		||||
  * Fix org repo creation being limited by user limits (#34030) (#34044)
 | 
			
		||||
  * Fix git client accessing renamed repo (#34034) (#34043)
 | 
			
		||||
  * Fix the issue with error message logging for the `check-attr` command on Windows OS. (#34035) (#34036)
 | 
			
		||||
  * Polyfill WeakRef (#34025) (#34028)
 | 
			
		||||
 | 
			
		||||
## [1.23.6](https://github.com/go-gitea/gitea/releases/tag/v1.23.6) - 2025-03-24
 | 
			
		||||
 | 
			
		||||
* SECURITY
 | 
			
		||||
  * Fix LFS URL (#33840) (#33843)
 | 
			
		||||
  * Update jwt and redis packages (#33984) (#33987)
 | 
			
		||||
  * Update golang crypto and net (#33989)
 | 
			
		||||
* BUGFIXES
 | 
			
		||||
  * Drop timeout for requests made to the internal hook api (#33947) (#33970)
 | 
			
		||||
  * Fix maven panic when no package exists (#33888) (#33889)
 | 
			
		||||
  * Fix markdown render (#33870) (#33875)
 | 
			
		||||
  * Fix auto concurrency cancellation skips commit status updates (#33764) (#33849)
 | 
			
		||||
  * Fix oauth2 auth (#33961) (#33962)
 | 
			
		||||
  * Fix incorrect 1.23 translations (#33932)
 | 
			
		||||
  * Try to figure out attribute checker problem (#33901) (#33902)
 | 
			
		||||
  * Ignore trivial errors when updating push data (#33864) (#33887)
 | 
			
		||||
  * Fix some UI problems for 1.23 (#33856)
 | 
			
		||||
  * Removing unwanted ui container (#33833) (#33835)
 | 
			
		||||
  * Support disable passkey auth (#33348) (#33819)
 | 
			
		||||
  * Do not call "git diff" when listing PRs (#33817)
 | 
			
		||||
  * Try to fix ACME (3rd) (#33807) (#33808)
 | 
			
		||||
  * Fix incorrect code search indexer options (#33992) #33999
 | 
			
		||||
 | 
			
		||||
## [1.23.5](https://github.com/go-gitea/gitea/releases/tag/v1.23.5) - 2025-03-04
 | 
			
		||||
 | 
			
		||||
* SECURITY
 | 
			
		||||
  * Bump x/oauth2 & x/crypto (#33704) (#33727)
 | 
			
		||||
* PERFORMANCE
 | 
			
		||||
  * Optimize user dashboard loading (#33686) (#33708)
 | 
			
		||||
* BUGFIXES
 | 
			
		||||
  * Fix navbar dropdown item align (#33782)
 | 
			
		||||
  * Fix inconsistent closed issue list icon (#33722) (#33728)
 | 
			
		||||
  * Fix for Maven Package Naming Convention Handling (#33678) (#33679)
 | 
			
		||||
  * Improve Open-with URL encoding (#33666) (#33680)
 | 
			
		||||
  * Deleting repository should unlink all related packages (#33653) (#33673)
 | 
			
		||||
  * Fix omitempty bug (#33663) (#33670)
 | 
			
		||||
  * Upgrade go-crypto from 1.1.4 to 1.1.6 (#33745) (#33754)
 | 
			
		||||
  * Fix OCI image.version annotation for releases to use full semver (#33698) (#33701)
 | 
			
		||||
  * Try to fix ACME path when renew (#33668) (#33693)
 | 
			
		||||
  * Fix mCaptcha bug (#33659) (#33661)
 | 
			
		||||
  * Git graph: don't show detached commits (#33645) (#33650)
 | 
			
		||||
  * Use MatchPhraseQuery for bleve code search (#33628)
 | 
			
		||||
  * Adjust appearence of commit status webhook (#33778) #33789
 | 
			
		||||
  * Upgrade golang net from 0.35.0 -> 0.36.0 (#33795) #33796
 | 
			
		||||
 | 
			
		||||
## [1.23.4](https://github.com/go-gitea/gitea/releases/tag/v1.23.4) - 2025-02-16
 | 
			
		||||
 | 
			
		||||
* SECURITY
 | 
			
		||||
@@ -952,36 +485,6 @@ been added to each release, please refer to the [blog](https://blog.gitea.com).
 | 
			
		||||
  * 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
 | 
			
		||||
 
 | 
			
		||||
@@ -30,7 +30,7 @@ These are the values to which people in the Gitea community should aspire.
 | 
			
		||||
- **Be constructive.**
 | 
			
		||||
  - Avoid derailing: stay on topic; if you want to talk about something else, start a new conversation.
 | 
			
		||||
  - Avoid unconstructive criticism: don't merely decry the current state of affairs; offer—or at least solicit—suggestions as to how things may be improved.
 | 
			
		||||
  - Avoid snarking (pithy, unproductive, sniping comments).
 | 
			
		||||
  - Avoid snarking (pithy, unproductive, sniping comments)
 | 
			
		||||
  - Avoid discussing potentially offensive or sensitive issues; this all too often leads to unnecessary conflict.
 | 
			
		||||
  - Avoid microaggressions (brief and commonplace verbal, behavioral and environmental indignities that communicate hostile, derogatory or negative slights and insults to a person or group).
 | 
			
		||||
- **Be responsible.**
 | 
			
		||||
@@ -42,7 +42,7 @@ People are complicated. You should expect to be misunderstood and to misundersta
 | 
			
		||||
 | 
			
		||||
### Our Pledge
 | 
			
		||||
 | 
			
		||||
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to make participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
 | 
			
		||||
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
 | 
			
		||||
 | 
			
		||||
### Our Standards
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -182,7 +182,7 @@ Here's how to run the test suite:
 | 
			
		||||
 | 
			
		||||
## Translation
 | 
			
		||||
 | 
			
		||||
All translation work happens on [Crowdin](https://translate.gitea.com).
 | 
			
		||||
All translation work happens on [Crowdin](https://crowdin.com/project/gitea).
 | 
			
		||||
The only translation that is maintained in this repository is [the English translation](https://github.com/go-gitea/gitea/blob/main/options/locale/locale_en-US.ini).
 | 
			
		||||
It is synced regularly with Crowdin. \
 | 
			
		||||
Other locales on main branch **should not** be updated manually as they will be overwritten with each sync. \
 | 
			
		||||
@@ -591,7 +591,7 @@ be reviewed by two maintainers and must pass the automatic tests.
 | 
			
		||||
## Releasing Gitea
 | 
			
		||||
 | 
			
		||||
- Let $vmaj, $vmin and $vpat be Major, Minor and Patch version numbers, $vpat should be rc1, rc2, 0, 1, ...... $vmaj.$vmin will be kept the same as milestones on github or gitea in future.
 | 
			
		||||
- Before releasing, confirm all the version's milestone issues or PRs has been resolved. Then discuss the release on Discord channel #maintainers and get agreed with almost all the owners and mergers. Or you can declare the version and if nobody is against it in about several hours.
 | 
			
		||||
- Before releasing, confirm all the version's milestone issues or PRs has been resolved. Then discuss the release on Discord channel #maintainers and get agreed with almost all the owners and mergers. Or you can declare the version and if nobody against in about serval hours.
 | 
			
		||||
- If this is a big version first you have to create PR for changelog on branch `main` with PRs with label `changelog` and after it has been merged do following steps:
 | 
			
		||||
  - Create `-dev` tag as `git tag -s -F release.notes v$vmaj.$vmin.0-dev` and push the tag as `git push origin v$vmaj.$vmin.0-dev`.
 | 
			
		||||
  - When CI has finished building tag then you have to create a new branch named `release/v$vmaj.$vmin`
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										50
									
								
								Dockerfile
									
									
									
									
									
								
							
							
						
						
									
										50
									
								
								Dockerfile
									
									
									
									
									
								
							@@ -1,8 +1,8 @@
 | 
			
		||||
# syntax=docker/dockerfile:1
 | 
			
		||||
# Build stage
 | 
			
		||||
FROM docker.io/library/golang:1.25-alpine3.22 AS build-env
 | 
			
		||||
FROM docker.io/library/golang:1.23-alpine3.21 AS build-env
 | 
			
		||||
 | 
			
		||||
ARG GOPROXY=direct
 | 
			
		||||
ARG GOPROXY
 | 
			
		||||
ENV GOPROXY=${GOPROXY:-direct}
 | 
			
		||||
 | 
			
		||||
ARG GITEA_VERSION
 | 
			
		||||
ARG TAGS="sqlite sqlite_unlock_notify"
 | 
			
		||||
@@ -14,32 +14,35 @@ RUN apk --no-cache add \
 | 
			
		||||
    build-base \
 | 
			
		||||
    git \
 | 
			
		||||
    nodejs \
 | 
			
		||||
    pnpm
 | 
			
		||||
    npm \
 | 
			
		||||
    && rm -rf /var/cache/apk/*
 | 
			
		||||
 | 
			
		||||
# Setup repo
 | 
			
		||||
COPY . ${GOPATH}/src/code.gitea.io/gitea
 | 
			
		||||
WORKDIR ${GOPATH}/src/code.gitea.io/gitea
 | 
			
		||||
# Use COPY but not "mount" because some directories like "node_modules" contain platform-depended contents and these directories need to be ignored.
 | 
			
		||||
# ".git" directory will be mounted later separately for getting version data.
 | 
			
		||||
# TODO: in the future, maybe we can pre-build the frontend assets on one platform and share them for different platforms, the benefit is that it won't be affected by webpack plugin compatibility problems, then the working directory can be fully mounted and the COPY is not needed.
 | 
			
		||||
COPY --exclude=.git/ . .
 | 
			
		||||
 | 
			
		||||
# Build gitea, .git mount is required for version data
 | 
			
		||||
RUN --mount=type=cache,target=/go/pkg/mod \
 | 
			
		||||
    --mount=type=cache,target="/root/.cache/go-build" \
 | 
			
		||||
    --mount=type=cache,target=/root/.local/share/pnpm/store \
 | 
			
		||||
    --mount=type=bind,source=".git/",target=".git/" \
 | 
			
		||||
    make
 | 
			
		||||
# Checkout version if set
 | 
			
		||||
RUN if [ -n "${GITEA_VERSION}" ]; then git checkout "${GITEA_VERSION}"; fi \
 | 
			
		||||
 && make clean-all build
 | 
			
		||||
 | 
			
		||||
# Begin env-to-ini build
 | 
			
		||||
RUN go build contrib/environment-to-ini/environment-to-ini.go
 | 
			
		||||
 | 
			
		||||
# Copy local files
 | 
			
		||||
COPY docker/root /tmp/local
 | 
			
		||||
 | 
			
		||||
# Set permissions for builds that made under windows which strips the executable bit from file
 | 
			
		||||
# Set permissions
 | 
			
		||||
RUN chmod 755 /tmp/local/usr/bin/entrypoint \
 | 
			
		||||
              /tmp/local/usr/local/bin/* \
 | 
			
		||||
              /tmp/local/usr/local/bin/gitea \
 | 
			
		||||
              /tmp/local/etc/s6/gitea/* \
 | 
			
		||||
              /tmp/local/etc/s6/openssh/* \
 | 
			
		||||
              /tmp/local/etc/s6/.s6-svscan/* \
 | 
			
		||||
              /go/src/code.gitea.io/gitea/gitea
 | 
			
		||||
              /go/src/code.gitea.io/gitea/gitea \
 | 
			
		||||
              /go/src/code.gitea.io/gitea/environment-to-ini
 | 
			
		||||
RUN chmod 644 /go/src/code.gitea.io/gitea/contrib/autocompletion/bash_autocomplete
 | 
			
		||||
 | 
			
		||||
FROM docker.io/library/alpine:3.22 AS gitea
 | 
			
		||||
FROM docker.io/library/alpine:3.21
 | 
			
		||||
LABEL maintainer="maintainers@gitea.io"
 | 
			
		||||
 | 
			
		||||
EXPOSE 22 3000
 | 
			
		||||
 | 
			
		||||
@@ -54,7 +57,8 @@ RUN apk --no-cache add \
 | 
			
		||||
    s6 \
 | 
			
		||||
    sqlite \
 | 
			
		||||
    su-exec \
 | 
			
		||||
    gnupg
 | 
			
		||||
    gnupg \
 | 
			
		||||
    && rm -rf /var/cache/apk/*
 | 
			
		||||
 | 
			
		||||
RUN addgroup \
 | 
			
		||||
    -S -g 1000 \
 | 
			
		||||
@@ -68,9 +72,6 @@ RUN addgroup \
 | 
			
		||||
    git && \
 | 
			
		||||
  echo "git:*" | chpasswd -e
 | 
			
		||||
 | 
			
		||||
COPY --from=build-env /tmp/local /
 | 
			
		||||
COPY --from=build-env /go/src/code.gitea.io/gitea/gitea /app/gitea/gitea
 | 
			
		||||
 | 
			
		||||
ENV USER=git
 | 
			
		||||
ENV GITEA_CUSTOM=/data/gitea
 | 
			
		||||
 | 
			
		||||
@@ -78,3 +79,8 @@ VOLUME ["/data"]
 | 
			
		||||
 | 
			
		||||
ENTRYPOINT ["/usr/bin/entrypoint"]
 | 
			
		||||
CMD ["/usr/bin/s6-svscan", "/etc/s6"]
 | 
			
		||||
 | 
			
		||||
COPY --from=build-env /tmp/local /
 | 
			
		||||
COPY --from=build-env /go/src/code.gitea.io/gitea/gitea /app/gitea/gitea
 | 
			
		||||
COPY --from=build-env /go/src/code.gitea.io/gitea/environment-to-ini /usr/local/bin/environment-to-ini
 | 
			
		||||
COPY --from=build-env /go/src/code.gitea.io/gitea/contrib/autocompletion/bash_autocomplete /etc/profile.d/gitea_bash_autocomplete.sh
 | 
			
		||||
 
 | 
			
		||||
@@ -1,39 +1,46 @@
 | 
			
		||||
# syntax=docker/dockerfile:1
 | 
			
		||||
# Build stage
 | 
			
		||||
FROM docker.io/library/golang:1.25-alpine3.22 AS build-env
 | 
			
		||||
FROM docker.io/library/golang:1.23-alpine3.21 AS build-env
 | 
			
		||||
 | 
			
		||||
ARG GOPROXY=direct
 | 
			
		||||
ARG GOPROXY
 | 
			
		||||
ENV GOPROXY=${GOPROXY:-direct}
 | 
			
		||||
 | 
			
		||||
ARG GITEA_VERSION
 | 
			
		||||
ARG TAGS="sqlite sqlite_unlock_notify"
 | 
			
		||||
ENV TAGS="bindata timetzdata $TAGS"
 | 
			
		||||
ARG CGO_EXTRA_CFLAGS
 | 
			
		||||
 | 
			
		||||
# Build deps
 | 
			
		||||
#Build deps
 | 
			
		||||
RUN apk --no-cache add \
 | 
			
		||||
    build-base \
 | 
			
		||||
    git \
 | 
			
		||||
    nodejs \
 | 
			
		||||
    pnpm
 | 
			
		||||
    npm \
 | 
			
		||||
    && rm -rf /var/cache/apk/*
 | 
			
		||||
 | 
			
		||||
# Setup repo
 | 
			
		||||
COPY . ${GOPATH}/src/code.gitea.io/gitea
 | 
			
		||||
WORKDIR ${GOPATH}/src/code.gitea.io/gitea
 | 
			
		||||
# See the comments in Dockerfile
 | 
			
		||||
COPY --exclude=.git/ . .
 | 
			
		||||
 | 
			
		||||
# Build gitea, .git mount is required for version data
 | 
			
		||||
RUN --mount=type=cache,target=/go/pkg/mod \
 | 
			
		||||
    --mount=type=cache,target="/root/.cache/go-build" \
 | 
			
		||||
    --mount=type=cache,target=/root/.local/share/pnpm/store \
 | 
			
		||||
    --mount=type=bind,source=".git/",target=".git/" \
 | 
			
		||||
    make
 | 
			
		||||
# Checkout version if set
 | 
			
		||||
RUN if [ -n "${GITEA_VERSION}" ]; then git checkout "${GITEA_VERSION}"; fi \
 | 
			
		||||
 && make clean-all build
 | 
			
		||||
 | 
			
		||||
# Begin env-to-ini build
 | 
			
		||||
RUN go build contrib/environment-to-ini/environment-to-ini.go
 | 
			
		||||
 | 
			
		||||
# Copy local files
 | 
			
		||||
COPY docker/rootless /tmp/local
 | 
			
		||||
 | 
			
		||||
# Set permissions for builds that made under windows which strips the executable bit from file
 | 
			
		||||
RUN chmod 755 /tmp/local/usr/local/bin/* \
 | 
			
		||||
              /go/src/code.gitea.io/gitea/gitea
 | 
			
		||||
# Set permissions
 | 
			
		||||
RUN chmod 755 /tmp/local/usr/local/bin/docker-entrypoint.sh \
 | 
			
		||||
              /tmp/local/usr/local/bin/docker-setup.sh \
 | 
			
		||||
              /tmp/local/usr/local/bin/gitea \
 | 
			
		||||
              /go/src/code.gitea.io/gitea/gitea \
 | 
			
		||||
              /go/src/code.gitea.io/gitea/environment-to-ini
 | 
			
		||||
RUN chmod 644 /go/src/code.gitea.io/gitea/contrib/autocompletion/bash_autocomplete
 | 
			
		||||
 | 
			
		||||
FROM docker.io/library/alpine:3.22 AS gitea-rootless
 | 
			
		||||
FROM docker.io/library/alpine:3.21
 | 
			
		||||
LABEL maintainer="maintainers@gitea.io"
 | 
			
		||||
 | 
			
		||||
EXPOSE 2222 3000
 | 
			
		||||
 | 
			
		||||
@@ -45,7 +52,7 @@ RUN apk --no-cache add \
 | 
			
		||||
    git \
 | 
			
		||||
    curl \
 | 
			
		||||
    gnupg \
 | 
			
		||||
    openssh-keygen
 | 
			
		||||
    && rm -rf /var/cache/apk/*
 | 
			
		||||
 | 
			
		||||
RUN addgroup \
 | 
			
		||||
    -S -g 1000 \
 | 
			
		||||
@@ -63,6 +70,8 @@ RUN chown git:git /var/lib/gitea /etc/gitea
 | 
			
		||||
 | 
			
		||||
COPY --from=build-env /tmp/local /
 | 
			
		||||
COPY --from=build-env --chown=root:root /go/src/code.gitea.io/gitea/gitea /app/gitea/gitea
 | 
			
		||||
COPY --from=build-env --chown=root:root /go/src/code.gitea.io/gitea/environment-to-ini /usr/local/bin/environment-to-ini
 | 
			
		||||
COPY --from=build-env /go/src/code.gitea.io/gitea/contrib/autocompletion/bash_autocomplete /etc/profile.d/gitea_bash_autocomplete.sh
 | 
			
		||||
 | 
			
		||||
# git:git
 | 
			
		||||
USER 1000:1000
 | 
			
		||||
 
 | 
			
		||||
@@ -31,12 +31,15 @@ Gary Kim <gary@garykim.dev> (@gary-kim)
 | 
			
		||||
Guillermo Prandi <gitea.maint@mailfilter.com.ar> (@guillep2k)
 | 
			
		||||
Mura Li <typeless@ctli.io> (@typeless)
 | 
			
		||||
6543 <6543@obermui.de> (@6543)
 | 
			
		||||
jaqra <jaqra@hotmail.com> (@jaqra)
 | 
			
		||||
David Svantesson <davidsvantesson@gmail.com> (@davidsvantesson)
 | 
			
		||||
a1012112796 <1012112796@qq.com> (@a1012112796)
 | 
			
		||||
Karl Heinz Marbaise <kama@soebes.de> (@khmarbaise)
 | 
			
		||||
Norwin Roosen <git@nroo.de> (@noerw)
 | 
			
		||||
Kyle Dumont <kdumontnu@gmail.com> (@kdumontnu)
 | 
			
		||||
Patrick Schratz <patrick.schratz@gmail.com> (@pat-s)
 | 
			
		||||
Janis Estelmann <admin@oldschoolhack.me> (@KN4CK3R)
 | 
			
		||||
Steven Kriegler <sk.bunsenbrenner@gmail.com> (@justusbunsi)
 | 
			
		||||
Jimmy Praet <jimmy.praet@telenet.be> (@jpraet)
 | 
			
		||||
Leon Hofmeister <dev.lh@web.de> (@delvh)
 | 
			
		||||
Wim <wim@42.be> (@42wim)
 | 
			
		||||
@@ -60,7 +63,3 @@ Yu Liu <1240335630@qq.com> (@HEREYUA)
 | 
			
		||||
Kemal Zebari <kemalzebra@gmail.com> (@kemzeb)
 | 
			
		||||
Rowan Bohde <rowan.bohde@gmail.com> (@bohde)
 | 
			
		||||
hiifong <i@hiif.ong> (@hiifong)
 | 
			
		||||
metiftikci <metiftikci@hotmail.com> (@metiftikci)
 | 
			
		||||
Christopher Homberger <christopher.homberger@web.de> (@ChristopherHX)
 | 
			
		||||
Tobias Balle-Petersen <tobiasbp@gmail.com> (@tobiasbp)
 | 
			
		||||
TheFox <thefox0x7@gmail.com> (@TheFox0x7)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										411
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										411
									
								
								Makefile
									
									
									
									
									
								
							@@ -18,30 +18,25 @@ DIST := dist
 | 
			
		||||
DIST_DIRS := $(DIST)/binaries $(DIST)/release
 | 
			
		||||
IMPORT := code.gitea.io/gitea
 | 
			
		||||
 | 
			
		||||
# By default use go's 1.25 experimental json v2 library when building
 | 
			
		||||
# TODO: remove when no longer experimental
 | 
			
		||||
export GOEXPERIMENT ?= jsonv2
 | 
			
		||||
 | 
			
		||||
GO ?= go
 | 
			
		||||
SHASUM ?= shasum -a 256
 | 
			
		||||
HAS_GO := $(shell hash $(GO) > /dev/null 2>&1 && echo yes)
 | 
			
		||||
COMMA := ,
 | 
			
		||||
 | 
			
		||||
XGO_VERSION := go-1.25.x
 | 
			
		||||
XGO_VERSION := go-1.23.x
 | 
			
		||||
 | 
			
		||||
AIR_PACKAGE ?= github.com/air-verse/air@v1
 | 
			
		||||
EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/v3/cmd/editorconfig-checker@v3
 | 
			
		||||
GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.9.2
 | 
			
		||||
GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.5.0
 | 
			
		||||
GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.15
 | 
			
		||||
MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.7.0
 | 
			
		||||
SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.33.1
 | 
			
		||||
EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/v3/cmd/editorconfig-checker@v3.0.3
 | 
			
		||||
GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.7.0
 | 
			
		||||
GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/cmd/golangci-lint@v1.62.2
 | 
			
		||||
GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.12
 | 
			
		||||
MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.6.0
 | 
			
		||||
SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.31.0
 | 
			
		||||
XGO_PACKAGE ?= src.techknowlogick.com/xgo@latest
 | 
			
		||||
GO_LICENSES_PACKAGE ?= github.com/google/go-licenses@v1
 | 
			
		||||
GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1
 | 
			
		||||
ACTIONLINT_PACKAGE ?= github.com/rhysd/actionlint/cmd/actionlint@v1
 | 
			
		||||
GOPLS_PACKAGE ?= golang.org/x/tools/gopls@v0.20.0
 | 
			
		||||
GOPLS_MODERNIZE_PACKAGE ?= golang.org/x/tools/gopls/internal/analysis/modernize/cmd/modernize@v0.20.0
 | 
			
		||||
GOPLS_PACKAGE ?= golang.org/x/tools/gopls@v0.17.0
 | 
			
		||||
 | 
			
		||||
DOCKER_IMAGE ?= gitea/gitea
 | 
			
		||||
DOCKER_TAG ?= latest
 | 
			
		||||
@@ -52,17 +47,6 @@ ifeq ($(HAS_GO), yes)
 | 
			
		||||
	CGO_CFLAGS ?= $(shell $(GO) env CGO_CFLAGS) $(CGO_EXTRA_CFLAGS)
 | 
			
		||||
endif
 | 
			
		||||
 | 
			
		||||
CGO_ENABLED ?= 0
 | 
			
		||||
ifneq (,$(findstring sqlite,$(TAGS))$(findstring pam,$(TAGS)))
 | 
			
		||||
	CGO_ENABLED = 1
 | 
			
		||||
endif
 | 
			
		||||
 | 
			
		||||
STATIC ?=
 | 
			
		||||
EXTLDFLAGS ?=
 | 
			
		||||
ifneq ($(STATIC),)
 | 
			
		||||
	EXTLDFLAGS = -extldflags "-static"
 | 
			
		||||
endif
 | 
			
		||||
 | 
			
		||||
ifeq ($(GOOS),windows)
 | 
			
		||||
	IS_WINDOWS := yes
 | 
			
		||||
else ifeq ($(patsubst Windows%,Windows,$(OS)),Windows)
 | 
			
		||||
@@ -89,26 +73,17 @@ EXTRA_GOFLAGS ?=
 | 
			
		||||
MAKE_VERSION := $(shell "$(MAKE)" -v | cat | head -n 1)
 | 
			
		||||
MAKE_EVIDENCE_DIR := .make_evidence
 | 
			
		||||
 | 
			
		||||
GOTESTFLAGS ?=
 | 
			
		||||
ifeq ($(RACE_ENABLED),true)
 | 
			
		||||
	GOFLAGS += -race
 | 
			
		||||
	GOTESTFLAGS += -race
 | 
			
		||||
endif
 | 
			
		||||
 | 
			
		||||
STORED_VERSION_FILE := VERSION
 | 
			
		||||
HUGO_VERSION ?= 0.111.3
 | 
			
		||||
 | 
			
		||||
GITHUB_REF_TYPE ?= branch
 | 
			
		||||
GITHUB_REF_NAME ?= $(shell git rev-parse --abbrev-ref HEAD)
 | 
			
		||||
 | 
			
		||||
# Enable typescript support in Node.js before 22.18
 | 
			
		||||
# TODO: Remove this once we can raise the minimum Node.js version to 22.18 (alpine >= 3.23)
 | 
			
		||||
NODE_VERSION := $(shell printf "%03d%03d%03d" $(shell node -v 2>/dev/null | cut -c2- | tr '.' ' '))
 | 
			
		||||
ifeq ($(shell test "$(NODE_VERSION)" -lt "022018000"; echo $$?),0)
 | 
			
		||||
	NODE_VARS := NODE_OPTIONS="--experimental-strip-types"
 | 
			
		||||
else
 | 
			
		||||
	NODE_VARS :=
 | 
			
		||||
endif
 | 
			
		||||
 | 
			
		||||
ifneq ($(GITHUB_REF_TYPE),branch)
 | 
			
		||||
	VERSION ?= $(subst v,,$(GITHUB_REF_NAME))
 | 
			
		||||
	GITEA_VERSION ?= $(VERSION)
 | 
			
		||||
@@ -134,17 +109,20 @@ endif
 | 
			
		||||
 | 
			
		||||
LDFLAGS := $(LDFLAGS) -X "main.MakeVersion=$(MAKE_VERSION)" -X "main.Version=$(GITEA_VERSION)" -X "main.Tags=$(TAGS)"
 | 
			
		||||
 | 
			
		||||
LINUX_ARCHS ?= linux/amd64,linux/386,linux/arm-5,linux/arm-6,linux/arm64,linux/riscv64
 | 
			
		||||
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/))
 | 
			
		||||
MIGRATE_TEST_PACKAGES ?= $(shell $(GO) list code.gitea.io/gitea/models/migrations/...)
 | 
			
		||||
 | 
			
		||||
FOMANTIC_WORK_DIR := web_src/fomantic
 | 
			
		||||
 | 
			
		||||
WEBPACK_SOURCES := $(shell find web_src/js web_src/css -type f)
 | 
			
		||||
WEBPACK_CONFIGS := webpack.config.ts tailwind.config.ts
 | 
			
		||||
WEBPACK_CONFIGS := webpack.config.js tailwind.config.js
 | 
			
		||||
WEBPACK_DEST := public/assets/js/index.js public/assets/css/index.css
 | 
			
		||||
WEBPACK_DEST_ENTRIES := public/assets/js public/assets/css public/assets/fonts
 | 
			
		||||
 | 
			
		||||
BINDATA_DEST_WILDCARD := modules/migration/bindata.* modules/public/bindata.* modules/options/bindata.* modules/templates/bindata.*
 | 
			
		||||
BINDATA_DEST := modules/public/bindata.go modules/options/bindata.go modules/templates/bindata.go
 | 
			
		||||
BINDATA_HASH := $(addsuffix .hash,$(BINDATA_DEST))
 | 
			
		||||
 | 
			
		||||
GENERATED_GO_DEST := modules/charset/invisible_gen.go modules/charset/ambiguous_gen.go
 | 
			
		||||
 | 
			
		||||
@@ -161,19 +139,25 @@ TAGS_EVIDENCE := $(MAKE_EVIDENCE_DIR)/tags
 | 
			
		||||
 | 
			
		||||
TEST_TAGS ?= $(TAGS_SPLIT) sqlite sqlite_unlock_notify
 | 
			
		||||
 | 
			
		||||
TAR_EXCLUDES := .git data indexers queues log node_modules $(EXECUTABLE) $(DIST) $(MAKE_EVIDENCE_DIR) $(AIR_TMP_DIR) $(GO_LICENSE_TMP_DIR)
 | 
			
		||||
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)
 | 
			
		||||
 | 
			
		||||
GO_DIRS := build cmd models modules routers services tests tools
 | 
			
		||||
GO_DIRS := build cmd models modules routers services tests
 | 
			
		||||
WEB_DIRS := web_src/js web_src/css
 | 
			
		||||
 | 
			
		||||
ESLINT_FILES := web_src/js tools *.ts tests/e2e
 | 
			
		||||
ESLINT_FILES := web_src/js tools *.js *.ts tests/e2e
 | 
			
		||||
STYLELINT_FILES := web_src/css web_src/js/components/*.vue
 | 
			
		||||
SPELLCHECK_FILES := $(GO_DIRS) $(WEB_DIRS) templates options/locale/locale_en-US.ini .github $(filter-out CHANGELOG.md, $(wildcard *.go *.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))
 | 
			
		||||
EDITORCONFIG_FILES := templates .github/workflows options/locale/locale_en-US.ini
 | 
			
		||||
 | 
			
		||||
GO_SOURCES := $(wildcard *.go)
 | 
			
		||||
GO_SOURCES += $(shell find $(GO_DIRS) -type f -name "*.go")
 | 
			
		||||
GO_SOURCES += $(shell find $(GO_DIRS) -type f -name "*.go" ! -path modules/options/bindata.go ! -path modules/public/bindata.go ! -path modules/templates/bindata.go)
 | 
			
		||||
GO_SOURCES += $(GENERATED_GO_DEST)
 | 
			
		||||
GO_SOURCES_NO_BINDATA := $(GO_SOURCES)
 | 
			
		||||
 | 
			
		||||
ifeq ($(filter $(TAGS_SPLIT),bindata),bindata)
 | 
			
		||||
	GO_SOURCES += $(BINDATA_DEST)
 | 
			
		||||
	GENERATED_GO_DEST += $(BINDATA_DEST)
 | 
			
		||||
endif
 | 
			
		||||
 | 
			
		||||
# Force installation of playwright dependencies by setting this flag
 | 
			
		||||
ifdef DEPS_PLAYWRIGHT
 | 
			
		||||
@@ -181,8 +165,10 @@ ifdef DEPS_PLAYWRIGHT
 | 
			
		||||
endif
 | 
			
		||||
 | 
			
		||||
SWAGGER_SPEC := templates/swagger/v1_json.tmpl
 | 
			
		||||
SWAGGER_SPEC_INPUT := templates/swagger/v1_input.json
 | 
			
		||||
SWAGGER_SPEC_S_TMPL := s|"basePath": *"/api/v1"|"basePath": "{{AppSubUrl \| JSEscape}}/api/v1"|g
 | 
			
		||||
SWAGGER_SPEC_S_JSON := s|"basePath": *"{{AppSubUrl \| JSEscape}}/api/v1"|"basePath": "/api/v1"|g
 | 
			
		||||
SWAGGER_EXCLUDE := code.gitea.io/sdk
 | 
			
		||||
SWAGGER_NEWLINE_COMMAND := -e '$$a\'
 | 
			
		||||
 | 
			
		||||
TEST_MYSQL_HOST ?= mysql:3306
 | 
			
		||||
TEST_MYSQL_DBNAME ?= testgitea
 | 
			
		||||
@@ -203,11 +189,67 @@ TEST_MSSQL_PASSWORD ?= MwantsaSecurePassword1
 | 
			
		||||
all: build
 | 
			
		||||
 | 
			
		||||
.PHONY: help
 | 
			
		||||
help: Makefile ## print Makefile help information.
 | 
			
		||||
	@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n  make \033[36m[TARGETS] default target: build\033[0m\n\n\033[35mTargets:\033[0m\n"} /^[0-9A-Za-z._-]+:.*?##/ { printf "  \033[36m%-45s\033[0m %s\n", $$1, $$2 }' Makefile #$(MAKEFILE_LIST)
 | 
			
		||||
	@printf "  \033[36m%-46s\033[0m %s\n" "test-e2e[#TestSpecificName]" "test end to end using playwright"
 | 
			
		||||
	@printf "  \033[36m%-46s\033[0m %s\n" "test[#TestSpecificName]" "run unit test"
 | 
			
		||||
	@printf "  \033[36m%-46s\033[0m %s\n" "test-sqlite[#TestSpecificName]" "run integration test for sqlite"
 | 
			
		||||
help:
 | 
			
		||||
	@echo "Make Routines:"
 | 
			
		||||
	@echo " - \"\"                               equivalent to \"build\""
 | 
			
		||||
	@echo " - build                            build everything"
 | 
			
		||||
	@echo " - frontend                         build frontend files"
 | 
			
		||||
	@echo " - backend                          build backend files"
 | 
			
		||||
	@echo " - watch                            watch everything and continuously rebuild"
 | 
			
		||||
	@echo " - watch-frontend                   watch frontend files and continuously rebuild"
 | 
			
		||||
	@echo " - watch-backend                    watch backend files and continuously rebuild"
 | 
			
		||||
	@echo " - clean                            delete backend and integration files"
 | 
			
		||||
	@echo " - clean-all                        delete backend, frontend and integration files"
 | 
			
		||||
	@echo " - deps                             install dependencies"
 | 
			
		||||
	@echo " - deps-frontend                    install frontend dependencies"
 | 
			
		||||
	@echo " - deps-backend                     install backend dependencies"
 | 
			
		||||
	@echo " - deps-tools                       install tool dependencies"
 | 
			
		||||
	@echo " - deps-py                          install python dependencies"
 | 
			
		||||
	@echo " - lint                             lint everything"
 | 
			
		||||
	@echo " - lint-fix                         lint everything and fix issues"
 | 
			
		||||
	@echo " - lint-actions                     lint action workflow files"
 | 
			
		||||
	@echo " - lint-frontend                    lint frontend files"
 | 
			
		||||
	@echo " - lint-frontend-fix                lint frontend files and fix issues"
 | 
			
		||||
	@echo " - lint-backend                     lint backend files"
 | 
			
		||||
	@echo " - lint-backend-fix                 lint backend files and fix issues"
 | 
			
		||||
	@echo " - lint-go                          lint go files"
 | 
			
		||||
	@echo " - lint-go-fix                      lint go files and fix issues"
 | 
			
		||||
	@echo " - lint-go-vet                      lint go files with vet"
 | 
			
		||||
	@echo " - lint-go-gopls                    lint go files with gopls"
 | 
			
		||||
	@echo " - lint-js                          lint js files"
 | 
			
		||||
	@echo " - lint-js-fix                      lint js files and fix issues"
 | 
			
		||||
	@echo " - lint-css                         lint css files"
 | 
			
		||||
	@echo " - lint-css-fix                     lint css files and fix issues"
 | 
			
		||||
	@echo " - lint-md                          lint markdown files"
 | 
			
		||||
	@echo " - lint-swagger                     lint swagger files"
 | 
			
		||||
	@echo " - lint-templates                   lint template files"
 | 
			
		||||
	@echo " - lint-yaml                        lint yaml files"
 | 
			
		||||
	@echo " - lint-spell                       lint spelling"
 | 
			
		||||
	@echo " - lint-spell-fix                   lint spelling and fix issues"
 | 
			
		||||
	@echo " - checks                           run various consistency checks"
 | 
			
		||||
	@echo " - checks-frontend                  check frontend files"
 | 
			
		||||
	@echo " - checks-backend                   check backend files"
 | 
			
		||||
	@echo " - test                             test everything"
 | 
			
		||||
	@echo " - test-frontend                    test frontend files"
 | 
			
		||||
	@echo " - test-backend                     test backend files"
 | 
			
		||||
	@echo " - test-e2e[\#TestSpecificName]     test end to end using playwright"
 | 
			
		||||
	@echo " - update                           update js and py dependencies"
 | 
			
		||||
	@echo " - update-js                        update js dependencies"
 | 
			
		||||
	@echo " - update-py                        update py dependencies"
 | 
			
		||||
	@echo " - webpack                          build webpack files"
 | 
			
		||||
	@echo " - svg                              build svg files"
 | 
			
		||||
	@echo " - fomantic                         build fomantic files"
 | 
			
		||||
	@echo " - generate                         run \"go generate\""
 | 
			
		||||
	@echo " - fmt                              format the Go code"
 | 
			
		||||
	@echo " - generate-license                 update license files"
 | 
			
		||||
	@echo " - generate-gitignore               update gitignore files"
 | 
			
		||||
	@echo " - generate-manpage                 generate manpage"
 | 
			
		||||
	@echo " - generate-swagger                 generate the swagger spec from code comments"
 | 
			
		||||
	@echo " - swagger-validate                 check if the swagger spec is valid"
 | 
			
		||||
	@echo " - go-licenses                      regenerate go licenses"
 | 
			
		||||
	@echo " - tidy                             run go mod tidy"
 | 
			
		||||
	@echo " - test[\#TestSpecificName]    	    run unit test"
 | 
			
		||||
	@echo " - test-sqlite[\#TestSpecificName]  run integration test for sqlite"
 | 
			
		||||
 | 
			
		||||
.PHONY: go-check
 | 
			
		||||
go-check:
 | 
			
		||||
@@ -230,23 +272,20 @@ git-check:
 | 
			
		||||
node-check:
 | 
			
		||||
	$(eval MIN_NODE_VERSION_STR := $(shell grep -Eo '"node":.*[0-9.]+"' package.json | sed -n 's/.*[^0-9.]\([0-9.]*\)"/\1/p'))
 | 
			
		||||
	$(eval MIN_NODE_VERSION := $(shell printf "%03d%03d%03d" $(shell echo '$(MIN_NODE_VERSION_STR)' | tr '.' ' ')))
 | 
			
		||||
	$(eval PNPM_MISSING := $(shell hash pnpm > /dev/null 2>&1 || echo 1))
 | 
			
		||||
	@if [ "$(NODE_VERSION)" -lt "$(MIN_NODE_VERSION)" ]; then \
 | 
			
		||||
		echo "Gitea requires Node.js $(MIN_NODE_VERSION_STR) or greater to build. You can get it at https://nodejs.org/en/download/"; \
 | 
			
		||||
		exit 1; \
 | 
			
		||||
	fi
 | 
			
		||||
	@if [ "$(PNPM_MISSING)" = "1" ]; then \
 | 
			
		||||
		echo "Gitea requires pnpm to build. You can install it at https://pnpm.io/installation"; \
 | 
			
		||||
	$(eval NODE_VERSION := $(shell printf "%03d%03d%03d" $(shell node -v | cut -c2- | tr '.' ' ');))
 | 
			
		||||
	$(eval NPM_MISSING := $(shell hash npm > /dev/null 2>&1 || echo 1))
 | 
			
		||||
	@if [ "$(NODE_VERSION)" -lt "$(MIN_NODE_VERSION)" -o "$(NPM_MISSING)" = "1" ]; then \
 | 
			
		||||
		echo "Gitea requires Node.js $(MIN_NODE_VERSION_STR) or greater and npm to build. You can get it at https://nodejs.org/en/download/"; \
 | 
			
		||||
		exit 1; \
 | 
			
		||||
	fi
 | 
			
		||||
 | 
			
		||||
.PHONY: clean-all
 | 
			
		||||
clean-all: clean ## delete backend, frontend and integration files
 | 
			
		||||
clean-all: clean
 | 
			
		||||
	rm -rf $(WEBPACK_DEST_ENTRIES) node_modules
 | 
			
		||||
 | 
			
		||||
.PHONY: clean
 | 
			
		||||
clean: ## delete backend and integration files
 | 
			
		||||
	rm -rf $(EXECUTABLE) $(DIST) $(BINDATA_DEST_WILDCARD) \
 | 
			
		||||
clean:
 | 
			
		||||
	rm -rf $(EXECUTABLE) $(DIST) $(BINDATA_DEST) $(BINDATA_HASH) \
 | 
			
		||||
		integrations*.test \
 | 
			
		||||
		e2e*.test \
 | 
			
		||||
		tests/integration/gitea-integration-* \
 | 
			
		||||
@@ -257,8 +296,8 @@ clean: ## delete backend and integration files
 | 
			
		||||
		tests/e2e/reports/ tests/e2e/test-artifacts/ tests/e2e/test-snapshots/
 | 
			
		||||
 | 
			
		||||
.PHONY: fmt
 | 
			
		||||
fmt: ## format the Go and template code
 | 
			
		||||
	@GOFUMPT_PACKAGE=$(GOFUMPT_PACKAGE) $(GO) run tools/code-batch-process.go gitea-fmt -w '{file-list}'
 | 
			
		||||
fmt:
 | 
			
		||||
	@GOFUMPT_PACKAGE=$(GOFUMPT_PACKAGE) $(GO) run build/code-batch-process.go gitea-fmt -w '{file-list}'
 | 
			
		||||
	$(eval TEMPLATES := $(shell find templates -type f -name '*.tmpl'))
 | 
			
		||||
	@# strip whitespace after '{{' or '(' and before '}}' or ')' unless there is only
 | 
			
		||||
	@# whitespace before it
 | 
			
		||||
@@ -272,20 +311,7 @@ fmt-check: fmt
 | 
			
		||||
	@diff=$$(git diff --color=always $(GO_SOURCES) templates $(WEB_DIRS)); \
 | 
			
		||||
	if [ -n "$$diff" ]; then \
 | 
			
		||||
	  echo "Please run 'make fmt' and commit the result:"; \
 | 
			
		||||
	  printf "%s" "$${diff}"; \
 | 
			
		||||
	  exit 1; \
 | 
			
		||||
	fi
 | 
			
		||||
 | 
			
		||||
.PHONY: fix
 | 
			
		||||
fix: ## apply automated fixes to Go code
 | 
			
		||||
	$(GO) run $(GOPLS_MODERNIZE_PACKAGE) -fix ./...
 | 
			
		||||
 | 
			
		||||
.PHONY: fix-check
 | 
			
		||||
fix-check: fix
 | 
			
		||||
	@diff=$$(git diff --color=always $(GO_SOURCES)); \
 | 
			
		||||
	if [ -n "$$diff" ]; then \
 | 
			
		||||
	  echo "Please run 'make fix' and commit the result:"; \
 | 
			
		||||
	  printf "%s" "$${diff}"; \
 | 
			
		||||
	  echo "$${diff}"; \
 | 
			
		||||
	  exit 1; \
 | 
			
		||||
	fi
 | 
			
		||||
 | 
			
		||||
@@ -299,95 +325,95 @@ TAGS_PREREQ := $(TAGS_EVIDENCE)
 | 
			
		||||
endif
 | 
			
		||||
 | 
			
		||||
.PHONY: generate-swagger
 | 
			
		||||
generate-swagger: $(SWAGGER_SPEC) ## generate the swagger spec from code comments
 | 
			
		||||
generate-swagger: $(SWAGGER_SPEC)
 | 
			
		||||
 | 
			
		||||
$(SWAGGER_SPEC): $(GO_SOURCES) $(SWAGGER_SPEC_INPUT)
 | 
			
		||||
	$(GO) run $(SWAGGER_PACKAGE) generate spec --exclude "$(SWAGGER_EXCLUDE)" --input "$(SWAGGER_SPEC_INPUT)" --output './$(SWAGGER_SPEC)'
 | 
			
		||||
$(SWAGGER_SPEC): $(GO_SOURCES_NO_BINDATA)
 | 
			
		||||
	$(GO) run $(SWAGGER_PACKAGE) generate spec -x "$(SWAGGER_EXCLUDE)" -o './$(SWAGGER_SPEC)'
 | 
			
		||||
	$(SED_INPLACE) '$(SWAGGER_SPEC_S_TMPL)' './$(SWAGGER_SPEC)'
 | 
			
		||||
	$(SED_INPLACE) $(SWAGGER_NEWLINE_COMMAND) './$(SWAGGER_SPEC)'
 | 
			
		||||
 | 
			
		||||
.PHONY: swagger-check
 | 
			
		||||
swagger-check: generate-swagger
 | 
			
		||||
	@diff=$$(git diff --color=always '$(SWAGGER_SPEC)'); \
 | 
			
		||||
	if [ -n "$$diff" ]; then \
 | 
			
		||||
		echo "Please run 'make generate-swagger' and commit the result:"; \
 | 
			
		||||
		printf "%s" "$${diff}"; \
 | 
			
		||||
		echo "$${diff}"; \
 | 
			
		||||
		exit 1; \
 | 
			
		||||
	fi
 | 
			
		||||
 | 
			
		||||
.PHONY: swagger-validate
 | 
			
		||||
swagger-validate: ## check if the swagger spec is valid
 | 
			
		||||
	@# swagger "validate" requires that the "basePath" must start with a slash, but we are using Golang template "{{...}}"
 | 
			
		||||
	@$(SED_INPLACE) -E -e 's|"basePath":( *)"(.*)"|"basePath":\1"/\2"|g' './$(SWAGGER_SPEC)' # add a prefix slash to basePath
 | 
			
		||||
	@# FIXME: there are some warnings
 | 
			
		||||
swagger-validate:
 | 
			
		||||
	$(SED_INPLACE) '$(SWAGGER_SPEC_S_JSON)' './$(SWAGGER_SPEC)'
 | 
			
		||||
	$(GO) run $(SWAGGER_PACKAGE) validate './$(SWAGGER_SPEC)'
 | 
			
		||||
	@$(SED_INPLACE) -E -e 's|"basePath":( *)"/(.*)"|"basePath":\1"\2"|g' './$(SWAGGER_SPEC)' # remove the prefix slash from basePath
 | 
			
		||||
	$(SED_INPLACE) '$(SWAGGER_SPEC_S_TMPL)' './$(SWAGGER_SPEC)'
 | 
			
		||||
 | 
			
		||||
.PHONY: checks
 | 
			
		||||
checks: checks-frontend checks-backend ## run various consistency checks
 | 
			
		||||
checks: checks-frontend checks-backend
 | 
			
		||||
 | 
			
		||||
.PHONY: checks-frontend
 | 
			
		||||
checks-frontend: lockfile-check svg-check ## check frontend files
 | 
			
		||||
checks-frontend: lockfile-check svg-check
 | 
			
		||||
 | 
			
		||||
.PHONY: checks-backend
 | 
			
		||||
checks-backend: tidy-check swagger-check fmt-check fix-check swagger-validate security-check ## check backend files
 | 
			
		||||
checks-backend: tidy-check swagger-check fmt-check swagger-validate security-check
 | 
			
		||||
 | 
			
		||||
.PHONY: lint
 | 
			
		||||
lint: lint-frontend lint-backend lint-spell ## lint everything
 | 
			
		||||
lint: lint-frontend lint-backend lint-spell
 | 
			
		||||
 | 
			
		||||
.PHONY: lint-fix
 | 
			
		||||
lint-fix: lint-frontend-fix lint-backend-fix lint-spell-fix ## lint everything and fix issues
 | 
			
		||||
lint-fix: lint-frontend-fix lint-backend-fix lint-spell-fix
 | 
			
		||||
 | 
			
		||||
.PHONY: lint-frontend
 | 
			
		||||
lint-frontend: lint-js lint-css ## lint frontend files
 | 
			
		||||
lint-frontend: lint-js lint-css
 | 
			
		||||
 | 
			
		||||
.PHONY: lint-frontend-fix
 | 
			
		||||
lint-frontend-fix: lint-js-fix lint-css-fix ## lint frontend files and fix issues
 | 
			
		||||
lint-frontend-fix: lint-js-fix lint-css-fix
 | 
			
		||||
 | 
			
		||||
.PHONY: lint-backend
 | 
			
		||||
lint-backend: lint-go lint-go-gitea-vet lint-go-gopls lint-editorconfig ## lint backend files
 | 
			
		||||
lint-backend: lint-go lint-go-vet lint-go-gopls lint-editorconfig
 | 
			
		||||
 | 
			
		||||
.PHONY: lint-backend-fix
 | 
			
		||||
lint-backend-fix: lint-go-fix lint-go-gitea-vet lint-editorconfig ## lint backend files and fix issues
 | 
			
		||||
lint-backend-fix: lint-go-fix lint-go-vet lint-editorconfig
 | 
			
		||||
 | 
			
		||||
.PHONY: lint-js
 | 
			
		||||
lint-js: node_modules ## lint js files
 | 
			
		||||
	$(NODE_VARS) pnpm exec eslint --color --max-warnings=0 --flag unstable_native_nodejs_ts_config $(ESLINT_FILES)
 | 
			
		||||
	$(NODE_VARS) pnpm exec vue-tsc
 | 
			
		||||
lint-js: node_modules
 | 
			
		||||
	npx eslint --color --max-warnings=0 --ext js,ts,vue $(ESLINT_FILES)
 | 
			
		||||
	npx vue-tsc
 | 
			
		||||
 | 
			
		||||
.PHONY: lint-js-fix
 | 
			
		||||
lint-js-fix: node_modules ## lint js files and fix issues
 | 
			
		||||
	$(NODE_VARS) pnpm exec eslint --color --max-warnings=0 --flag unstable_native_nodejs_ts_config $(ESLINT_FILES) --fix
 | 
			
		||||
	$(NODE_VARS) pnpm exec vue-tsc
 | 
			
		||||
lint-js-fix: node_modules
 | 
			
		||||
	npx eslint --color --max-warnings=0 --ext js,ts,vue $(ESLINT_FILES) --fix
 | 
			
		||||
	npx vue-tsc
 | 
			
		||||
 | 
			
		||||
.PHONY: lint-css
 | 
			
		||||
lint-css: node_modules ## lint css files
 | 
			
		||||
	$(NODE_VARS) pnpm exec stylelint --color --max-warnings=0 $(STYLELINT_FILES)
 | 
			
		||||
lint-css: node_modules
 | 
			
		||||
	npx stylelint --color --max-warnings=0 $(STYLELINT_FILES)
 | 
			
		||||
 | 
			
		||||
.PHONY: lint-css-fix
 | 
			
		||||
lint-css-fix: node_modules ## lint css files and fix issues
 | 
			
		||||
	$(NODE_VARS) pnpm exec stylelint --color --max-warnings=0 $(STYLELINT_FILES) --fix
 | 
			
		||||
lint-css-fix: node_modules
 | 
			
		||||
	npx stylelint --color --max-warnings=0 $(STYLELINT_FILES) --fix
 | 
			
		||||
 | 
			
		||||
.PHONY: lint-swagger
 | 
			
		||||
lint-swagger: node_modules ## lint swagger files
 | 
			
		||||
	$(NODE_VARS) pnpm exec spectral lint -q -F hint $(SWAGGER_SPEC)
 | 
			
		||||
lint-swagger: node_modules
 | 
			
		||||
	npx spectral lint -q -F hint $(SWAGGER_SPEC)
 | 
			
		||||
 | 
			
		||||
.PHONY: lint-md
 | 
			
		||||
lint-md: node_modules ## lint markdown files
 | 
			
		||||
	$(NODE_VARS) pnpm exec markdownlint *.md
 | 
			
		||||
lint-md: node_modules
 | 
			
		||||
	npx markdownlint *.md
 | 
			
		||||
 | 
			
		||||
.PHONY: lint-spell
 | 
			
		||||
lint-spell: ## lint spelling
 | 
			
		||||
	@go run $(MISSPELL_PACKAGE) -dict assets/misspellings.csv -error $(SPELLCHECK_FILES)
 | 
			
		||||
lint-spell:
 | 
			
		||||
	@go run $(MISSPELL_PACKAGE) -dict tools/misspellings.csv -error $(SPELLCHECK_FILES)
 | 
			
		||||
 | 
			
		||||
.PHONY: lint-spell-fix
 | 
			
		||||
lint-spell-fix: ## lint spelling and fix issues
 | 
			
		||||
	@go run $(MISSPELL_PACKAGE) -dict assets/misspellings.csv -w $(SPELLCHECK_FILES)
 | 
			
		||||
lint-spell-fix:
 | 
			
		||||
	@go run $(MISSPELL_PACKAGE) -dict tools/misspellings.csv -w $(SPELLCHECK_FILES)
 | 
			
		||||
 | 
			
		||||
.PHONY: lint-go
 | 
			
		||||
lint-go: ## lint go files
 | 
			
		||||
lint-go:
 | 
			
		||||
	$(GO) run $(GOLANGCI_LINT_PACKAGE) run
 | 
			
		||||
 | 
			
		||||
.PHONY: lint-go-fix
 | 
			
		||||
lint-go-fix: ## lint go files and fix issues
 | 
			
		||||
lint-go-fix:
 | 
			
		||||
	$(GO) run $(GOLANGCI_LINT_PACKAGE) run --fix
 | 
			
		||||
 | 
			
		||||
# workaround step for the lint-go-windows CI task because 'go run' can not
 | 
			
		||||
@@ -397,59 +423,58 @@ lint-go-windows:
 | 
			
		||||
	@GOOS= GOARCH= $(GO) install $(GOLANGCI_LINT_PACKAGE)
 | 
			
		||||
	golangci-lint run
 | 
			
		||||
 | 
			
		||||
.PHONY: lint-go-gitea-vet
 | 
			
		||||
lint-go-gitea-vet: ## lint go files with gitea-vet
 | 
			
		||||
	@echo "Running gitea-vet..."
 | 
			
		||||
.PHONY: lint-go-vet
 | 
			
		||||
lint-go-vet:
 | 
			
		||||
	@echo "Running go vet..."
 | 
			
		||||
	@GOOS= GOARCH= $(GO) build code.gitea.io/gitea-vet
 | 
			
		||||
	@$(GO) vet -vettool=gitea-vet ./...
 | 
			
		||||
 | 
			
		||||
.PHONY: lint-go-gopls
 | 
			
		||||
lint-go-gopls: ## lint go files with gopls
 | 
			
		||||
lint-go-gopls:
 | 
			
		||||
	@echo "Running gopls check..."
 | 
			
		||||
	@GO=$(GO) GOPLS_PACKAGE=$(GOPLS_PACKAGE) tools/lint-go-gopls.sh $(GO_SOURCES)
 | 
			
		||||
	@GO=$(GO) GOPLS_PACKAGE=$(GOPLS_PACKAGE) tools/lint-go-gopls.sh $(GO_SOURCES_NO_BINDATA)
 | 
			
		||||
 | 
			
		||||
.PHONY: lint-editorconfig
 | 
			
		||||
lint-editorconfig:
 | 
			
		||||
	@echo "Running editorconfig check..."
 | 
			
		||||
	@$(GO) run $(EDITORCONFIG_CHECKER_PACKAGE) $(EDITORCONFIG_FILES)
 | 
			
		||||
 | 
			
		||||
.PHONY: lint-actions
 | 
			
		||||
lint-actions: ## lint action workflow files
 | 
			
		||||
lint-actions:
 | 
			
		||||
	$(GO) run $(ACTIONLINT_PACKAGE)
 | 
			
		||||
 | 
			
		||||
.PHONY: lint-templates
 | 
			
		||||
lint-templates: .venv node_modules ## lint template files
 | 
			
		||||
	@node tools/lint-templates-svg.ts
 | 
			
		||||
	@uv run --frozen djlint $(shell find templates -type f -iname '*.tmpl')
 | 
			
		||||
lint-templates: .venv node_modules
 | 
			
		||||
	@node tools/lint-templates-svg.js
 | 
			
		||||
	@poetry run djlint $(shell find templates -type f -iname '*.tmpl')
 | 
			
		||||
 | 
			
		||||
.PHONY: lint-yaml
 | 
			
		||||
lint-yaml: .venv ## lint yaml files
 | 
			
		||||
	@uv run --frozen yamllint -s .
 | 
			
		||||
lint-yaml: .venv
 | 
			
		||||
	@poetry run yamllint .
 | 
			
		||||
 | 
			
		||||
.PHONY: watch
 | 
			
		||||
watch: ## watch everything and continuously rebuild
 | 
			
		||||
watch:
 | 
			
		||||
	@bash tools/watch.sh
 | 
			
		||||
 | 
			
		||||
.PHONY: watch-frontend
 | 
			
		||||
watch-frontend: node-check node_modules ## watch frontend files and continuously rebuild
 | 
			
		||||
watch-frontend: node-check node_modules
 | 
			
		||||
	@rm -rf $(WEBPACK_DEST_ENTRIES)
 | 
			
		||||
	NODE_ENV=development $(NODE_VARS) pnpm exec webpack --watch --progress --disable-interpret
 | 
			
		||||
	NODE_ENV=development npx webpack --watch --progress
 | 
			
		||||
 | 
			
		||||
.PHONY: watch-backend
 | 
			
		||||
watch-backend: go-check ## watch backend files and continuously rebuild
 | 
			
		||||
watch-backend: go-check
 | 
			
		||||
	GITEA_RUN_MODE=dev $(GO) run $(AIR_PACKAGE) -c .air.toml
 | 
			
		||||
 | 
			
		||||
.PHONY: test
 | 
			
		||||
test: test-frontend test-backend ## test everything
 | 
			
		||||
test: test-frontend test-backend
 | 
			
		||||
 | 
			
		||||
.PHONY: test-backend
 | 
			
		||||
test-backend: ## test backend files
 | 
			
		||||
test-backend:
 | 
			
		||||
	@echo "Running go test with $(GOTESTFLAGS) -tags '$(TEST_TAGS)'..."
 | 
			
		||||
	@$(GO) test $(GOTESTFLAGS) -tags='$(TEST_TAGS)' $(GO_TEST_PACKAGES)
 | 
			
		||||
 | 
			
		||||
.PHONY: test-frontend
 | 
			
		||||
test-frontend: node_modules ## test frontend files
 | 
			
		||||
	$(NODE_VARS) pnpm exec vitest
 | 
			
		||||
test-frontend: node_modules
 | 
			
		||||
	npx vitest
 | 
			
		||||
 | 
			
		||||
.PHONY: test-check
 | 
			
		||||
test-check:
 | 
			
		||||
@@ -457,7 +482,7 @@ test-check:
 | 
			
		||||
	@diff=$$(git status -s); \
 | 
			
		||||
	if [ -n "$$diff" ]; then \
 | 
			
		||||
		echo "make test-backend has changed files in the source tree:"; \
 | 
			
		||||
		printf "%s" "$${diff}"; \
 | 
			
		||||
		echo "$${diff}"; \
 | 
			
		||||
		echo "You should change the tests to create these files in a temporary directory."; \
 | 
			
		||||
		echo "Do not simply add these files to .gitignore"; \
 | 
			
		||||
		exit 1; \
 | 
			
		||||
@@ -472,7 +497,7 @@ test\#%:
 | 
			
		||||
coverage:
 | 
			
		||||
	grep '^\(mode: .*\)\|\(.*:[0-9]\+\.[0-9]\+,[0-9]\+\.[0-9]\+ [0-9]\+ [0-9]\+\)$$' coverage.out > coverage-bodged.out
 | 
			
		||||
	grep '^\(mode: .*\)\|\(.*:[0-9]\+\.[0-9]\+,[0-9]\+\.[0-9]\+ [0-9]\+ [0-9]\+\)$$' integration.coverage.out > integration.coverage-bodged.out
 | 
			
		||||
	$(GO) run tools/gocovmerge.go integration.coverage-bodged.out coverage-bodged.out > coverage.all
 | 
			
		||||
	$(GO) run build/gocovmerge.go integration.coverage-bodged.out coverage-bodged.out > coverage.all
 | 
			
		||||
 | 
			
		||||
.PHONY: unit-test-coverage
 | 
			
		||||
unit-test-coverage:
 | 
			
		||||
@@ -480,7 +505,7 @@ unit-test-coverage:
 | 
			
		||||
	@$(GO) test $(GOTESTFLAGS) -timeout=20m -tags='$(TEST_TAGS)' -cover -coverprofile coverage.out $(GO_TEST_PACKAGES) && echo "\n==>\033[32m Ok\033[m\n" || exit 1
 | 
			
		||||
 | 
			
		||||
.PHONY: tidy
 | 
			
		||||
tidy: ## run go mod tidy
 | 
			
		||||
tidy:
 | 
			
		||||
	$(eval MIN_GO_VERSION := $(shell grep -Eo '^go\s+[0-9]+\.[0-9.]+' go.mod | cut -d' ' -f2))
 | 
			
		||||
	$(GO) mod tidy -compat=$(MIN_GO_VERSION)
 | 
			
		||||
	@$(MAKE) --no-print-directory $(GO_LICENSE_FILE)
 | 
			
		||||
@@ -494,17 +519,15 @@ tidy-check: tidy
 | 
			
		||||
	@diff=$$(git diff --color=always go.mod go.sum $(GO_LICENSE_FILE)); \
 | 
			
		||||
	if [ -n "$$diff" ]; then \
 | 
			
		||||
		echo "Please run 'make tidy' and commit the result:"; \
 | 
			
		||||
		printf "%s" "$${diff}"; \
 | 
			
		||||
		echo "$${diff}"; \
 | 
			
		||||
		exit 1; \
 | 
			
		||||
	fi
 | 
			
		||||
 | 
			
		||||
.PHONY: go-licenses
 | 
			
		||||
go-licenses: $(GO_LICENSE_FILE) ## regenerate go licenses
 | 
			
		||||
go-licenses: $(GO_LICENSE_FILE)
 | 
			
		||||
 | 
			
		||||
$(GO_LICENSE_FILE): go.mod go.sum
 | 
			
		||||
	@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 $(GO_LICENSES_PACKAGE) save . --force --save_path=$(GO_LICENSE_TMP_DIR) 2>/dev/null
 | 
			
		||||
	$(GO) run build/generate-go-licenses.go $(GO_LICENSE_TMP_DIR) $(GO_LICENSE_FILE)
 | 
			
		||||
	@rm -rf $(GO_LICENSE_TMP_DIR)
 | 
			
		||||
 | 
			
		||||
@@ -592,7 +615,7 @@ test-mssql-migration: migrations.mssql.test migrations.individual.mssql.test
 | 
			
		||||
 | 
			
		||||
.PHONY: playwright
 | 
			
		||||
playwright: deps-frontend
 | 
			
		||||
	$(NODE_VARS) pnpm exec playwright install $(PLAYWRIGHT_FLAGS)
 | 
			
		||||
	npx playwright install $(PLAYWRIGHT_FLAGS)
 | 
			
		||||
 | 
			
		||||
.PHONY: test-e2e%
 | 
			
		||||
test-e2e%: TEST_TYPE ?= e2e
 | 
			
		||||
@@ -748,17 +771,17 @@ install: $(wildcard *.go)
 | 
			
		||||
	CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) install -v -tags '$(TAGS)' -ldflags '-s -w $(LDFLAGS)'
 | 
			
		||||
 | 
			
		||||
.PHONY: build
 | 
			
		||||
build: frontend backend ## build everything
 | 
			
		||||
build: frontend backend
 | 
			
		||||
 | 
			
		||||
.PHONY: frontend
 | 
			
		||||
frontend: $(WEBPACK_DEST) ## build frontend files
 | 
			
		||||
frontend: $(WEBPACK_DEST)
 | 
			
		||||
 | 
			
		||||
.PHONY: backend
 | 
			
		||||
backend: go-check generate-backend $(EXECUTABLE) ## build backend files
 | 
			
		||||
backend: go-check generate-backend $(EXECUTABLE)
 | 
			
		||||
 | 
			
		||||
# We generate the backend before the frontend in case we in future we want to generate things in the frontend from generated files in backend
 | 
			
		||||
.PHONY: generate
 | 
			
		||||
generate: generate-backend ## run "go generate"
 | 
			
		||||
generate: generate-backend
 | 
			
		||||
 | 
			
		||||
.PHONY: generate-backend
 | 
			
		||||
generate-backend: $(TAGS_PREREQ) generate-go
 | 
			
		||||
@@ -770,13 +793,10 @@ generate-go: $(TAGS_PREREQ)
 | 
			
		||||
 | 
			
		||||
.PHONY: security-check
 | 
			
		||||
security-check:
 | 
			
		||||
	GOEXPERIMENT= go run $(GOVULNCHECK_PACKAGE) -show color ./...
 | 
			
		||||
	go run $(GOVULNCHECK_PACKAGE) ./...
 | 
			
		||||
 | 
			
		||||
$(EXECUTABLE): $(GO_SOURCES) $(TAGS_PREREQ)
 | 
			
		||||
ifneq ($(and $(STATIC),$(findstring pam,$(TAGS))),)
 | 
			
		||||
  $(error pam support set via TAGS doesn't support static builds)
 | 
			
		||||
endif
 | 
			
		||||
	CGO_ENABLED="$(CGO_ENABLED)" CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) build $(GOFLAGS) $(EXTRA_GOFLAGS) -tags '$(TAGS)' -ldflags '-s -w $(EXTLDFLAGS) $(LDFLAGS)' -o $@
 | 
			
		||||
	CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) build $(GOFLAGS) $(EXTRA_GOFLAGS) -tags '$(TAGS)' -ldflags '-s -w $(LDFLAGS)' -o $@
 | 
			
		||||
 | 
			
		||||
.PHONY: release
 | 
			
		||||
release: frontend generate release-windows release-linux release-darwin release-freebsd release-copy release-compress vendor release-sources release-check
 | 
			
		||||
@@ -826,20 +846,20 @@ release-sources: | $(DIST_DIRS)
 | 
			
		||||
	rm -f $(STORED_VERSION_FILE)
 | 
			
		||||
 | 
			
		||||
.PHONY: deps
 | 
			
		||||
deps: deps-frontend deps-backend deps-tools deps-py ## install dependencies
 | 
			
		||||
deps: deps-frontend deps-backend deps-tools deps-py
 | 
			
		||||
 | 
			
		||||
.PHONY: deps-py
 | 
			
		||||
deps-py: .venv ## install python dependencies
 | 
			
		||||
deps-py: .venv
 | 
			
		||||
 | 
			
		||||
.PHONY: deps-frontend
 | 
			
		||||
deps-frontend: node_modules ## install frontend dependencies
 | 
			
		||||
deps-frontend: node_modules
 | 
			
		||||
 | 
			
		||||
.PHONY: deps-backend
 | 
			
		||||
deps-backend: ## install backend dependencies
 | 
			
		||||
deps-backend:
 | 
			
		||||
	$(GO) mod download
 | 
			
		||||
 | 
			
		||||
.PHONY: deps-tools
 | 
			
		||||
deps-tools: ## install tool dependencies
 | 
			
		||||
deps-tools:
 | 
			
		||||
	$(GO) install $(AIR_PACKAGE) & \
 | 
			
		||||
	$(GO) install $(EDITORCONFIG_CHECKER_PACKAGE) & \
 | 
			
		||||
	$(GO) install $(GOFUMPT_PACKAGE) & \
 | 
			
		||||
@@ -852,50 +872,62 @@ deps-tools: ## install tool dependencies
 | 
			
		||||
	$(GO) install $(GOVULNCHECK_PACKAGE) & \
 | 
			
		||||
	$(GO) install $(ACTIONLINT_PACKAGE) & \
 | 
			
		||||
	$(GO) install $(GOPLS_PACKAGE) & \
 | 
			
		||||
	$(GO) install $(GOPLS_MODERNIZE_PACKAGE) & \
 | 
			
		||||
	wait
 | 
			
		||||
 | 
			
		||||
node_modules: pnpm-lock.yaml
 | 
			
		||||
	$(NODE_VARS) pnpm install --frozen-lockfile
 | 
			
		||||
node_modules: package-lock.json
 | 
			
		||||
	npm install --no-save
 | 
			
		||||
	@touch node_modules
 | 
			
		||||
 | 
			
		||||
.venv: uv.lock
 | 
			
		||||
	uv sync
 | 
			
		||||
.venv: poetry.lock
 | 
			
		||||
	poetry install
 | 
			
		||||
	@touch .venv
 | 
			
		||||
 | 
			
		||||
.PHONY: update
 | 
			
		||||
update: update-js update-py ## update js and py dependencies
 | 
			
		||||
update: update-js update-py
 | 
			
		||||
 | 
			
		||||
.PHONY: update-js
 | 
			
		||||
update-js: node-check | node_modules ## update js dependencies
 | 
			
		||||
	$(NODE_VARS) pnpm exec updates -u -f package.json
 | 
			
		||||
	rm -rf node_modules pnpm-lock.yaml
 | 
			
		||||
	$(NODE_VARS) pnpm install
 | 
			
		||||
	$(NODE_VARS) pnpm exec nolyfill install
 | 
			
		||||
	$(NODE_VARS) pnpm install
 | 
			
		||||
update-js: node-check | node_modules
 | 
			
		||||
	npx updates -u -f package.json
 | 
			
		||||
	rm -rf node_modules package-lock.json
 | 
			
		||||
	npm install --package-lock
 | 
			
		||||
	npx nolyfill install
 | 
			
		||||
	npm install --package-lock
 | 
			
		||||
	@touch node_modules
 | 
			
		||||
 | 
			
		||||
.PHONY: update-py
 | 
			
		||||
update-py: node-check | node_modules ## update py dependencies
 | 
			
		||||
	$(NODE_VARS) pnpm exec updates -u -f pyproject.toml
 | 
			
		||||
	rm -rf .venv uv.lock
 | 
			
		||||
	uv sync
 | 
			
		||||
update-py: node-check | node_modules
 | 
			
		||||
	npx updates -u -f pyproject.toml
 | 
			
		||||
	rm -rf .venv poetry.lock
 | 
			
		||||
	poetry install
 | 
			
		||||
	@touch .venv
 | 
			
		||||
 | 
			
		||||
.PHONY: webpack
 | 
			
		||||
webpack: $(WEBPACK_DEST) ## build webpack files
 | 
			
		||||
.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.*
 | 
			
		||||
 | 
			
		||||
$(WEBPACK_DEST): $(WEBPACK_SOURCES) $(WEBPACK_CONFIGS) pnpm-lock.yaml
 | 
			
		||||
.PHONY: webpack
 | 
			
		||||
webpack: $(WEBPACK_DEST)
 | 
			
		||||
 | 
			
		||||
$(WEBPACK_DEST): $(WEBPACK_SOURCES) $(WEBPACK_CONFIGS) package-lock.json
 | 
			
		||||
	@$(MAKE) -s node-check node_modules
 | 
			
		||||
	@rm -rf $(WEBPACK_DEST_ENTRIES)
 | 
			
		||||
	@echo "Running webpack..."
 | 
			
		||||
	@BROWSERSLIST_IGNORE_OLD_DATA=true $(NODE_VARS) pnpm exec webpack --disable-interpret
 | 
			
		||||
	@BROWSERSLIST_IGNORE_OLD_DATA=true npx webpack
 | 
			
		||||
	@touch $(WEBPACK_DEST)
 | 
			
		||||
 | 
			
		||||
.PHONY: svg
 | 
			
		||||
svg: node-check | node_modules ## build svg files
 | 
			
		||||
svg: node-check | node_modules
 | 
			
		||||
	rm -rf $(SVG_DEST_DIR)
 | 
			
		||||
	node tools/generate-svg.ts
 | 
			
		||||
	node tools/generate-svg.js
 | 
			
		||||
 | 
			
		||||
.PHONY: svg-check
 | 
			
		||||
svg-check: svg
 | 
			
		||||
@@ -903,18 +935,18 @@ svg-check: svg
 | 
			
		||||
	@diff=$$(git diff --color=always --cached $(SVG_DEST_DIR)); \
 | 
			
		||||
	if [ -n "$$diff" ]; then \
 | 
			
		||||
		echo "Please run 'make svg' and 'git add $(SVG_DEST_DIR)' and commit the result:"; \
 | 
			
		||||
		printf "%s" "$${diff}"; \
 | 
			
		||||
		echo "$${diff}"; \
 | 
			
		||||
		exit 1; \
 | 
			
		||||
	fi
 | 
			
		||||
 | 
			
		||||
.PHONY: lockfile-check
 | 
			
		||||
lockfile-check:
 | 
			
		||||
	$(NODE_VARS) pnpm install --frozen-lockfile
 | 
			
		||||
	@diff=$$(git diff --color=always pnpm-lock.yaml); \
 | 
			
		||||
	npm install --package-lock-only
 | 
			
		||||
	@diff=$$(git diff --color=always package-lock.json); \
 | 
			
		||||
	if [ -n "$$diff" ]; then \
 | 
			
		||||
		echo "pnpm-lock.yaml is inconsistent with package.json"; \
 | 
			
		||||
		echo "Please run 'pnpm install --frozen-lockfile' and commit the result:"; \
 | 
			
		||||
		printf "%s" "$${diff}"; \
 | 
			
		||||
		echo "package-lock.json is inconsistent with package.json"; \
 | 
			
		||||
		echo "Please run 'npm install --package-lock-only' and commit the result:"; \
 | 
			
		||||
		echo "$${diff}"; \
 | 
			
		||||
		exit 1; \
 | 
			
		||||
	fi
 | 
			
		||||
 | 
			
		||||
@@ -928,16 +960,21 @@ update-translations:
 | 
			
		||||
	mv ./translations/*.ini ./options/locale/
 | 
			
		||||
	rmdir ./translations
 | 
			
		||||
 | 
			
		||||
.PHONY: generate-license
 | 
			
		||||
generate-license:
 | 
			
		||||
	$(GO) run build/generate-licenses.go
 | 
			
		||||
 | 
			
		||||
.PHONY: generate-gitignore
 | 
			
		||||
generate-gitignore: ## update gitignore files
 | 
			
		||||
generate-gitignore:
 | 
			
		||||
	$(GO) run build/generate-gitignores.go
 | 
			
		||||
 | 
			
		||||
.PHONY: generate-images
 | 
			
		||||
generate-images: | node_modules ## generate images
 | 
			
		||||
	cd tools && node generate-images.ts $(TAGS)
 | 
			
		||||
generate-images: | node_modules
 | 
			
		||||
	npm install --no-save fabric@6 imagemin-zopfli@7
 | 
			
		||||
	node tools/generate-images.js $(TAGS)
 | 
			
		||||
 | 
			
		||||
.PHONY: generate-manpage
 | 
			
		||||
generate-manpage: ## generate manpage
 | 
			
		||||
generate-manpage:
 | 
			
		||||
	@[ -f gitea ] || make backend
 | 
			
		||||
	@mkdir -p man/man1/ man/man5
 | 
			
		||||
	@./gitea docs --man > man/man1/gitea.1
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										112
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										112
									
								
								README.md
									
									
									
									
									
								
							@@ -9,9 +9,9 @@
 | 
			
		||||
[](https://opencollective.com/gitea "Become a backer/sponsor of gitea")
 | 
			
		||||
[](https://opensource.org/licenses/MIT "License: MIT")
 | 
			
		||||
[](https://gitpod.io/#https://github.com/go-gitea/gitea)
 | 
			
		||||
[](https://translate.gitea.com "Crowdin")
 | 
			
		||||
[](https://crowdin.com/project/gitea "Crowdin")
 | 
			
		||||
 | 
			
		||||
[繁體中文](./README.zh-tw.md) | [简体中文](./README.zh-cn.md)
 | 
			
		||||
[View this document in Chinese](./README_ZH.md)
 | 
			
		||||
 | 
			
		||||
## Purpose
 | 
			
		||||
 | 
			
		||||
@@ -31,14 +31,6 @@ For accessing free Gitea service (with a limited number of repositories), you ca
 | 
			
		||||
 | 
			
		||||
To quickly deploy your own dedicated Gitea instance on Gitea Cloud, you can start a free trial at [cloud.gitea.com](https://cloud.gitea.com).
 | 
			
		||||
 | 
			
		||||
## Documentation
 | 
			
		||||
 | 
			
		||||
You can find comprehensive documentation on our official [documentation website](https://docs.gitea.com/).
 | 
			
		||||
 | 
			
		||||
It includes installation, administration, usage, development, contributing guides, and more to help you get started and explore all features effectively.
 | 
			
		||||
 | 
			
		||||
If you have any suggestions or would like to contribute to it, you can visit the [documentation repository](https://gitea.com/gitea/docs)
 | 
			
		||||
 | 
			
		||||
## Building
 | 
			
		||||
 | 
			
		||||
From the root of the source tree, run:
 | 
			
		||||
@@ -52,7 +44,7 @@ or if SQLite support is required:
 | 
			
		||||
The `build` target is split into two sub-targets:
 | 
			
		||||
 | 
			
		||||
- `make backend` which requires [Go Stable](https://go.dev/dl/), the required version is defined in [go.mod](/go.mod).
 | 
			
		||||
- `make frontend` which requires [Node.js LTS](https://nodejs.org/en/download/) or greater and [pnpm](https://pnpm.io/installation).
 | 
			
		||||
- `make frontend` which requires [Node.js LTS](https://nodejs.org/en/download/) or greater.
 | 
			
		||||
 | 
			
		||||
Internet connectivity is required to download the go and npm modules. When building from the official source tarballs which include pre-built frontend files, the `frontend` target will not be triggered, making it possible to build without Node.js.
 | 
			
		||||
 | 
			
		||||
@@ -60,8 +52,6 @@ More info: https://docs.gitea.com/installation/install-from-source
 | 
			
		||||
 | 
			
		||||
## Using
 | 
			
		||||
 | 
			
		||||
After building, a binary file named `gitea` will be generated in the root of the source tree by default. To run it, use:
 | 
			
		||||
 | 
			
		||||
    ./gitea web
 | 
			
		||||
 | 
			
		||||
> [!NOTE]
 | 
			
		||||
@@ -78,25 +68,22 @@ Expected workflow is: Fork -> Patch -> Push -> Pull Request
 | 
			
		||||
 | 
			
		||||
## Translating
 | 
			
		||||
 | 
			
		||||
[](https://translate.gitea.com)
 | 
			
		||||
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.
 | 
			
		||||
 | 
			
		||||
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).
 | 
			
		||||
[](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/).
 | 
			
		||||
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), where you can discover more third-party projects, including SDKs, plugins, themes, and more.
 | 
			
		||||
We maintain a list of Gitea-related projects at [gitea/awesome-gitea](https://gitea.com/gitea/awesome-gitea).
 | 
			
		||||
 | 
			
		||||
## Communication
 | 
			
		||||
 | 
			
		||||
[](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/).
 | 
			
		||||
The official Gitea CLI is developed at [gitea/tea](https://gitea.com/gitea/tea).
 | 
			
		||||
 | 
			
		||||
## Authors
 | 
			
		||||
 | 
			
		||||
@@ -135,79 +122,18 @@ Gitea is pronounced [/ɡɪ’ti:/](https://youtu.be/EM71-2uDAoY) as in "gi-tea"
 | 
			
		||||
 | 
			
		||||
We're [working on it](https://github.com/go-gitea/gitea/issues/1029).
 | 
			
		||||
 | 
			
		||||
**Where can I find the security patches?**
 | 
			
		||||
 | 
			
		||||
In the [release log](https://github.com/go-gitea/gitea/releases) or the [change log](https://github.com/go-gitea/gitea/blob/main/CHANGELOG.md), search for the keyword `SECURITY` to find the security patches.
 | 
			
		||||
 | 
			
		||||
## License
 | 
			
		||||
 | 
			
		||||
This project is licensed under the MIT License.
 | 
			
		||||
See the [LICENSE](https://github.com/go-gitea/gitea/blob/main/LICENSE) file
 | 
			
		||||
for the full license text.
 | 
			
		||||
 | 
			
		||||
## Further information
 | 
			
		||||
## Screenshots
 | 
			
		||||
 | 
			
		||||
<details>
 | 
			
		||||
<summary>Looking for an overview of the interface? Check it out!</summary>
 | 
			
		||||
Looking for an overview of the interface? Check it out!
 | 
			
		||||
 | 
			
		||||
### Login/Register Page
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
### User Dashboard
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||

 | 
			
		||||

 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
### User Profile
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
### Explore
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||

 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
### Repository
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||

 | 
			
		||||

 | 
			
		||||

 | 
			
		||||

 | 
			
		||||

 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
#### Repository Issue
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
#### Repository Pull Requests
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||

 | 
			
		||||

 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
#### Repository Actions
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
#### Repository Activity
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||

 | 
			
		||||

 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
### Organization
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
</details>
 | 
			
		||||
||||
 | 
			
		||||
|:---:|:---:|:---:|
 | 
			
		||||
||||
 | 
			
		||||
||||
 | 
			
		||||
||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										206
									
								
								README.zh-cn.md
									
									
									
									
									
								
							
							
						
						
									
										206
									
								
								README.zh-cn.md
									
									
									
									
									
								
							@@ -1,206 +0,0 @@
 | 
			
		||||
# Gitea
 | 
			
		||||
 | 
			
		||||
[](https://github.com/go-gitea/gitea/actions/workflows/release-nightly.yml?query=branch%3Amain "Release Nightly")
 | 
			
		||||
[](https://discord.gg/Gitea "Join the Discord chat at https://discord.gg/Gitea")
 | 
			
		||||
[](https://goreportcard.com/report/code.gitea.io/gitea "Go Report Card")
 | 
			
		||||
[](https://pkg.go.dev/code.gitea.io/gitea "GoDoc")
 | 
			
		||||
[](https://github.com/go-gitea/gitea/releases/latest "GitHub release")
 | 
			
		||||
[](https://www.codetriage.com/go-gitea/gitea "Help Contribute to Open Source")
 | 
			
		||||
[](https://opencollective.com/gitea "Become a backer/sponsor of gitea")
 | 
			
		||||
[](https://opensource.org/licenses/MIT "License: MIT")
 | 
			
		||||
[](https://gitpod.io/#https://github.com/go-gitea/gitea)
 | 
			
		||||
[](https://translate.gitea.com "Crowdin")
 | 
			
		||||
 | 
			
		||||
[English](./README.md) | [繁體中文](./README.zh-tw.md)
 | 
			
		||||
 | 
			
		||||
## 目的
 | 
			
		||||
 | 
			
		||||
这个项目的目标是提供最简单、最快速、最无痛的方式来设置自托管的 Git 服务。
 | 
			
		||||
 | 
			
		||||
由于 Gitea 是用 Go 语言编写的,它可以在 Go 支持的所有平台和架构上运行,包括 Linux、macOS 和 Windows 的 x86、amd64、ARM 和 PowerPC 架构。这个项目自 2016 年 11 月从 [Gogs](https://gogs.io) [分叉](https://blog.gitea.com/welcome-to-gitea/) 而来,但已经有了很多变化。
 | 
			
		||||
 | 
			
		||||
在线演示可以访问 [demo.gitea.com](https://demo.gitea.com)。
 | 
			
		||||
 | 
			
		||||
要访问免费的 Gitea 服务(有一定数量的仓库限制),可以访问 [gitea.com](https://gitea.com/user/login)。
 | 
			
		||||
 | 
			
		||||
要快速部署您自己的专用 Gitea 实例,可以在 [cloud.gitea.com](https://cloud.gitea.com) 开始免费试用。
 | 
			
		||||
 | 
			
		||||
## 文件
 | 
			
		||||
 | 
			
		||||
您可以在我们的官方 [文件网站](https://docs.gitea.com/) 上找到全面的文件。
 | 
			
		||||
 | 
			
		||||
它包括安装、管理、使用、开发、贡献指南等,帮助您快速入门并有效地探索所有功能。
 | 
			
		||||
 | 
			
		||||
如果您有任何建议或想要贡献,可以访问 [文件仓库](https://gitea.com/gitea/docs)
 | 
			
		||||
 | 
			
		||||
## 构建
 | 
			
		||||
 | 
			
		||||
从源代码树的根目录运行:
 | 
			
		||||
 | 
			
		||||
    TAGS="bindata" make build
 | 
			
		||||
 | 
			
		||||
如果需要 SQLite 支持:
 | 
			
		||||
 | 
			
		||||
    TAGS="bindata sqlite sqlite_unlock_notify" make build
 | 
			
		||||
 | 
			
		||||
`build` 目标分为两个子目标:
 | 
			
		||||
 | 
			
		||||
- `make backend` 需要 [Go Stable](https://go.dev/dl/),所需版本在 [go.mod](/go.mod) 中定义。
 | 
			
		||||
- `make frontend` 需要 [Node.js LTS](https://nodejs.org/en/download/) 或更高版本。
 | 
			
		||||
 | 
			
		||||
需要互联网连接来下载 go 和 npm 模块。从包含预构建前端文件的官方源代码压缩包构建时,不会触发 `frontend` 目标,因此可以在没有 Node.js 的情况下构建。
 | 
			
		||||
 | 
			
		||||
更多信息:https://docs.gitea.com/installation/install-from-source
 | 
			
		||||
 | 
			
		||||
## 使用
 | 
			
		||||
 | 
			
		||||
构建后,默认情况下会在源代码树的根目录生成一个名为 `gitea` 的二进制文件。要运行它,请使用:
 | 
			
		||||
 | 
			
		||||
    ./gitea web
 | 
			
		||||
 | 
			
		||||
> [!注意]
 | 
			
		||||
> 如果您对使用我们的 API 感兴趣,我们提供了实验性支持,并附有 [文件](https://docs.gitea.com/api)。
 | 
			
		||||
 | 
			
		||||
## 贡献
 | 
			
		||||
 | 
			
		||||
预期的工作流程是:Fork -> Patch -> Push -> Pull Request
 | 
			
		||||
 | 
			
		||||
> [!注意]
 | 
			
		||||
>
 | 
			
		||||
> 1. **在开始进行 Pull Request 之前,您必须阅读 [贡献者指南](CONTRIBUTING.md)。**
 | 
			
		||||
> 2. 如果您在项目中发现了漏洞,请私下写信给 **security@gitea.io**。谢谢!
 | 
			
		||||
 | 
			
		||||
## 翻译
 | 
			
		||||
 | 
			
		||||
[](https://translate.gitea.com)
 | 
			
		||||
 | 
			
		||||
翻译通过 [Crowdin](https://translate.gitea.com) 进行。如果您想翻译成新的语言,请在 Crowdin 项目中请求管理员添加新语言。
 | 
			
		||||
 | 
			
		||||
您也可以创建一个 issue 来添加语言,或者在 discord 的 #translation 频道上询问。如果您需要上下文或发现一些翻译问题,可以在字符串上留言或在 Discord 上询问。对于一般的翻译问题,文档中有一个部分。目前有点空,但我们希望随着问题的出现而填充它。
 | 
			
		||||
 | 
			
		||||
更多信息请参阅 [文件](https://docs.gitea.com/contributing/localization)。
 | 
			
		||||
 | 
			
		||||
## 官方和第三方项目
 | 
			
		||||
 | 
			
		||||
我们提供了一个官方的 [go-sdk](https://gitea.com/gitea/go-sdk),一个名为 [tea](https://gitea.com/gitea/tea) 的 CLI 工具和一个 Gitea Action 的 [action runner](https://gitea.com/gitea/act_runner)。
 | 
			
		||||
 | 
			
		||||
我们在 [gitea/awesome-gitea](https://gitea.com/gitea/awesome-gitea) 维护了一个 Gitea 相关项目的列表,您可以在那里发现更多的第三方项目,包括 SDK、插件、主题等。
 | 
			
		||||
 | 
			
		||||
## 通讯
 | 
			
		||||
 | 
			
		||||
[](https://discord.gg/Gitea "Join the Discord chat at https://discord.gg/Gitea")
 | 
			
		||||
 | 
			
		||||
如果您有任何文件未涵盖的问题,可以在我们的 [Discord 服务器](https://discord.gg/Gitea) 上与我们联系,或者在 [discourse 论坛](https://forum.gitea.com/) 上创建帖子。
 | 
			
		||||
 | 
			
		||||
## 作者
 | 
			
		||||
 | 
			
		||||
- [维护者](https://github.com/orgs/go-gitea/people)
 | 
			
		||||
- [贡献者](https://github.com/go-gitea/gitea/graphs/contributors)
 | 
			
		||||
- [翻译者](options/locale/TRANSLATORS)
 | 
			
		||||
 | 
			
		||||
## 支持者
 | 
			
		||||
 | 
			
		||||
感谢所有支持者! 🙏 [[成为支持者](https://opencollective.com/gitea#backer)]
 | 
			
		||||
 | 
			
		||||
<a href="https://opencollective.com/gitea#backers" target="_blank"><img src="https://opencollective.com/gitea/backers.svg?width=890"></a>
 | 
			
		||||
 | 
			
		||||
## 赞助商
 | 
			
		||||
 | 
			
		||||
通过成为赞助商来支持这个项目。您的标志将显示在这里,并带有链接到您的网站。 [[成为赞助商](https://opencollective.com/gitea#sponsor)]
 | 
			
		||||
 | 
			
		||||
<a href="https://opencollective.com/gitea/sponsor/0/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/0/avatar.svg"></a>
 | 
			
		||||
<a href="https://opencollective.com/gitea/sponsor/1/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/1/avatar.svg"></a>
 | 
			
		||||
<a href="https://opencollective.com/gitea/sponsor/2/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/2/avatar.svg"></a>
 | 
			
		||||
<a href="https://opencollective.com/gitea/sponsor/3/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/3/avatar.svg"></a>
 | 
			
		||||
<a href="https://opencollective.com/gitea/sponsor/4/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/4/avatar.svg"></a>
 | 
			
		||||
<a href="https://opencollective.com/gitea/sponsor/5/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/5/avatar.svg"></a>
 | 
			
		||||
<a href="https://opencollective.com/gitea/sponsor/6/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/6/avatar.svg"></a>
 | 
			
		||||
<a href="https://opencollective.com/gitea/sponsor/7/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/7/avatar.svg"></a>
 | 
			
		||||
<a href="https://opencollective.com/gitea/sponsor/8/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/8/avatar.svg"></a>
 | 
			
		||||
<a href="https://opencollective.com/gitea/sponsor/9/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/9/avatar.svg"></a>
 | 
			
		||||
 | 
			
		||||
## 常见问题
 | 
			
		||||
 | 
			
		||||
**Gitea 怎么发音?**
 | 
			
		||||
 | 
			
		||||
Gitea 的发音是 [/ɡɪ’ti:/](https://youtu.be/EM71-2uDAoY),就像 "gi-tea" 一样,g 是硬音。
 | 
			
		||||
 | 
			
		||||
**为什么这个项目没有托管在 Gitea 实例上?**
 | 
			
		||||
 | 
			
		||||
我们正在 [努力](https://github.com/go-gitea/gitea/issues/1029)。
 | 
			
		||||
 | 
			
		||||
**在哪里可以找到安全补丁?**
 | 
			
		||||
 | 
			
		||||
在 [发布日志](https://github.com/go-gitea/gitea/releases) 或 [变更日志](https://github.com/go-gitea/gitea/blob/main/CHANGELOG.md) 中,搜索关键词 `SECURITY` 以找到安全补丁。
 | 
			
		||||
 | 
			
		||||
## 许可证
 | 
			
		||||
 | 
			
		||||
这个项目是根据 MIT 许可证授权的。
 | 
			
		||||
请参阅 [LICENSE](https://github.com/go-gitea/gitea/blob/main/LICENSE) 文件以获取完整的许可证文本。
 | 
			
		||||
 | 
			
		||||
## 进一步信息
 | 
			
		||||
 | 
			
		||||
<details>
 | 
			
		||||
<summary>寻找界面概述?查看这里!</summary>
 | 
			
		||||
 | 
			
		||||
### 登录/注册页面
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
### 用户仪表板
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||

 | 
			
		||||

 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
### 用户资料
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
### 探索
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||

 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
### 仓库
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||

 | 
			
		||||

 | 
			
		||||

 | 
			
		||||

 | 
			
		||||

 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
#### 仓库问题
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
#### 仓库拉取请求
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||

 | 
			
		||||

 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
#### 仓库操作
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
#### 仓库活动
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||

 | 
			
		||||

 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
### 组织
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
</details>
 | 
			
		||||
							
								
								
									
										206
									
								
								README.zh-tw.md
									
									
									
									
									
								
							
							
						
						
									
										206
									
								
								README.zh-tw.md
									
									
									
									
									
								
							@@ -1,206 +0,0 @@
 | 
			
		||||
# Gitea
 | 
			
		||||
 | 
			
		||||
[](https://github.com/go-gitea/gitea/actions/workflows/release-nightly.yml?query=branch%3Amain "Release Nightly")
 | 
			
		||||
[](https://discord.gg/Gitea "Join the Discord chat at https://discord.gg/Gitea")
 | 
			
		||||
[](https://goreportcard.com/report/code.gitea.io/gitea "Go Report Card")
 | 
			
		||||
[](https://pkg.go.dev/code.gitea.io/gitea "GoDoc")
 | 
			
		||||
[](https://github.com/go-gitea/gitea/releases/latest "GitHub release")
 | 
			
		||||
[](https://www.codetriage.com/go-gitea/gitea "Help Contribute to Open Source")
 | 
			
		||||
[](https://opencollective.com/gitea "Become a backer/sponsor of gitea")
 | 
			
		||||
[](https://opensource.org/licenses/MIT "License: MIT")
 | 
			
		||||
[](https://gitpod.io/#https://github.com/go-gitea/gitea)
 | 
			
		||||
[](https://translate.gitea.com "Crowdin")
 | 
			
		||||
 | 
			
		||||
[English](./README.md) | [简体中文](./README.zh-cn.md)
 | 
			
		||||
 | 
			
		||||
## 目的
 | 
			
		||||
 | 
			
		||||
這個項目的目標是提供最簡單、最快速、最無痛的方式來設置自託管的 Git 服務。
 | 
			
		||||
 | 
			
		||||
由於 Gitea 是用 Go 語言編寫的,它可以在 Go 支援的所有平台和架構上運行,包括 Linux、macOS 和 Windows 的 x86、amd64、ARM 和 PowerPC 架構。這個項目自 2016 年 11 月從 [Gogs](https://gogs.io) [分叉](https://blog.gitea.com/welcome-to-gitea/) 而來,但已經有了很多變化。
 | 
			
		||||
 | 
			
		||||
在線演示可以訪問 [demo.gitea.com](https://demo.gitea.com)。
 | 
			
		||||
 | 
			
		||||
要訪問免費的 Gitea 服務(有一定數量的倉庫限制),可以訪問 [gitea.com](https://gitea.com/user/login)。
 | 
			
		||||
 | 
			
		||||
要快速部署您自己的專用 Gitea 實例,可以在 [cloud.gitea.com](https://cloud.gitea.com) 開始免費試用。
 | 
			
		||||
 | 
			
		||||
## 文件
 | 
			
		||||
 | 
			
		||||
您可以在我們的官方 [文件網站](https://docs.gitea.com/) 上找到全面的文件。
 | 
			
		||||
 | 
			
		||||
它包括安裝、管理、使用、開發、貢獻指南等,幫助您快速入門並有效地探索所有功能。
 | 
			
		||||
 | 
			
		||||
如果您有任何建議或想要貢獻,可以訪問 [文件倉庫](https://gitea.com/gitea/docs)
 | 
			
		||||
 | 
			
		||||
## 構建
 | 
			
		||||
 | 
			
		||||
從源代碼樹的根目錄運行:
 | 
			
		||||
 | 
			
		||||
    TAGS="bindata" make build
 | 
			
		||||
 | 
			
		||||
如果需要 SQLite 支援:
 | 
			
		||||
 | 
			
		||||
    TAGS="bindata sqlite sqlite_unlock_notify" make build
 | 
			
		||||
 | 
			
		||||
`build` 目標分為兩個子目標:
 | 
			
		||||
 | 
			
		||||
- `make backend` 需要 [Go Stable](https://go.dev/dl/),所需版本在 [go.mod](/go.mod) 中定義。
 | 
			
		||||
- `make frontend` 需要 [Node.js LTS](https://nodejs.org/en/download/) 或更高版本。
 | 
			
		||||
 | 
			
		||||
需要互聯網連接來下載 go 和 npm 模塊。從包含預構建前端文件的官方源代碼壓縮包構建時,不會觸發 `frontend` 目標,因此可以在沒有 Node.js 的情況下構建。
 | 
			
		||||
 | 
			
		||||
更多信息:https://docs.gitea.com/installation/install-from-source
 | 
			
		||||
 | 
			
		||||
## 使用
 | 
			
		||||
 | 
			
		||||
構建後,默認情況下會在源代碼樹的根目錄生成一個名為 `gitea` 的二進制文件。要運行它,請使用:
 | 
			
		||||
 | 
			
		||||
    ./gitea web
 | 
			
		||||
 | 
			
		||||
> [!注意]
 | 
			
		||||
> 如果您對使用我們的 API 感興趣,我們提供了實驗性支援,並附有 [文件](https://docs.gitea.com/api)。
 | 
			
		||||
 | 
			
		||||
## 貢獻
 | 
			
		||||
 | 
			
		||||
預期的工作流程是:Fork -> Patch -> Push -> Pull Request
 | 
			
		||||
 | 
			
		||||
> [!注意]
 | 
			
		||||
>
 | 
			
		||||
> 1. **在開始進行 Pull Request 之前,您必須閱讀 [貢獻者指南](CONTRIBUTING.md)。**
 | 
			
		||||
> 2. 如果您在項目中發現了漏洞,請私下寫信給 **security@gitea.io**。謝謝!
 | 
			
		||||
 | 
			
		||||
## 翻譯
 | 
			
		||||
 | 
			
		||||
[](https://translate.gitea.com)
 | 
			
		||||
 | 
			
		||||
翻譯通過 [Crowdin](https://translate.gitea.com) 進行。如果您想翻譯成新的語言,請在 Crowdin 項目中請求管理員添加新語言。
 | 
			
		||||
 | 
			
		||||
您也可以創建一個 issue 來添加語言,或者在 discord 的 #translation 頻道上詢問。如果您需要上下文或發現一些翻譯問題,可以在字符串上留言或在 Discord 上詢問。對於一般的翻譯問題,文檔中有一個部分。目前有點空,但我們希望隨著問題的出現而填充它。
 | 
			
		||||
 | 
			
		||||
更多信息請參閱 [文件](https://docs.gitea.com/contributing/localization)。
 | 
			
		||||
 | 
			
		||||
## 官方和第三方項目
 | 
			
		||||
 | 
			
		||||
我們提供了一個官方的 [go-sdk](https://gitea.com/gitea/go-sdk),一個名為 [tea](https://gitea.com/gitea/tea) 的 CLI 工具和一個 Gitea Action 的 [action runner](https://gitea.com/gitea/act_runner)。
 | 
			
		||||
 | 
			
		||||
我們在 [gitea/awesome-gitea](https://gitea.com/gitea/awesome-gitea) 維護了一個 Gitea 相關項目的列表,您可以在那裡發現更多的第三方項目,包括 SDK、插件、主題等。
 | 
			
		||||
 | 
			
		||||
## 通訊
 | 
			
		||||
 | 
			
		||||
[](https://discord.gg/Gitea "Join the Discord chat at https://discord.gg/Gitea")
 | 
			
		||||
 | 
			
		||||
如果您有任何文件未涵蓋的問題,可以在我們的 [Discord 服務器](https://discord.gg/Gitea) 上與我們聯繫,或者在 [discourse 論壇](https://forum.gitea.com/) 上創建帖子。
 | 
			
		||||
 | 
			
		||||
## 作者
 | 
			
		||||
 | 
			
		||||
- [維護者](https://github.com/orgs/go-gitea/people)
 | 
			
		||||
- [貢獻者](https://github.com/go-gitea/gitea/graphs/contributors)
 | 
			
		||||
- [翻譯者](options/locale/TRANSLATORS)
 | 
			
		||||
 | 
			
		||||
## 支持者
 | 
			
		||||
 | 
			
		||||
感謝所有支持者! 🙏 [[成為支持者](https://opencollective.com/gitea#backer)]
 | 
			
		||||
 | 
			
		||||
<a href="https://opencollective.com/gitea#backers" target="_blank"><img src="https://opencollective.com/gitea/backers.svg?width=890"></a>
 | 
			
		||||
 | 
			
		||||
## 贊助商
 | 
			
		||||
 | 
			
		||||
通過成為贊助商來支持這個項目。您的標誌將顯示在這裡,並帶有鏈接到您的網站。 [[成為贊助商](https://opencollective.com/gitea#sponsor)]
 | 
			
		||||
 | 
			
		||||
<a href="https://opencollective.com/gitea/sponsor/0/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/0/avatar.svg"></a>
 | 
			
		||||
<a href="https://opencollective.com/gitea/sponsor/1/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/1/avatar.svg"></a>
 | 
			
		||||
<a href="https://opencollective.com/gitea/sponsor/2/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/2/avatar.svg"></a>
 | 
			
		||||
<a href="https://opencollective.com/gitea/sponsor/3/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/3/avatar.svg"></a>
 | 
			
		||||
<a href="https://opencollective.com/gitea/sponsor/4/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/4/avatar.svg"></a>
 | 
			
		||||
<a href="https://opencollective.com/gitea/sponsor/5/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/5/avatar.svg"></a>
 | 
			
		||||
<a href="https://opencollective.com/gitea/sponsor/6/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/6/avatar.svg"></a>
 | 
			
		||||
<a href="https://opencollective.com/gitea/sponsor/7/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/7/avatar.svg"></a>
 | 
			
		||||
<a href="https://opencollective.com/gitea/sponsor/8/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/8/avatar.svg"></a>
 | 
			
		||||
<a href="https://opencollective.com/gitea/sponsor/9/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/9/avatar.svg"></a>
 | 
			
		||||
 | 
			
		||||
## 常見問題
 | 
			
		||||
 | 
			
		||||
**Gitea 怎麼發音?**
 | 
			
		||||
 | 
			
		||||
Gitea 的發音是 [/ɡɪ’ti:/](https://youtu.be/EM71-2uDAoY),就像 "gi-tea" 一樣,g 是硬音。
 | 
			
		||||
 | 
			
		||||
**為什麼這個項目沒有託管在 Gitea 實例上?**
 | 
			
		||||
 | 
			
		||||
我們正在 [努力](https://github.com/go-gitea/gitea/issues/1029)。
 | 
			
		||||
 | 
			
		||||
**在哪裡可以找到安全補丁?**
 | 
			
		||||
 | 
			
		||||
在 [發佈日誌](https://github.com/go-gitea/gitea/releases) 或 [變更日誌](https://github.com/go-gitea/gitea/blob/main/CHANGELOG.md) 中,搜索關鍵詞 `SECURITY` 以找到安全補丁。
 | 
			
		||||
 | 
			
		||||
## 許可證
 | 
			
		||||
 | 
			
		||||
這個項目是根據 MIT 許可證授權的。
 | 
			
		||||
請參閱 [LICENSE](https://github.com/go-gitea/gitea/blob/main/LICENSE) 文件以獲取完整的許可證文本。
 | 
			
		||||
 | 
			
		||||
## 進一步信息
 | 
			
		||||
 | 
			
		||||
<details>
 | 
			
		||||
<summary>尋找界面概述?查看這裡!</summary>
 | 
			
		||||
 | 
			
		||||
### 登錄/註冊頁面
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
### 用戶儀表板
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||

 | 
			
		||||

 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
### 用戶資料
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
### 探索
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||

 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
### 倉庫
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||

 | 
			
		||||

 | 
			
		||||

 | 
			
		||||

 | 
			
		||||

 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
#### 倉庫問題
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
#### 倉庫拉取請求
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||

 | 
			
		||||

 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
#### 倉庫操作
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
#### 倉庫活動
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||

 | 
			
		||||

 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
### 組織
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
</details>
 | 
			
		||||
							
								
								
									
										61
									
								
								README_ZH.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								README_ZH.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,61 @@
 | 
			
		||||
# Gitea
 | 
			
		||||
 | 
			
		||||
[](https://github.com/go-gitea/gitea/actions/workflows/release-nightly.yml?query=branch%3Amain "Release Nightly")
 | 
			
		||||
[](https://discord.gg/Gitea "Join the Discord chat at https://discord.gg/Gitea")
 | 
			
		||||
[](https://goreportcard.com/report/code.gitea.io/gitea "Go Report Card")
 | 
			
		||||
[](https://pkg.go.dev/code.gitea.io/gitea "GoDoc")
 | 
			
		||||
[](https://github.com/go-gitea/gitea/releases/latest "GitHub release")
 | 
			
		||||
[](https://www.codetriage.com/go-gitea/gitea "Help Contribute to Open Source")
 | 
			
		||||
[](https://opencollective.com/gitea "Become a backer/sponsor of gitea")
 | 
			
		||||
[](https://opensource.org/licenses/MIT "License: MIT")
 | 
			
		||||
[](https://gitpod.io/#https://github.com/go-gitea/gitea)
 | 
			
		||||
[](https://crowdin.com/project/gitea "Crowdin")
 | 
			
		||||
 | 
			
		||||
[View this document in English](./README.md)
 | 
			
		||||
 | 
			
		||||
## 目标
 | 
			
		||||
 | 
			
		||||
Gitea 的首要目标是创建一个极易安装,运行非常快速,安装和使用体验良好的自建 Git 服务。我们采用 Go 作为后端语言,这使我们只要生成一个可执行程序即可。并且他还支持跨平台,支持 Linux, macOS 和 Windows 以及各种架构,除了 x86,amd64,还包括 ARM 和 PowerPC。
 | 
			
		||||
 | 
			
		||||
如果你想试用在线演示和报告问题,请访问 [demo.gitea.com](https://demo.gitea.com/)。
 | 
			
		||||
 | 
			
		||||
如果你想使用免费的 Gitea 服务(有仓库数量限制),请访问 [gitea.com](https://gitea.com/user/login)。
 | 
			
		||||
 | 
			
		||||
如果你想在 Gitea Cloud 上快速部署你自己独享的 Gitea 实例,请访问 [cloud.gitea.com](https://cloud.gitea.com) 开始免费试用。
 | 
			
		||||
 | 
			
		||||
## 提示
 | 
			
		||||
 | 
			
		||||
1. **开始贡献代码之前请确保你已经看过了 [贡献者向导(英文)](CONTRIBUTING.md)**.
 | 
			
		||||
2. 所有的安全问题,请私下发送邮件给 **security@gitea.io**。谢谢!
 | 
			
		||||
3. 如果你要使用API,请参见 [API 文档](https://godoc.org/code.gitea.io/sdk/gitea).
 | 
			
		||||
 | 
			
		||||
## 文档
 | 
			
		||||
 | 
			
		||||
关于如何安装请访问我们的 [文档站](https://docs.gitea.com/zh-cn/category/installation),如果没有找到对应的文档,你也可以通过 [Discord - 英文](https://discord.gg/gitea) 和 QQ群 328432459 来和我们交流。
 | 
			
		||||
 | 
			
		||||
## 贡献流程
 | 
			
		||||
 | 
			
		||||
Fork -> Patch -> Push -> Pull Request
 | 
			
		||||
 | 
			
		||||
## 翻译
 | 
			
		||||
 | 
			
		||||
多语言翻译是基于Crowdin进行的.
 | 
			
		||||
[](https://crowdin.com/project/gitea)
 | 
			
		||||
 | 
			
		||||
## 作者
 | 
			
		||||
 | 
			
		||||
* [Maintainers](https://github.com/orgs/go-gitea/people)
 | 
			
		||||
* [Contributors](https://github.com/go-gitea/gitea/graphs/contributors)
 | 
			
		||||
* [Translators](options/locale/TRANSLATORS)
 | 
			
		||||
 | 
			
		||||
## 授权许可
 | 
			
		||||
 | 
			
		||||
本项目采用 MIT 开源授权许可证,完整的授权说明已放置在 [LICENSE](https://github.com/go-gitea/gitea/blob/main/LICENSE) 文件中。
 | 
			
		||||
 | 
			
		||||
## 截图
 | 
			
		||||
 | 
			
		||||
||||
 | 
			
		||||
|:---:|:---:|:---:|
 | 
			
		||||
||||
 | 
			
		||||
||||
 | 
			
		||||
||||
 | 
			
		||||
							
								
								
									
										54
									
								
								SECURITY.md
									
									
									
									
									
								
							
							
						
						
									
										54
									
								
								SECURITY.md
									
									
									
									
									
								
							@@ -14,12 +14,12 @@ Please **DO NOT** file a public issue, instead send your report privately to `se
 | 
			
		||||
 | 
			
		||||
Due to the sensitive nature of security information, you can use the below GPG public key to encrypt your mail body.
 | 
			
		||||
 | 
			
		||||
The PGP key is valid until July 4, 2026.
 | 
			
		||||
The PGP key is valid until July 9, 2025.
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
Key ID: 6FCD2D5B
 | 
			
		||||
Key Type: RSA
 | 
			
		||||
Expires: 7/4/2026
 | 
			
		||||
Expires: 7/9/2025
 | 
			
		||||
Key Size: 4096/4096
 | 
			
		||||
Fingerprint: 3DE0 3D1E 144A 7F06 9359 99DC AAFD 2381 6FCD 2D5B
 | 
			
		||||
```
 | 
			
		||||
@@ -42,18 +42,18 @@ lzpAjnN9/KLtQroutrm+Ft0mdjDiJUeFVl1cOHDhoyfCsQh62HumoyZoZvqzQd6e
 | 
			
		||||
AbN11nq6aViMe2Q3je1AbiBnRnQSHxt1Tc8X4IshO3MQK1Sk7oPI6LA5oQARAQAB
 | 
			
		||||
tCJHaXRlYSBTZWN1cml0eSA8c2VjdXJpdHlAZ2l0ZWEuaW8+iQJXBBMBCABBAhsD
 | 
			
		||||
BQsJCAcCAiICBhUKCQgLAgQWAgMBAh4HAheAFiEEPeA9HhRKfwaTWZncqv0jgW/N
 | 
			
		||||
LVsFAmhoHmkFCQeT6esACgkQqv0jgW/NLVuFLRAAmjBQSKRAgs2bFIEj7HLAbDp4
 | 
			
		||||
f+XkdH+GsT3jRPOZ9QZgmtM+TfoE4yNgIVfOl+s4RdjM/W4QzqZuPQ55hbEHd056
 | 
			
		||||
cJmm7B+6GsHFcdrPmh65sOCEIyh4+t45dUfeWpFsDPqm9j1UHXAJQIpB8vDEVAPH
 | 
			
		||||
t+3wLCk8GMPJs1o5tIyMmaO23ngvkwn8eG7KgY+rp2PzObrb5g7ppci0ILzILkrp
 | 
			
		||||
HVjZsEfUWRgSVF7LuU5ppqDKrlcqwUpQq6n3kGMZcLrCp6ACKP04TBmTfUxNwdL7
 | 
			
		||||
I0N7apI2Pbct9T1Gv/lYAUFWyU2c3gh/EBLbO6BukaLOFRQHrtNfdJV/YnMPlcXr
 | 
			
		||||
LUJjK9K4eAH9DsrZqrisz/LthsC2BaNIN3KRMTk5YTYgmIh8GXzSgihORmtDFELC
 | 
			
		||||
RroID3pTuS0zjXh+wpY9GuPTh7UW23p42Daxca4fAT4k5EclvDRUrL21xMopPMiL
 | 
			
		||||
HuNdELz4FVchRTy05PjzKVyjVInDNojE2KUxnjxZDzYJ6aT/g+coD5yfntYm8BEj
 | 
			
		||||
+ZzL0ndZES54hzKLpv7zwBQwFzam68clZYmDPILOPTflQDfpGEWmJK4undFU5obz
 | 
			
		||||
ZsQRz0R3ulspChATbZxO0d5LX2obLpKO9X3b5VoO1KF+R8Vjw1Y0KxrNZ6rIcfqH
 | 
			
		||||
Z50QVQKSe9dm08K0ON+5Ag0EYrVn/gEQALrFLQjCR3GjuHSindz0rd3Fnx/t7Sen
 | 
			
		||||
LVsFAmaMse0FCQW4fW8ACgkQqv0jgW/NLVtXLg/+PF4G9Jhlui15BTNlEBJAV2P/
 | 
			
		||||
1QlAV2krk0fP7tykn0FR9RfGIfVV/kwC1f+ouosYPQDDevl9LWdUIM+g94DtNo2o
 | 
			
		||||
7ACpcL3morvt5lVGpIZHL8TbX0qmFRXL/pB/cB+K6IwYvh2mrbp2zH+r4SCRyFYq
 | 
			
		||||
BjgXYFTI1MylJ1ShAjU6Z+m3oJ+2xs5LzHS0X6zkTjzA2Zl4zQzciQ9T+wJcE7Zi
 | 
			
		||||
HXdM1+YMF8KGNP8J9Rpug5oNDJ98lgZirRY7c3A/1xmYBiPnULwuuymdqEZO7l70
 | 
			
		||||
SeAlE1RWYX8kbOBnBb/KY4XwE3Vic1oEzc9DiPWVH1ElX86WNNsFzuyULiwoBoWg
 | 
			
		||||
pqZGhL9x1p5+46RGQSDczsHM7YGVtfYOiDo2PAVrmwsT0BnXnK8Oe3YIkvmUPEJu
 | 
			
		||||
OkLt0Z6A5n8pz8zhQzuApwBsK4ncJ8zTCpvz/pfKKqZC/Vnoh3gKGhDGvOZ+b5IJ
 | 
			
		||||
0kUTe2JsbnwFixDUMDtacQ1op8XOyLoLVmgqLn0+Pws4XPBlMof2bioFir3yHKnP
 | 
			
		||||
gNchsF1agrlSIo5GA8u4ga+IlCSfvFIKrl7+cxacKcJYt/vbOU5KcvVJI5EtHKCG
 | 
			
		||||
xfHjHY2ah1Qww7SxW6IXiRZZzPpsL2mBM2CD7N3qh9bV2s27wxYCdUodsIZbiyHe
 | 
			
		||||
oWPzfBnkmiAN8KlZxHm5Ag0EYrVn/gEQALrFLQjCR3GjuHSindz0rd3Fnx/t7Sen
 | 
			
		||||
T+p07yCSSoSlmnJHCQmwh4vfg1blyz0zZ4vkIhtpHsEgc+ZAG+WQXSsJ2iRz+eSN
 | 
			
		||||
GwoOQl4XC3n+QWkc1ws+btr48+6UqXIQU+F8TPQyx/PIgi2nZXJB7f5+mjCqsk46
 | 
			
		||||
XvH4nTr4kJjuqMSR/++wvre2qNQRa/q/dTsK0OaN/mJsdX6Oi+aGNaQJUhIG7F+E
 | 
			
		||||
@@ -65,19 +65,19 @@ s+GsP9I3cmWWQcKYxWHtE8xTXnNCVPFZQj2nwhJzae8ypfOtulBRA3dUKWGKuDH/
 | 
			
		||||
axFENhUsT397aOU3qkP/od4a64JyNIEo4CTTSPVeWd7njsGqli2U3A4xL2CcyYvt
 | 
			
		||||
D/MWcMBGEoLSNTswwKdom4FaJpn5KThnK/T0bQcmJblJhoCtppXisbexZnCpuS0x
 | 
			
		||||
Zdlm2T14KJ3LABEBAAGJAjwEGAEIACYCGwwWIQQ94D0eFEp/BpNZmdyq/SOBb80t
 | 
			
		||||
WwUCaGgeJAUJB5PppgAKCRCq/SOBb80tW/NWEACB6Jrf0gWlk7e+hNCdnbM0ZVWU
 | 
			
		||||
f2sHNFfXxxsdhpcDgKbNHtkZb8nZgv8AX+5fTtUwMVa3vKcdw30xFiIM5N7cCIPV
 | 
			
		||||
vg/5z5BtfEaitnabEUG2iiVDIy8IHXIcK10rX+7BosA3QDl2PsiBHwyi5G13lRk8
 | 
			
		||||
zGTSNDuOalug33h5/lr2dPigamkq74Aoy29q8Rjad6GfWHipL2bFimgtY+Zdi0BH
 | 
			
		||||
NLk4EJXxj1SgVx5dtkQzWJReBA5M+FQ4QYQZBO+f4TDoOLmjui152uhkoLBQbGAa
 | 
			
		||||
WWJFTVxm0bG5MXloEL3gA8DfU7XDwuW/sHJC5pBko8RpQViooOhckMepZV3Y83DK
 | 
			
		||||
bwLYa3JmPgj2rEv4993dvrJbQhpGd082HOxOsllCs8pgNq1SnXpWYfcGTgGKC3ts
 | 
			
		||||
U8YZUUJUQ7mi2L8Tv3ix20c9EiGmA30JAmA8eZTC3cWup91ZkkVBFRml2czTXajd
 | 
			
		||||
RWZ6GbHV5503ueDQcB8yBVgF3CSixs67+dGSbD3p86OqGrjAcJzM5TFbNKcnGLdE
 | 
			
		||||
kGbZpNwAISy750lXzXKmyrh5RTCeTOQerbwCMBvHZO+HAevA/LXDTw2OAiSIQlP5
 | 
			
		||||
sYA4sFYLQ30OAkgJcmdp/pSgVj/erNtSN07ClrOpDb/uFpQymO6K2h0Pst3feNVK
 | 
			
		||||
9M2VbqL9C51z/wyHLg==
 | 
			
		||||
=SfZA
 | 
			
		||||
WwUCZoyyjQUJBbh+DwAKCRCq/SOBb80tW18XD/9MXztmf01MT+1kZdBouZ/7Rp/7
 | 
			
		||||
9kuqo//B1G+RXau4oFtPqb67kNe2WaIc3u5B73PUHsMf3i6z4ib2KbMhZZerLn0O
 | 
			
		||||
dRglcuPeNWmsASY3dH/XVG0cT0zvvWegagd12TJEl3Vs+7XNrOw4cwDj9L1+GH9m
 | 
			
		||||
kSt4uaANWn/6a3RvMRhiVEYuNwhAzcKaactPmYqrLJgoVLbRSDkgyHaMQ2jKgLxk
 | 
			
		||||
ifS/fvluGV0ub2Po6DJiqfRpd1tDvPhe9y1+r1WFDZsOcvTcZUfSt/7dXMGfqGu0
 | 
			
		||||
2daVFlfeSXSALrDE5uc0UxodHCpP3sqRYDZevGLBRaaTkIjYXG/+N898+7K5WJF4
 | 
			
		||||
xXOLWxM2cwGkG7eC9pugcDnBp9XlF7O+GBiZ05JUe5flXDQFZ+h3exjopu6KHF1B
 | 
			
		||||
RnzNy8LC0UKb+AuvRIOLV92a9Q9wGWU/jaVDu6nZ0umAeuSzxiHoDsonm0Fl9QAz
 | 
			
		||||
2/xCokebuoeLrEK7R2af3X86mqq3sVO4ax+HPYChzOaVQBiHUW/TAldWcldYYphR
 | 
			
		||||
/e2WsbmQfvCRtz/bZfo+aUVnrHNjzVMtF2SszdVmA/04Y8pS28MqtuRqhm5DPOOd
 | 
			
		||||
g1YeUywK5jRZ1twyo1kzJEFPLaoeaXaycsR1PMVBW0Urik5mrR/pOWq7PPoZoKb2
 | 
			
		||||
lXYLE8bwkuQTmsyL1g==
 | 
			
		||||
=9i7d
 | 
			
		||||
-----END PGP PUBLIC KEY BLOCK-----
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										172
									
								
								assets/go-licenses.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										172
									
								
								assets/go-licenses.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										11
									
								
								build.go
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								build.go
									
									
									
									
									
								
							@@ -5,10 +5,19 @@
 | 
			
		||||
 | 
			
		||||
package main
 | 
			
		||||
 | 
			
		||||
// Libraries that are included to vendor utilities used during Makefile build.
 | 
			
		||||
// Libraries that are included to vendor utilities used during build.
 | 
			
		||||
// These libraries will not be included in a normal compilation.
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	// for embed
 | 
			
		||||
	_ "github.com/shurcooL/vfsgen"
 | 
			
		||||
 | 
			
		||||
	// for cover merge
 | 
			
		||||
	_ "golang.org/x/tools/cover"
 | 
			
		||||
 | 
			
		||||
	// for vet
 | 
			
		||||
	_ "code.gitea.io/gitea-vet"
 | 
			
		||||
 | 
			
		||||
	// for swagger
 | 
			
		||||
	_ "github.com/go-swagger/go-swagger/cmd/swagger"
 | 
			
		||||
)
 | 
			
		||||
 
 | 
			
		||||
@@ -12,11 +12,10 @@ import (
 | 
			
		||||
	"os/exec"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"regexp"
 | 
			
		||||
	"slices"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/tools/codeformat"
 | 
			
		||||
	"code.gitea.io/gitea/build/codeformat"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Windows has a limitation for command line arguments, the size can not exceed 32KB.
 | 
			
		||||
@@ -182,7 +181,7 @@ func parseArgs() (mainOptions map[string]string, subCmd string, subArgs []string
 | 
			
		||||
			break
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return mainOptions, subCmd, subArgs
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func showUsage() {
 | 
			
		||||
@@ -218,6 +217,15 @@ func newFileCollectorFromMainOptions(mainOptions map[string]string) (fc *fileCol
 | 
			
		||||
	return newFileCollector(fileFilter, batchSize)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func containsString(a []string, s string) bool {
 | 
			
		||||
	for _, v := range a {
 | 
			
		||||
		if v == s {
 | 
			
		||||
			return true
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func giteaFormatGoImports(files []string, doWriteFile bool) error {
 | 
			
		||||
	for _, file := range files {
 | 
			
		||||
		if err := codeformat.FormatGoImports(file, doWriteFile); err != nil {
 | 
			
		||||
@@ -256,10 +264,10 @@ func main() {
 | 
			
		||||
		logVerbose("batch cmd: %s %v", subCmd, substArgs)
 | 
			
		||||
		switch subCmd {
 | 
			
		||||
		case "gitea-fmt":
 | 
			
		||||
			if slices.Contains(subArgs, "-d") {
 | 
			
		||||
			if containsString(subArgs, "-d") {
 | 
			
		||||
				log.Print("the -d option is not supported by gitea-fmt")
 | 
			
		||||
			}
 | 
			
		||||
			cmdErrors = append(cmdErrors, giteaFormatGoImports(files, slices.Contains(subArgs, "-w")))
 | 
			
		||||
			cmdErrors = append(cmdErrors, giteaFormatGoImports(files, containsString(subArgs, "-w")))
 | 
			
		||||
			cmdErrors = append(cmdErrors, passThroughCmd("gofmt", append([]string{"-w", "-r", "interface{} -> any"}, substArgs...)))
 | 
			
		||||
			cmdErrors = append(cmdErrors, passThroughCmd("go", append([]string{"run", os.Getenv("GOFUMPT_PACKAGE"), "-extra"}, substArgs...)))
 | 
			
		||||
		default:
 | 
			
		||||
@@ -6,22 +6,87 @@
 | 
			
		||||
package main
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"crypto/sha1"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"log"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"strconv"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/modules/assetfs"
 | 
			
		||||
	"github.com/shurcooL/vfsgen"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func main() {
 | 
			
		||||
	if len(os.Args) != 3 {
 | 
			
		||||
		fmt.Println("usage: ./generate-bindata {local-directory} {bindata-filename}")
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
func needsUpdate(dir, filename string) (bool, []byte) {
 | 
			
		||||
	needRegen := false
 | 
			
		||||
	_, err := os.Stat(filename)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		needRegen = true
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	dir, filename := os.Args[1], os.Args[2]
 | 
			
		||||
	fmt.Printf("generating bindata for %s to %s\n", dir, filename)
 | 
			
		||||
	if err := assetfs.GenerateEmbedBindata(dir, filename); err != nil {
 | 
			
		||||
		fmt.Printf("failed: %s\n", err.Error())
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	oldHash, err := os.ReadFile(filename + ".hash")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		oldHash = []byte{}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	hasher := sha1.New()
 | 
			
		||||
 | 
			
		||||
	err = filepath.WalkDir(dir, func(path string, d os.DirEntry, err error) error {
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		info, err := d.Info()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		_, _ = hasher.Write([]byte(d.Name()))
 | 
			
		||||
		_, _ = hasher.Write([]byte(info.ModTime().String()))
 | 
			
		||||
		_, _ = hasher.Write([]byte(strconv.FormatInt(info.Size(), 16)))
 | 
			
		||||
		return nil
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return true, oldHash
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	newHash := hasher.Sum([]byte{})
 | 
			
		||||
 | 
			
		||||
	if bytes.Compare(oldHash, newHash) != 0 {
 | 
			
		||||
		return true, newHash
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return needRegen, newHash
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func main() {
 | 
			
		||||
	if len(os.Args) < 4 {
 | 
			
		||||
		log.Fatal("Insufficient number of arguments. Need: directory packageName filename")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	dir, packageName, filename := os.Args[1], os.Args[2], os.Args[3]
 | 
			
		||||
	var useGlobalModTime bool
 | 
			
		||||
	if len(os.Args) == 5 {
 | 
			
		||||
		useGlobalModTime, _ = strconv.ParseBool(os.Args[4])
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	update, newHash := needsUpdate(dir, filename)
 | 
			
		||||
 | 
			
		||||
	if !update {
 | 
			
		||||
		fmt.Printf("bindata for %s already up-to-date\n", packageName)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	fmt.Printf("generating bindata for %s\n", packageName)
 | 
			
		||||
	var fsTemplates http.FileSystem = http.Dir(dir)
 | 
			
		||||
	err := vfsgen.Generate(fsTemplates, vfsgen.Options{
 | 
			
		||||
		PackageName:      packageName,
 | 
			
		||||
		BuildTags:        "bindata",
 | 
			
		||||
		VariableName:     "Assets",
 | 
			
		||||
		Filename:         filename,
 | 
			
		||||
		UseGlobalModTime: useGlobalModTime,
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatalf("%v\n", err)
 | 
			
		||||
	}
 | 
			
		||||
	_ = os.WriteFile(filename+".hash", newHash, 0o666)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										176
									
								
								build/generate-licenses.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										176
									
								
								build/generate-licenses.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,176 @@
 | 
			
		||||
// 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")
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										41
									
								
								build/license/aliasgenerator.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								build/license/aliasgenerator.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,41 @@
 | 
			
		||||
// 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 ""
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										39
									
								
								build/license/aliasgenerator_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								build/license/aliasgenerator_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,39 @@
 | 
			
		||||
// 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)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -4,13 +4,12 @@
 | 
			
		||||
package cmd
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"fmt"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/modules/private"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
 | 
			
		||||
	"github.com/urfave/cli/v3"
 | 
			
		||||
	"github.com/urfave/cli/v2"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
@@ -18,7 +17,7 @@ var (
 | 
			
		||||
	CmdActions = &cli.Command{
 | 
			
		||||
		Name:  "actions",
 | 
			
		||||
		Usage: "Manage Gitea Actions",
 | 
			
		||||
		Commands: []*cli.Command{
 | 
			
		||||
		Subcommands: []*cli.Command{
 | 
			
		||||
			subcmdActionsGenRunnerToken,
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
@@ -39,7 +38,10 @@ var (
 | 
			
		||||
	}
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func runGenerateActionsRunnerToken(ctx context.Context, c *cli.Command) error {
 | 
			
		||||
func runGenerateActionsRunnerToken(c *cli.Context) error {
 | 
			
		||||
	ctx, cancel := installSignals()
 | 
			
		||||
	defer cancel()
 | 
			
		||||
 | 
			
		||||
	setting.MustInstalled()
 | 
			
		||||
 | 
			
		||||
	scope := c.String("scope")
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										47
									
								
								cmd/admin.go
									
									
									
									
									
								
							
							
						
						
									
										47
									
								
								cmd/admin.go
									
									
									
									
									
								
							@@ -15,7 +15,7 @@ import (
 | 
			
		||||
	"code.gitea.io/gitea/modules/log"
 | 
			
		||||
	repo_module "code.gitea.io/gitea/modules/repository"
 | 
			
		||||
 | 
			
		||||
	"github.com/urfave/cli/v3"
 | 
			
		||||
	"github.com/urfave/cli/v2"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
@@ -23,7 +23,7 @@ var (
 | 
			
		||||
	CmdAdmin = &cli.Command{
 | 
			
		||||
		Name:  "admin",
 | 
			
		||||
		Usage: "Perform common administrative operations",
 | 
			
		||||
		Commands: []*cli.Command{
 | 
			
		||||
		Subcommands: []*cli.Command{
 | 
			
		||||
			subcmdUser,
 | 
			
		||||
			subcmdRepoSyncReleases,
 | 
			
		||||
			subcmdRegenerate,
 | 
			
		||||
@@ -41,7 +41,7 @@ var (
 | 
			
		||||
	subcmdRegenerate = &cli.Command{
 | 
			
		||||
		Name:  "regenerate",
 | 
			
		||||
		Usage: "Regenerate specific files",
 | 
			
		||||
		Commands: []*cli.Command{
 | 
			
		||||
		Subcommands: []*cli.Command{
 | 
			
		||||
			microcmdRegenHooks,
 | 
			
		||||
			microcmdRegenKeys,
 | 
			
		||||
		},
 | 
			
		||||
@@ -50,15 +50,15 @@ var (
 | 
			
		||||
	subcmdAuth = &cli.Command{
 | 
			
		||||
		Name:  "auth",
 | 
			
		||||
		Usage: "Modify external auth providers",
 | 
			
		||||
		Commands: []*cli.Command{
 | 
			
		||||
			microcmdAuthAddOauth(),
 | 
			
		||||
			microcmdAuthUpdateOauth(),
 | 
			
		||||
			microcmdAuthAddLdapBindDn(),
 | 
			
		||||
			microcmdAuthUpdateLdapBindDn(),
 | 
			
		||||
			microcmdAuthAddLdapSimpleAuth(),
 | 
			
		||||
			microcmdAuthUpdateLdapSimpleAuth(),
 | 
			
		||||
			microcmdAuthAddSMTP(),
 | 
			
		||||
			microcmdAuthUpdateSMTP(),
 | 
			
		||||
		Subcommands: []*cli.Command{
 | 
			
		||||
			microcmdAuthAddOauth,
 | 
			
		||||
			microcmdAuthUpdateOauth,
 | 
			
		||||
			microcmdAuthAddLdapBindDn,
 | 
			
		||||
			microcmdAuthUpdateLdapBindDn,
 | 
			
		||||
			microcmdAuthAddLdapSimpleAuth,
 | 
			
		||||
			microcmdAuthUpdateLdapSimpleAuth,
 | 
			
		||||
			microcmdAuthAddSMTP,
 | 
			
		||||
			microcmdAuthUpdateSMTP,
 | 
			
		||||
			microcmdAuthList,
 | 
			
		||||
			microcmdAuthDelete,
 | 
			
		||||
		},
 | 
			
		||||
@@ -71,8 +71,8 @@ var (
 | 
			
		||||
		Flags: []cli.Flag{
 | 
			
		||||
			&cli.StringFlag{
 | 
			
		||||
				Name:  "title",
 | 
			
		||||
				Usage:    "a title of a message",
 | 
			
		||||
				Required: true,
 | 
			
		||||
				Usage: `a title of a message`,
 | 
			
		||||
				Value: "",
 | 
			
		||||
			},
 | 
			
		||||
			&cli.StringFlag{
 | 
			
		||||
				Name:  "content",
 | 
			
		||||
@@ -86,27 +86,28 @@ var (
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func idFlag() *cli.Int64Flag {
 | 
			
		||||
	return &cli.Int64Flag{
 | 
			
		||||
	idFlag = &cli.Int64Flag{
 | 
			
		||||
		Name:  "id",
 | 
			
		||||
		Usage: "ID of authentication source",
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func runRepoSyncReleases(_ *cli.Context) error {
 | 
			
		||||
	ctx, cancel := installSignals()
 | 
			
		||||
	defer cancel()
 | 
			
		||||
 | 
			
		||||
func runRepoSyncReleases(ctx context.Context, _ *cli.Command) error {
 | 
			
		||||
	if err := initDB(ctx); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := git.InitSimple(); err != nil {
 | 
			
		||||
	if err := git.InitSimple(ctx); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	log.Trace("Synchronizing repository releases (this may take a while)")
 | 
			
		||||
	for page := 1; ; page++ {
 | 
			
		||||
		repos, count, err := repo_model.SearchRepositoryByName(ctx, repo_model.SearchRepoOptions{
 | 
			
		||||
		repos, count, err := repo_model.SearchRepositoryByName(ctx, &repo_model.SearchRepoOptions{
 | 
			
		||||
			ListOptions: db.ListOptions{
 | 
			
		||||
				PageSize: repo_model.RepositoryListDefaultPageSize,
 | 
			
		||||
				Page:     page,
 | 
			
		||||
@@ -121,7 +122,7 @@ func runRepoSyncReleases(ctx context.Context, _ *cli.Command) error {
 | 
			
		||||
		}
 | 
			
		||||
		log.Trace("Processing next %d repos of %d", len(repos), count)
 | 
			
		||||
		for _, repo := range repos {
 | 
			
		||||
			log.Trace("Synchronizing repo %s with path %s", repo.FullName(), repo.RelativePath())
 | 
			
		||||
			log.Trace("Synchronizing repo %s with path %s", repo.FullName(), repo.RepoPath())
 | 
			
		||||
			gitRepo, err := gitrepo.OpenRepository(ctx, repo)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				log.Warn("OpenRepository: %v", err)
 | 
			
		||||
@@ -147,7 +148,7 @@ func runRepoSyncReleases(ctx context.Context, _ *cli.Command) error {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			log.Trace("repo %s releases synchronized to tags: from %d to %d",
 | 
			
		||||
			log.Trace(" repo %s releases synchronized to tags: from %d to %d",
 | 
			
		||||
				repo.FullName(), oldnum, count)
 | 
			
		||||
			gitRepo.Close()
 | 
			
		||||
		}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,6 @@
 | 
			
		||||
package cmd
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"os"
 | 
			
		||||
@@ -14,14 +13,14 @@ import (
 | 
			
		||||
	"code.gitea.io/gitea/models/db"
 | 
			
		||||
	auth_service "code.gitea.io/gitea/services/auth"
 | 
			
		||||
 | 
			
		||||
	"github.com/urfave/cli/v3"
 | 
			
		||||
	"github.com/urfave/cli/v2"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	microcmdAuthDelete = &cli.Command{
 | 
			
		||||
		Name:   "delete",
 | 
			
		||||
		Usage:  "Delete specific auth source",
 | 
			
		||||
		Flags:  []cli.Flag{idFlag()},
 | 
			
		||||
		Flags:  []cli.Flag{idFlag},
 | 
			
		||||
		Action: runDeleteAuth,
 | 
			
		||||
	}
 | 
			
		||||
	microcmdAuthList = &cli.Command{
 | 
			
		||||
@@ -57,7 +56,10 @@ var (
 | 
			
		||||
	}
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func runListAuth(ctx context.Context, c *cli.Command) error {
 | 
			
		||||
func runListAuth(c *cli.Context) error {
 | 
			
		||||
	ctx, cancel := installSignals()
 | 
			
		||||
	defer cancel()
 | 
			
		||||
 | 
			
		||||
	if err := initDB(ctx); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
@@ -88,11 +90,14 @@ func runListAuth(ctx context.Context, c *cli.Command) error {
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func runDeleteAuth(ctx context.Context, c *cli.Command) error {
 | 
			
		||||
func runDeleteAuth(c *cli.Context) error {
 | 
			
		||||
	if !c.IsSet("id") {
 | 
			
		||||
		return errors.New("--id flag is missing")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ctx, cancel := installSignals()
 | 
			
		||||
	defer cancel()
 | 
			
		||||
 | 
			
		||||
	if err := initDB(ctx); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -9,10 +9,9 @@ import (
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/models/auth"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
	"code.gitea.io/gitea/services/auth/source/ldap"
 | 
			
		||||
 | 
			
		||||
	"github.com/urfave/cli/v3"
 | 
			
		||||
	"github.com/urfave/cli/v2"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type (
 | 
			
		||||
@@ -24,8 +23,8 @@ type (
 | 
			
		||||
	}
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func commonLdapCLIFlags() []cli.Flag {
 | 
			
		||||
	return []cli.Flag{
 | 
			
		||||
var (
 | 
			
		||||
	commonLdapCLIFlags = []cli.Flag{
 | 
			
		||||
		&cli.StringFlag{
 | 
			
		||||
			Name:  "name",
 | 
			
		||||
			Usage: "Authentication name.",
 | 
			
		||||
@@ -103,10 +102,8 @@ func commonLdapCLIFlags() []cli.Flag {
 | 
			
		||||
			Usage: "The attribute of the user’s LDAP record containing the user’s avatar.",
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func ldapBindDnCLIFlags() []cli.Flag {
 | 
			
		||||
	return append(commonLdapCLIFlags(),
 | 
			
		||||
	ldapBindDnCLIFlags = append(commonLdapCLIFlags,
 | 
			
		||||
		&cli.StringFlag{
 | 
			
		||||
			Name:  "bind-dn",
 | 
			
		||||
			Usage: "The DN to bind to the LDAP server with when searching for the user.",
 | 
			
		||||
@@ -130,88 +127,50 @@ func ldapBindDnCLIFlags() []cli.Flag {
 | 
			
		||||
		&cli.UintFlag{
 | 
			
		||||
			Name:  "page-size",
 | 
			
		||||
			Usage: "Search page size.",
 | 
			
		||||
		},
 | 
			
		||||
		&cli.BoolFlag{
 | 
			
		||||
			Name:  "enable-groups",
 | 
			
		||||
			Usage: "Enable LDAP groups",
 | 
			
		||||
		},
 | 
			
		||||
		&cli.StringFlag{
 | 
			
		||||
			Name:  "group-search-base-dn",
 | 
			
		||||
			Usage: "The LDAP base DN at which group accounts will be searched for",
 | 
			
		||||
		},
 | 
			
		||||
		&cli.StringFlag{
 | 
			
		||||
			Name:  "group-member-attribute",
 | 
			
		||||
			Usage: "Group attribute containing list of users",
 | 
			
		||||
		},
 | 
			
		||||
		&cli.StringFlag{
 | 
			
		||||
			Name:  "group-user-attribute",
 | 
			
		||||
			Usage: "User attribute listed in group",
 | 
			
		||||
		},
 | 
			
		||||
		&cli.StringFlag{
 | 
			
		||||
			Name:  "group-filter",
 | 
			
		||||
			Usage: "Verify group membership in LDAP",
 | 
			
		||||
		},
 | 
			
		||||
		&cli.StringFlag{
 | 
			
		||||
			Name:  "group-team-map",
 | 
			
		||||
			Usage: "Map LDAP groups to Organization teams",
 | 
			
		||||
		},
 | 
			
		||||
		&cli.BoolFlag{
 | 
			
		||||
			Name:  "group-team-map-removal",
 | 
			
		||||
			Usage: "Remove users from synchronized teams if user does not belong to corresponding LDAP group",
 | 
			
		||||
		})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func ldapSimpleAuthCLIFlags() []cli.Flag {
 | 
			
		||||
	return append(commonLdapCLIFlags(),
 | 
			
		||||
	ldapSimpleAuthCLIFlags = append(commonLdapCLIFlags,
 | 
			
		||||
		&cli.StringFlag{
 | 
			
		||||
			Name:  "user-dn",
 | 
			
		||||
			Usage: "The user's DN.",
 | 
			
		||||
		})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func microcmdAuthAddLdapBindDn() *cli.Command {
 | 
			
		||||
	return &cli.Command{
 | 
			
		||||
	microcmdAuthAddLdapBindDn = &cli.Command{
 | 
			
		||||
		Name:  "add-ldap",
 | 
			
		||||
		Usage: "Add new LDAP (via Bind DN) authentication source",
 | 
			
		||||
		Action: func(ctx context.Context, cmd *cli.Command) error {
 | 
			
		||||
			return newAuthService().addLdapBindDn(ctx, cmd)
 | 
			
		||||
		Action: func(c *cli.Context) error {
 | 
			
		||||
			return newAuthService().addLdapBindDn(c)
 | 
			
		||||
		},
 | 
			
		||||
		Flags: ldapBindDnCLIFlags(),
 | 
			
		||||
		Flags: ldapBindDnCLIFlags,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func microcmdAuthUpdateLdapBindDn() *cli.Command {
 | 
			
		||||
	return &cli.Command{
 | 
			
		||||
	microcmdAuthUpdateLdapBindDn = &cli.Command{
 | 
			
		||||
		Name:  "update-ldap",
 | 
			
		||||
		Usage: "Update existing LDAP (via Bind DN) authentication source",
 | 
			
		||||
		Action: func(ctx context.Context, cmd *cli.Command) error {
 | 
			
		||||
			return newAuthService().updateLdapBindDn(ctx, cmd)
 | 
			
		||||
		Action: func(c *cli.Context) error {
 | 
			
		||||
			return newAuthService().updateLdapBindDn(c)
 | 
			
		||||
		},
 | 
			
		||||
		Flags: append([]cli.Flag{idFlag()}, ldapBindDnCLIFlags()...),
 | 
			
		||||
		Flags: append([]cli.Flag{idFlag}, ldapBindDnCLIFlags...),
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func microcmdAuthAddLdapSimpleAuth() *cli.Command {
 | 
			
		||||
	return &cli.Command{
 | 
			
		||||
	microcmdAuthAddLdapSimpleAuth = &cli.Command{
 | 
			
		||||
		Name:  "add-ldap-simple",
 | 
			
		||||
		Usage: "Add new LDAP (simple auth) authentication source",
 | 
			
		||||
		Action: func(ctx context.Context, cmd *cli.Command) error {
 | 
			
		||||
			return newAuthService().addLdapSimpleAuth(ctx, cmd)
 | 
			
		||||
		Action: func(c *cli.Context) error {
 | 
			
		||||
			return newAuthService().addLdapSimpleAuth(c)
 | 
			
		||||
		},
 | 
			
		||||
		Flags: ldapSimpleAuthCLIFlags(),
 | 
			
		||||
		Flags: ldapSimpleAuthCLIFlags,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func microcmdAuthUpdateLdapSimpleAuth() *cli.Command {
 | 
			
		||||
	return &cli.Command{
 | 
			
		||||
	microcmdAuthUpdateLdapSimpleAuth = &cli.Command{
 | 
			
		||||
		Name:  "update-ldap-simple",
 | 
			
		||||
		Usage: "Update existing LDAP (simple auth) authentication source",
 | 
			
		||||
		Action: func(ctx context.Context, cmd *cli.Command) error {
 | 
			
		||||
			return newAuthService().updateLdapSimpleAuth(ctx, cmd)
 | 
			
		||||
		Action: func(c *cli.Context) error {
 | 
			
		||||
			return newAuthService().updateLdapSimpleAuth(c)
 | 
			
		||||
		},
 | 
			
		||||
		Flags: append([]cli.Flag{idFlag()}, ldapSimpleAuthCLIFlags()...),
 | 
			
		||||
		Flags: append([]cli.Flag{idFlag}, ldapSimpleAuthCLIFlags...),
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// newAuthService creates a service with default functions.
 | 
			
		||||
func newAuthService() *authService {
 | 
			
		||||
@@ -223,8 +182,8 @@ func newAuthService() *authService {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// parseAuthSourceLdap assigns values on authSource according to command line flags.
 | 
			
		||||
func parseAuthSourceLdap(c *cli.Command, authSource *auth.Source) {
 | 
			
		||||
// parseAuthSource assigns values on authSource according to command line flags.
 | 
			
		||||
func parseAuthSource(c *cli.Context, authSource *auth.Source) {
 | 
			
		||||
	if c.IsSet("name") {
 | 
			
		||||
		authSource.Name = c.String("name")
 | 
			
		||||
	}
 | 
			
		||||
@@ -240,11 +199,10 @@ func parseAuthSourceLdap(c *cli.Command, authSource *auth.Source) {
 | 
			
		||||
	if c.IsSet("disable-synchronize-users") {
 | 
			
		||||
		authSource.IsSyncEnabled = !c.Bool("disable-synchronize-users")
 | 
			
		||||
	}
 | 
			
		||||
	authSource.TwoFactorPolicy = util.Iif(c.Bool("skip-local-2fa"), "skip", "")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// parseLdapConfig assigns values on config according to command line flags.
 | 
			
		||||
func parseLdapConfig(c *cli.Command, config *ldap.Source) error {
 | 
			
		||||
func parseLdapConfig(c *cli.Context, config *ldap.Source) error {
 | 
			
		||||
	if c.IsSet("name") {
 | 
			
		||||
		config.Name = c.String("name")
 | 
			
		||||
	}
 | 
			
		||||
@@ -257,7 +215,7 @@ func parseLdapConfig(c *cli.Command, config *ldap.Source) error {
 | 
			
		||||
	if c.IsSet("security-protocol") {
 | 
			
		||||
		p, ok := findLdapSecurityProtocolByName(c.String("security-protocol"))
 | 
			
		||||
		if !ok {
 | 
			
		||||
			return fmt.Errorf("unknown security protocol name: %s", c.String("security-protocol"))
 | 
			
		||||
			return fmt.Errorf("Unknown security protocol name: %s", c.String("security-protocol"))
 | 
			
		||||
		}
 | 
			
		||||
		config.SecurityProtocol = p
 | 
			
		||||
	}
 | 
			
		||||
@@ -312,26 +270,8 @@ func parseLdapConfig(c *cli.Command, config *ldap.Source) error {
 | 
			
		||||
	if c.IsSet("allow-deactivate-all") {
 | 
			
		||||
		config.AllowDeactivateAll = c.Bool("allow-deactivate-all")
 | 
			
		||||
	}
 | 
			
		||||
	if c.IsSet("enable-groups") {
 | 
			
		||||
		config.GroupsEnabled = c.Bool("enable-groups")
 | 
			
		||||
	}
 | 
			
		||||
	if c.IsSet("group-search-base-dn") {
 | 
			
		||||
		config.GroupDN = c.String("group-search-base-dn")
 | 
			
		||||
	}
 | 
			
		||||
	if c.IsSet("group-member-attribute") {
 | 
			
		||||
		config.GroupMemberUID = c.String("group-member-attribute")
 | 
			
		||||
	}
 | 
			
		||||
	if c.IsSet("group-user-attribute") {
 | 
			
		||||
		config.UserUID = c.String("group-user-attribute")
 | 
			
		||||
	}
 | 
			
		||||
	if c.IsSet("group-filter") {
 | 
			
		||||
		config.GroupFilter = c.String("group-filter")
 | 
			
		||||
	}
 | 
			
		||||
	if c.IsSet("group-team-map") {
 | 
			
		||||
		config.GroupTeamMap = c.String("group-team-map")
 | 
			
		||||
	}
 | 
			
		||||
	if c.IsSet("group-team-map-removal") {
 | 
			
		||||
		config.GroupTeamMapRemoval = c.Bool("group-team-map-removal")
 | 
			
		||||
	if c.IsSet("skip-local-2fa") {
 | 
			
		||||
		config.SkipLocalTwoFA = c.Bool("skip-local-2fa")
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
@@ -349,27 +289,32 @@ func findLdapSecurityProtocolByName(name string) (ldap.SecurityProtocol, bool) {
 | 
			
		||||
 | 
			
		||||
// getAuthSource gets the login source by its id defined in the command line flags.
 | 
			
		||||
// It returns an error if the id is not set, does not match any source or if the source is not of expected type.
 | 
			
		||||
func (a *authService) getAuthSource(ctx context.Context, c *cli.Command, authType auth.Type) (*auth.Source, error) {
 | 
			
		||||
func (a *authService) getAuthSource(ctx context.Context, c *cli.Context, authType auth.Type) (*auth.Source, error) {
 | 
			
		||||
	if err := argsSet(c, "id"); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	authSource, err := a.getAuthSourceByID(ctx, c.Int64("id"))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if authSource.Type != authType {
 | 
			
		||||
		return nil, fmt.Errorf("invalid authentication type. expected: %s, actual: %s", authType.String(), authSource.Type.String())
 | 
			
		||||
		return nil, fmt.Errorf("Invalid authentication type. expected: %s, actual: %s", authType.String(), authSource.Type.String())
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return authSource, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// addLdapBindDn adds a new LDAP via Bind DN authentication source.
 | 
			
		||||
func (a *authService) addLdapBindDn(ctx context.Context, c *cli.Command) error {
 | 
			
		||||
func (a *authService) addLdapBindDn(c *cli.Context) error {
 | 
			
		||||
	if err := argsSet(c, "name", "security-protocol", "host", "port", "user-search-base", "user-filter", "email-attribute"); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ctx, cancel := installSignals()
 | 
			
		||||
	defer cancel()
 | 
			
		||||
 | 
			
		||||
	if err := a.initDB(ctx); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
@@ -382,7 +327,7 @@ func (a *authService) addLdapBindDn(ctx context.Context, c *cli.Command) error {
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	parseAuthSourceLdap(c, authSource)
 | 
			
		||||
	parseAuthSource(c, authSource)
 | 
			
		||||
	if err := parseLdapConfig(c, authSource.Cfg.(*ldap.Source)); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
@@ -391,7 +336,10 @@ func (a *authService) addLdapBindDn(ctx context.Context, c *cli.Command) error {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// updateLdapBindDn updates a new LDAP via Bind DN authentication source.
 | 
			
		||||
func (a *authService) updateLdapBindDn(ctx context.Context, c *cli.Command) error {
 | 
			
		||||
func (a *authService) updateLdapBindDn(c *cli.Context) error {
 | 
			
		||||
	ctx, cancel := installSignals()
 | 
			
		||||
	defer cancel()
 | 
			
		||||
 | 
			
		||||
	if err := a.initDB(ctx); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
@@ -401,7 +349,7 @@ func (a *authService) updateLdapBindDn(ctx context.Context, c *cli.Command) erro
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	parseAuthSourceLdap(c, authSource)
 | 
			
		||||
	parseAuthSource(c, authSource)
 | 
			
		||||
	if err := parseLdapConfig(c, authSource.Cfg.(*ldap.Source)); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
@@ -410,11 +358,14 @@ func (a *authService) updateLdapBindDn(ctx context.Context, c *cli.Command) erro
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// addLdapSimpleAuth adds a new LDAP (simple auth) authentication source.
 | 
			
		||||
func (a *authService) addLdapSimpleAuth(ctx context.Context, c *cli.Command) error {
 | 
			
		||||
func (a *authService) addLdapSimpleAuth(c *cli.Context) error {
 | 
			
		||||
	if err := argsSet(c, "name", "security-protocol", "host", "port", "user-dn", "user-filter", "email-attribute"); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ctx, cancel := installSignals()
 | 
			
		||||
	defer cancel()
 | 
			
		||||
 | 
			
		||||
	if err := a.initDB(ctx); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
@@ -427,7 +378,7 @@ func (a *authService) addLdapSimpleAuth(ctx context.Context, c *cli.Command) err
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	parseAuthSourceLdap(c, authSource)
 | 
			
		||||
	parseAuthSource(c, authSource)
 | 
			
		||||
	if err := parseLdapConfig(c, authSource.Cfg.(*ldap.Source)); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
@@ -436,7 +387,10 @@ func (a *authService) addLdapSimpleAuth(ctx context.Context, c *cli.Command) err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// updateLdapSimpleAuth updates a new LDAP (simple auth) authentication source.
 | 
			
		||||
func (a *authService) updateLdapSimpleAuth(ctx context.Context, c *cli.Command) error {
 | 
			
		||||
func (a *authService) updateLdapSimpleAuth(c *cli.Context) error {
 | 
			
		||||
	ctx, cancel := installSignals()
 | 
			
		||||
	defer cancel()
 | 
			
		||||
 | 
			
		||||
	if err := a.initDB(ctx); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
@@ -446,7 +400,7 @@ func (a *authService) updateLdapSimpleAuth(ctx context.Context, c *cli.Command)
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	parseAuthSourceLdap(c, authSource)
 | 
			
		||||
	parseAuthSource(c, authSource)
 | 
			
		||||
	if err := parseLdapConfig(c, authSource.Cfg.(*ldap.Source)); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -8,16 +8,17 @@ import (
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/models/auth"
 | 
			
		||||
	"code.gitea.io/gitea/modules/test"
 | 
			
		||||
	"code.gitea.io/gitea/services/auth/source/ldap"
 | 
			
		||||
 | 
			
		||||
	"github.com/stretchr/testify/assert"
 | 
			
		||||
	"github.com/urfave/cli/v3"
 | 
			
		||||
	"github.com/urfave/cli/v2"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestAddLdapBindDn(t *testing.T) {
 | 
			
		||||
	// Mock cli functions to do not exit on error
 | 
			
		||||
	defer test.MockVariableValue(&cli.OsExiter, func(code int) {})()
 | 
			
		||||
	osExiter := cli.OsExiter
 | 
			
		||||
	defer func() { cli.OsExiter = osExiter }()
 | 
			
		||||
	cli.OsExiter = func(code int) {}
 | 
			
		||||
 | 
			
		||||
	// Test cases
 | 
			
		||||
	cases := []struct {
 | 
			
		||||
@@ -50,13 +51,6 @@ func TestAddLdapBindDn(t *testing.T) {
 | 
			
		||||
				"--attributes-in-bind",
 | 
			
		||||
				"--synchronize-users",
 | 
			
		||||
				"--page-size", "99",
 | 
			
		||||
				"--enable-groups",
 | 
			
		||||
				"--group-search-base-dn", "ou=group,dc=full-domain-bind,dc=org",
 | 
			
		||||
				"--group-member-attribute", "memberUid",
 | 
			
		||||
				"--group-user-attribute", "uid",
 | 
			
		||||
				"--group-filter", "(|(cn=gitea_users)(cn=admins))",
 | 
			
		||||
				"--group-team-map", `{"cn=my-group,cn=groups,dc=example,dc=org": {"MyGiteaOrganization": ["MyGiteaTeam1", "MyGiteaTeam2"]}}`,
 | 
			
		||||
				"--group-team-map-removal",
 | 
			
		||||
			},
 | 
			
		||||
			source: &auth.Source{
 | 
			
		||||
				Type:          auth.LDAP,
 | 
			
		||||
@@ -84,13 +78,6 @@ func TestAddLdapBindDn(t *testing.T) {
 | 
			
		||||
					AdminFilter:           "(memberOf=cn=admin-group,ou=example,dc=full-domain-bind,dc=org)",
 | 
			
		||||
					RestrictedFilter:      "(memberOf=cn=restricted-group,ou=example,dc=full-domain-bind,dc=org)",
 | 
			
		||||
					Enabled:               true,
 | 
			
		||||
					GroupsEnabled:         true,
 | 
			
		||||
					GroupDN:               "ou=group,dc=full-domain-bind,dc=org",
 | 
			
		||||
					GroupMemberUID:        "memberUid",
 | 
			
		||||
					UserUID:               "uid",
 | 
			
		||||
					GroupFilter:           "(|(cn=gitea_users)(cn=admins))",
 | 
			
		||||
					GroupTeamMap:          `{"cn=my-group,cn=groups,dc=example,dc=org": {"MyGiteaOrganization": ["MyGiteaTeam1", "MyGiteaTeam2"]}}`,
 | 
			
		||||
					GroupTeamMapRemoval:   true,
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
@@ -134,7 +121,7 @@ func TestAddLdapBindDn(t *testing.T) {
 | 
			
		||||
				"--user-filter", "(memberOf=cn=user-group,ou=example,dc=domain,dc=org)",
 | 
			
		||||
				"--email-attribute", "mail",
 | 
			
		||||
			},
 | 
			
		||||
			errMsg: "unknown security protocol name: zzzzz",
 | 
			
		||||
			errMsg: "Unknown security protocol name: zzzzz",
 | 
			
		||||
		},
 | 
			
		||||
		// case 3
 | 
			
		||||
		{
 | 
			
		||||
@@ -228,23 +215,22 @@ func TestAddLdapBindDn(t *testing.T) {
 | 
			
		||||
				return nil
 | 
			
		||||
			},
 | 
			
		||||
			updateAuthSource: func(ctx context.Context, authSource *auth.Source) error {
 | 
			
		||||
				assert.FailNow(t, "updateAuthSource called", "case %d: should not call updateAuthSource", n)
 | 
			
		||||
				assert.FailNow(t, "case %d: should not call updateAuthSource", n)
 | 
			
		||||
				return nil
 | 
			
		||||
			},
 | 
			
		||||
			getAuthSourceByID: func(ctx context.Context, id int64) (*auth.Source, error) {
 | 
			
		||||
				assert.FailNow(t, "getAuthSourceByID called", "case %d: should not call getAuthSourceByID", n)
 | 
			
		||||
				assert.FailNow(t, "case %d: should not call getAuthSourceByID", n)
 | 
			
		||||
				return nil, nil
 | 
			
		||||
			},
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Create a copy of command to test
 | 
			
		||||
		app := cli.Command{
 | 
			
		||||
			Flags:  microcmdAuthAddLdapBindDn().Flags,
 | 
			
		||||
			Action: service.addLdapBindDn,
 | 
			
		||||
		}
 | 
			
		||||
		app := cli.NewApp()
 | 
			
		||||
		app.Flags = microcmdAuthAddLdapBindDn.Flags
 | 
			
		||||
		app.Action = service.addLdapBindDn
 | 
			
		||||
 | 
			
		||||
		// Run it
 | 
			
		||||
		err := app.Run(t.Context(), c.args)
 | 
			
		||||
		err := app.Run(c.args)
 | 
			
		||||
		if c.errMsg != "" {
 | 
			
		||||
			assert.EqualError(t, err, c.errMsg, "case %d: error should match", n)
 | 
			
		||||
		} else {
 | 
			
		||||
@@ -256,7 +242,9 @@ func TestAddLdapBindDn(t *testing.T) {
 | 
			
		||||
 | 
			
		||||
func TestAddLdapSimpleAuth(t *testing.T) {
 | 
			
		||||
	// Mock cli functions to do not exit on error
 | 
			
		||||
	defer test.MockVariableValue(&cli.OsExiter, func(code int) {})()
 | 
			
		||||
	osExiter := cli.OsExiter
 | 
			
		||||
	defer func() { cli.OsExiter = osExiter }()
 | 
			
		||||
	cli.OsExiter = func(code int) {}
 | 
			
		||||
 | 
			
		||||
	// Test cases
 | 
			
		||||
	cases := []struct {
 | 
			
		||||
@@ -346,12 +334,12 @@ func TestAddLdapSimpleAuth(t *testing.T) {
 | 
			
		||||
				"--name", "ldap (simple auth) source",
 | 
			
		||||
				"--security-protocol", "zzzzz",
 | 
			
		||||
				"--host", "ldap-server",
 | 
			
		||||
				"--port", "1234",
 | 
			
		||||
				"--port", "123",
 | 
			
		||||
				"--user-filter", "(&(objectClass=posixAccount)(cn=%s))",
 | 
			
		||||
				"--email-attribute", "mail",
 | 
			
		||||
				"--user-dn", "cn=%s,ou=Users,dc=domain,dc=org",
 | 
			
		||||
			},
 | 
			
		||||
			errMsg: "unknown security protocol name: zzzzz",
 | 
			
		||||
			errMsg: "Unknown security protocol name: zzzzz",
 | 
			
		||||
		},
 | 
			
		||||
		// case 3
 | 
			
		||||
		{
 | 
			
		||||
@@ -458,23 +446,22 @@ func TestAddLdapSimpleAuth(t *testing.T) {
 | 
			
		||||
				return nil
 | 
			
		||||
			},
 | 
			
		||||
			updateAuthSource: func(ctx context.Context, authSource *auth.Source) error {
 | 
			
		||||
				assert.FailNow(t, "updateAuthSource called", "case %d: should not call updateAuthSource", n)
 | 
			
		||||
				assert.FailNow(t, "case %d: should not call updateAuthSource", n)
 | 
			
		||||
				return nil
 | 
			
		||||
			},
 | 
			
		||||
			getAuthSourceByID: func(ctx context.Context, id int64) (*auth.Source, error) {
 | 
			
		||||
				assert.FailNow(t, "getAuthSourceById called", "case %d: should not call getAuthSourceByID", n)
 | 
			
		||||
				assert.FailNow(t, "case %d: should not call getAuthSourceByID", n)
 | 
			
		||||
				return nil, nil
 | 
			
		||||
			},
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Create a copy of command to test
 | 
			
		||||
		app := &cli.Command{
 | 
			
		||||
			Flags:  microcmdAuthAddLdapSimpleAuth().Flags,
 | 
			
		||||
			Action: service.addLdapSimpleAuth,
 | 
			
		||||
		}
 | 
			
		||||
		app := cli.NewApp()
 | 
			
		||||
		app.Flags = microcmdAuthAddLdapSimpleAuth.Flags
 | 
			
		||||
		app.Action = service.addLdapSimpleAuth
 | 
			
		||||
 | 
			
		||||
		// Run it
 | 
			
		||||
		err := app.Run(t.Context(), c.args)
 | 
			
		||||
		err := app.Run(c.args)
 | 
			
		||||
		if c.errMsg != "" {
 | 
			
		||||
			assert.EqualError(t, err, c.errMsg, "case %d: error should match", n)
 | 
			
		||||
		} else {
 | 
			
		||||
@@ -486,7 +473,9 @@ func TestAddLdapSimpleAuth(t *testing.T) {
 | 
			
		||||
 | 
			
		||||
func TestUpdateLdapBindDn(t *testing.T) {
 | 
			
		||||
	// Mock cli functions to do not exit on error
 | 
			
		||||
	defer test.MockVariableValue(&cli.OsExiter, func(code int) {})()
 | 
			
		||||
	osExiter := cli.OsExiter
 | 
			
		||||
	defer func() { cli.OsExiter = osExiter }()
 | 
			
		||||
	cli.OsExiter = func(code int) {}
 | 
			
		||||
 | 
			
		||||
	// Test cases
 | 
			
		||||
	cases := []struct {
 | 
			
		||||
@@ -521,13 +510,6 @@ func TestUpdateLdapBindDn(t *testing.T) {
 | 
			
		||||
				"--bind-password", "secret-bind-full",
 | 
			
		||||
				"--synchronize-users",
 | 
			
		||||
				"--page-size", "99",
 | 
			
		||||
				"--enable-groups",
 | 
			
		||||
				"--group-search-base-dn", "ou=group,dc=full-domain-bind,dc=org",
 | 
			
		||||
				"--group-member-attribute", "memberUid",
 | 
			
		||||
				"--group-user-attribute", "uid",
 | 
			
		||||
				"--group-filter", "(|(cn=gitea_users)(cn=admins))",
 | 
			
		||||
				"--group-team-map", `{"cn=my-group,cn=groups,dc=example,dc=org": {"MyGiteaOrganization": ["MyGiteaTeam1", "MyGiteaTeam2"]}}`,
 | 
			
		||||
				"--group-team-map-removal",
 | 
			
		||||
			},
 | 
			
		||||
			id: 23,
 | 
			
		||||
			existingAuthSource: &auth.Source{
 | 
			
		||||
@@ -563,13 +545,6 @@ func TestUpdateLdapBindDn(t *testing.T) {
 | 
			
		||||
					AdminFilter:           "(memberOf=cn=admin-group,ou=example,dc=full-domain-bind,dc=org)",
 | 
			
		||||
					RestrictedFilter:      "(memberOf=cn=restricted-group,ou=example,dc=full-domain-bind,dc=org)",
 | 
			
		||||
					Enabled:               true,
 | 
			
		||||
					GroupsEnabled:         true,
 | 
			
		||||
					GroupDN:               "ou=group,dc=full-domain-bind,dc=org",
 | 
			
		||||
					GroupMemberUID:        "memberUid",
 | 
			
		||||
					UserUID:               "uid",
 | 
			
		||||
					GroupFilter:           "(|(cn=gitea_users)(cn=admins))",
 | 
			
		||||
					GroupTeamMap:          `{"cn=my-group,cn=groups,dc=example,dc=org": {"MyGiteaOrganization": ["MyGiteaTeam1", "MyGiteaTeam2"]}}`,
 | 
			
		||||
					GroupTeamMapRemoval:   true,
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
@@ -861,7 +836,7 @@ func TestUpdateLdapBindDn(t *testing.T) {
 | 
			
		||||
				"--id", "1",
 | 
			
		||||
				"--security-protocol", "xxxxx",
 | 
			
		||||
			},
 | 
			
		||||
			errMsg: "unknown security protocol name: xxxxx",
 | 
			
		||||
			errMsg: "Unknown security protocol name: xxxxx",
 | 
			
		||||
		},
 | 
			
		||||
		// case 22
 | 
			
		||||
		{
 | 
			
		||||
@@ -880,7 +855,7 @@ func TestUpdateLdapBindDn(t *testing.T) {
 | 
			
		||||
				Type: auth.OAuth2,
 | 
			
		||||
				Cfg:  &ldap.Source{},
 | 
			
		||||
			},
 | 
			
		||||
			errMsg: "invalid authentication type. expected: LDAP (via BindDN), actual: OAuth2",
 | 
			
		||||
			errMsg: "Invalid authentication type. expected: LDAP (via BindDN), actual: OAuth2",
 | 
			
		||||
		},
 | 
			
		||||
		// case 24
 | 
			
		||||
		{
 | 
			
		||||
@@ -922,7 +897,7 @@ func TestUpdateLdapBindDn(t *testing.T) {
 | 
			
		||||
				return nil
 | 
			
		||||
			},
 | 
			
		||||
			createAuthSource: func(ctx context.Context, authSource *auth.Source) error {
 | 
			
		||||
				assert.FailNow(t, "createAuthSource called", "case %d: should not call createAuthSource", n)
 | 
			
		||||
				assert.FailNow(t, "case %d: should not call createAuthSource", n)
 | 
			
		||||
				return nil
 | 
			
		||||
			},
 | 
			
		||||
			updateAuthSource: func(ctx context.Context, authSource *auth.Source) error {
 | 
			
		||||
@@ -944,12 +919,12 @@ func TestUpdateLdapBindDn(t *testing.T) {
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Create a copy of command to test
 | 
			
		||||
		app := cli.Command{
 | 
			
		||||
			Flags:  microcmdAuthUpdateLdapBindDn().Flags,
 | 
			
		||||
			Action: service.updateLdapBindDn,
 | 
			
		||||
		}
 | 
			
		||||
		app := cli.NewApp()
 | 
			
		||||
		app.Flags = microcmdAuthUpdateLdapBindDn.Flags
 | 
			
		||||
		app.Action = service.updateLdapBindDn
 | 
			
		||||
 | 
			
		||||
		// Run it
 | 
			
		||||
		err := app.Run(t.Context(), c.args)
 | 
			
		||||
		err := app.Run(c.args)
 | 
			
		||||
		if c.errMsg != "" {
 | 
			
		||||
			assert.EqualError(t, err, c.errMsg, "case %d: error should match", n)
 | 
			
		||||
		} else {
 | 
			
		||||
@@ -961,7 +936,9 @@ func TestUpdateLdapBindDn(t *testing.T) {
 | 
			
		||||
 | 
			
		||||
func TestUpdateLdapSimpleAuth(t *testing.T) {
 | 
			
		||||
	// Mock cli functions to do not exit on error
 | 
			
		||||
	defer test.MockVariableValue(&cli.OsExiter, func(code int) {})()
 | 
			
		||||
	osExiter := cli.OsExiter
 | 
			
		||||
	defer func() { cli.OsExiter = osExiter }()
 | 
			
		||||
	cli.OsExiter = func(code int) {}
 | 
			
		||||
 | 
			
		||||
	// Test cases
 | 
			
		||||
	cases := []struct {
 | 
			
		||||
@@ -1252,7 +1229,7 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
 | 
			
		||||
				"--id", "1",
 | 
			
		||||
				"--security-protocol", "xxxxx",
 | 
			
		||||
			},
 | 
			
		||||
			errMsg: "unknown security protocol name: xxxxx",
 | 
			
		||||
			errMsg: "Unknown security protocol name: xxxxx",
 | 
			
		||||
		},
 | 
			
		||||
		// case 18
 | 
			
		||||
		{
 | 
			
		||||
@@ -1271,7 +1248,7 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
 | 
			
		||||
				Type: auth.PAM,
 | 
			
		||||
				Cfg:  &ldap.Source{},
 | 
			
		||||
			},
 | 
			
		||||
			errMsg: "invalid authentication type. expected: LDAP (simple auth), actual: PAM",
 | 
			
		||||
			errMsg: "Invalid authentication type. expected: LDAP (simple auth), actual: PAM",
 | 
			
		||||
		},
 | 
			
		||||
		// case 20
 | 
			
		||||
		{
 | 
			
		||||
@@ -1310,7 +1287,7 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
 | 
			
		||||
				return nil
 | 
			
		||||
			},
 | 
			
		||||
			createAuthSource: func(ctx context.Context, authSource *auth.Source) error {
 | 
			
		||||
				assert.FailNow(t, "createAuthSource called", "case %d: should not call createAuthSource", n)
 | 
			
		||||
				assert.FailNow(t, "case %d: should not call createAuthSource", n)
 | 
			
		||||
				return nil
 | 
			
		||||
			},
 | 
			
		||||
			updateAuthSource: func(ctx context.Context, authSource *auth.Source) error {
 | 
			
		||||
@@ -1332,12 +1309,12 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Create a copy of command to test
 | 
			
		||||
		app := cli.Command{
 | 
			
		||||
			Flags:  microcmdAuthUpdateLdapSimpleAuth().Flags,
 | 
			
		||||
			Action: service.updateLdapSimpleAuth,
 | 
			
		||||
		}
 | 
			
		||||
		app := cli.NewApp()
 | 
			
		||||
		app.Flags = microcmdAuthUpdateLdapSimpleAuth.Flags
 | 
			
		||||
		app.Action = service.updateLdapSimpleAuth
 | 
			
		||||
 | 
			
		||||
		// Run it
 | 
			
		||||
		err := app.Run(t.Context(), c.args)
 | 
			
		||||
		err := app.Run(c.args)
 | 
			
		||||
		if c.errMsg != "" {
 | 
			
		||||
			assert.EqualError(t, err, c.errMsg, "case %d: error should match", n)
 | 
			
		||||
		} else {
 | 
			
		||||
 
 | 
			
		||||
@@ -4,20 +4,18 @@
 | 
			
		||||
package cmd
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"net/url"
 | 
			
		||||
 | 
			
		||||
	auth_model "code.gitea.io/gitea/models/auth"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
	"code.gitea.io/gitea/services/auth/source/oauth2"
 | 
			
		||||
 | 
			
		||||
	"github.com/urfave/cli/v3"
 | 
			
		||||
	"github.com/urfave/cli/v2"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func oauthCLIFlags() []cli.Flag {
 | 
			
		||||
	return []cli.Flag{
 | 
			
		||||
var (
 | 
			
		||||
	oauthCLIFlags = []cli.Flag{
 | 
			
		||||
		&cli.StringFlag{
 | 
			
		||||
			Name:  "name",
 | 
			
		||||
			Value: "",
 | 
			
		||||
@@ -87,14 +85,6 @@ func oauthCLIFlags() []cli.Flag {
 | 
			
		||||
			Value: nil,
 | 
			
		||||
			Usage: "Scopes to request when to authenticate against this OAuth2 source",
 | 
			
		||||
		},
 | 
			
		||||
		&cli.StringFlag{
 | 
			
		||||
			Name:  "ssh-public-key-claim-name",
 | 
			
		||||
			Usage: "Claim name that provides SSH public keys",
 | 
			
		||||
		},
 | 
			
		||||
		&cli.StringFlag{
 | 
			
		||||
			Name:  "full-name-claim-name",
 | 
			
		||||
			Usage: "Claim name that provides user's full name",
 | 
			
		||||
		},
 | 
			
		||||
		&cli.StringFlag{
 | 
			
		||||
			Name:  "required-claim-name",
 | 
			
		||||
			Value: "",
 | 
			
		||||
@@ -130,34 +120,23 @@ func oauthCLIFlags() []cli.Flag {
 | 
			
		||||
			Usage: "Activate automatic team membership removal depending on groups",
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func microcmdAuthAddOauth() *cli.Command {
 | 
			
		||||
	return &cli.Command{
 | 
			
		||||
	microcmdAuthAddOauth = &cli.Command{
 | 
			
		||||
		Name:   "add-oauth",
 | 
			
		||||
		Usage:  "Add new Oauth authentication source",
 | 
			
		||||
		Action: func(ctx context.Context, cmd *cli.Command) error {
 | 
			
		||||
			return newAuthService().runAddOauth(ctx, cmd)
 | 
			
		||||
		},
 | 
			
		||||
		Flags: oauthCLIFlags(),
 | 
			
		||||
		Action: runAddOauth,
 | 
			
		||||
		Flags:  oauthCLIFlags,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func microcmdAuthUpdateOauth() *cli.Command {
 | 
			
		||||
	return &cli.Command{
 | 
			
		||||
	microcmdAuthUpdateOauth = &cli.Command{
 | 
			
		||||
		Name:   "update-oauth",
 | 
			
		||||
		Usage:  "Update existing Oauth authentication source",
 | 
			
		||||
		Action: func(ctx context.Context, cmd *cli.Command) error {
 | 
			
		||||
			return newAuthService().runUpdateOauth(ctx, cmd)
 | 
			
		||||
		},
 | 
			
		||||
		Flags: append(oauthCLIFlags()[:1], append([]cli.Flag{&cli.Int64Flag{
 | 
			
		||||
			Name:  "id",
 | 
			
		||||
			Usage: "ID of authentication source",
 | 
			
		||||
		}}, oauthCLIFlags()[1:]...)...),
 | 
			
		||||
		Action: runUpdateOauth,
 | 
			
		||||
		Flags:  append(oauthCLIFlags[:1], append([]cli.Flag{idFlag}, oauthCLIFlags[1:]...)...),
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func parseOAuth2Config(c *cli.Command) *oauth2.Source {
 | 
			
		||||
func parseOAuth2Config(c *cli.Context) *oauth2.Source {
 | 
			
		||||
	var customURLMapping *oauth2.CustomURLMapping
 | 
			
		||||
	if c.IsSet("use-custom-urls") {
 | 
			
		||||
		customURLMapping = &oauth2.CustomURLMapping{
 | 
			
		||||
@@ -177,6 +156,7 @@ func parseOAuth2Config(c *cli.Command) *oauth2.Source {
 | 
			
		||||
		OpenIDConnectAutoDiscoveryURL: c.String("auto-discover-url"),
 | 
			
		||||
		CustomURLMapping:              customURLMapping,
 | 
			
		||||
		IconURL:                       c.String("icon-url"),
 | 
			
		||||
		SkipLocalTwoFA:                c.Bool("skip-local-2fa"),
 | 
			
		||||
		Scopes:                        c.StringSlice("scopes"),
 | 
			
		||||
		RequiredClaimName:             c.String("required-claim-name"),
 | 
			
		||||
		RequiredClaimValue:            c.String("required-claim-value"),
 | 
			
		||||
@@ -185,13 +165,14 @@ func parseOAuth2Config(c *cli.Command) *oauth2.Source {
 | 
			
		||||
		RestrictedGroup:               c.String("restricted-group"),
 | 
			
		||||
		GroupTeamMap:                  c.String("group-team-map"),
 | 
			
		||||
		GroupTeamMapRemoval:           c.Bool("group-team-map-removal"),
 | 
			
		||||
		SSHPublicKeyClaimName:         c.String("ssh-public-key-claim-name"),
 | 
			
		||||
		FullNameClaimName:             c.String("full-name-claim-name"),
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (a *authService) runAddOauth(ctx context.Context, c *cli.Command) error {
 | 
			
		||||
	if err := a.initDB(ctx); err != nil {
 | 
			
		||||
func runAddOauth(c *cli.Context) error {
 | 
			
		||||
	ctx, cancel := installSignals()
 | 
			
		||||
	defer cancel()
 | 
			
		||||
 | 
			
		||||
	if err := initDB(ctx); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -203,25 +184,27 @@ func (a *authService) runAddOauth(ctx context.Context, c *cli.Command) error {
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return a.createAuthSource(ctx, &auth_model.Source{
 | 
			
		||||
	return auth_model.CreateSource(ctx, &auth_model.Source{
 | 
			
		||||
		Type:     auth_model.OAuth2,
 | 
			
		||||
		Name:     c.String("name"),
 | 
			
		||||
		IsActive: true,
 | 
			
		||||
		Cfg:      config,
 | 
			
		||||
		TwoFactorPolicy: util.Iif(c.Bool("skip-local-2fa"), "skip", ""),
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (a *authService) runUpdateOauth(ctx context.Context, c *cli.Command) error {
 | 
			
		||||
func runUpdateOauth(c *cli.Context) error {
 | 
			
		||||
	if !c.IsSet("id") {
 | 
			
		||||
		return errors.New("--id flag is missing")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := a.initDB(ctx); err != nil {
 | 
			
		||||
	ctx, cancel := installSignals()
 | 
			
		||||
	defer cancel()
 | 
			
		||||
 | 
			
		||||
	if err := initDB(ctx); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	source, err := a.getAuthSourceByID(ctx, c.Int64("id"))
 | 
			
		||||
	source, err := auth_model.GetSourceByID(ctx, c.Int64("id"))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
@@ -278,12 +261,6 @@ func (a *authService) runUpdateOauth(ctx context.Context, c *cli.Command) error
 | 
			
		||||
	if c.IsSet("group-team-map-removal") {
 | 
			
		||||
		oAuth2Config.GroupTeamMapRemoval = c.Bool("group-team-map-removal")
 | 
			
		||||
	}
 | 
			
		||||
	if c.IsSet("ssh-public-key-claim-name") {
 | 
			
		||||
		oAuth2Config.SSHPublicKeyClaimName = c.String("ssh-public-key-claim-name")
 | 
			
		||||
	}
 | 
			
		||||
	if c.IsSet("full-name-claim-name") {
 | 
			
		||||
		oAuth2Config.FullNameClaimName = c.String("full-name-claim-name")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// update custom URL mapping
 | 
			
		||||
	customURLMapping := &oauth2.CustomURLMapping{}
 | 
			
		||||
@@ -317,6 +294,6 @@ func (a *authService) runUpdateOauth(ctx context.Context, c *cli.Command) error
 | 
			
		||||
 | 
			
		||||
	oAuth2Config.CustomURLMapping = customURLMapping
 | 
			
		||||
	source.Cfg = oAuth2Config
 | 
			
		||||
	source.TwoFactorPolicy = util.Iif(c.Bool("skip-local-2fa"), "skip", "")
 | 
			
		||||
	return a.updateAuthSource(ctx, source)
 | 
			
		||||
 | 
			
		||||
	return auth_model.UpdateSource(ctx, source)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,343 +0,0 @@
 | 
			
		||||
// Copyright 2025 The Gitea Authors. All rights reserved.
 | 
			
		||||
// SPDX-License-Identifier: MIT
 | 
			
		||||
 | 
			
		||||
package cmd
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	auth_model "code.gitea.io/gitea/models/auth"
 | 
			
		||||
	"code.gitea.io/gitea/services/auth/source/oauth2"
 | 
			
		||||
 | 
			
		||||
	"github.com/stretchr/testify/assert"
 | 
			
		||||
	"github.com/urfave/cli/v3"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestAddOauth(t *testing.T) {
 | 
			
		||||
	testCases := []struct {
 | 
			
		||||
		name   string
 | 
			
		||||
		args   []string
 | 
			
		||||
		source *auth_model.Source
 | 
			
		||||
		errMsg string
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			name: "valid config",
 | 
			
		||||
			args: []string{
 | 
			
		||||
				"--name", "test",
 | 
			
		||||
				"--provider", "github",
 | 
			
		||||
				"--key", "some_key",
 | 
			
		||||
				"--secret", "some_secret",
 | 
			
		||||
			},
 | 
			
		||||
			source: &auth_model.Source{
 | 
			
		||||
				Type:     auth_model.OAuth2,
 | 
			
		||||
				Name:     "test",
 | 
			
		||||
				IsActive: true,
 | 
			
		||||
				Cfg: &oauth2.Source{
 | 
			
		||||
					Scopes:       []string{},
 | 
			
		||||
					Provider:     "github",
 | 
			
		||||
					ClientID:     "some_key",
 | 
			
		||||
					ClientSecret: "some_secret",
 | 
			
		||||
				},
 | 
			
		||||
				TwoFactorPolicy: "",
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "valid config with openid connect",
 | 
			
		||||
			args: []string{
 | 
			
		||||
				"--name", "test",
 | 
			
		||||
				"--provider", "openidConnect",
 | 
			
		||||
				"--key", "some_key",
 | 
			
		||||
				"--secret", "some_secret",
 | 
			
		||||
				"--auto-discover-url", "https://example.com",
 | 
			
		||||
			},
 | 
			
		||||
			source: &auth_model.Source{
 | 
			
		||||
				Type:     auth_model.OAuth2,
 | 
			
		||||
				Name:     "test",
 | 
			
		||||
				IsActive: true,
 | 
			
		||||
				Cfg: &oauth2.Source{
 | 
			
		||||
					Scopes:                        []string{},
 | 
			
		||||
					Provider:                      "openidConnect",
 | 
			
		||||
					ClientID:                      "some_key",
 | 
			
		||||
					ClientSecret:                  "some_secret",
 | 
			
		||||
					OpenIDConnectAutoDiscoveryURL: "https://example.com",
 | 
			
		||||
				},
 | 
			
		||||
				TwoFactorPolicy: "",
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "valid config with options",
 | 
			
		||||
			args: []string{
 | 
			
		||||
				"--name", "test",
 | 
			
		||||
				"--provider", "gitlab",
 | 
			
		||||
				"--key", "some_key",
 | 
			
		||||
				"--secret", "some_secret",
 | 
			
		||||
				"--use-custom-urls", "true",
 | 
			
		||||
				"--custom-token-url", "https://example.com/token",
 | 
			
		||||
				"--custom-auth-url", "https://example.com/auth",
 | 
			
		||||
				"--custom-profile-url", "https://example.com/profile",
 | 
			
		||||
				"--custom-email-url", "https://example.com/email",
 | 
			
		||||
				"--custom-tenant-id", "some_tenant",
 | 
			
		||||
				"--icon-url", "https://example.com/icon",
 | 
			
		||||
				"--scopes", "scope1,scope2",
 | 
			
		||||
				"--skip-local-2fa", "true",
 | 
			
		||||
				"--required-claim-name", "claim_name",
 | 
			
		||||
				"--required-claim-value", "claim_value",
 | 
			
		||||
				"--group-claim-name", "group_name",
 | 
			
		||||
				"--admin-group", "admin",
 | 
			
		||||
				"--restricted-group", "restricted",
 | 
			
		||||
				"--group-team-map", `{"group1": [1,2]}`,
 | 
			
		||||
				"--group-team-map-removal=true",
 | 
			
		||||
				"--ssh-public-key-claim-name", "attr_ssh_pub_key",
 | 
			
		||||
				"--full-name-claim-name", "attr_full_name",
 | 
			
		||||
			},
 | 
			
		||||
			source: &auth_model.Source{
 | 
			
		||||
				Type:     auth_model.OAuth2,
 | 
			
		||||
				Name:     "test",
 | 
			
		||||
				IsActive: true,
 | 
			
		||||
				Cfg: &oauth2.Source{
 | 
			
		||||
					Provider:     "gitlab",
 | 
			
		||||
					ClientID:     "some_key",
 | 
			
		||||
					ClientSecret: "some_secret",
 | 
			
		||||
					CustomURLMapping: &oauth2.CustomURLMapping{
 | 
			
		||||
						TokenURL:   "https://example.com/token",
 | 
			
		||||
						AuthURL:    "https://example.com/auth",
 | 
			
		||||
						ProfileURL: "https://example.com/profile",
 | 
			
		||||
						EmailURL:   "https://example.com/email",
 | 
			
		||||
						Tenant:     "some_tenant",
 | 
			
		||||
					},
 | 
			
		||||
					IconURL:               "https://example.com/icon",
 | 
			
		||||
					Scopes:                []string{"scope1", "scope2"},
 | 
			
		||||
					RequiredClaimName:     "claim_name",
 | 
			
		||||
					RequiredClaimValue:    "claim_value",
 | 
			
		||||
					GroupClaimName:        "group_name",
 | 
			
		||||
					AdminGroup:            "admin",
 | 
			
		||||
					RestrictedGroup:       "restricted",
 | 
			
		||||
					GroupTeamMap:          `{"group1": [1,2]}`,
 | 
			
		||||
					GroupTeamMapRemoval:   true,
 | 
			
		||||
					SSHPublicKeyClaimName: "attr_ssh_pub_key",
 | 
			
		||||
					FullNameClaimName:     "attr_full_name",
 | 
			
		||||
				},
 | 
			
		||||
				TwoFactorPolicy: "skip",
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, tc := range testCases {
 | 
			
		||||
		t.Run(tc.name, func(t *testing.T) {
 | 
			
		||||
			var createdSource *auth_model.Source
 | 
			
		||||
			a := &authService{
 | 
			
		||||
				initDB: func(ctx context.Context) error {
 | 
			
		||||
					return nil
 | 
			
		||||
				},
 | 
			
		||||
				createAuthSource: func(ctx context.Context, source *auth_model.Source) error {
 | 
			
		||||
					createdSource = source
 | 
			
		||||
					return nil
 | 
			
		||||
				},
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			app := &cli.Command{
 | 
			
		||||
				Flags:  microcmdAuthAddOauth().Flags,
 | 
			
		||||
				Action: a.runAddOauth,
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			args := []string{"oauth-test"}
 | 
			
		||||
			args = append(args, tc.args...)
 | 
			
		||||
 | 
			
		||||
			err := app.Run(t.Context(), args)
 | 
			
		||||
 | 
			
		||||
			if tc.errMsg != "" {
 | 
			
		||||
				assert.EqualError(t, err, tc.errMsg)
 | 
			
		||||
			} else {
 | 
			
		||||
				assert.NoError(t, err)
 | 
			
		||||
				assert.Equal(t, tc.source, createdSource)
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestUpdateOauth(t *testing.T) {
 | 
			
		||||
	testCases := []struct {
 | 
			
		||||
		name               string
 | 
			
		||||
		args               []string
 | 
			
		||||
		id                 int64
 | 
			
		||||
		existingAuthSource *auth_model.Source
 | 
			
		||||
		authSource         *auth_model.Source
 | 
			
		||||
		errMsg             string
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			name: "missing id",
 | 
			
		||||
			args: []string{
 | 
			
		||||
				"--name", "test",
 | 
			
		||||
			},
 | 
			
		||||
			errMsg: "--id flag is missing",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "valid config",
 | 
			
		||||
			id:   1,
 | 
			
		||||
			existingAuthSource: &auth_model.Source{
 | 
			
		||||
				ID:       1,
 | 
			
		||||
				Type:     auth_model.OAuth2,
 | 
			
		||||
				Name:     "old name",
 | 
			
		||||
				IsActive: true,
 | 
			
		||||
				Cfg: &oauth2.Source{
 | 
			
		||||
					Provider:     "github",
 | 
			
		||||
					ClientID:     "old_key",
 | 
			
		||||
					ClientSecret: "old_secret",
 | 
			
		||||
				},
 | 
			
		||||
				TwoFactorPolicy: "",
 | 
			
		||||
			},
 | 
			
		||||
			args: []string{
 | 
			
		||||
				"--id", "1",
 | 
			
		||||
				"--name", "test",
 | 
			
		||||
				"--provider", "gitlab",
 | 
			
		||||
				"--key", "new_key",
 | 
			
		||||
				"--secret", "new_secret",
 | 
			
		||||
			},
 | 
			
		||||
			authSource: &auth_model.Source{
 | 
			
		||||
				ID:       1,
 | 
			
		||||
				Type:     auth_model.OAuth2,
 | 
			
		||||
				Name:     "test",
 | 
			
		||||
				IsActive: true,
 | 
			
		||||
				Cfg: &oauth2.Source{
 | 
			
		||||
					Provider:         "gitlab",
 | 
			
		||||
					ClientID:         "new_key",
 | 
			
		||||
					ClientSecret:     "new_secret",
 | 
			
		||||
					CustomURLMapping: &oauth2.CustomURLMapping{},
 | 
			
		||||
				},
 | 
			
		||||
				TwoFactorPolicy: "",
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "valid config with options",
 | 
			
		||||
			id:   1,
 | 
			
		||||
			existingAuthSource: &auth_model.Source{
 | 
			
		||||
				ID:       1,
 | 
			
		||||
				Type:     auth_model.OAuth2,
 | 
			
		||||
				Name:     "old name",
 | 
			
		||||
				IsActive: true,
 | 
			
		||||
				Cfg: &oauth2.Source{
 | 
			
		||||
					Provider:     "gitlab",
 | 
			
		||||
					ClientID:     "old_key",
 | 
			
		||||
					ClientSecret: "old_secret",
 | 
			
		||||
					CustomURLMapping: &oauth2.CustomURLMapping{
 | 
			
		||||
						TokenURL:   "https://old.example.com/token",
 | 
			
		||||
						AuthURL:    "https://old.example.com/auth",
 | 
			
		||||
						ProfileURL: "https://old.example.com/profile",
 | 
			
		||||
						EmailURL:   "https://old.example.com/email",
 | 
			
		||||
						Tenant:     "old_tenant",
 | 
			
		||||
					},
 | 
			
		||||
					IconURL:               "https://old.example.com/icon",
 | 
			
		||||
					Scopes:                []string{"old_scope1", "old_scope2"},
 | 
			
		||||
					RequiredClaimName:     "old_claim_name",
 | 
			
		||||
					RequiredClaimValue:    "old_claim_value",
 | 
			
		||||
					GroupClaimName:        "old_group_name",
 | 
			
		||||
					AdminGroup:            "old_admin",
 | 
			
		||||
					RestrictedGroup:       "old_restricted",
 | 
			
		||||
					GroupTeamMap:          `{"old_group1": [1,2]}`,
 | 
			
		||||
					GroupTeamMapRemoval:   true,
 | 
			
		||||
					SSHPublicKeyClaimName: "old_ssh_pub_key",
 | 
			
		||||
					FullNameClaimName:     "old_full_name",
 | 
			
		||||
				},
 | 
			
		||||
				TwoFactorPolicy: "",
 | 
			
		||||
			},
 | 
			
		||||
			args: []string{
 | 
			
		||||
				"--id", "1",
 | 
			
		||||
				"--name", "test",
 | 
			
		||||
				"--provider", "github",
 | 
			
		||||
				"--key", "new_key",
 | 
			
		||||
				"--secret", "new_secret",
 | 
			
		||||
				"--use-custom-urls", "true",
 | 
			
		||||
				"--custom-token-url", "https://example.com/token",
 | 
			
		||||
				"--custom-auth-url", "https://example.com/auth",
 | 
			
		||||
				"--custom-profile-url", "https://example.com/profile",
 | 
			
		||||
				"--custom-email-url", "https://example.com/email",
 | 
			
		||||
				"--custom-tenant-id", "new_tenant",
 | 
			
		||||
				"--icon-url", "https://example.com/icon",
 | 
			
		||||
				"--scopes", "scope1,scope2",
 | 
			
		||||
				"--skip-local-2fa=true",
 | 
			
		||||
				"--required-claim-name", "claim_name",
 | 
			
		||||
				"--required-claim-value", "claim_value",
 | 
			
		||||
				"--group-claim-name", "group_name",
 | 
			
		||||
				"--admin-group", "admin",
 | 
			
		||||
				"--restricted-group", "restricted",
 | 
			
		||||
				"--group-team-map", `{"group1": [1,2]}`,
 | 
			
		||||
				"--group-team-map-removal=false",
 | 
			
		||||
				"--ssh-public-key-claim-name", "new_ssh_pub_key",
 | 
			
		||||
				"--full-name-claim-name", "new_full_name",
 | 
			
		||||
			},
 | 
			
		||||
			authSource: &auth_model.Source{
 | 
			
		||||
				ID:       1,
 | 
			
		||||
				Type:     auth_model.OAuth2,
 | 
			
		||||
				Name:     "test",
 | 
			
		||||
				IsActive: true,
 | 
			
		||||
				Cfg: &oauth2.Source{
 | 
			
		||||
					Provider:     "github",
 | 
			
		||||
					ClientID:     "new_key",
 | 
			
		||||
					ClientSecret: "new_secret",
 | 
			
		||||
					CustomURLMapping: &oauth2.CustomURLMapping{
 | 
			
		||||
						TokenURL:   "https://example.com/token",
 | 
			
		||||
						AuthURL:    "https://example.com/auth",
 | 
			
		||||
						ProfileURL: "https://example.com/profile",
 | 
			
		||||
						EmailURL:   "https://example.com/email",
 | 
			
		||||
						Tenant:     "new_tenant",
 | 
			
		||||
					},
 | 
			
		||||
					IconURL:               "https://example.com/icon",
 | 
			
		||||
					Scopes:                []string{"scope1", "scope2"},
 | 
			
		||||
					RequiredClaimName:     "claim_name",
 | 
			
		||||
					RequiredClaimValue:    "claim_value",
 | 
			
		||||
					GroupClaimName:        "group_name",
 | 
			
		||||
					AdminGroup:            "admin",
 | 
			
		||||
					RestrictedGroup:       "restricted",
 | 
			
		||||
					GroupTeamMap:          `{"group1": [1,2]}`,
 | 
			
		||||
					GroupTeamMapRemoval:   false,
 | 
			
		||||
					SSHPublicKeyClaimName: "new_ssh_pub_key",
 | 
			
		||||
					FullNameClaimName:     "new_full_name",
 | 
			
		||||
				},
 | 
			
		||||
				TwoFactorPolicy: "skip",
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, tc := range testCases {
 | 
			
		||||
		t.Run(tc.name, func(t *testing.T) {
 | 
			
		||||
			a := &authService{
 | 
			
		||||
				initDB: func(ctx context.Context) error {
 | 
			
		||||
					return nil
 | 
			
		||||
				},
 | 
			
		||||
				getAuthSourceByID: func(ctx context.Context, id int64) (*auth_model.Source, error) {
 | 
			
		||||
					return &auth_model.Source{
 | 
			
		||||
						ID:       1,
 | 
			
		||||
						Type:     auth_model.OAuth2,
 | 
			
		||||
						Name:     "test",
 | 
			
		||||
						IsActive: true,
 | 
			
		||||
						Cfg: &oauth2.Source{
 | 
			
		||||
							CustomURLMapping: &oauth2.CustomURLMapping{},
 | 
			
		||||
						},
 | 
			
		||||
						TwoFactorPolicy: "skip",
 | 
			
		||||
					}, nil
 | 
			
		||||
				},
 | 
			
		||||
				updateAuthSource: func(ctx context.Context, source *auth_model.Source) error {
 | 
			
		||||
					assert.Equal(t, tc.authSource, source)
 | 
			
		||||
					return nil
 | 
			
		||||
				},
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			app := &cli.Command{
 | 
			
		||||
				Flags:  microcmdAuthUpdateOauth().Flags,
 | 
			
		||||
				Action: a.runUpdateOauth,
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			args := []string{"oauth-test"}
 | 
			
		||||
			args = append(args, tc.args...)
 | 
			
		||||
 | 
			
		||||
			err := app.Run(t.Context(), args)
 | 
			
		||||
 | 
			
		||||
			if tc.errMsg != "" {
 | 
			
		||||
				assert.EqualError(t, err, tc.errMsg)
 | 
			
		||||
			} else {
 | 
			
		||||
				assert.NoError(t, err)
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -1,271 +0,0 @@
 | 
			
		||||
// Copyright 2025 The Gitea Authors. All rights reserved.
 | 
			
		||||
// SPDX-License-Identifier: MIT
 | 
			
		||||
 | 
			
		||||
package cmd
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	auth_model "code.gitea.io/gitea/models/auth"
 | 
			
		||||
	"code.gitea.io/gitea/services/auth/source/smtp"
 | 
			
		||||
 | 
			
		||||
	"github.com/stretchr/testify/assert"
 | 
			
		||||
	"github.com/urfave/cli/v3"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestAddSMTP(t *testing.T) {
 | 
			
		||||
	testCases := []struct {
 | 
			
		||||
		name   string
 | 
			
		||||
		args   []string
 | 
			
		||||
		source *auth_model.Source
 | 
			
		||||
		errMsg string
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			name: "missing name",
 | 
			
		||||
			args: []string{
 | 
			
		||||
				"--host", "localhost",
 | 
			
		||||
				"--port", "25",
 | 
			
		||||
			},
 | 
			
		||||
			errMsg: "name must be set",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "missing host",
 | 
			
		||||
			args: []string{
 | 
			
		||||
				"--name", "test",
 | 
			
		||||
				"--port", "25",
 | 
			
		||||
			},
 | 
			
		||||
			errMsg: "host must be set",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "missing port",
 | 
			
		||||
			args: []string{
 | 
			
		||||
				"--name", "test",
 | 
			
		||||
				"--host", "localhost",
 | 
			
		||||
			},
 | 
			
		||||
			errMsg: "port must be set",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "valid config",
 | 
			
		||||
			args: []string{
 | 
			
		||||
				"--name", "test",
 | 
			
		||||
				"--host", "localhost",
 | 
			
		||||
				"--port", "25",
 | 
			
		||||
			},
 | 
			
		||||
			source: &auth_model.Source{
 | 
			
		||||
				Type:     auth_model.SMTP,
 | 
			
		||||
				Name:     "test",
 | 
			
		||||
				IsActive: true,
 | 
			
		||||
				Cfg: &smtp.Source{
 | 
			
		||||
					Auth: "PLAIN",
 | 
			
		||||
					Host: "localhost",
 | 
			
		||||
					Port: 25,
 | 
			
		||||
				},
 | 
			
		||||
				TwoFactorPolicy: "",
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "valid config with options",
 | 
			
		||||
			args: []string{
 | 
			
		||||
				"--name", "test",
 | 
			
		||||
				"--host", "localhost",
 | 
			
		||||
				"--port", "25",
 | 
			
		||||
				"--auth-type", "LOGIN",
 | 
			
		||||
				"--force-smtps",
 | 
			
		||||
				"--skip-verify",
 | 
			
		||||
				"--helo-hostname", "example.com",
 | 
			
		||||
				"--disable-helo=true",
 | 
			
		||||
				"--allowed-domains", "example.com,example.org",
 | 
			
		||||
				"--skip-local-2fa",
 | 
			
		||||
				"--active=false",
 | 
			
		||||
			},
 | 
			
		||||
			source: &auth_model.Source{
 | 
			
		||||
				Type:     auth_model.SMTP,
 | 
			
		||||
				Name:     "test",
 | 
			
		||||
				IsActive: false,
 | 
			
		||||
				Cfg: &smtp.Source{
 | 
			
		||||
					Auth:           "LOGIN",
 | 
			
		||||
					Host:           "localhost",
 | 
			
		||||
					Port:           25,
 | 
			
		||||
					ForceSMTPS:     true,
 | 
			
		||||
					SkipVerify:     true,
 | 
			
		||||
					HeloHostname:   "example.com",
 | 
			
		||||
					DisableHelo:    true,
 | 
			
		||||
					AllowedDomains: "example.com,example.org",
 | 
			
		||||
				},
 | 
			
		||||
				TwoFactorPolicy: "skip",
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, tc := range testCases {
 | 
			
		||||
		t.Run(tc.name, func(t *testing.T) {
 | 
			
		||||
			a := &authService{
 | 
			
		||||
				initDB: func(ctx context.Context) error {
 | 
			
		||||
					return nil
 | 
			
		||||
				},
 | 
			
		||||
				createAuthSource: func(ctx context.Context, source *auth_model.Source) error {
 | 
			
		||||
					assert.Equal(t, tc.source, source)
 | 
			
		||||
					return nil
 | 
			
		||||
				},
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			cmd := &cli.Command{
 | 
			
		||||
				Flags:  microcmdAuthAddSMTP().Flags,
 | 
			
		||||
				Action: a.runAddSMTP,
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			args := []string{"smtp-test"}
 | 
			
		||||
			args = append(args, tc.args...)
 | 
			
		||||
 | 
			
		||||
			t.Log(args)
 | 
			
		||||
			err := cmd.Run(t.Context(), args)
 | 
			
		||||
 | 
			
		||||
			if tc.errMsg != "" {
 | 
			
		||||
				assert.EqualError(t, err, tc.errMsg)
 | 
			
		||||
			} else {
 | 
			
		||||
				assert.NoError(t, err)
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestUpdateSMTP(t *testing.T) {
 | 
			
		||||
	testCases := []struct {
 | 
			
		||||
		name               string
 | 
			
		||||
		args               []string
 | 
			
		||||
		existingAuthSource *auth_model.Source
 | 
			
		||||
		authSource         *auth_model.Source
 | 
			
		||||
		errMsg             string
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			name: "missing id",
 | 
			
		||||
			args: []string{
 | 
			
		||||
				"--name", "test",
 | 
			
		||||
				"--host", "localhost",
 | 
			
		||||
				"--port", "25",
 | 
			
		||||
			},
 | 
			
		||||
			errMsg: "--id flag is missing",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "valid config",
 | 
			
		||||
			existingAuthSource: &auth_model.Source{
 | 
			
		||||
				ID:       1,
 | 
			
		||||
				Type:     auth_model.SMTP,
 | 
			
		||||
				Name:     "old name",
 | 
			
		||||
				IsActive: true,
 | 
			
		||||
				Cfg: &smtp.Source{
 | 
			
		||||
					Auth: "PLAIN",
 | 
			
		||||
					Host: "old host",
 | 
			
		||||
					Port: 26,
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
			args: []string{
 | 
			
		||||
				"--id", "1",
 | 
			
		||||
				"--name", "test",
 | 
			
		||||
				"--host", "localhost",
 | 
			
		||||
				"--port", "25",
 | 
			
		||||
			},
 | 
			
		||||
			authSource: &auth_model.Source{
 | 
			
		||||
				ID:       1,
 | 
			
		||||
				Type:     auth_model.SMTP,
 | 
			
		||||
				Name:     "test",
 | 
			
		||||
				IsActive: true,
 | 
			
		||||
				Cfg: &smtp.Source{
 | 
			
		||||
					Auth: "PLAIN",
 | 
			
		||||
					Host: "localhost",
 | 
			
		||||
					Port: 25,
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "valid config with options",
 | 
			
		||||
			existingAuthSource: &auth_model.Source{
 | 
			
		||||
				ID:       1,
 | 
			
		||||
				Type:     auth_model.SMTP,
 | 
			
		||||
				Name:     "old name",
 | 
			
		||||
				IsActive: true,
 | 
			
		||||
				Cfg: &smtp.Source{
 | 
			
		||||
					Auth:           "PLAIN",
 | 
			
		||||
					Host:           "old host",
 | 
			
		||||
					Port:           26,
 | 
			
		||||
					HeloHostname:   "old.example.com",
 | 
			
		||||
					AllowedDomains: "old.example.com",
 | 
			
		||||
				},
 | 
			
		||||
				TwoFactorPolicy: "",
 | 
			
		||||
			},
 | 
			
		||||
			args: []string{
 | 
			
		||||
				"--id", "1",
 | 
			
		||||
				"--name", "test",
 | 
			
		||||
				"--host", "localhost",
 | 
			
		||||
				"--port", "25",
 | 
			
		||||
				"--auth-type", "LOGIN",
 | 
			
		||||
				"--force-smtps",
 | 
			
		||||
				"--skip-verify",
 | 
			
		||||
				"--helo-hostname", "example.com",
 | 
			
		||||
				"--disable-helo",
 | 
			
		||||
				"--allowed-domains", "example.com,example.org",
 | 
			
		||||
				"--skip-local-2fa",
 | 
			
		||||
				"--active=false",
 | 
			
		||||
			},
 | 
			
		||||
			authSource: &auth_model.Source{
 | 
			
		||||
				ID:       1,
 | 
			
		||||
				Type:     auth_model.SMTP,
 | 
			
		||||
				Name:     "test",
 | 
			
		||||
				IsActive: false,
 | 
			
		||||
				Cfg: &smtp.Source{
 | 
			
		||||
					Auth:           "LOGIN",
 | 
			
		||||
					Host:           "localhost",
 | 
			
		||||
					Port:           25,
 | 
			
		||||
					ForceSMTPS:     true,
 | 
			
		||||
					SkipVerify:     true,
 | 
			
		||||
					HeloHostname:   "example.com",
 | 
			
		||||
					DisableHelo:    true,
 | 
			
		||||
					AllowedDomains: "example.com,example.org",
 | 
			
		||||
				},
 | 
			
		||||
				TwoFactorPolicy: "skip",
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, tc := range testCases {
 | 
			
		||||
		t.Run(tc.name, func(t *testing.T) {
 | 
			
		||||
			a := &authService{
 | 
			
		||||
				initDB: func(ctx context.Context) error {
 | 
			
		||||
					return nil
 | 
			
		||||
				},
 | 
			
		||||
				getAuthSourceByID: func(ctx context.Context, id int64) (*auth_model.Source, error) {
 | 
			
		||||
					return &auth_model.Source{
 | 
			
		||||
						ID:       1,
 | 
			
		||||
						Type:     auth_model.SMTP,
 | 
			
		||||
						Name:     "test",
 | 
			
		||||
						IsActive: true,
 | 
			
		||||
						Cfg: &smtp.Source{
 | 
			
		||||
							Auth: "PLAIN",
 | 
			
		||||
						},
 | 
			
		||||
					}, nil
 | 
			
		||||
				},
 | 
			
		||||
 | 
			
		||||
				updateAuthSource: func(ctx context.Context, source *auth_model.Source) error {
 | 
			
		||||
					assert.Equal(t, tc.authSource, source)
 | 
			
		||||
					return nil
 | 
			
		||||
				},
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			app := &cli.Command{
 | 
			
		||||
				Flags:  microcmdAuthUpdateSMTP().Flags,
 | 
			
		||||
				Action: a.runUpdateSMTP,
 | 
			
		||||
			}
 | 
			
		||||
			args := []string{"smtp-tests"}
 | 
			
		||||
			args = append(args, tc.args...)
 | 
			
		||||
 | 
			
		||||
			err := app.Run(t.Context(), args)
 | 
			
		||||
 | 
			
		||||
			if tc.errMsg != "" {
 | 
			
		||||
				assert.EqualError(t, err, tc.errMsg)
 | 
			
		||||
			} else {
 | 
			
		||||
				assert.NoError(t, err)
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -4,7 +4,6 @@
 | 
			
		||||
package cmd
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
@@ -12,11 +11,11 @@ import (
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
	"code.gitea.io/gitea/services/auth/source/smtp"
 | 
			
		||||
 | 
			
		||||
	"github.com/urfave/cli/v3"
 | 
			
		||||
	"github.com/urfave/cli/v2"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func smtpCLIFlags() []cli.Flag {
 | 
			
		||||
	return []cli.Flag{
 | 
			
		||||
var (
 | 
			
		||||
	smtpCLIFlags = []cli.Flag{
 | 
			
		||||
		&cli.StringFlag{
 | 
			
		||||
			Name:  "name",
 | 
			
		||||
			Value: "",
 | 
			
		||||
@@ -39,10 +38,12 @@ func smtpCLIFlags() []cli.Flag {
 | 
			
		||||
		&cli.BoolFlag{
 | 
			
		||||
			Name:  "force-smtps",
 | 
			
		||||
			Usage: "SMTPS is always used on port 465. Set this to force SMTPS on other ports.",
 | 
			
		||||
			Value: true,
 | 
			
		||||
		},
 | 
			
		||||
		&cli.BoolFlag{
 | 
			
		||||
			Name:  "skip-verify",
 | 
			
		||||
			Usage: "Skip TLS verify.",
 | 
			
		||||
			Value: true,
 | 
			
		||||
		},
 | 
			
		||||
		&cli.StringFlag{
 | 
			
		||||
			Name:  "helo-hostname",
 | 
			
		||||
@@ -52,6 +53,7 @@ func smtpCLIFlags() []cli.Flag {
 | 
			
		||||
		&cli.BoolFlag{
 | 
			
		||||
			Name:  "disable-helo",
 | 
			
		||||
			Usage: "Disable SMTP helo.",
 | 
			
		||||
			Value: true,
 | 
			
		||||
		},
 | 
			
		||||
		&cli.StringFlag{
 | 
			
		||||
			Name:  "allowed-domains",
 | 
			
		||||
@@ -61,6 +63,7 @@ func smtpCLIFlags() []cli.Flag {
 | 
			
		||||
		&cli.BoolFlag{
 | 
			
		||||
			Name:  "skip-local-2fa",
 | 
			
		||||
			Usage: "Skip 2FA to log on.",
 | 
			
		||||
			Value: true,
 | 
			
		||||
		},
 | 
			
		||||
		&cli.BoolFlag{
 | 
			
		||||
			Name:  "active",
 | 
			
		||||
@@ -68,34 +71,23 @@ func smtpCLIFlags() []cli.Flag {
 | 
			
		||||
			Value: true,
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func microcmdAuthUpdateSMTP() *cli.Command {
 | 
			
		||||
	return &cli.Command{
 | 
			
		||||
		Name:  "update-smtp",
 | 
			
		||||
		Usage: "Update existing SMTP authentication source",
 | 
			
		||||
		Action: func(ctx context.Context, cmd *cli.Command) error {
 | 
			
		||||
			return newAuthService().runUpdateSMTP(ctx, cmd)
 | 
			
		||||
		},
 | 
			
		||||
		Flags: append(smtpCLIFlags()[:1], append([]cli.Flag{&cli.Int64Flag{
 | 
			
		||||
			Name:  "id",
 | 
			
		||||
			Usage: "ID of authentication source",
 | 
			
		||||
		}}, smtpCLIFlags()[1:]...)...),
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func microcmdAuthAddSMTP() *cli.Command {
 | 
			
		||||
	return &cli.Command{
 | 
			
		||||
	microcmdAuthAddSMTP = &cli.Command{
 | 
			
		||||
		Name:   "add-smtp",
 | 
			
		||||
		Usage:  "Add new SMTP authentication source",
 | 
			
		||||
		Action: func(ctx context.Context, cmd *cli.Command) error {
 | 
			
		||||
			return newAuthService().runAddSMTP(ctx, cmd)
 | 
			
		||||
		},
 | 
			
		||||
		Flags: smtpCLIFlags(),
 | 
			
		||||
		Action: runAddSMTP,
 | 
			
		||||
		Flags:  smtpCLIFlags,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func parseSMTPConfig(c *cli.Command, conf *smtp.Source) error {
 | 
			
		||||
	microcmdAuthUpdateSMTP = &cli.Command{
 | 
			
		||||
		Name:   "update-smtp",
 | 
			
		||||
		Usage:  "Update existing SMTP authentication source",
 | 
			
		||||
		Action: runUpdateSMTP,
 | 
			
		||||
		Flags:  append(smtpCLIFlags[:1], append([]cli.Flag{idFlag}, smtpCLIFlags[1:]...)...),
 | 
			
		||||
	}
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func parseSMTPConfig(c *cli.Context, conf *smtp.Source) error {
 | 
			
		||||
	if c.IsSet("auth-type") {
 | 
			
		||||
		conf.Auth = c.String("auth-type")
 | 
			
		||||
		validAuthTypes := []string{"PLAIN", "LOGIN", "CRAM-MD5"}
 | 
			
		||||
@@ -125,11 +117,17 @@ func parseSMTPConfig(c *cli.Command, conf *smtp.Source) error {
 | 
			
		||||
	if c.IsSet("disable-helo") {
 | 
			
		||||
		conf.DisableHelo = c.Bool("disable-helo")
 | 
			
		||||
	}
 | 
			
		||||
	if c.IsSet("skip-local-2fa") {
 | 
			
		||||
		conf.SkipLocalTwoFA = c.Bool("skip-local-2fa")
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (a *authService) runAddSMTP(ctx context.Context, c *cli.Command) error {
 | 
			
		||||
	if err := a.initDB(ctx); err != nil {
 | 
			
		||||
func runAddSMTP(c *cli.Context) error {
 | 
			
		||||
	ctx, cancel := installSignals()
 | 
			
		||||
	defer cancel()
 | 
			
		||||
 | 
			
		||||
	if err := initDB(ctx); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -157,25 +155,27 @@ func (a *authService) runAddSMTP(ctx context.Context, c *cli.Command) error {
 | 
			
		||||
		smtpConfig.Auth = "PLAIN"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return a.createAuthSource(ctx, &auth_model.Source{
 | 
			
		||||
	return auth_model.CreateSource(ctx, &auth_model.Source{
 | 
			
		||||
		Type:     auth_model.SMTP,
 | 
			
		||||
		Name:     c.String("name"),
 | 
			
		||||
		IsActive: active,
 | 
			
		||||
		Cfg:      &smtpConfig,
 | 
			
		||||
		TwoFactorPolicy: util.Iif(c.Bool("skip-local-2fa"), "skip", ""),
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (a *authService) runUpdateSMTP(ctx context.Context, c *cli.Command) error {
 | 
			
		||||
func runUpdateSMTP(c *cli.Context) error {
 | 
			
		||||
	if !c.IsSet("id") {
 | 
			
		||||
		return errors.New("--id flag is missing")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := a.initDB(ctx); err != nil {
 | 
			
		||||
	ctx, cancel := installSignals()
 | 
			
		||||
	defer cancel()
 | 
			
		||||
 | 
			
		||||
	if err := initDB(ctx); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	source, err := a.getAuthSourceByID(ctx, c.Int64("id"))
 | 
			
		||||
	source, err := auth_model.GetSourceByID(ctx, c.Int64("id"))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
@@ -195,6 +195,6 @@ func (a *authService) runUpdateSMTP(ctx context.Context, c *cli.Command) error {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	source.Cfg = smtpConfig
 | 
			
		||||
	source.TwoFactorPolicy = util.Iif(c.Bool("skip-local-2fa"), "skip", "")
 | 
			
		||||
	return a.updateAuthSource(ctx, source)
 | 
			
		||||
 | 
			
		||||
	return auth_model.UpdateSource(ctx, source)
 | 
			
		||||
}
 | 
			
		||||
@@ -4,13 +4,11 @@
 | 
			
		||||
package cmd
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/modules/graceful"
 | 
			
		||||
	asymkey_service "code.gitea.io/gitea/services/asymkey"
 | 
			
		||||
	repo_service "code.gitea.io/gitea/services/repository"
 | 
			
		||||
 | 
			
		||||
	"github.com/urfave/cli/v3"
 | 
			
		||||
	"github.com/urfave/cli/v2"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
@@ -27,14 +25,20 @@ var (
 | 
			
		||||
	}
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func runRegenerateHooks(ctx context.Context, _ *cli.Command) error {
 | 
			
		||||
func runRegenerateHooks(_ *cli.Context) error {
 | 
			
		||||
	ctx, cancel := installSignals()
 | 
			
		||||
	defer cancel()
 | 
			
		||||
 | 
			
		||||
	if err := initDB(ctx); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	return repo_service.SyncRepositoryHooks(graceful.GetManager().ShutdownContext())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func runRegenerateKeys(ctx context.Context, _ *cli.Command) error {
 | 
			
		||||
func runRegenerateKeys(_ *cli.Context) error {
 | 
			
		||||
	ctx, cancel := installSignals()
 | 
			
		||||
	defer cancel()
 | 
			
		||||
 | 
			
		||||
	if err := initDB(ctx); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,18 +4,18 @@
 | 
			
		||||
package cmd
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"github.com/urfave/cli/v3"
 | 
			
		||||
	"github.com/urfave/cli/v2"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var subcmdUser = &cli.Command{
 | 
			
		||||
	Name:  "user",
 | 
			
		||||
	Usage: "Modify users",
 | 
			
		||||
	Commands: []*cli.Command{
 | 
			
		||||
		microcmdUserCreate(),
 | 
			
		||||
	Subcommands: []*cli.Command{
 | 
			
		||||
		microcmdUserCreate,
 | 
			
		||||
		microcmdUserList,
 | 
			
		||||
		microcmdUserChangePassword(),
 | 
			
		||||
		microcmdUserDelete(),
 | 
			
		||||
		microcmdUserChangePassword,
 | 
			
		||||
		microcmdUserDelete,
 | 
			
		||||
		microcmdUserGenerateAccessToken,
 | 
			
		||||
		microcmdUserMustChangePassword(),
 | 
			
		||||
		microcmdUserMustChangePassword,
 | 
			
		||||
	},
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,6 @@
 | 
			
		||||
package cmd
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
 | 
			
		||||
@@ -14,11 +13,10 @@ import (
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	user_service "code.gitea.io/gitea/services/user"
 | 
			
		||||
 | 
			
		||||
	"github.com/urfave/cli/v3"
 | 
			
		||||
	"github.com/urfave/cli/v2"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func microcmdUserChangePassword() *cli.Command {
 | 
			
		||||
	return &cli.Command{
 | 
			
		||||
var microcmdUserChangePassword = &cli.Command{
 | 
			
		||||
	Name:   "change-password",
 | 
			
		||||
	Usage:  "Change a user's password",
 | 
			
		||||
	Action: runChangePassword,
 | 
			
		||||
@@ -26,14 +24,14 @@ func microcmdUserChangePassword() *cli.Command {
 | 
			
		||||
		&cli.StringFlag{
 | 
			
		||||
			Name:    "username",
 | 
			
		||||
			Aliases: []string{"u"},
 | 
			
		||||
			Value:   "",
 | 
			
		||||
			Usage:   "The user to change password for",
 | 
			
		||||
				Required: true,
 | 
			
		||||
		},
 | 
			
		||||
		&cli.StringFlag{
 | 
			
		||||
			Name:    "password",
 | 
			
		||||
			Aliases: []string{"p"},
 | 
			
		||||
			Value:   "",
 | 
			
		||||
			Usage:   "New password to set for user",
 | 
			
		||||
				Required: true,
 | 
			
		||||
		},
 | 
			
		||||
		&cli.BoolFlag{
 | 
			
		||||
			Name:  "must-change-password",
 | 
			
		||||
@@ -41,14 +39,18 @@ func microcmdUserChangePassword() *cli.Command {
 | 
			
		||||
			Value: true,
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func runChangePassword(ctx context.Context, c *cli.Command) error {
 | 
			
		||||
	if !setting.IsInTesting {
 | 
			
		||||
		if err := initDB(ctx); err != nil {
 | 
			
		||||
func runChangePassword(c *cli.Context) error {
 | 
			
		||||
	if err := argsSet(c, "username", "password"); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ctx, cancel := installSignals()
 | 
			
		||||
	defer cancel()
 | 
			
		||||
 | 
			
		||||
	if err := initDB(ctx); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	user, err := user_model.GetUserByName(ctx, c.String("username"))
 | 
			
		||||
 
 | 
			
		||||
@@ -1,91 +0,0 @@
 | 
			
		||||
// Copyright 2025 The Gitea Authors. All rights reserved.
 | 
			
		||||
// SPDX-License-Identifier: MIT
 | 
			
		||||
 | 
			
		||||
package cmd
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/models/db"
 | 
			
		||||
	"code.gitea.io/gitea/models/unittest"
 | 
			
		||||
	user_model "code.gitea.io/gitea/models/user"
 | 
			
		||||
 | 
			
		||||
	"github.com/stretchr/testify/assert"
 | 
			
		||||
	"github.com/stretchr/testify/require"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestChangePasswordCommand(t *testing.T) {
 | 
			
		||||
	ctx := t.Context()
 | 
			
		||||
 | 
			
		||||
	defer func() {
 | 
			
		||||
		require.NoError(t, db.TruncateBeans(t.Context(), &user_model.User{}))
 | 
			
		||||
	}()
 | 
			
		||||
 | 
			
		||||
	t.Run("change password successfully", func(t *testing.T) {
 | 
			
		||||
		// defer func() {
 | 
			
		||||
		// 	require.NoError(t, db.TruncateBeans(t.Context(), &user_model.User{}))
 | 
			
		||||
		// }()
 | 
			
		||||
		// Prepare test user
 | 
			
		||||
		unittest.AssertNotExistsBean(t, &user_model.User{LowerName: "testuser"})
 | 
			
		||||
		err := microcmdUserCreate().Run(ctx, []string{"create", "--username", "testuser", "--email", "testuser@gitea.local", "--random-password"})
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
		// load test user
 | 
			
		||||
		userBase := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
 | 
			
		||||
 | 
			
		||||
		// Change the password
 | 
			
		||||
		err = microcmdUserChangePassword().Run(ctx, []string{"change-password", "--username", "testuser", "--password", "newpassword"})
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
		// Verify the password has been changed
 | 
			
		||||
		user := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
 | 
			
		||||
		assert.NotEqual(t, userBase.Passwd, user.Passwd)
 | 
			
		||||
		assert.NotEqual(t, userBase.Salt, user.Salt)
 | 
			
		||||
 | 
			
		||||
		// Additional check for must-change-password flag
 | 
			
		||||
		require.NoError(t, microcmdUserChangePassword().Run(ctx, []string{"change-password", "--username", "testuser", "--password", "anotherpassword", "--must-change-password=false"}))
 | 
			
		||||
		user = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
 | 
			
		||||
		assert.False(t, user.MustChangePassword)
 | 
			
		||||
 | 
			
		||||
		require.NoError(t, microcmdUserChangePassword().Run(ctx, []string{"change-password", "--username", "testuser", "--password", "yetanotherpassword", "--must-change-password"}))
 | 
			
		||||
		user = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
 | 
			
		||||
		assert.True(t, user.MustChangePassword)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("failure cases", func(t *testing.T) {
 | 
			
		||||
		testCases := []struct {
 | 
			
		||||
			name        string
 | 
			
		||||
			args        []string
 | 
			
		||||
			expectedErr string
 | 
			
		||||
		}{
 | 
			
		||||
			{
 | 
			
		||||
				name:        "user does not exist",
 | 
			
		||||
				args:        []string{"change-password", "--username", "nonexistentuser", "--password", "newpassword"},
 | 
			
		||||
				expectedErr: "user does not exist",
 | 
			
		||||
			},
 | 
			
		||||
			{
 | 
			
		||||
				name:        "missing username",
 | 
			
		||||
				args:        []string{"change-password", "--password", "newpassword"},
 | 
			
		||||
				expectedErr: `"username" not set`,
 | 
			
		||||
			},
 | 
			
		||||
			{
 | 
			
		||||
				name:        "missing password",
 | 
			
		||||
				args:        []string{"change-password", "--username", "testuser"},
 | 
			
		||||
				expectedErr: `"password" not set`,
 | 
			
		||||
			},
 | 
			
		||||
			{
 | 
			
		||||
				name:        "too short password",
 | 
			
		||||
				args:        []string{"change-password", "--username", "testuser", "--password", "1"},
 | 
			
		||||
				expectedErr: "password is not long enough",
 | 
			
		||||
			},
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		for _, tc := range testCases {
 | 
			
		||||
			t.Run(tc.name, func(t *testing.T) {
 | 
			
		||||
				err := microcmdUserChangePassword().Run(ctx, tc.args)
 | 
			
		||||
				require.Error(t, err)
 | 
			
		||||
				require.Contains(t, err.Error(), tc.expectedErr)
 | 
			
		||||
			})
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
@@ -7,7 +7,6 @@ import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	auth_model "code.gitea.io/gitea/models/auth"
 | 
			
		||||
	"code.gitea.io/gitea/models/db"
 | 
			
		||||
@@ -16,18 +15,14 @@ import (
 | 
			
		||||
	"code.gitea.io/gitea/modules/optional"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
 | 
			
		||||
	"github.com/urfave/cli/v3"
 | 
			
		||||
	"github.com/urfave/cli/v2"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func microcmdUserCreate() *cli.Command {
 | 
			
		||||
	return &cli.Command{
 | 
			
		||||
var microcmdUserCreate = &cli.Command{
 | 
			
		||||
	Name:   "create",
 | 
			
		||||
	Usage:  "Create a new user in database",
 | 
			
		||||
	Action: runCreateUser,
 | 
			
		||||
		MutuallyExclusiveFlags: []cli.MutuallyExclusiveFlags{
 | 
			
		||||
			{
 | 
			
		||||
				Flags: [][]cli.Flag{
 | 
			
		||||
					{
 | 
			
		||||
	Flags: []cli.Flag{
 | 
			
		||||
		&cli.StringFlag{
 | 
			
		||||
			Name:  "name",
 | 
			
		||||
			Usage: "Username. DEPRECATED: use username instead",
 | 
			
		||||
@@ -36,17 +31,6 @@ func microcmdUserCreate() *cli.Command {
 | 
			
		||||
			Name:  "username",
 | 
			
		||||
			Usage: "Username",
 | 
			
		||||
		},
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
				Required: true,
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		Flags: []cli.Flag{
 | 
			
		||||
			&cli.StringFlag{
 | 
			
		||||
				Name:  "user-type",
 | 
			
		||||
				Usage: "Set user's type: individual or bot",
 | 
			
		||||
				Value: "individual",
 | 
			
		||||
			},
 | 
			
		||||
		&cli.StringFlag{
 | 
			
		||||
			Name:  "password",
 | 
			
		||||
			Usage: "User password",
 | 
			
		||||
@@ -54,7 +38,6 @@ func microcmdUserCreate() *cli.Command {
 | 
			
		||||
		&cli.StringFlag{
 | 
			
		||||
			Name:  "email",
 | 
			
		||||
			Usage: "User email address",
 | 
			
		||||
				Required: true,
 | 
			
		||||
		},
 | 
			
		||||
		&cli.BoolFlag{
 | 
			
		||||
			Name:  "admin",
 | 
			
		||||
@@ -67,7 +50,7 @@ func microcmdUserCreate() *cli.Command {
 | 
			
		||||
		&cli.BoolFlag{
 | 
			
		||||
			Name:               "must-change-password",
 | 
			
		||||
			Usage:              "User must change password after initial login, defaults to true for all users except the first one (can be disabled by --must-change-password=false)",
 | 
			
		||||
				HideDefault: true,
 | 
			
		||||
			DisableDefaultText: true,
 | 
			
		||||
		},
 | 
			
		||||
		&cli.IntFlag{
 | 
			
		||||
			Name:  "random-password-length",
 | 
			
		||||
@@ -78,48 +61,27 @@ func microcmdUserCreate() *cli.Command {
 | 
			
		||||
			Name:  "access-token",
 | 
			
		||||
			Usage: "Generate access token for the user",
 | 
			
		||||
		},
 | 
			
		||||
			&cli.StringFlag{
 | 
			
		||||
				Name:  "access-token-name",
 | 
			
		||||
				Usage: `Name of the generated access token`,
 | 
			
		||||
				Value: "gitea-admin",
 | 
			
		||||
			},
 | 
			
		||||
			&cli.StringFlag{
 | 
			
		||||
				Name:  "access-token-scopes",
 | 
			
		||||
				Usage: `Scopes of the generated access token, comma separated. Examples: "all", "public-only,read:issue", "write:repository,write:user"`,
 | 
			
		||||
				Value: "all",
 | 
			
		||||
			},
 | 
			
		||||
		&cli.BoolFlag{
 | 
			
		||||
			Name:  "restricted",
 | 
			
		||||
			Usage: "Make a restricted user account",
 | 
			
		||||
		},
 | 
			
		||||
			&cli.StringFlag{
 | 
			
		||||
				Name:  "fullname",
 | 
			
		||||
				Usage: `The full, human-readable name of the user`,
 | 
			
		||||
	},
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func runCreateUser(ctx context.Context, c *cli.Command) 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()
 | 
			
		||||
 | 
			
		||||
	userTypes := map[string]user_model.UserType{
 | 
			
		||||
		"individual": user_model.UserTypeIndividual,
 | 
			
		||||
		"bot":        user_model.UserTypeBot,
 | 
			
		||||
	if err := argsSet(c, "email"); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	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") {
 | 
			
		||||
		return errors.New("cannot set both --name and --username flags")
 | 
			
		||||
	}
 | 
			
		||||
	if !c.IsSet("name") && !c.IsSet("username") {
 | 
			
		||||
		return errors.New("one of --name or --username flags must be set")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if c.IsSet("password") && c.IsSet("random-password") {
 | 
			
		||||
@@ -131,12 +93,16 @@ func runCreateUser(ctx context.Context, c *cli.Command) error {
 | 
			
		||||
		username = c.String("username")
 | 
			
		||||
	} else {
 | 
			
		||||
		username = c.String("name")
 | 
			
		||||
		_, _ = fmt.Fprintf(c.ErrWriter, "--name flag is deprecated. Use --username instead.\n")
 | 
			
		||||
		_, _ = fmt.Fprintf(c.App.ErrWriter, "--name flag is deprecated. Use --username instead.\n")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ctx := c.Context
 | 
			
		||||
	if !setting.IsInTesting {
 | 
			
		||||
		// FIXME: need to refactor the "initDB" related code later
 | 
			
		||||
		// FIXME: need to refactor the "installSignals/initDB" related code later
 | 
			
		||||
		// it doesn't make sense to call it in (almost) every command action function
 | 
			
		||||
		var cancel context.CancelFunc
 | 
			
		||||
		ctx, cancel = installSignals()
 | 
			
		||||
		defer cancel()
 | 
			
		||||
		if err := initDB(ctx); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
@@ -151,21 +117,17 @@ func runCreateUser(ctx context.Context, c *cli.Command) error {
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		// codeql[disable-next-line=go/clear-text-logging]
 | 
			
		||||
		fmt.Printf("generated random password is '%s'\n", password)
 | 
			
		||||
	} else if userType == user_model.UserTypeIndividual {
 | 
			
		||||
	} else {
 | 
			
		||||
		return errors.New("must set either password or random-password flag")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	isAdmin := c.Bool("admin")
 | 
			
		||||
	mustChangePassword := true // always default to true
 | 
			
		||||
	if c.IsSet("must-change-password") {
 | 
			
		||||
		if userType != user_model.UserTypeIndividual {
 | 
			
		||||
			return errors.New("must-change-password flag can only be set for individual users")
 | 
			
		||||
		}
 | 
			
		||||
		// if the flag is set, use the value provided by the user
 | 
			
		||||
		mustChangePassword = c.Bool("must-change-password")
 | 
			
		||||
	} else if userType == user_model.UserTypeIndividual {
 | 
			
		||||
	} else {
 | 
			
		||||
		// check whether there are users in the database
 | 
			
		||||
		hasUserRecord, err := db.IsTableNotEmpty(&user_model.User{})
 | 
			
		||||
		if err != nil {
 | 
			
		||||
@@ -189,12 +151,10 @@ func runCreateUser(ctx context.Context, c *cli.Command) error {
 | 
			
		||||
	u := &user_model.User{
 | 
			
		||||
		Name:               username,
 | 
			
		||||
		Email:              c.String("email"),
 | 
			
		||||
		IsAdmin:            isAdmin,
 | 
			
		||||
		Type:               userType,
 | 
			
		||||
		Passwd:             password,
 | 
			
		||||
		IsAdmin:            isAdmin,
 | 
			
		||||
		MustChangePassword: mustChangePassword,
 | 
			
		||||
		Visibility:         visibility,
 | 
			
		||||
		FullName:           c.String("fullname"),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	overwriteDefault := &user_model.CreateUserOverwriteOptions{
 | 
			
		||||
@@ -202,40 +162,23 @@ func runCreateUser(ctx context.Context, c *cli.Command) error {
 | 
			
		||||
		IsRestricted: restricted,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var accessTokenName string
 | 
			
		||||
	var accessTokenScope auth_model.AccessTokenScope
 | 
			
		||||
	if c.IsSet("access-token") {
 | 
			
		||||
		accessTokenName = strings.TrimSpace(c.String("access-token-name"))
 | 
			
		||||
		if accessTokenName == "" {
 | 
			
		||||
			return errors.New("access-token-name cannot be empty")
 | 
			
		||||
		}
 | 
			
		||||
		var err error
 | 
			
		||||
		accessTokenScope, err = auth_model.AccessTokenScope(c.String("access-token-scopes")).Normalize()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return fmt.Errorf("invalid access token scope provided: %w", err)
 | 
			
		||||
		}
 | 
			
		||||
		if !accessTokenScope.HasPermissionScope() {
 | 
			
		||||
			return errors.New("access token does not have any permission")
 | 
			
		||||
		}
 | 
			
		||||
	} else if c.IsSet("access-token-name") || c.IsSet("access-token-scopes") {
 | 
			
		||||
		return errors.New("access-token-name and access-token-scopes flags are only valid when access-token flag is set")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// arguments should be prepared before creating the user & access token, in case there is anything wrong
 | 
			
		||||
 | 
			
		||||
	// create the user
 | 
			
		||||
	if err := user_model.CreateUser(ctx, u, &user_model.Meta{}, overwriteDefault); err != nil {
 | 
			
		||||
		return fmt.Errorf("CreateUser: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
	fmt.Printf("New user '%s' has been successfully created!\n", username)
 | 
			
		||||
 | 
			
		||||
	// create the access token
 | 
			
		||||
	if accessTokenScope != "" {
 | 
			
		||||
		t := &auth_model.AccessToken{Name: accessTokenName, UID: u.ID, Scope: accessTokenScope}
 | 
			
		||||
	if c.Bool("access-token") {
 | 
			
		||||
		t := &auth_model.AccessToken{
 | 
			
		||||
			Name: "gitea-admin",
 | 
			
		||||
			UID:  u.ID,
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if err := auth_model.NewAccessToken(ctx, t); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		fmt.Printf("Access token was successfully created... %s\n", t.Token)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	fmt.Printf("New user '%s' has been successfully created!\n", username)
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -8,127 +8,37 @@ import (
 | 
			
		||||
	"strings"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	auth_model "code.gitea.io/gitea/models/auth"
 | 
			
		||||
	"code.gitea.io/gitea/models/db"
 | 
			
		||||
	"code.gitea.io/gitea/models/unittest"
 | 
			
		||||
	user_model "code.gitea.io/gitea/models/user"
 | 
			
		||||
 | 
			
		||||
	"github.com/stretchr/testify/assert"
 | 
			
		||||
	"github.com/stretchr/testify/require"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestAdminUserCreate(t *testing.T) {
 | 
			
		||||
	app := NewMainApp(AppVersion{})
 | 
			
		||||
 | 
			
		||||
	reset := func() {
 | 
			
		||||
		require.NoError(t, db.TruncateBeans(t.Context(), &user_model.User{}))
 | 
			
		||||
		require.NoError(t, db.TruncateBeans(t.Context(), &user_model.EmailAddress{}))
 | 
			
		||||
		require.NoError(t, db.TruncateBeans(t.Context(), &auth_model.AccessToken{}))
 | 
			
		||||
		assert.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.User{}))
 | 
			
		||||
		assert.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.EmailAddress{}))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	t.Run("MustChangePassword", func(t *testing.T) {
 | 
			
		||||
		type check struct {
 | 
			
		||||
			IsAdmin            bool
 | 
			
		||||
			MustChangePassword bool
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		createCheck := func(name, args string) check {
 | 
			
		||||
			require.NoError(t, microcmdUserCreate().Run(t.Context(), strings.Fields(fmt.Sprintf("create --username %s --email %s@gitea.local %s --password foobar", name, name, args))))
 | 
			
		||||
	type createCheck struct{ IsAdmin, MustChangePassword bool }
 | 
			
		||||
	createUser := func(name, args string) createCheck {
 | 
			
		||||
		assert.NoError(t, app.Run(strings.Fields(fmt.Sprintf("./gitea admin user create --username %s --email %s@gitea.local %s --password foobar", name, name, args))))
 | 
			
		||||
		u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: name})
 | 
			
		||||
			return check{IsAdmin: u.IsAdmin, MustChangePassword: u.MustChangePassword}
 | 
			
		||||
		return createCheck{u.IsAdmin, u.MustChangePassword}
 | 
			
		||||
	}
 | 
			
		||||
	reset()
 | 
			
		||||
		assert.Equal(t, check{IsAdmin: false, MustChangePassword: false}, createCheck("u", ""), "first non-admin user doesn't need to change password")
 | 
			
		||||
	assert.Equal(t, createCheck{IsAdmin: false, MustChangePassword: false}, createUser("u", ""), "first non-admin user doesn't need to change password")
 | 
			
		||||
 | 
			
		||||
	reset()
 | 
			
		||||
		assert.Equal(t, check{IsAdmin: true, MustChangePassword: false}, createCheck("u", "--admin"), "first admin user doesn't need to change password")
 | 
			
		||||
	assert.Equal(t, createCheck{IsAdmin: true, MustChangePassword: false}, createUser("u", "--admin"), "first admin user doesn't need to change password")
 | 
			
		||||
 | 
			
		||||
	reset()
 | 
			
		||||
		assert.Equal(t, check{IsAdmin: true, MustChangePassword: true}, createCheck("u", "--admin --must-change-password"))
 | 
			
		||||
		assert.Equal(t, check{IsAdmin: true, MustChangePassword: true}, createCheck("u2", "--admin"))
 | 
			
		||||
		assert.Equal(t, check{IsAdmin: true, MustChangePassword: false}, createCheck("u3", "--admin --must-change-password=false"))
 | 
			
		||||
		assert.Equal(t, check{IsAdmin: false, MustChangePassword: true}, createCheck("u4", ""))
 | 
			
		||||
		assert.Equal(t, check{IsAdmin: false, MustChangePassword: false}, createCheck("u5", "--must-change-password=false"))
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	createUser := func(name string, args ...string) error {
 | 
			
		||||
		return microcmdUserCreate().Run(t.Context(), append([]string{"create", "--username", name, "--email", name + "@gitea.local"}, args...))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	t.Run("UserType", func(t *testing.T) {
 | 
			
		||||
		reset()
 | 
			
		||||
		assert.ErrorContains(t, createUser("u", "--user-type", "invalid"), "invalid user type")
 | 
			
		||||
		assert.ErrorContains(t, createUser("u", "--user-type", "bot", "--password", "123"), "can only be set for individual users")
 | 
			
		||||
		assert.ErrorContains(t, createUser("u", "--user-type", "bot", "--must-change-password"), "can only be set for individual users")
 | 
			
		||||
 | 
			
		||||
		assert.NoError(t, createUser("u", "--user-type", "bot"))
 | 
			
		||||
		u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "u"})
 | 
			
		||||
		assert.Equal(t, user_model.UserTypeBot, u.Type)
 | 
			
		||||
		assert.Empty(t, u.Passwd)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("AccessToken", func(t *testing.T) {
 | 
			
		||||
		// no generated access token
 | 
			
		||||
		reset()
 | 
			
		||||
		assert.NoError(t, createUser("u", "--random-password"))
 | 
			
		||||
		assert.Equal(t, 1, unittest.GetCount(t, &user_model.User{}))
 | 
			
		||||
		assert.Equal(t, 0, unittest.GetCount(t, &auth_model.AccessToken{}))
 | 
			
		||||
 | 
			
		||||
		// using "--access-token" only means "all" access
 | 
			
		||||
		reset()
 | 
			
		||||
		assert.NoError(t, createUser("u", "--random-password", "--access-token"))
 | 
			
		||||
		assert.Equal(t, 1, unittest.GetCount(t, &user_model.User{}))
 | 
			
		||||
		assert.Equal(t, 1, unittest.GetCount(t, &auth_model.AccessToken{}))
 | 
			
		||||
		accessToken := unittest.AssertExistsAndLoadBean(t, &auth_model.AccessToken{Name: "gitea-admin"})
 | 
			
		||||
		hasScopes, err := accessToken.Scope.HasScope(auth_model.AccessTokenScopeWriteAdmin, auth_model.AccessTokenScopeWriteRepository)
 | 
			
		||||
		assert.NoError(t, err)
 | 
			
		||||
		assert.True(t, hasScopes)
 | 
			
		||||
 | 
			
		||||
		// using "--access-token" with name & scopes
 | 
			
		||||
		reset()
 | 
			
		||||
		assert.NoError(t, createUser("u", "--random-password", "--access-token", "--access-token-name", "new-token-name", "--access-token-scopes", "read:issue,read:user"))
 | 
			
		||||
		assert.Equal(t, 1, unittest.GetCount(t, &user_model.User{}))
 | 
			
		||||
		assert.Equal(t, 1, unittest.GetCount(t, &auth_model.AccessToken{}))
 | 
			
		||||
		accessToken = unittest.AssertExistsAndLoadBean(t, &auth_model.AccessToken{Name: "new-token-name"})
 | 
			
		||||
		hasScopes, err = accessToken.Scope.HasScope(auth_model.AccessTokenScopeReadIssue, auth_model.AccessTokenScopeReadUser)
 | 
			
		||||
		assert.NoError(t, err)
 | 
			
		||||
		assert.True(t, hasScopes)
 | 
			
		||||
		hasScopes, err = accessToken.Scope.HasScope(auth_model.AccessTokenScopeWriteAdmin, auth_model.AccessTokenScopeWriteRepository)
 | 
			
		||||
		assert.NoError(t, err)
 | 
			
		||||
		assert.False(t, hasScopes)
 | 
			
		||||
 | 
			
		||||
		// using "--access-token-name" without "--access-token"
 | 
			
		||||
		reset()
 | 
			
		||||
		err = createUser("u", "--random-password", "--access-token-name", "new-token-name")
 | 
			
		||||
		assert.Equal(t, 0, unittest.GetCount(t, &user_model.User{}))
 | 
			
		||||
		assert.Equal(t, 0, unittest.GetCount(t, &auth_model.AccessToken{}))
 | 
			
		||||
		assert.ErrorContains(t, err, "access-token-name and access-token-scopes flags are only valid when access-token flag is set")
 | 
			
		||||
 | 
			
		||||
		// using "--access-token-scopes" without "--access-token"
 | 
			
		||||
		reset()
 | 
			
		||||
		err = createUser("u", "--random-password", "--access-token-scopes", "read:issue")
 | 
			
		||||
		assert.Equal(t, 0, unittest.GetCount(t, &user_model.User{}))
 | 
			
		||||
		assert.Equal(t, 0, unittest.GetCount(t, &auth_model.AccessToken{}))
 | 
			
		||||
		assert.ErrorContains(t, err, "access-token-name and access-token-scopes flags are only valid when access-token flag is set")
 | 
			
		||||
 | 
			
		||||
		// empty permission
 | 
			
		||||
		reset()
 | 
			
		||||
		err = createUser("u", "--random-password", "--access-token", "--access-token-scopes", "public-only")
 | 
			
		||||
		assert.Equal(t, 0, unittest.GetCount(t, &user_model.User{}))
 | 
			
		||||
		assert.Equal(t, 0, unittest.GetCount(t, &auth_model.AccessToken{}))
 | 
			
		||||
		assert.ErrorContains(t, err, "access token does not have any permission")
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("UserFields", func(t *testing.T) {
 | 
			
		||||
		reset()
 | 
			
		||||
		assert.NoError(t, createUser("u-FullNameWithSpace", "--random-password", "--fullname", "First O'Middle Last"))
 | 
			
		||||
		unittest.AssertExistsAndLoadBean(t, &user_model.User{
 | 
			
		||||
			Name:      "u-FullNameWithSpace",
 | 
			
		||||
			LowerName: "u-fullnamewithspace",
 | 
			
		||||
			FullName:  "First O'Middle Last",
 | 
			
		||||
			Email:     "u-FullNameWithSpace@gitea.local",
 | 
			
		||||
		})
 | 
			
		||||
 | 
			
		||||
		assert.NoError(t, createUser("u-FullNameEmpty", "--random-password", "--fullname", ""))
 | 
			
		||||
		u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "u-fullnameempty"})
 | 
			
		||||
		assert.Empty(t, u.FullName)
 | 
			
		||||
	})
 | 
			
		||||
	assert.Equal(t, createCheck{IsAdmin: true, MustChangePassword: true}, createUser("u", "--admin --must-change-password"))
 | 
			
		||||
	assert.Equal(t, createCheck{IsAdmin: true, MustChangePassword: true}, createUser("u2", "--admin"))
 | 
			
		||||
	assert.Equal(t, createCheck{IsAdmin: true, MustChangePassword: false}, createUser("u3", "--admin --must-change-password=false"))
 | 
			
		||||
	assert.Equal(t, createCheck{IsAdmin: false, MustChangePassword: true}, createUser("u4", ""))
 | 
			
		||||
	assert.Equal(t, createCheck{IsAdmin: false, MustChangePassword: false}, createUser("u5", "--must-change-password=false"))
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,21 +4,18 @@
 | 
			
		||||
package cmd
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	user_model "code.gitea.io/gitea/models/user"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	"code.gitea.io/gitea/modules/storage"
 | 
			
		||||
	user_service "code.gitea.io/gitea/services/user"
 | 
			
		||||
 | 
			
		||||
	"github.com/urfave/cli/v3"
 | 
			
		||||
	"github.com/urfave/cli/v2"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func microcmdUserDelete() *cli.Command {
 | 
			
		||||
	return &cli.Command{
 | 
			
		||||
var microcmdUserDelete = &cli.Command{
 | 
			
		||||
	Name:  "delete",
 | 
			
		||||
	Usage: "Delete specific user by id, name or email",
 | 
			
		||||
	Flags: []cli.Flag{
 | 
			
		||||
@@ -42,19 +39,19 @@ func microcmdUserDelete() *cli.Command {
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
	Action: runDeleteUser,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func runDeleteUser(ctx context.Context, c *cli.Command) error {
 | 
			
		||||
func runDeleteUser(c *cli.Context) error {
 | 
			
		||||
	if !c.IsSet("id") && !c.IsSet("username") && !c.IsSet("email") {
 | 
			
		||||
		return errors.New("You must provide the id, username or email of a user to delete")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !setting.IsInTesting {
 | 
			
		||||
	ctx, cancel := installSignals()
 | 
			
		||||
	defer cancel()
 | 
			
		||||
 | 
			
		||||
	if err := initDB(ctx); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := storage.Init(); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
@@ -73,11 +70,11 @@ func runDeleteUser(ctx context.Context, c *cli.Command) error {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	if c.IsSet("username") && user.LowerName != strings.ToLower(strings.TrimSpace(c.String("username"))) {
 | 
			
		||||
		return fmt.Errorf("the user %s who has email %s does not match the provided username %s", user.Name, c.String("email"), c.String("username"))
 | 
			
		||||
		return fmt.Errorf("The user %s who has email %s does not match the provided username %s", user.Name, c.String("email"), c.String("username"))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if c.IsSet("id") && user.ID != c.Int64("id") {
 | 
			
		||||
		return fmt.Errorf("the user %s does not match the provided id %d", user.Name, c.Int64("id"))
 | 
			
		||||
		return fmt.Errorf("The user %s does not match the provided id %d", user.Name, c.Int64("id"))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return user_service.DeleteUser(ctx, user, c.Bool("purge"))
 | 
			
		||||
 
 | 
			
		||||
@@ -1,111 +0,0 @@
 | 
			
		||||
// Copyright 2025 The Gitea Authors. All rights reserved.
 | 
			
		||||
// SPDX-License-Identifier: MIT
 | 
			
		||||
 | 
			
		||||
package cmd
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	auth_model "code.gitea.io/gitea/models/auth"
 | 
			
		||||
	"code.gitea.io/gitea/models/db"
 | 
			
		||||
	"code.gitea.io/gitea/models/unittest"
 | 
			
		||||
	user_model "code.gitea.io/gitea/models/user"
 | 
			
		||||
 | 
			
		||||
	"github.com/stretchr/testify/require"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestAdminUserDelete(t *testing.T) {
 | 
			
		||||
	ctx := t.Context()
 | 
			
		||||
	defer func() {
 | 
			
		||||
		require.NoError(t, db.TruncateBeans(t.Context(), &user_model.User{}))
 | 
			
		||||
		require.NoError(t, db.TruncateBeans(t.Context(), &user_model.EmailAddress{}))
 | 
			
		||||
		require.NoError(t, db.TruncateBeans(t.Context(), &auth_model.AccessToken{}))
 | 
			
		||||
	}()
 | 
			
		||||
 | 
			
		||||
	setupTestUser := func(t *testing.T) {
 | 
			
		||||
		unittest.AssertNotExistsBean(t, &user_model.User{LowerName: "testuser"})
 | 
			
		||||
		err := microcmdUserCreate().Run(t.Context(), []string{"create", "--username", "testuser", "--email", "testuser@gitea.local", "--random-password"})
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	t.Run("delete user by id", func(t *testing.T) {
 | 
			
		||||
		setupTestUser(t)
 | 
			
		||||
 | 
			
		||||
		u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
 | 
			
		||||
		err := microcmdUserDelete().Run(ctx, []string{"delete-test", "--id", strconv.FormatInt(u.ID, 10)})
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
		unittest.AssertNotExistsBean(t, &user_model.User{LowerName: "testuser"})
 | 
			
		||||
	})
 | 
			
		||||
	t.Run("delete user by username", func(t *testing.T) {
 | 
			
		||||
		setupTestUser(t)
 | 
			
		||||
 | 
			
		||||
		err := microcmdUserDelete().Run(ctx, []string{"delete-test", "--username", "testuser"})
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
		unittest.AssertNotExistsBean(t, &user_model.User{LowerName: "testuser"})
 | 
			
		||||
	})
 | 
			
		||||
	t.Run("delete user by email", func(t *testing.T) {
 | 
			
		||||
		setupTestUser(t)
 | 
			
		||||
 | 
			
		||||
		err := microcmdUserDelete().Run(ctx, []string{"delete-test", "--email", "testuser@gitea.local"})
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
		unittest.AssertNotExistsBean(t, &user_model.User{LowerName: "testuser"})
 | 
			
		||||
	})
 | 
			
		||||
	t.Run("delete user by all 3 attributes", func(t *testing.T) {
 | 
			
		||||
		setupTestUser(t)
 | 
			
		||||
 | 
			
		||||
		u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
 | 
			
		||||
		err := microcmdUserDelete().Run(ctx, []string{"delete", "--id", strconv.FormatInt(u.ID, 10), "--username", "testuser", "--email", "testuser@gitea.local"})
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
		unittest.AssertNotExistsBean(t, &user_model.User{LowerName: "testuser"})
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestAdminUserDeleteFailure(t *testing.T) {
 | 
			
		||||
	testCases := []struct {
 | 
			
		||||
		name        string
 | 
			
		||||
		args        []string
 | 
			
		||||
		expectedErr string
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			name:        "no user to delete",
 | 
			
		||||
			args:        []string{"delete", "--username", "nonexistentuser"},
 | 
			
		||||
			expectedErr: "user does not exist",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:        "user exists but provided username does not match",
 | 
			
		||||
			args:        []string{"delete", "--email", "testuser@gitea.local", "--username", "wrongusername"},
 | 
			
		||||
			expectedErr: "the user testuser who has email testuser@gitea.local does not match the provided username wrongusername",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:        "user exists but provided id does not match",
 | 
			
		||||
			args:        []string{"delete", "--username", "testuser", "--id", "999"},
 | 
			
		||||
			expectedErr: "the user testuser does not match the provided id 999",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:        "no required flags are provided",
 | 
			
		||||
			args:        []string{"delete"},
 | 
			
		||||
			expectedErr: "You must provide the id, username or email of a user to delete",
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, tc := range testCases {
 | 
			
		||||
		t.Run(tc.name, func(t *testing.T) {
 | 
			
		||||
			ctx := t.Context()
 | 
			
		||||
			if strings.Contains(tc.name, "user exists") {
 | 
			
		||||
				unittest.AssertNotExistsBean(t, &user_model.User{LowerName: "testuser"})
 | 
			
		||||
				err := microcmdUserCreate().Run(t.Context(), []string{"create", "--username", "testuser", "--email", "testuser@gitea.local", "--random-password"})
 | 
			
		||||
				require.NoError(t, err)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			err := microcmdUserDelete().Run(ctx, tc.args)
 | 
			
		||||
			require.Error(t, err)
 | 
			
		||||
			require.Contains(t, err.Error(), tc.expectedErr)
 | 
			
		||||
		})
 | 
			
		||||
 | 
			
		||||
		require.NoError(t, db.TruncateBeans(t.Context(), &user_model.User{}))
 | 
			
		||||
		require.NoError(t, db.TruncateBeans(t.Context(), &user_model.EmailAddress{}))
 | 
			
		||||
		require.NoError(t, db.TruncateBeans(t.Context(), &auth_model.AccessToken{}))
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -4,14 +4,13 @@
 | 
			
		||||
package cmd
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
 | 
			
		||||
	auth_model "code.gitea.io/gitea/models/auth"
 | 
			
		||||
	user_model "code.gitea.io/gitea/models/user"
 | 
			
		||||
 | 
			
		||||
	"github.com/urfave/cli/v3"
 | 
			
		||||
	"github.com/urfave/cli/v2"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var microcmdUserGenerateAccessToken = &cli.Command{
 | 
			
		||||
@@ -35,18 +34,21 @@ var microcmdUserGenerateAccessToken = &cli.Command{
 | 
			
		||||
		},
 | 
			
		||||
		&cli.StringFlag{
 | 
			
		||||
			Name:  "scopes",
 | 
			
		||||
			Value: "all",
 | 
			
		||||
			Usage: `Comma separated list of scopes to apply to access token, examples: "all", "public-only,read:issue", "write:repository,write:user"`,
 | 
			
		||||
			Value: "",
 | 
			
		||||
			Usage: "Comma separated list of scopes to apply to access token",
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
	Action: runGenerateAccessToken,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func runGenerateAccessToken(ctx context.Context, c *cli.Command) error {
 | 
			
		||||
func runGenerateAccessToken(c *cli.Context) error {
 | 
			
		||||
	if !c.IsSet("username") {
 | 
			
		||||
		return errors.New("you must provide a username to generate a token for")
 | 
			
		||||
		return errors.New("You must provide a username to generate a token for")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ctx, cancel := installSignals()
 | 
			
		||||
	defer cancel()
 | 
			
		||||
 | 
			
		||||
	if err := initDB(ctx); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
@@ -75,9 +77,6 @@ func runGenerateAccessToken(ctx context.Context, c *cli.Command) error {
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("invalid access token scope provided: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
	if !accessTokenScope.HasPermissionScope() {
 | 
			
		||||
		return errors.New("access token does not have any permission")
 | 
			
		||||
	}
 | 
			
		||||
	t.Scope = accessTokenScope
 | 
			
		||||
 | 
			
		||||
	// create the token
 | 
			
		||||
 
 | 
			
		||||
@@ -4,14 +4,13 @@
 | 
			
		||||
package cmd
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"os"
 | 
			
		||||
	"text/tabwriter"
 | 
			
		||||
 | 
			
		||||
	user_model "code.gitea.io/gitea/models/user"
 | 
			
		||||
 | 
			
		||||
	"github.com/urfave/cli/v3"
 | 
			
		||||
	"github.com/urfave/cli/v2"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var microcmdUserList = &cli.Command{
 | 
			
		||||
@@ -26,7 +25,10 @@ var microcmdUserList = &cli.Command{
 | 
			
		||||
	},
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func runListUsers(ctx context.Context, c *cli.Command) error {
 | 
			
		||||
func runListUsers(c *cli.Context) error {
 | 
			
		||||
	ctx, cancel := installSignals()
 | 
			
		||||
	defer cancel()
 | 
			
		||||
 | 
			
		||||
	if err := initDB(ctx); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,18 +4,15 @@
 | 
			
		||||
package cmd
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
 | 
			
		||||
	user_model "code.gitea.io/gitea/models/user"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
 | 
			
		||||
	"github.com/urfave/cli/v3"
 | 
			
		||||
	"github.com/urfave/cli/v2"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func microcmdUserMustChangePassword() *cli.Command {
 | 
			
		||||
	return &cli.Command{
 | 
			
		||||
var microcmdUserMustChangePassword = &cli.Command{
 | 
			
		||||
	Name:   "must-change-password",
 | 
			
		||||
	Usage:  "Set the must change password flag for the provided users or all users",
 | 
			
		||||
	Action: runMustChangePassword,
 | 
			
		||||
@@ -35,10 +32,12 @@ func microcmdUserMustChangePassword() *cli.Command {
 | 
			
		||||
			Usage: "Instead of setting the must-change-password flag, unset it",
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func runMustChangePassword(ctx context.Context, c *cli.Command) error {
 | 
			
		||||
func runMustChangePassword(c *cli.Context) error {
 | 
			
		||||
	ctx, cancel := installSignals()
 | 
			
		||||
	defer cancel()
 | 
			
		||||
 | 
			
		||||
	if c.NArg() == 0 && !c.IsSet("all") {
 | 
			
		||||
		return errors.New("either usernames or --all must be provided")
 | 
			
		||||
	}
 | 
			
		||||
@@ -47,18 +46,15 @@ func runMustChangePassword(ctx context.Context, c *cli.Command) error {
 | 
			
		||||
	all := c.Bool("all")
 | 
			
		||||
	exclude := c.StringSlice("exclude")
 | 
			
		||||
 | 
			
		||||
	if !setting.IsInTesting {
 | 
			
		||||
	if err := initDB(ctx); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	n, err := user_model.SetMustChangePassword(ctx, all, mustChangePassword, c.Args().Slice(), exclude)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// codeql[disable-next-line=go/clear-text-logging]
 | 
			
		||||
	fmt.Printf("Updated %d users setting MustChangePassword to %t\n", n, mustChangePassword)
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,78 +0,0 @@
 | 
			
		||||
// Copyright 2025 The Gitea Authors. All rights reserved.
 | 
			
		||||
// SPDX-License-Identifier: MIT
 | 
			
		||||
 | 
			
		||||
package cmd
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/models/db"
 | 
			
		||||
	"code.gitea.io/gitea/models/unittest"
 | 
			
		||||
	user_model "code.gitea.io/gitea/models/user"
 | 
			
		||||
 | 
			
		||||
	"github.com/stretchr/testify/assert"
 | 
			
		||||
	"github.com/stretchr/testify/require"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestMustChangePassword(t *testing.T) {
 | 
			
		||||
	defer func() {
 | 
			
		||||
		require.NoError(t, db.TruncateBeans(t.Context(), &user_model.User{}))
 | 
			
		||||
	}()
 | 
			
		||||
	err := microcmdUserCreate().Run(t.Context(), []string{"create", "--username", "testuser", "--email", "testuser@gitea.local", "--random-password"})
 | 
			
		||||
	require.NoError(t, err)
 | 
			
		||||
	err = microcmdUserCreate().Run(t.Context(), []string{"create", "--username", "testuserexclude", "--email", "testuserexclude@gitea.local", "--random-password"})
 | 
			
		||||
	require.NoError(t, err)
 | 
			
		||||
	// Reset password change flag
 | 
			
		||||
	err = microcmdUserMustChangePassword().Run(t.Context(), []string{"change-test", "--all", "--unset"})
 | 
			
		||||
	require.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
	testUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
 | 
			
		||||
	assert.False(t, testUser.MustChangePassword)
 | 
			
		||||
	testUserExclude := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuserexclude"})
 | 
			
		||||
	assert.False(t, testUserExclude.MustChangePassword)
 | 
			
		||||
 | 
			
		||||
	// Make all users change password
 | 
			
		||||
	err = microcmdUserMustChangePassword().Run(t.Context(), []string{"change-test", "--all"})
 | 
			
		||||
	require.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
	testUser = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
 | 
			
		||||
	assert.True(t, testUser.MustChangePassword)
 | 
			
		||||
	testUserExclude = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuserexclude"})
 | 
			
		||||
	assert.True(t, testUserExclude.MustChangePassword)
 | 
			
		||||
 | 
			
		||||
	// Reset password change flag but exclude all tested users
 | 
			
		||||
	err = microcmdUserMustChangePassword().Run(t.Context(), []string{"change-test", "--all", "--unset", "--exclude", "testuser,testuserexclude"})
 | 
			
		||||
	require.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
	testUser = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
 | 
			
		||||
	assert.True(t, testUser.MustChangePassword)
 | 
			
		||||
	testUserExclude = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuserexclude"})
 | 
			
		||||
	assert.True(t, testUserExclude.MustChangePassword)
 | 
			
		||||
 | 
			
		||||
	// Reset password change flag by listing multiple users
 | 
			
		||||
	err = microcmdUserMustChangePassword().Run(t.Context(), []string{"change-test", "--unset", "testuser", "testuserexclude"})
 | 
			
		||||
	require.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
	testUser = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
 | 
			
		||||
	assert.False(t, testUser.MustChangePassword)
 | 
			
		||||
	testUserExclude = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuserexclude"})
 | 
			
		||||
	assert.False(t, testUserExclude.MustChangePassword)
 | 
			
		||||
 | 
			
		||||
	// Exclude a user from all user
 | 
			
		||||
	err = microcmdUserMustChangePassword().Run(t.Context(), []string{"change-test", "--all", "--exclude", "testuserexclude"})
 | 
			
		||||
	require.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
	testUser = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
 | 
			
		||||
	assert.True(t, testUser.MustChangePassword)
 | 
			
		||||
	testUserExclude = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuserexclude"})
 | 
			
		||||
	assert.False(t, testUserExclude.MustChangePassword)
 | 
			
		||||
 | 
			
		||||
	// Unset a flag for single user
 | 
			
		||||
	err = microcmdUserMustChangePassword().Run(t.Context(), []string{"change-test", "--unset", "testuser"})
 | 
			
		||||
	require.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
	testUser = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
 | 
			
		||||
	assert.False(t, testUser.MustChangePassword)
 | 
			
		||||
	testUserExclude = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuserexclude"})
 | 
			
		||||
	assert.False(t, testUserExclude.MustChangePassword)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										62
									
								
								cmd/cert.go
									
									
									
									
									
								
							
							
						
						
									
										62
									
								
								cmd/cert.go
									
									
									
									
									
								
							@@ -6,7 +6,6 @@
 | 
			
		||||
package cmd
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"crypto/ecdsa"
 | 
			
		||||
	"crypto/elliptic"
 | 
			
		||||
	"crypto/rand"
 | 
			
		||||
@@ -14,7 +13,6 @@ import (
 | 
			
		||||
	"crypto/x509"
 | 
			
		||||
	"crypto/x509/pkix"
 | 
			
		||||
	"encoding/pem"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"log"
 | 
			
		||||
	"math/big"
 | 
			
		||||
	"net"
 | 
			
		||||
@@ -22,12 +20,11 @@ import (
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/urfave/cli/v3"
 | 
			
		||||
	"github.com/urfave/cli/v2"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// cmdCert represents the available cert sub-command.
 | 
			
		||||
func cmdCert() *cli.Command {
 | 
			
		||||
	return &cli.Command{
 | 
			
		||||
// CmdCert represents the available cert sub-command.
 | 
			
		||||
var CmdCert = &cli.Command{
 | 
			
		||||
	Name:  "cert",
 | 
			
		||||
	Usage: "Generate self-signed certificate",
 | 
			
		||||
	Description: `Generate a self-signed X.509 certificate for a TLS server.
 | 
			
		||||
@@ -36,8 +33,8 @@ Outputs to 'cert.pem' and 'key.pem' and will overwrite existing files.`,
 | 
			
		||||
	Flags: []cli.Flag{
 | 
			
		||||
		&cli.StringFlag{
 | 
			
		||||
			Name:  "host",
 | 
			
		||||
			Value: "",
 | 
			
		||||
			Usage: "Comma-separated hostnames and IPs to generate a certificate for",
 | 
			
		||||
				Required: true,
 | 
			
		||||
		},
 | 
			
		||||
		&cli.StringFlag{
 | 
			
		||||
			Name:  "ecdsa-curve",
 | 
			
		||||
@@ -63,18 +60,7 @@ Outputs to 'cert.pem' and 'key.pem' and will overwrite existing files.`,
 | 
			
		||||
			Name:  "ca",
 | 
			
		||||
			Usage: "whether this cert should be its own Certificate Authority",
 | 
			
		||||
		},
 | 
			
		||||
			&cli.StringFlag{
 | 
			
		||||
				Name:  "out",
 | 
			
		||||
				Value: "cert.pem",
 | 
			
		||||
				Usage: "Path to the file where there certificate will be saved",
 | 
			
		||||
	},
 | 
			
		||||
			&cli.StringFlag{
 | 
			
		||||
				Name:  "keyout",
 | 
			
		||||
				Value: "key.pem",
 | 
			
		||||
				Usage: "Path to the file where there certificate key will be saved",
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func publicKey(priv any) any {
 | 
			
		||||
@@ -103,7 +89,11 @@ func pemBlockForKey(priv any) *pem.Block {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func runCert(_ context.Context, c *cli.Command) error {
 | 
			
		||||
func runCert(c *cli.Context) error {
 | 
			
		||||
	if err := argsSet(c, "host"); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var priv any
 | 
			
		||||
	var err error
 | 
			
		||||
	switch c.String("ecdsa-curve") {
 | 
			
		||||
@@ -118,17 +108,17 @@ func runCert(_ context.Context, c *cli.Command) error {
 | 
			
		||||
	case "P521":
 | 
			
		||||
		priv, err = ecdsa.GenerateKey(elliptic.P521(), rand.Reader)
 | 
			
		||||
	default:
 | 
			
		||||
		err = fmt.Errorf("unrecognized elliptic curve: %q", c.String("ecdsa-curve"))
 | 
			
		||||
		log.Fatalf("Unrecognized elliptic curve: %q", c.String("ecdsa-curve"))
 | 
			
		||||
	}
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("failed to generate private key: %w", err)
 | 
			
		||||
		log.Fatalf("Failed to generate private key: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var notBefore time.Time
 | 
			
		||||
	if startDate := c.String("start-date"); startDate != "" {
 | 
			
		||||
		notBefore, err = time.Parse("Jan 2 15:04:05 2006", startDate)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return fmt.Errorf("failed to parse creation date %w", err)
 | 
			
		||||
			log.Fatalf("Failed to parse creation date: %v", err)
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		notBefore = time.Now()
 | 
			
		||||
@@ -139,7 +129,7 @@ func runCert(_ context.Context, c *cli.Command) error {
 | 
			
		||||
	serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
 | 
			
		||||
	serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("failed to generate serial number: %w", err)
 | 
			
		||||
		log.Fatalf("Failed to generate serial number: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	template := x509.Certificate{
 | 
			
		||||
@@ -156,8 +146,8 @@ func runCert(_ context.Context, c *cli.Command) error {
 | 
			
		||||
		BasicConstraintsValid: true,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	hosts := strings.SplitSeq(c.String("host"), ",")
 | 
			
		||||
	for h := range hosts {
 | 
			
		||||
	hosts := strings.Split(c.String("host"), ",")
 | 
			
		||||
	for _, h := range hosts {
 | 
			
		||||
		if ip := net.ParseIP(h); ip != nil {
 | 
			
		||||
			template.IPAddresses = append(template.IPAddresses, ip)
 | 
			
		||||
		} else {
 | 
			
		||||
@@ -172,35 +162,35 @@ func runCert(_ context.Context, c *cli.Command) error {
 | 
			
		||||
 | 
			
		||||
	derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, publicKey(priv), priv)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("failed to create certificate: %w", err)
 | 
			
		||||
		log.Fatalf("Failed to create certificate: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	certOut, err := os.Create(c.String("out"))
 | 
			
		||||
	certOut, err := os.Create("cert.pem")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("failed to open %s for writing: %w", c.String("keyout"), err)
 | 
			
		||||
		log.Fatalf("Failed to open cert.pem for writing: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
	err = pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("failed to encode certificate: %w", err)
 | 
			
		||||
		log.Fatalf("Failed to encode certificate: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
	err = certOut.Close()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("failed to write cert: %w", err)
 | 
			
		||||
		log.Fatalf("Failed to write cert: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
	fmt.Fprintf(c.Writer, "Written cert to %s\n", c.String("out"))
 | 
			
		||||
	log.Println("Written cert.pem")
 | 
			
		||||
 | 
			
		||||
	keyOut, err := os.OpenFile(c.String("keyout"), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600)
 | 
			
		||||
	keyOut, err := os.OpenFile("key.pem", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("failed to open %s for writing: %w", c.String("keyout"), err)
 | 
			
		||||
		log.Fatalf("Failed to open key.pem for writing: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
	err = pem.Encode(keyOut, pemBlockForKey(priv))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("failed to encode key: %w", err)
 | 
			
		||||
		log.Fatalf("Failed to encode key: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
	err = keyOut.Close()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("failed to write key: %w", err)
 | 
			
		||||
		log.Fatalf("Failed to write key: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
	fmt.Fprintf(c.Writer, "Written key to %s\n", c.String("keyout"))
 | 
			
		||||
	log.Println("Written key.pem")
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										123
									
								
								cmd/cert_test.go
									
									
									
									
									
								
							
							
						
						
									
										123
									
								
								cmd/cert_test.go
									
									
									
									
									
								
							@@ -1,123 +0,0 @@
 | 
			
		||||
// Copyright 2025 The Gitea Authors. All rights reserved.
 | 
			
		||||
// SPDX-License-Identifier: MIT
 | 
			
		||||
 | 
			
		||||
package cmd
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"github.com/stretchr/testify/assert"
 | 
			
		||||
	"github.com/stretchr/testify/require"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestCertCommand(t *testing.T) {
 | 
			
		||||
	cases := []struct {
 | 
			
		||||
		name string
 | 
			
		||||
		args []string
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			name: "RSA cert generation",
 | 
			
		||||
			args: []string{
 | 
			
		||||
				"cert-test",
 | 
			
		||||
				"--host", "localhost",
 | 
			
		||||
				"--rsa-bits", "2048",
 | 
			
		||||
				"--duration", "1h",
 | 
			
		||||
				"--start-date", "Jan 1 00:00:00 2024",
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "ECDSA cert generation",
 | 
			
		||||
			args: []string{
 | 
			
		||||
				"cert-test",
 | 
			
		||||
				"--host", "localhost",
 | 
			
		||||
				"--ecdsa-curve", "P256",
 | 
			
		||||
				"--duration", "1h",
 | 
			
		||||
				"--start-date", "Jan 1 00:00:00 2024",
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "mixed host, certificate authority",
 | 
			
		||||
			args: []string{
 | 
			
		||||
				"cert-test",
 | 
			
		||||
				"--host", "localhost,127.0.0.1",
 | 
			
		||||
				"--duration", "1h",
 | 
			
		||||
				"--start-date", "Jan 1 00:00:00 2024",
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, c := range cases {
 | 
			
		||||
		t.Run(c.name, func(t *testing.T) {
 | 
			
		||||
			app := cmdCert()
 | 
			
		||||
			tempDir := t.TempDir()
 | 
			
		||||
 | 
			
		||||
			certFile := filepath.Join(tempDir, "cert.pem")
 | 
			
		||||
			keyFile := filepath.Join(tempDir, "key.pem")
 | 
			
		||||
 | 
			
		||||
			err := app.Run(t.Context(), append(c.args, "--out", certFile, "--keyout", keyFile))
 | 
			
		||||
			require.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
			assert.FileExists(t, certFile)
 | 
			
		||||
			assert.FileExists(t, keyFile)
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestCertCommandFailures(t *testing.T) {
 | 
			
		||||
	cases := []struct {
 | 
			
		||||
		name   string
 | 
			
		||||
		args   []string
 | 
			
		||||
		errMsg string
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			name: "Start Date Parsing failure",
 | 
			
		||||
			args: []string{
 | 
			
		||||
				"cert-test",
 | 
			
		||||
				"--host", "localhost",
 | 
			
		||||
				"--start-date", "invalid-date",
 | 
			
		||||
			},
 | 
			
		||||
			errMsg: "parsing time",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "Unknown curve",
 | 
			
		||||
			args: []string{
 | 
			
		||||
				"cert-test",
 | 
			
		||||
				"--host", "localhost",
 | 
			
		||||
				"--ecdsa-curve", "invalid-curve",
 | 
			
		||||
			},
 | 
			
		||||
			errMsg: "unrecognized elliptic curve",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "Key generation failure",
 | 
			
		||||
			args: []string{
 | 
			
		||||
				"cert-test",
 | 
			
		||||
				"--host", "localhost",
 | 
			
		||||
				"--rsa-bits", "invalid-bits",
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "Missing parameters",
 | 
			
		||||
			args: []string{
 | 
			
		||||
				"cert-test",
 | 
			
		||||
			},
 | 
			
		||||
			errMsg: `"host" not set`,
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
	for _, c := range cases {
 | 
			
		||||
		t.Run(c.name, func(t *testing.T) {
 | 
			
		||||
			app := cmdCert()
 | 
			
		||||
			tempDir := t.TempDir()
 | 
			
		||||
 | 
			
		||||
			certFile := filepath.Join(tempDir, "cert.pem")
 | 
			
		||||
			keyFile := filepath.Join(tempDir, "key.pem")
 | 
			
		||||
			err := app.Run(t.Context(), append(c.args, "--out", certFile, "--keyout", keyFile))
 | 
			
		||||
			require.Error(t, err)
 | 
			
		||||
			if c.errMsg != "" {
 | 
			
		||||
				assert.ErrorContains(t, err, c.errMsg)
 | 
			
		||||
			}
 | 
			
		||||
			assert.NoFileExists(t, certFile)
 | 
			
		||||
			assert.NoFileExists(t, keyFile)
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										31
									
								
								cmd/cmd.go
									
									
									
									
									
								
							
							
						
						
									
										31
									
								
								cmd/cmd.go
									
									
									
									
									
								
							@@ -18,19 +18,20 @@ import (
 | 
			
		||||
	"code.gitea.io/gitea/models/db"
 | 
			
		||||
	"code.gitea.io/gitea/modules/log"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
 | 
			
		||||
	"github.com/urfave/cli/v3"
 | 
			
		||||
	"github.com/urfave/cli/v2"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// argsSet checks that all the required arguments are set. args is a list of
 | 
			
		||||
// arguments that must be set in the passed Context.
 | 
			
		||||
func argsSet(c *cli.Command, args ...string) error {
 | 
			
		||||
func argsSet(c *cli.Context, args ...string) error {
 | 
			
		||||
	for _, a := range args {
 | 
			
		||||
		if !c.IsSet(a) {
 | 
			
		||||
			return errors.New(a + " is not set")
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if c.Value(a) == nil {
 | 
			
		||||
		if util.IsEmptyString(c.String(a)) {
 | 
			
		||||
			return errors.New(a + " is required")
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
@@ -108,7 +109,7 @@ func setupConsoleLogger(level log.Level, colorize bool, out io.Writer) {
 | 
			
		||||
	log.GetManager().GetLogger(log.DEFAULT).ReplaceAllWriters(writer)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func globalBool(c *cli.Command, name string) bool {
 | 
			
		||||
func globalBool(c *cli.Context, name string) bool {
 | 
			
		||||
	for _, ctx := range c.Lineage() {
 | 
			
		||||
		if ctx.Bool(name) {
 | 
			
		||||
			return true
 | 
			
		||||
@@ -119,14 +120,8 @@ func globalBool(c *cli.Command, name string) bool {
 | 
			
		||||
 | 
			
		||||
// PrepareConsoleLoggerLevel by default, use INFO level for console logger, but some sub-commands (for git/ssh protocol) shouldn't output any log to stdout.
 | 
			
		||||
// Any log appears in git stdout pipe will break the git protocol, eg: client can't push and hangs forever.
 | 
			
		||||
func PrepareConsoleLoggerLevel(defaultLevel log.Level) func(context.Context, *cli.Command) (context.Context, error) {
 | 
			
		||||
	return func(ctx context.Context, c *cli.Command) (context.Context, error) {
 | 
			
		||||
		if setting.InstallLock {
 | 
			
		||||
			// During config loading, there might also be logs (for example: deprecation warnings).
 | 
			
		||||
			// It must make sure that console logger is set up before config is loaded.
 | 
			
		||||
			log.Error("Config is loaded before console logger is setup, it will cause bugs. Please fix it.")
 | 
			
		||||
			return nil, errors.New("console logger must be setup before config is loaded")
 | 
			
		||||
		}
 | 
			
		||||
func PrepareConsoleLoggerLevel(defaultLevel log.Level) func(*cli.Context) error {
 | 
			
		||||
	return func(c *cli.Context) error {
 | 
			
		||||
		level := defaultLevel
 | 
			
		||||
		if globalBool(c, "quiet") {
 | 
			
		||||
			level = log.FATAL
 | 
			
		||||
@@ -135,16 +130,6 @@ func PrepareConsoleLoggerLevel(defaultLevel log.Level) func(context.Context, *cl
 | 
			
		||||
			level = log.TRACE
 | 
			
		||||
		}
 | 
			
		||||
		log.SetConsoleLogger(log.DEFAULT, "console-default", level)
 | 
			
		||||
		return ctx, nil
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func isValidDefaultSubCommand(cmd *cli.Command) (string, bool) {
 | 
			
		||||
	// Dirty patch for urfave/cli's strange design.
 | 
			
		||||
	// "./gitea bad-cmd" should not start the web server.
 | 
			
		||||
	rootArgs := cmd.Root().Args().Slice()
 | 
			
		||||
	if len(rootArgs) != 0 && rootArgs[0] != cmd.Name {
 | 
			
		||||
		return rootArgs[0], false
 | 
			
		||||
	}
 | 
			
		||||
	return "", true
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,38 +0,0 @@
 | 
			
		||||
// Copyright 2025 The Gitea Authors. All rights reserved.
 | 
			
		||||
// SPDX-License-Identifier: MIT
 | 
			
		||||
 | 
			
		||||
package cmd
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"github.com/stretchr/testify/assert"
 | 
			
		||||
	"github.com/urfave/cli/v3"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestDefaultCommand(t *testing.T) {
 | 
			
		||||
	test := func(t *testing.T, args []string, expectedRetName string, expectedRetValid bool) {
 | 
			
		||||
		called := false
 | 
			
		||||
		cmd := &cli.Command{
 | 
			
		||||
			DefaultCommand: "test",
 | 
			
		||||
			Commands: []*cli.Command{
 | 
			
		||||
				{
 | 
			
		||||
					Name: "test",
 | 
			
		||||
					Action: func(ctx context.Context, command *cli.Command) error {
 | 
			
		||||
						retName, retValid := isValidDefaultSubCommand(command)
 | 
			
		||||
						assert.Equal(t, expectedRetName, retName)
 | 
			
		||||
						assert.Equal(t, expectedRetValid, retValid)
 | 
			
		||||
						called = true
 | 
			
		||||
						return nil
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
		}
 | 
			
		||||
		assert.NoError(t, cmd.Run(t.Context(), args))
 | 
			
		||||
		assert.True(t, called)
 | 
			
		||||
	}
 | 
			
		||||
	test(t, []string{"./gitea"}, "", true)
 | 
			
		||||
	test(t, []string{"./gitea", "test"}, "", true)
 | 
			
		||||
	test(t, []string{"./gitea", "other"}, "other", false)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										156
									
								
								cmd/config.go
									
									
									
									
									
								
							
							
						
						
									
										156
									
								
								cmd/config.go
									
									
									
									
									
								
							@@ -1,156 +0,0 @@
 | 
			
		||||
// Copyright 2025 The Gitea Authors. All rights reserved.
 | 
			
		||||
// SPDX-License-Identifier: MIT
 | 
			
		||||
 | 
			
		||||
package cmd
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"os"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
 | 
			
		||||
	"github.com/urfave/cli/v3"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func cmdConfig() *cli.Command {
 | 
			
		||||
	subcmdConfigEditIni := &cli.Command{
 | 
			
		||||
		Name:  "edit-ini",
 | 
			
		||||
		Usage: "Load an existing INI file, apply environment variables, keep specified keys, and output to a new INI file.",
 | 
			
		||||
		Description: `
 | 
			
		||||
Help users to edit the Gitea configuration INI file.
 | 
			
		||||
 | 
			
		||||
# Keep Specified Keys
 | 
			
		||||
 | 
			
		||||
If you need to re-create the configuration file with only a subset of keys,
 | 
			
		||||
you can provide an INI template file for the kept keys and use the "--config-keep-keys" flag.
 | 
			
		||||
For example, if a helm chart needs to reset the settings and only keep SECRET_KEY,
 | 
			
		||||
it can use a template file (only keys take effect, values are ignored):
 | 
			
		||||
 | 
			
		||||
  [security]
 | 
			
		||||
  SECRET_KEY=
 | 
			
		||||
 | 
			
		||||
$ ./gitea config edit-ini --config app-old.ini --config-keep-keys app-keys.ini --out app-new.ini
 | 
			
		||||
 | 
			
		||||
# Map Environment Variables to INI Configuration
 | 
			
		||||
 | 
			
		||||
Environment variables of the form "GITEA__section_name__KEY_NAME"
 | 
			
		||||
will be mapped to the ini section "[section_name]" and the key
 | 
			
		||||
"KEY_NAME" with the value as provided.
 | 
			
		||||
 | 
			
		||||
Environment variables of the form "GITEA__section_name__KEY_NAME__FILE"
 | 
			
		||||
will be mapped to the ini section "[section_name]" and the key
 | 
			
		||||
"KEY_NAME" with the value loaded from the specified file.
 | 
			
		||||
 | 
			
		||||
Environment variable keys can only contain characters "0-9A-Z_",
 | 
			
		||||
if a section or key name contains dot ".", it needs to be escaped as _0x2E_.
 | 
			
		||||
For example, to apply this config:
 | 
			
		||||
 | 
			
		||||
	[git.config]
 | 
			
		||||
	foo.bar=val
 | 
			
		||||
 | 
			
		||||
$ export GITEA__git_0x2E_config__foo_0x2E_bar=val
 | 
			
		||||
 | 
			
		||||
# Put All Together
 | 
			
		||||
 | 
			
		||||
$ ./gitea config edit-ini --config app.ini --config-keep-keys app-keys.ini --apply-env {--in-place|--out app-new.ini}
 | 
			
		||||
`,
 | 
			
		||||
		Flags: []cli.Flag{
 | 
			
		||||
			// "--config" flag is provided by global flags, and this flag is also used by "environment-to-ini" script wrapper
 | 
			
		||||
			// "--in-place" is also used by "environment-to-ini" script wrapper for its old behavior: always overwrite the existing config file
 | 
			
		||||
			&cli.BoolFlag{
 | 
			
		||||
				Name:  "in-place",
 | 
			
		||||
				Usage: "Output to the same config file as input. This flag will be ignored if --out is set.",
 | 
			
		||||
			},
 | 
			
		||||
			&cli.StringFlag{
 | 
			
		||||
				Name:  "config-keep-keys",
 | 
			
		||||
				Usage: "An INI template file containing keys for keeping. Only the keys defined in the INI template will be kept from old config. If not set, all keys will be kept.",
 | 
			
		||||
			},
 | 
			
		||||
			&cli.BoolFlag{
 | 
			
		||||
				Name:  "apply-env",
 | 
			
		||||
				Usage: "Apply all GITEA__* variables from the environment to the config.",
 | 
			
		||||
			},
 | 
			
		||||
			&cli.StringFlag{
 | 
			
		||||
				Name:  "out",
 | 
			
		||||
				Usage: "Destination config file to write to.",
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		Action: runConfigEditIni,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return &cli.Command{
 | 
			
		||||
		Name:  "config",
 | 
			
		||||
		Usage: "Manage Gitea configuration",
 | 
			
		||||
		Commands: []*cli.Command{
 | 
			
		||||
			subcmdConfigEditIni,
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func runConfigEditIni(_ context.Context, c *cli.Command) error {
 | 
			
		||||
	// the config system may change the environment variables, so get a copy first, to be used later
 | 
			
		||||
	env := append([]string{}, os.Environ()...)
 | 
			
		||||
 | 
			
		||||
	// don't use the guessed setting.CustomConf, instead, require the user to provide --config explicitly
 | 
			
		||||
	if !c.IsSet("config") {
 | 
			
		||||
		return errors.New("flag is required but not set: --config")
 | 
			
		||||
	}
 | 
			
		||||
	configFileIn := c.String("config")
 | 
			
		||||
 | 
			
		||||
	cfgIn, err := setting.NewConfigProviderFromFile(configFileIn)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("failed to load config file %q: %v", configFileIn, err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// determine output config file: use "--out" flag or use "--in-place" flag to overwrite input file
 | 
			
		||||
	inPlace := c.Bool("in-place")
 | 
			
		||||
	configFileOut := c.String("out")
 | 
			
		||||
	if configFileOut == "" {
 | 
			
		||||
		if !inPlace {
 | 
			
		||||
			return errors.New("either --in-place or --out must be specified")
 | 
			
		||||
		}
 | 
			
		||||
		configFileOut = configFileIn // in-place edit
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	needWriteOut := configFileOut != configFileIn
 | 
			
		||||
 | 
			
		||||
	cfgOut := cfgIn
 | 
			
		||||
	configKeepKeys := c.String("config-keep-keys")
 | 
			
		||||
	if configKeepKeys != "" {
 | 
			
		||||
		needWriteOut = true
 | 
			
		||||
		cfgOut, err = setting.NewConfigProviderFromFile(configKeepKeys)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return fmt.Errorf("failed to load config-keep-keys template file %q: %v", configKeepKeys, err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		for _, secOut := range cfgOut.Sections() {
 | 
			
		||||
			for _, keyOut := range secOut.Keys() {
 | 
			
		||||
				secIn := cfgIn.Section(secOut.Name())
 | 
			
		||||
				keyIn := setting.ConfigSectionKey(secIn, keyOut.Name())
 | 
			
		||||
				if keyIn != nil {
 | 
			
		||||
					keyOut.SetValue(keyIn.String())
 | 
			
		||||
				} else {
 | 
			
		||||
					secOut.DeleteKey(keyOut.Name())
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			if len(secOut.Keys()) == 0 {
 | 
			
		||||
				cfgOut.DeleteSection(secOut.Name())
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if c.Bool("apply-env") {
 | 
			
		||||
		if setting.EnvironmentToConfig(cfgOut, env) {
 | 
			
		||||
			needWriteOut = true
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if needWriteOut {
 | 
			
		||||
		err = cfgOut.SaveTo(configFileOut)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
@@ -1,85 +0,0 @@
 | 
			
		||||
// Copyright 2025 The Gitea Authors. All rights reserved.
 | 
			
		||||
// SPDX-License-Identifier: MIT
 | 
			
		||||
 | 
			
		||||
package cmd
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"os"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"github.com/stretchr/testify/require"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestConfigEdit(t *testing.T) {
 | 
			
		||||
	tmpDir := t.TempDir()
 | 
			
		||||
	configOld := tmpDir + "/app-old.ini"
 | 
			
		||||
	configTemplate := tmpDir + "/app-template.ini"
 | 
			
		||||
	_ = os.WriteFile(configOld, []byte(`
 | 
			
		||||
[sec]
 | 
			
		||||
k1=v1
 | 
			
		||||
k2=v2
 | 
			
		||||
`), os.ModePerm)
 | 
			
		||||
 | 
			
		||||
	_ = os.WriteFile(configTemplate, []byte(`
 | 
			
		||||
[sec]
 | 
			
		||||
k1=in-template
 | 
			
		||||
 | 
			
		||||
[sec2]
 | 
			
		||||
k3=v3
 | 
			
		||||
`), os.ModePerm)
 | 
			
		||||
 | 
			
		||||
	t.Setenv("GITEA__EnV__KeY", "val")
 | 
			
		||||
 | 
			
		||||
	t.Run("OutputToNewWithEnv", func(t *testing.T) {
 | 
			
		||||
		configNew := tmpDir + "/app-new.ini"
 | 
			
		||||
		err := NewMainApp(AppVersion{}).Run(t.Context(), []string{
 | 
			
		||||
			"./gitea", "--config", configOld,
 | 
			
		||||
			"config", "edit-ini",
 | 
			
		||||
			"--apply-env",
 | 
			
		||||
			"--config-keep-keys", configTemplate,
 | 
			
		||||
			"--out", configNew,
 | 
			
		||||
		})
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
		// "k1" old value is kept because its key is in the template
 | 
			
		||||
		// "k2" is removed because it isn't in the template
 | 
			
		||||
		// "k3" isn't in new config because it isn't in the old config
 | 
			
		||||
		// [env] is applied from environment variable
 | 
			
		||||
		data, _ := os.ReadFile(configNew)
 | 
			
		||||
		require.Equal(t, `[sec]
 | 
			
		||||
k1 = v1
 | 
			
		||||
 | 
			
		||||
[env]
 | 
			
		||||
KeY = val
 | 
			
		||||
`, string(data))
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("OutputToExisting(environment-to-ini)", func(t *testing.T) {
 | 
			
		||||
		// the legacy "environment-to-ini" (now a wrapper script) behavior:
 | 
			
		||||
		// if no "--out", then "--in-place" must be used to overwrite the existing "--config" file
 | 
			
		||||
		err := NewMainApp(AppVersion{}).Run(t.Context(), []string{
 | 
			
		||||
			"./gitea", "config", "edit-ini",
 | 
			
		||||
			"--apply-env",
 | 
			
		||||
			"--config", configOld,
 | 
			
		||||
		})
 | 
			
		||||
		require.ErrorContains(t, err, "either --in-place or --out must be specified")
 | 
			
		||||
 | 
			
		||||
		// simulate the "environment-to-ini" behavior with "--in-place"
 | 
			
		||||
		err = NewMainApp(AppVersion{}).Run(t.Context(), []string{
 | 
			
		||||
			"./gitea", "config", "edit-ini",
 | 
			
		||||
			"--in-place",
 | 
			
		||||
			"--apply-env",
 | 
			
		||||
			"--config", configOld,
 | 
			
		||||
		})
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
		data, _ := os.ReadFile(configOld)
 | 
			
		||||
		require.Equal(t, `[sec]
 | 
			
		||||
k1 = v1
 | 
			
		||||
k2 = v2
 | 
			
		||||
 | 
			
		||||
[env]
 | 
			
		||||
KeY = val
 | 
			
		||||
`, string(data))
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										18
									
								
								cmd/docs.go
									
									
									
									
									
								
							
							
						
						
									
										18
									
								
								cmd/docs.go
									
									
									
									
									
								
							@@ -4,13 +4,11 @@
 | 
			
		||||
package cmd
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"os"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	cli_docs "github.com/urfave/cli-docs/v3"
 | 
			
		||||
	"github.com/urfave/cli/v3"
 | 
			
		||||
	"github.com/urfave/cli/v2"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// CmdDocs represents the available docs sub-command.
 | 
			
		||||
@@ -32,16 +30,16 @@ var CmdDocs = &cli.Command{
 | 
			
		||||
	},
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func runDocs(_ context.Context, cmd *cli.Command) error {
 | 
			
		||||
	docs, err := cli_docs.ToMarkdown(cmd.Root())
 | 
			
		||||
	if cmd.Bool("man") {
 | 
			
		||||
		docs, err = cli_docs.ToMan(cmd.Root())
 | 
			
		||||
func runDocs(ctx *cli.Context) error {
 | 
			
		||||
	docs, err := ctx.App.ToMarkdown()
 | 
			
		||||
	if ctx.Bool("man") {
 | 
			
		||||
		docs, err = ctx.App.ToMan()
 | 
			
		||||
	}
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !cmd.Bool("man") {
 | 
			
		||||
	if !ctx.Bool("man") {
 | 
			
		||||
		// Clean up markdown. The following bug was fixed in v2, but is present in v1.
 | 
			
		||||
		// It affects markdown output (even though the issue is referring to man pages)
 | 
			
		||||
		// https://github.com/urfave/cli/issues/1040
 | 
			
		||||
@@ -53,8 +51,8 @@ func runDocs(_ context.Context, cmd *cli.Command) error {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	out := os.Stdout
 | 
			
		||||
	if cmd.String("output") != "" {
 | 
			
		||||
		fi, err := os.Create(cmd.String("output"))
 | 
			
		||||
	if ctx.String("output") != "" {
 | 
			
		||||
		fi, err := os.Create(ctx.String("output"))
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,6 @@
 | 
			
		||||
package cmd
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	golog "log"
 | 
			
		||||
	"os"
 | 
			
		||||
@@ -20,7 +19,7 @@ import (
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	"code.gitea.io/gitea/services/doctor"
 | 
			
		||||
 | 
			
		||||
	"github.com/urfave/cli/v3"
 | 
			
		||||
	"github.com/urfave/cli/v2"
 | 
			
		||||
	"xorm.io/xorm"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@@ -30,7 +29,7 @@ var CmdDoctor = &cli.Command{
 | 
			
		||||
	Usage:       "Diagnose and optionally fix problems, convert or re-create database tables",
 | 
			
		||||
	Description: "A command to diagnose problems with the current Gitea instance according to the given configuration. Some problems can optionally be fixed by modifying the database or data storage.",
 | 
			
		||||
 | 
			
		||||
	Commands: []*cli.Command{
 | 
			
		||||
	Subcommands: []*cli.Command{
 | 
			
		||||
		cmdDoctorCheck,
 | 
			
		||||
		cmdRecreateTable,
 | 
			
		||||
		cmdDoctorConvert,
 | 
			
		||||
@@ -93,13 +92,16 @@ You should back-up your database before doing this and ensure that your database
 | 
			
		||||
	Action: runRecreateTable,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func runRecreateTable(ctx context.Context, cmd *cli.Command) error {
 | 
			
		||||
func runRecreateTable(ctx *cli.Context) error {
 | 
			
		||||
	stdCtx, cancel := installSignals()
 | 
			
		||||
	defer cancel()
 | 
			
		||||
 | 
			
		||||
	// Redirect the default golog to here
 | 
			
		||||
	golog.SetFlags(0)
 | 
			
		||||
	golog.SetPrefix("")
 | 
			
		||||
	golog.SetOutput(log.LoggerToWriter(log.GetLogger(log.DEFAULT).Info))
 | 
			
		||||
 | 
			
		||||
	debug := cmd.Bool("debug")
 | 
			
		||||
	debug := ctx.Bool("debug")
 | 
			
		||||
	setting.MustInstalled()
 | 
			
		||||
	setting.LoadDBSetting()
 | 
			
		||||
 | 
			
		||||
@@ -110,15 +112,15 @@ func runRecreateTable(ctx context.Context, cmd *cli.Command) error {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	setting.Database.LogSQL = debug
 | 
			
		||||
	if err := db.InitEngine(ctx); err != nil {
 | 
			
		||||
	if err := db.InitEngine(stdCtx); err != nil {
 | 
			
		||||
		fmt.Println(err)
 | 
			
		||||
		fmt.Println("Check if you are using the right config file. You can use a --config directive to specify one.")
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	args := cmd.Args()
 | 
			
		||||
	names := make([]string, 0, cmd.NArg())
 | 
			
		||||
	for i := 0; i < cmd.NArg(); i++ {
 | 
			
		||||
	args := ctx.Args()
 | 
			
		||||
	names := make([]string, 0, ctx.NArg())
 | 
			
		||||
	for i := 0; i < ctx.NArg(); i++ {
 | 
			
		||||
		names = append(names, args.Get(i))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -128,25 +130,24 @@ func runRecreateTable(ctx context.Context, cmd *cli.Command) error {
 | 
			
		||||
	}
 | 
			
		||||
	recreateTables := migrate_base.RecreateTables(beans...)
 | 
			
		||||
 | 
			
		||||
	return db.InitEngineWithMigration(context.Background(), func(ctx context.Context, x *xorm.Engine) error {
 | 
			
		||||
		if err := migrations.EnsureUpToDate(ctx, x); err != nil {
 | 
			
		||||
	return db.InitEngineWithMigration(stdCtx, func(x *xorm.Engine) error {
 | 
			
		||||
		if err := migrations.EnsureUpToDate(x); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		return recreateTables(x)
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func setupDoctorDefaultLogger(cmd *cli.Command, colorize bool) {
 | 
			
		||||
func setupDoctorDefaultLogger(ctx *cli.Context, colorize bool) {
 | 
			
		||||
	// Silence the default loggers
 | 
			
		||||
	setupConsoleLogger(log.FATAL, log.CanColorStderr, os.Stderr)
 | 
			
		||||
 | 
			
		||||
	logFile := cmd.String("log-file")
 | 
			
		||||
	switch logFile {
 | 
			
		||||
	case "":
 | 
			
		||||
	logFile := ctx.String("log-file")
 | 
			
		||||
	if logFile == "" {
 | 
			
		||||
		return // if no doctor log-file is set, do not show any log from default logger
 | 
			
		||||
	case "-":
 | 
			
		||||
	} else if logFile == "-" {
 | 
			
		||||
		setupConsoleLogger(log.TRACE, colorize, os.Stdout)
 | 
			
		||||
	default:
 | 
			
		||||
	} else {
 | 
			
		||||
		logFile, _ = filepath.Abs(logFile)
 | 
			
		||||
		writeMode := log.WriterMode{Level: log.TRACE, WriterOption: log.WriterFileOption{FileName: logFile}}
 | 
			
		||||
		writer, err := log.NewEventWriter("console-to-file", "file", writeMode)
 | 
			
		||||
@@ -158,20 +159,23 @@ func setupDoctorDefaultLogger(cmd *cli.Command, colorize bool) {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func runDoctorCheck(ctx context.Context, cmd *cli.Command) error {
 | 
			
		||||
func runDoctorCheck(ctx *cli.Context) error {
 | 
			
		||||
	stdCtx, cancel := installSignals()
 | 
			
		||||
	defer cancel()
 | 
			
		||||
 | 
			
		||||
	colorize := log.CanColorStdout
 | 
			
		||||
	if cmd.IsSet("color") {
 | 
			
		||||
		colorize = cmd.Bool("color")
 | 
			
		||||
	if ctx.IsSet("color") {
 | 
			
		||||
		colorize = ctx.Bool("color")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	setupDoctorDefaultLogger(cmd, colorize)
 | 
			
		||||
	setupDoctorDefaultLogger(ctx, colorize)
 | 
			
		||||
 | 
			
		||||
	// Finally redirect the default golang's log to here
 | 
			
		||||
	golog.SetFlags(0)
 | 
			
		||||
	golog.SetPrefix("")
 | 
			
		||||
	golog.SetOutput(log.LoggerToWriter(log.GetLogger(log.DEFAULT).Info))
 | 
			
		||||
 | 
			
		||||
	if cmd.IsSet("list") {
 | 
			
		||||
	if ctx.IsSet("list") {
 | 
			
		||||
		w := tabwriter.NewWriter(os.Stdout, 0, 8, 1, '\t', 0)
 | 
			
		||||
		_, _ = w.Write([]byte("Default\tName\tTitle\n"))
 | 
			
		||||
		doctor.SortChecks(doctor.Checks)
 | 
			
		||||
@@ -189,12 +193,12 @@ func runDoctorCheck(ctx context.Context, cmd *cli.Command) error {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var checks []*doctor.Check
 | 
			
		||||
	if cmd.Bool("all") {
 | 
			
		||||
	if ctx.Bool("all") {
 | 
			
		||||
		checks = make([]*doctor.Check, len(doctor.Checks))
 | 
			
		||||
		copy(checks, doctor.Checks)
 | 
			
		||||
	} else if cmd.IsSet("run") {
 | 
			
		||||
		addDefault := cmd.Bool("default")
 | 
			
		||||
		runNamesSet := container.SetOf(cmd.StringSlice("run")...)
 | 
			
		||||
	} else if ctx.IsSet("run") {
 | 
			
		||||
		addDefault := ctx.Bool("default")
 | 
			
		||||
		runNamesSet := container.SetOf(ctx.StringSlice("run")...)
 | 
			
		||||
		for _, check := range doctor.Checks {
 | 
			
		||||
			if (addDefault && check.IsDefault) || runNamesSet.Contains(check.Name) {
 | 
			
		||||
				checks = append(checks, check)
 | 
			
		||||
@@ -211,5 +215,5 @@ func runDoctorCheck(ctx context.Context, cmd *cli.Command) error {
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return doctor.RunChecks(ctx, colorize, cmd.Bool("fix"), checks)
 | 
			
		||||
	return doctor.RunChecks(stdCtx, colorize, ctx.Bool("fix"), checks)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,14 +4,13 @@
 | 
			
		||||
package cmd
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"fmt"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/models/db"
 | 
			
		||||
	"code.gitea.io/gitea/modules/log"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
 | 
			
		||||
	"github.com/urfave/cli/v3"
 | 
			
		||||
	"github.com/urfave/cli/v2"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// cmdDoctorConvert represents the available convert sub-command.
 | 
			
		||||
@@ -22,8 +21,11 @@ var cmdDoctorConvert = &cli.Command{
 | 
			
		||||
	Action:      runDoctorConvert,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func runDoctorConvert(ctx context.Context, cmd *cli.Command) error {
 | 
			
		||||
	if err := initDB(ctx); err != nil {
 | 
			
		||||
func runDoctorConvert(ctx *cli.Context) error {
 | 
			
		||||
	stdCtx, cancel := installSignals()
 | 
			
		||||
	defer cancel()
 | 
			
		||||
 | 
			
		||||
	if err := initDB(stdCtx); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,7 @@ import (
 | 
			
		||||
	"code.gitea.io/gitea/services/doctor"
 | 
			
		||||
 | 
			
		||||
	"github.com/stretchr/testify/assert"
 | 
			
		||||
	"github.com/urfave/cli/v3"
 | 
			
		||||
	"github.com/urfave/cli/v2"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestDoctorRun(t *testing.T) {
 | 
			
		||||
@@ -22,13 +22,12 @@ func TestDoctorRun(t *testing.T) {
 | 
			
		||||
 | 
			
		||||
		SkipDatabaseInitialization: true,
 | 
			
		||||
	})
 | 
			
		||||
	app := &cli.Command{
 | 
			
		||||
		Commands: []*cli.Command{cmdDoctorCheck},
 | 
			
		||||
	}
 | 
			
		||||
	err := app.Run(t.Context(), []string{"./gitea", "check", "--run", "test-check"})
 | 
			
		||||
	app := cli.NewApp()
 | 
			
		||||
	app.Commands = []*cli.Command{cmdDoctorCheck}
 | 
			
		||||
	err := app.Run([]string{"./gitea", "check", "--run", "test-check"})
 | 
			
		||||
	assert.NoError(t, err)
 | 
			
		||||
	err = app.Run(t.Context(), []string{"./gitea", "check", "--run", "no-such"})
 | 
			
		||||
	err = app.Run([]string{"./gitea", "check", "--run", "no-such"})
 | 
			
		||||
	assert.ErrorContains(t, err, `unknown checks: "no-such"`)
 | 
			
		||||
	err = app.Run(t.Context(), []string{"./gitea", "check", "--run", "test-check,no-such"})
 | 
			
		||||
	err = app.Run([]string{"./gitea", "check", "--run", "test-check,no-such"})
 | 
			
		||||
	assert.ErrorContains(t, err, `unknown checks: "no-such"`)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										79
									
								
								cmd/dump.go
									
									
									
									
									
								
							
							
						
						
									
										79
									
								
								cmd/dump.go
									
									
									
									
									
								
							@@ -5,7 +5,7 @@
 | 
			
		||||
package cmd
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
@@ -20,7 +20,8 @@ import (
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
 | 
			
		||||
	"gitea.com/go-chi/session"
 | 
			
		||||
	"github.com/urfave/cli/v3"
 | 
			
		||||
	"github.com/mholt/archiver/v3"
 | 
			
		||||
	"github.com/urfave/cli/v2"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// CmdDump represents the available dump sub-command.
 | 
			
		||||
@@ -92,7 +93,7 @@ var CmdDump = &cli.Command{
 | 
			
		||||
		},
 | 
			
		||||
		&cli.StringFlag{
 | 
			
		||||
			Name:  "type",
 | 
			
		||||
			Usage: `Dump output format, default to "zip", supported types: ` + strings.Join(dump.SupportedOutputTypes, ", "),
 | 
			
		||||
			Usage: fmt.Sprintf(`Dump output format, default to "zip", supported types: %s`, strings.Join(dump.SupportedOutputTypes, ", ")),
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
}
 | 
			
		||||
@@ -101,17 +102,17 @@ func fatal(format string, args ...any) {
 | 
			
		||||
	log.Fatal(format, args...)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func runDump(ctx context.Context, cmd *cli.Command) error {
 | 
			
		||||
func runDump(ctx *cli.Context) error {
 | 
			
		||||
	setting.MustInstalled()
 | 
			
		||||
 | 
			
		||||
	quite := cmd.Bool("quiet")
 | 
			
		||||
	verbose := cmd.Bool("verbose")
 | 
			
		||||
	quite := ctx.Bool("quiet")
 | 
			
		||||
	verbose := ctx.Bool("verbose")
 | 
			
		||||
	if verbose && quite {
 | 
			
		||||
		fatal("Option --quiet and --verbose cannot both be set")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// outFileName is either "-" or a file name (will be made absolute)
 | 
			
		||||
	outFileName, outType := dump.PrepareFileNameAndType(cmd.String("file"), cmd.String("type"))
 | 
			
		||||
	outFileName, outType := dump.PrepareFileNameAndType(ctx.String("file"), ctx.String("type"))
 | 
			
		||||
	if outType == "" {
 | 
			
		||||
		fatal("Invalid output type")
 | 
			
		||||
	}
 | 
			
		||||
@@ -136,7 +137,10 @@ func runDump(ctx context.Context, cmd *cli.Command) error {
 | 
			
		||||
	setting.DisableLoggerInit()
 | 
			
		||||
	setting.LoadSettings() // cannot access session settings otherwise
 | 
			
		||||
 | 
			
		||||
	err := db.InitEngine(ctx)
 | 
			
		||||
	stdCtx, cancel := installSignals()
 | 
			
		||||
	defer cancel()
 | 
			
		||||
 | 
			
		||||
	err := db.InitEngine(stdCtx)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
@@ -145,20 +149,24 @@ func runDump(ctx context.Context, cmd *cli.Command) error {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	dumper, err := dump.NewDumper(ctx, outType, outFile)
 | 
			
		||||
	archiverGeneric, err := archiver.ByExtension("." + outType)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fatal("Failed to create archive %q: %v", outFile, err)
 | 
			
		||||
		return err
 | 
			
		||||
		fatal("Unable to get archiver for extension: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
	dumper.Verbose = verbose
 | 
			
		||||
	dumper.GlobalExcludeAbsPath(outFileName)
 | 
			
		||||
	defer func() {
 | 
			
		||||
		if err := dumper.Close(); err != nil {
 | 
			
		||||
			fatal("Failed to save archive %q: %v", outFileName, err)
 | 
			
		||||
		}
 | 
			
		||||
	}()
 | 
			
		||||
 | 
			
		||||
	if cmd.IsSet("skip-repository") && cmd.Bool("skip-repository") {
 | 
			
		||||
	archiverWriter := archiverGeneric.(archiver.Writer)
 | 
			
		||||
	if err := archiverWriter.Create(outFile); err != nil {
 | 
			
		||||
		fatal("Creating archiver.Writer failed: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
	defer archiverWriter.Close()
 | 
			
		||||
 | 
			
		||||
	dumper := &dump.Dumper{
 | 
			
		||||
		Writer:  archiverWriter,
 | 
			
		||||
		Verbose: verbose,
 | 
			
		||||
	}
 | 
			
		||||
	dumper.GlobalExcludeAbsPath(outFileName)
 | 
			
		||||
 | 
			
		||||
	if ctx.IsSet("skip-repository") && ctx.Bool("skip-repository") {
 | 
			
		||||
		log.Info("Skip dumping local repositories")
 | 
			
		||||
	} else {
 | 
			
		||||
		log.Info("Dumping local repositories... %s", setting.RepoRootPath)
 | 
			
		||||
@@ -166,7 +174,7 @@ func runDump(ctx context.Context, cmd *cli.Command) error {
 | 
			
		||||
			fatal("Failed to include repositories: %v", err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if cmd.IsSet("skip-lfs-data") && cmd.Bool("skip-lfs-data") {
 | 
			
		||||
		if ctx.IsSet("skip-lfs-data") && ctx.Bool("skip-lfs-data") {
 | 
			
		||||
			log.Info("Skip dumping LFS data")
 | 
			
		||||
		} else if !setting.LFS.StartServer {
 | 
			
		||||
			log.Info("LFS isn't enabled. Skip dumping LFS data")
 | 
			
		||||
@@ -175,18 +183,18 @@ func runDump(ctx context.Context, cmd *cli.Command) error {
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
			return dumper.AddFileByReader(object, info, path.Join("data", "lfs", objPath))
 | 
			
		||||
			return dumper.AddReader(object, info, path.Join("data", "lfs", objPath))
 | 
			
		||||
		}); err != nil {
 | 
			
		||||
			fatal("Failed to dump LFS objects: %v", err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if cmd.Bool("skip-db") {
 | 
			
		||||
	if ctx.Bool("skip-db") {
 | 
			
		||||
		// Ensure that we don't dump the database file that may reside in setting.AppDataPath or elsewhere.
 | 
			
		||||
		dumper.GlobalExcludeAbsPath(setting.Database.Path)
 | 
			
		||||
		log.Info("Skipping database")
 | 
			
		||||
	} else {
 | 
			
		||||
		tmpDir := cmd.String("tempdir")
 | 
			
		||||
		tmpDir := ctx.String("tempdir")
 | 
			
		||||
		if _, err := os.Stat(tmpDir); os.IsNotExist(err) {
 | 
			
		||||
			fatal("Path does not exist: %s", tmpDir)
 | 
			
		||||
		}
 | 
			
		||||
@@ -202,7 +210,7 @@ func runDump(ctx context.Context, cmd *cli.Command) error {
 | 
			
		||||
			}
 | 
			
		||||
		}()
 | 
			
		||||
 | 
			
		||||
		targetDBType := cmd.String("database")
 | 
			
		||||
		targetDBType := ctx.String("database")
 | 
			
		||||
		if len(targetDBType) > 0 && targetDBType != setting.Database.Type.String() {
 | 
			
		||||
			log.Info("Dumping database %s => %s...", setting.Database.Type, targetDBType)
 | 
			
		||||
		} else {
 | 
			
		||||
@@ -213,17 +221,17 @@ func runDump(ctx context.Context, cmd *cli.Command) error {
 | 
			
		||||
			fatal("Failed to dump database: %v", err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if err = dumper.AddFileByPath("gitea-db.sql", dbDump.Name()); err != nil {
 | 
			
		||||
		if err = dumper.AddFile("gitea-db.sql", dbDump.Name()); err != nil {
 | 
			
		||||
			fatal("Failed to include gitea-db.sql: %v", err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	log.Info("Adding custom configuration file from %s", setting.CustomConf)
 | 
			
		||||
	if err = dumper.AddFileByPath("app.ini", setting.CustomConf); err != nil {
 | 
			
		||||
	if err = dumper.AddFile("app.ini", setting.CustomConf); err != nil {
 | 
			
		||||
		fatal("Failed to include specified app.ini: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if cmd.IsSet("skip-custom-dir") && cmd.Bool("skip-custom-dir") {
 | 
			
		||||
	if ctx.IsSet("skip-custom-dir") && ctx.Bool("skip-custom-dir") {
 | 
			
		||||
		log.Info("Skipping custom directory")
 | 
			
		||||
	} else {
 | 
			
		||||
		customDir, err := os.Stat(setting.CustomPath)
 | 
			
		||||
@@ -256,7 +264,7 @@ func runDump(ctx context.Context, cmd *cli.Command) error {
 | 
			
		||||
			excludes = append(excludes, opts.ProviderConfig)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if cmd.IsSet("skip-index") && cmd.Bool("skip-index") {
 | 
			
		||||
		if ctx.IsSet("skip-index") && ctx.Bool("skip-index") {
 | 
			
		||||
			excludes = append(excludes, setting.Indexer.RepoPath)
 | 
			
		||||
			excludes = append(excludes, setting.Indexer.IssuePath)
 | 
			
		||||
		}
 | 
			
		||||
@@ -265,26 +273,25 @@ func runDump(ctx context.Context, cmd *cli.Command) error {
 | 
			
		||||
		excludes = append(excludes, setting.LFS.Storage.Path)
 | 
			
		||||
		excludes = append(excludes, setting.Attachment.Storage.Path)
 | 
			
		||||
		excludes = append(excludes, setting.Packages.Storage.Path)
 | 
			
		||||
		excludes = append(excludes, setting.RepoArchive.Storage.Path)
 | 
			
		||||
		excludes = append(excludes, setting.Log.RootPath)
 | 
			
		||||
		if err := dumper.AddRecursiveExclude("data", setting.AppDataPath, excludes); err != nil {
 | 
			
		||||
			fatal("Failed to include data directory: %v", err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if cmd.IsSet("skip-attachment-data") && cmd.Bool("skip-attachment-data") {
 | 
			
		||||
	if ctx.IsSet("skip-attachment-data") && ctx.Bool("skip-attachment-data") {
 | 
			
		||||
		log.Info("Skip dumping attachment data")
 | 
			
		||||
	} else if err := storage.Attachments.IterateObjects("", func(objPath string, object storage.Object) error {
 | 
			
		||||
		info, err := object.Stat()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		return dumper.AddFileByReader(object, info, path.Join("data", "attachments", objPath))
 | 
			
		||||
		return dumper.AddReader(object, info, path.Join("data", "attachments", objPath))
 | 
			
		||||
	}); err != nil {
 | 
			
		||||
		fatal("Failed to dump attachments: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if cmd.IsSet("skip-package-data") && cmd.Bool("skip-package-data") {
 | 
			
		||||
	if ctx.IsSet("skip-package-data") && ctx.Bool("skip-package-data") {
 | 
			
		||||
		log.Info("Skip dumping package data")
 | 
			
		||||
	} else if !setting.Packages.Enabled {
 | 
			
		||||
		log.Info("Packages isn't enabled. Skip dumping package data")
 | 
			
		||||
@@ -293,7 +300,7 @@ func runDump(ctx context.Context, cmd *cli.Command) error {
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		return dumper.AddFileByReader(object, info, path.Join("data", "packages", objPath))
 | 
			
		||||
		return dumper.AddReader(object, info, path.Join("data", "packages", objPath))
 | 
			
		||||
	}); err != nil {
 | 
			
		||||
		fatal("Failed to dump packages: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
@@ -301,7 +308,7 @@ func runDump(ctx context.Context, cmd *cli.Command) error {
 | 
			
		||||
	// Doesn't check if LogRootPath exists before processing --skip-log intentionally,
 | 
			
		||||
	// ensuring that it's clear the dump is skipped whether the directory's initialized
 | 
			
		||||
	// yet or not.
 | 
			
		||||
	if cmd.IsSet("skip-log") && cmd.Bool("skip-log") {
 | 
			
		||||
	if ctx.IsSet("skip-log") && ctx.Bool("skip-log") {
 | 
			
		||||
		log.Info("Skip dumping log files")
 | 
			
		||||
	} else {
 | 
			
		||||
		isExist, err := util.IsExist(setting.Log.RootPath)
 | 
			
		||||
@@ -318,6 +325,10 @@ func runDump(ctx context.Context, cmd *cli.Command) error {
 | 
			
		||||
	if outFileName == "-" {
 | 
			
		||||
		log.Info("Finish dumping to stdout")
 | 
			
		||||
	} else {
 | 
			
		||||
		if err = archiverWriter.Close(); err != nil {
 | 
			
		||||
			_ = os.Remove(outFileName)
 | 
			
		||||
			fatal("Failed to save %q: %v", outFileName, err)
 | 
			
		||||
		}
 | 
			
		||||
		if err = os.Chmod(outFileName, 0o600); err != nil {
 | 
			
		||||
			log.Info("Can't change file access permissions mask to 0600: %v", err)
 | 
			
		||||
		}
 | 
			
		||||
 
 | 
			
		||||
@@ -19,7 +19,7 @@ import (
 | 
			
		||||
	"code.gitea.io/gitea/services/convert"
 | 
			
		||||
	"code.gitea.io/gitea/services/migrations"
 | 
			
		||||
 | 
			
		||||
	"github.com/urfave/cli/v3"
 | 
			
		||||
	"github.com/urfave/cli/v2"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// CmdDumpRepository represents the available dump repository sub-command.
 | 
			
		||||
@@ -79,18 +79,16 @@ wiki, issues, labels, releases, release_assets, milestones, pull_requests, comme
 | 
			
		||||
	},
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func runDumpRepository(ctx context.Context, cmd *cli.Command) error {
 | 
			
		||||
	setupConsoleLogger(log.INFO, log.CanColorStderr, os.Stderr)
 | 
			
		||||
func runDumpRepository(ctx *cli.Context) error {
 | 
			
		||||
	stdCtx, cancel := installSignals()
 | 
			
		||||
	defer cancel()
 | 
			
		||||
 | 
			
		||||
	setting.DisableLoggerInit()
 | 
			
		||||
	setting.LoadSettings() // cannot access skip_tls_verify settings otherwise
 | 
			
		||||
 | 
			
		||||
	if err := initDB(ctx); err != nil {
 | 
			
		||||
	if err := initDB(stdCtx); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// migrations.GiteaLocalUploader depends on git module
 | 
			
		||||
	if err := git.InitSimple(); err != nil {
 | 
			
		||||
	if err := git.InitSimple(context.Background()); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -102,8 +100,8 @@ func runDumpRepository(ctx context.Context, cmd *cli.Command) error {
 | 
			
		||||
 | 
			
		||||
	var (
 | 
			
		||||
		serviceType structs.GitServiceType
 | 
			
		||||
		cloneAddr   = cmd.String("clone_addr")
 | 
			
		||||
		serviceStr  = cmd.String("git_service")
 | 
			
		||||
		cloneAddr   = ctx.String("clone_addr")
 | 
			
		||||
		serviceStr  = ctx.String("git_service")
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	if strings.HasPrefix(strings.ToLower(cloneAddr), "https://github.com/") {
 | 
			
		||||
@@ -121,13 +119,13 @@ func runDumpRepository(ctx context.Context, cmd *cli.Command) error {
 | 
			
		||||
	opts := base.MigrateOptions{
 | 
			
		||||
		GitServiceType: serviceType,
 | 
			
		||||
		CloneAddr:      cloneAddr,
 | 
			
		||||
		AuthUsername:   cmd.String("auth_username"),
 | 
			
		||||
		AuthPassword:   cmd.String("auth_password"),
 | 
			
		||||
		AuthToken:      cmd.String("auth_token"),
 | 
			
		||||
		RepoName:       cmd.String("repo_name"),
 | 
			
		||||
		AuthUsername:   ctx.String("auth_username"),
 | 
			
		||||
		AuthPassword:   ctx.String("auth_password"),
 | 
			
		||||
		AuthToken:      ctx.String("auth_token"),
 | 
			
		||||
		RepoName:       ctx.String("repo_name"),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(cmd.String("units")) == 0 {
 | 
			
		||||
	if len(ctx.String("units")) == 0 {
 | 
			
		||||
		opts.Wiki = true
 | 
			
		||||
		opts.Issues = true
 | 
			
		||||
		opts.Milestones = true
 | 
			
		||||
@@ -137,8 +135,8 @@ func runDumpRepository(ctx context.Context, cmd *cli.Command) error {
 | 
			
		||||
		opts.PullRequests = true
 | 
			
		||||
		opts.ReleaseAssets = true
 | 
			
		||||
	} else {
 | 
			
		||||
		units := strings.SplitSeq(cmd.String("units"), ",")
 | 
			
		||||
		for unit := range units {
 | 
			
		||||
		units := strings.Split(ctx.String("units"), ",")
 | 
			
		||||
		for _, unit := range units {
 | 
			
		||||
			switch strings.ToLower(strings.TrimSpace(unit)) {
 | 
			
		||||
			case "":
 | 
			
		||||
				continue
 | 
			
		||||
@@ -166,7 +164,7 @@ func runDumpRepository(ctx context.Context, cmd *cli.Command) error {
 | 
			
		||||
 | 
			
		||||
	// the repo_dir will be removed if error occurs in DumpRepository
 | 
			
		||||
	// make sure the directory doesn't exist or is empty, prevent from deleting user files
 | 
			
		||||
	repoDir := cmd.String("repo_dir")
 | 
			
		||||
	repoDir := ctx.String("repo_dir")
 | 
			
		||||
	if exists, err := util.IsExist(repoDir); err != nil {
 | 
			
		||||
		return fmt.Errorf("unable to stat repo_dir %q: %w", repoDir, err)
 | 
			
		||||
	} else if exists {
 | 
			
		||||
@@ -179,9 +177,9 @@ func runDumpRepository(ctx context.Context, cmd *cli.Command) error {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := migrations.DumpRepository(
 | 
			
		||||
		ctx,
 | 
			
		||||
		context.Background(),
 | 
			
		||||
		repoDir,
 | 
			
		||||
		cmd.String("owner_name"),
 | 
			
		||||
		ctx.String("owner_name"),
 | 
			
		||||
		opts,
 | 
			
		||||
	); err != nil {
 | 
			
		||||
		log.Fatal("Failed to dump repository: %v", err)
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,6 @@
 | 
			
		||||
package cmd
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"os"
 | 
			
		||||
@@ -12,7 +11,6 @@ import (
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/modules/assetfs"
 | 
			
		||||
	"code.gitea.io/gitea/modules/glob"
 | 
			
		||||
	"code.gitea.io/gitea/modules/log"
 | 
			
		||||
	"code.gitea.io/gitea/modules/options"
 | 
			
		||||
	"code.gitea.io/gitea/modules/public"
 | 
			
		||||
@@ -20,7 +18,8 @@ import (
 | 
			
		||||
	"code.gitea.io/gitea/modules/templates"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
 | 
			
		||||
	"github.com/urfave/cli/v3"
 | 
			
		||||
	"github.com/gobwas/glob"
 | 
			
		||||
	"github.com/urfave/cli/v2"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// CmdEmbedded represents the available extract sub-command.
 | 
			
		||||
@@ -29,7 +28,7 @@ var (
 | 
			
		||||
		Name:        "embedded",
 | 
			
		||||
		Usage:       "Extract embedded resources",
 | 
			
		||||
		Description: "A command for extracting embedded resources, like templates and images",
 | 
			
		||||
		Commands: []*cli.Command{
 | 
			
		||||
		Subcommands: []*cli.Command{
 | 
			
		||||
			subcmdList,
 | 
			
		||||
			subcmdView,
 | 
			
		||||
			subcmdExtract,
 | 
			
		||||
@@ -101,7 +100,7 @@ type assetFile struct {
 | 
			
		||||
	path string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func initEmbeddedExtractor(c *cli.Command) error {
 | 
			
		||||
func initEmbeddedExtractor(c *cli.Context) error {
 | 
			
		||||
	setupConsoleLogger(log.ERROR, log.CanColorStderr, os.Stderr)
 | 
			
		||||
 | 
			
		||||
	patterns, err := compileCollectPatterns(c.Args().Slice())
 | 
			
		||||
@@ -116,31 +115,31 @@ func initEmbeddedExtractor(c *cli.Command) error {
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func runList(_ context.Context, c *cli.Command) error {
 | 
			
		||||
func runList(c *cli.Context) error {
 | 
			
		||||
	if err := runListDo(c); err != nil {
 | 
			
		||||
		_, _ = fmt.Fprintf(os.Stderr, "%v\n", err)
 | 
			
		||||
		fmt.Fprintf(os.Stderr, "%v\n", err)
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func runView(_ context.Context, c *cli.Command) error {
 | 
			
		||||
func runView(c *cli.Context) error {
 | 
			
		||||
	if err := runViewDo(c); err != nil {
 | 
			
		||||
		_, _ = fmt.Fprintf(os.Stderr, "%v\n", err)
 | 
			
		||||
		fmt.Fprintf(os.Stderr, "%v\n", err)
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func runExtract(_ context.Context, c *cli.Command) error {
 | 
			
		||||
func runExtract(c *cli.Context) error {
 | 
			
		||||
	if err := runExtractDo(c); err != nil {
 | 
			
		||||
		_, _ = fmt.Fprintf(os.Stderr, "%v\n", err)
 | 
			
		||||
		fmt.Fprintf(os.Stderr, "%v\n", err)
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func runListDo(c *cli.Command) error {
 | 
			
		||||
func runListDo(c *cli.Context) error {
 | 
			
		||||
	if err := initEmbeddedExtractor(c); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
@@ -152,7 +151,7 @@ func runListDo(c *cli.Command) error {
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func runViewDo(c *cli.Command) error {
 | 
			
		||||
func runViewDo(c *cli.Context) error {
 | 
			
		||||
	if err := initEmbeddedExtractor(c); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
@@ -175,7 +174,7 @@ func runViewDo(c *cli.Command) error {
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func runExtractDo(c *cli.Command) error {
 | 
			
		||||
func runExtractDo(c *cli.Context) error {
 | 
			
		||||
	if err := initEmbeddedExtractor(c); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
@@ -217,7 +216,7 @@ func runExtractDo(c *cli.Command) error {
 | 
			
		||||
	for _, a := range matchedAssetFiles {
 | 
			
		||||
		if err := extractAsset(destdir, a, overwrite, rename); err != nil {
 | 
			
		||||
			// Non-fatal error
 | 
			
		||||
			_, _ = fmt.Fprintf(os.Stderr, "%s: %v\n", a.path, err)
 | 
			
		||||
			fmt.Fprintf(os.Stderr, "%s: %v", a.path, err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -272,7 +271,7 @@ func extractAsset(d string, a assetFile, overwrite, rename bool) error {
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func collectAssetFilesByPattern(c *cli.Command, globs []glob.Glob, path string, layer *assetfs.Layer) {
 | 
			
		||||
func collectAssetFilesByPattern(c *cli.Context, globs []glob.Glob, path string, layer *assetfs.Layer) {
 | 
			
		||||
	fs := assetfs.Layered(layer)
 | 
			
		||||
	files, err := fs.ListAllFiles(".", true)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
@@ -295,14 +294,16 @@ func collectAssetFilesByPattern(c *cli.Command, globs []glob.Glob, path string,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func compileCollectPatterns(args []string) (_ []glob.Glob, err error) {
 | 
			
		||||
func compileCollectPatterns(args []string) ([]glob.Glob, error) {
 | 
			
		||||
	if len(args) == 0 {
 | 
			
		||||
		args = []string{"**"}
 | 
			
		||||
	}
 | 
			
		||||
	pat := make([]glob.Glob, len(args))
 | 
			
		||||
	for i := range args {
 | 
			
		||||
		if pat[i], err = glob.Compile(args[i], '/'); err != nil {
 | 
			
		||||
			return nil, fmt.Errorf("invalid glob patterh %q: %w", args[i], err)
 | 
			
		||||
		if g, err := glob.Compile(args[i], '/'); err != nil {
 | 
			
		||||
			return nil, fmt.Errorf("'%s': Invalid glob pattern: %w", args[i], err)
 | 
			
		||||
		} else { //nolint:revive
 | 
			
		||||
			pat[i] = g
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return pat, nil
 | 
			
		||||
 
 | 
			
		||||
@@ -5,14 +5,13 @@
 | 
			
		||||
package cmd
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"os"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/modules/generate"
 | 
			
		||||
 | 
			
		||||
	"github.com/mattn/go-isatty"
 | 
			
		||||
	"github.com/urfave/cli/v3"
 | 
			
		||||
	"github.com/urfave/cli/v2"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
@@ -20,7 +19,7 @@ var (
 | 
			
		||||
	CmdGenerate = &cli.Command{
 | 
			
		||||
		Name:  "generate",
 | 
			
		||||
		Usage: "Generate Gitea's secrets/keys/tokens",
 | 
			
		||||
		Commands: []*cli.Command{
 | 
			
		||||
		Subcommands: []*cli.Command{
 | 
			
		||||
			subcmdSecret,
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
@@ -28,7 +27,7 @@ var (
 | 
			
		||||
	subcmdSecret = &cli.Command{
 | 
			
		||||
		Name:  "secret",
 | 
			
		||||
		Usage: "Generate a secret token",
 | 
			
		||||
		Commands: []*cli.Command{
 | 
			
		||||
		Subcommands: []*cli.Command{
 | 
			
		||||
			microcmdGenerateInternalToken,
 | 
			
		||||
			microcmdGenerateLfsJwtSecret,
 | 
			
		||||
			microcmdGenerateSecretKey,
 | 
			
		||||
@@ -55,7 +54,7 @@ var (
 | 
			
		||||
	}
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func runGenerateInternalToken(_ context.Context, c *cli.Command) error {
 | 
			
		||||
func runGenerateInternalToken(c *cli.Context) error {
 | 
			
		||||
	internalToken, err := generate.NewInternalToken()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
@@ -70,7 +69,7 @@ func runGenerateInternalToken(_ context.Context, c *cli.Command) error {
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func runGenerateLfsJwtSecret(_ context.Context, c *cli.Command) error {
 | 
			
		||||
func runGenerateLfsJwtSecret(c *cli.Context) error {
 | 
			
		||||
	_, jwtSecretBase64, err := generate.NewJwtSecretWithBase64()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
@@ -85,13 +84,12 @@ func runGenerateLfsJwtSecret(_ context.Context, c *cli.Command) error {
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func runGenerateSecretKey(_ context.Context, c *cli.Command) error {
 | 
			
		||||
func runGenerateSecretKey(c *cli.Context) error {
 | 
			
		||||
	secretKey, err := generate.NewSecretKey()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// codeql[disable-next-line=go/clear-text-logging]
 | 
			
		||||
	fmt.Printf("%s", secretKey)
 | 
			
		||||
 | 
			
		||||
	if isatty.IsTerminal(os.Stdout.Fd()) {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										36
									
								
								cmd/hook.go
									
									
									
									
									
								
							
							
						
						
									
										36
									
								
								cmd/hook.go
									
									
									
									
									
								
							@@ -15,17 +15,16 @@ import (
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/modules/git"
 | 
			
		||||
	"code.gitea.io/gitea/modules/git/gitcmd"
 | 
			
		||||
	"code.gitea.io/gitea/modules/log"
 | 
			
		||||
	"code.gitea.io/gitea/modules/private"
 | 
			
		||||
	repo_module "code.gitea.io/gitea/modules/repository"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
 | 
			
		||||
	"github.com/urfave/cli/v3"
 | 
			
		||||
	"github.com/urfave/cli/v2"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	hookBatchSize = 500
 | 
			
		||||
	hookBatchSize = 30
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
@@ -33,10 +32,9 @@ var (
 | 
			
		||||
	CmdHook = &cli.Command{
 | 
			
		||||
		Name:        "hook",
 | 
			
		||||
		Usage:       "(internal) Should only be called by Git",
 | 
			
		||||
		Hidden:      true, // internal commands shouldn't be visible
 | 
			
		||||
		Description: "Delegate commands to corresponding Git hooks",
 | 
			
		||||
		Before:      PrepareConsoleLoggerLevel(log.FATAL),
 | 
			
		||||
		Commands: []*cli.Command{
 | 
			
		||||
		Subcommands: []*cli.Command{
 | 
			
		||||
			subcmdHookPreReceive,
 | 
			
		||||
			subcmdHookUpdate,
 | 
			
		||||
			subcmdHookPostReceive,
 | 
			
		||||
@@ -163,10 +161,12 @@ func (n *nilWriter) WriteString(s string) (int, error) {
 | 
			
		||||
	return len(s), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func runHookPreReceive(ctx context.Context, c *cli.Command) error {
 | 
			
		||||
func runHookPreReceive(c *cli.Context) error {
 | 
			
		||||
	if isInternal, _ := strconv.ParseBool(os.Getenv(repo_module.EnvIsInternal)); isInternal {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
	ctx, cancel := installSignals()
 | 
			
		||||
	defer cancel()
 | 
			
		||||
 | 
			
		||||
	setup(ctx, c.Bool("debug"))
 | 
			
		||||
 | 
			
		||||
@@ -186,7 +186,7 @@ Gitea or set your environment appropriately.`, "")
 | 
			
		||||
	userID, _ := strconv.ParseInt(os.Getenv(repo_module.EnvPusherID), 10, 64)
 | 
			
		||||
	prID, _ := strconv.ParseInt(os.Getenv(repo_module.EnvPRID), 10, 64)
 | 
			
		||||
	deployKeyID, _ := strconv.ParseInt(os.Getenv(repo_module.EnvDeployKeyID), 10, 64)
 | 
			
		||||
	actionPerm, _ := strconv.Atoi(os.Getenv(repo_module.EnvActionPerm))
 | 
			
		||||
	actionPerm, _ := strconv.ParseInt(os.Getenv(repo_module.EnvActionPerm), 10, 64)
 | 
			
		||||
 | 
			
		||||
	hookOptions := private.HookOptions{
 | 
			
		||||
		UserID:                          userID,
 | 
			
		||||
@@ -196,7 +196,7 @@ Gitea or set your environment appropriately.`, "")
 | 
			
		||||
		GitPushOptions:                  pushOptions(),
 | 
			
		||||
		PullRequestID:                   prID,
 | 
			
		||||
		DeployKeyID:                     deployKeyID,
 | 
			
		||||
		ActionPerm:                      actionPerm,
 | 
			
		||||
		ActionPerm:                      int(actionPerm),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	scanner := bufio.NewScanner(os.Stdin)
 | 
			
		||||
@@ -292,7 +292,7 @@ Gitea or set your environment appropriately.`, "")
 | 
			
		||||
 | 
			
		||||
// runHookUpdate avoid to do heavy operations on update hook because it will be
 | 
			
		||||
// invoked for every ref update which does not like pre-receive and post-receive
 | 
			
		||||
func runHookUpdate(_ context.Context, c *cli.Command) error {
 | 
			
		||||
func runHookUpdate(c *cli.Context) error {
 | 
			
		||||
	if isInternal, _ := strconv.ParseBool(os.Getenv(repo_module.EnvIsInternal)); isInternal {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
@@ -309,12 +309,15 @@ func runHookUpdate(_ context.Context, c *cli.Command) error {
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func runHookPostReceive(ctx context.Context, c *cli.Command) error {
 | 
			
		||||
func runHookPostReceive(c *cli.Context) error {
 | 
			
		||||
	ctx, cancel := installSignals()
 | 
			
		||||
	defer cancel()
 | 
			
		||||
 | 
			
		||||
	setup(ctx, c.Bool("debug"))
 | 
			
		||||
 | 
			
		||||
	// First of all run update-server-info no matter what
 | 
			
		||||
	if _, _, err := gitcmd.NewCommand("update-server-info").RunStdString(ctx); err != nil {
 | 
			
		||||
		return fmt.Errorf("failed to call 'git update-server-info': %w", err)
 | 
			
		||||
	if _, _, err := git.NewCommand(ctx, "update-server-info").RunStdString(nil); err != nil {
 | 
			
		||||
		return fmt.Errorf("Failed to call 'git update-server-info': %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Now if we're an internal don't do anything else
 | 
			
		||||
@@ -482,7 +485,7 @@ func hookPrintResult(output, isCreate bool, branch, url string) {
 | 
			
		||||
func pushOptions() map[string]string {
 | 
			
		||||
	opts := make(map[string]string)
 | 
			
		||||
	if pushCount, err := strconv.Atoi(os.Getenv(private.GitPushOptionCount)); err == nil {
 | 
			
		||||
		for idx := range pushCount {
 | 
			
		||||
		for idx := 0; idx < pushCount; idx++ {
 | 
			
		||||
			opt := os.Getenv(fmt.Sprintf("GIT_PUSH_OPTION_%d", idx))
 | 
			
		||||
			kv := strings.SplitN(opt, "=", 2)
 | 
			
		||||
			if len(kv) == 2 {
 | 
			
		||||
@@ -493,7 +496,10 @@ func pushOptions() map[string]string {
 | 
			
		||||
	return opts
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func runHookProcReceive(ctx context.Context, c *cli.Command) error {
 | 
			
		||||
func runHookProcReceive(c *cli.Context) error {
 | 
			
		||||
	ctx, cancel := installSignals()
 | 
			
		||||
	defer cancel()
 | 
			
		||||
 | 
			
		||||
	setup(ctx, c.Bool("debug"))
 | 
			
		||||
 | 
			
		||||
	if len(os.Getenv("SSH_ORIGINAL_COMMAND")) == 0 {
 | 
			
		||||
@@ -734,7 +740,7 @@ func readPktLine(ctx context.Context, in *bufio.Reader, requestType pktLineType)
 | 
			
		||||
 | 
			
		||||
	// read prefix
 | 
			
		||||
	lengthBytes := make([]byte, 4)
 | 
			
		||||
	for i := range 4 {
 | 
			
		||||
	for i := 0; i < 4; i++ {
 | 
			
		||||
		lengthBytes[i], err = in.ReadByte()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, fail(ctx, "Protocol: stdin error", "Pkt-Line: read stdin failed : %v", err)
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,7 @@ package cmd
 | 
			
		||||
import (
 | 
			
		||||
	"bufio"
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"context"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
@@ -14,7 +15,7 @@ import (
 | 
			
		||||
 | 
			
		||||
func TestPktLine(t *testing.T) {
 | 
			
		||||
	// test read
 | 
			
		||||
	ctx := t.Context()
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
	s := strings.NewReader("0000")
 | 
			
		||||
	r := bufio.NewReader(s)
 | 
			
		||||
	result, err := readPktLine(ctx, r, pktLineTypeFlush)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										11
									
								
								cmd/keys.go
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								cmd/keys.go
									
									
									
									
									
								
							@@ -4,7 +4,6 @@
 | 
			
		||||
package cmd
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"strings"
 | 
			
		||||
@@ -12,14 +11,13 @@ import (
 | 
			
		||||
	"code.gitea.io/gitea/modules/log"
 | 
			
		||||
	"code.gitea.io/gitea/modules/private"
 | 
			
		||||
 | 
			
		||||
	"github.com/urfave/cli/v3"
 | 
			
		||||
	"github.com/urfave/cli/v2"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// CmdKeys represents the available keys sub-command
 | 
			
		||||
var CmdKeys = &cli.Command{
 | 
			
		||||
	Name:        "keys",
 | 
			
		||||
	Usage:       "(internal) Should only be called by SSH server",
 | 
			
		||||
	Hidden:      true, // internal commands shouldn't be visible
 | 
			
		||||
	Description: "Queries the Gitea database to get the authorized command for a given ssh key fingerprint",
 | 
			
		||||
	Before:      PrepareConsoleLoggerLevel(log.FATAL),
 | 
			
		||||
	Action:      runKeys,
 | 
			
		||||
@@ -51,7 +49,7 @@ var CmdKeys = &cli.Command{
 | 
			
		||||
	},
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func runKeys(ctx context.Context, c *cli.Command) error {
 | 
			
		||||
func runKeys(c *cli.Context) error {
 | 
			
		||||
	if !c.IsSet("username") {
 | 
			
		||||
		return errors.New("No username provided")
 | 
			
		||||
	}
 | 
			
		||||
@@ -70,6 +68,9 @@ func runKeys(ctx context.Context, c *cli.Command) error {
 | 
			
		||||
		return errors.New("No key type and content provided")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ctx, cancel := installSignals()
 | 
			
		||||
	defer cancel()
 | 
			
		||||
 | 
			
		||||
	setup(ctx, c.Bool("debug"))
 | 
			
		||||
 | 
			
		||||
	authorizedString, extra := private.AuthorizedPublicKeyByContent(ctx, content)
 | 
			
		||||
@@ -77,6 +78,6 @@ func runKeys(ctx context.Context, c *cli.Command) error {
 | 
			
		||||
	if extra.Error != nil {
 | 
			
		||||
		return extra.Error
 | 
			
		||||
	}
 | 
			
		||||
	_, _ = fmt.Fprintln(c.Root().Writer, strings.TrimSpace(authorizedString.Text))
 | 
			
		||||
	_, _ = fmt.Fprintln(c.App.Writer, strings.TrimSpace(authorizedString.Text))
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,18 +4,24 @@
 | 
			
		||||
package cmd
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"fmt"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/modules/private"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
 | 
			
		||||
	"github.com/urfave/cli/v3"
 | 
			
		||||
	"github.com/urfave/cli/v2"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func runSendMail(ctx context.Context, c *cli.Command) error {
 | 
			
		||||
func runSendMail(c *cli.Context) error {
 | 
			
		||||
	ctx, cancel := installSignals()
 | 
			
		||||
	defer cancel()
 | 
			
		||||
 | 
			
		||||
	setting.MustInstalled()
 | 
			
		||||
 | 
			
		||||
	if err := argsSet(c, "title"); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	subject := c.String("title")
 | 
			
		||||
	confirmSkiped := c.Bool("force")
 | 
			
		||||
	body := c.String("content")
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										173
									
								
								cmd/main.go
									
									
									
									
									
								
							
							
						
						
									
										173
									
								
								cmd/main.go
									
									
									
									
									
								
							@@ -4,40 +4,36 @@
 | 
			
		||||
package cmd
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"os"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/modules/log"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
 | 
			
		||||
	"github.com/urfave/cli/v3"
 | 
			
		||||
	"github.com/urfave/cli/v2"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var cliHelpPrinterOld = cli.HelpPrinter
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	cli.HelpPrinter = cliHelpPrinterNew
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// cliHelpPrinterNew helps to print "DEFAULT CONFIGURATION" for the following cases ( "-c" can apper in any position):
 | 
			
		||||
// * ./gitea -c /dev/null -h
 | 
			
		||||
// * ./gitea -c help /dev/null help
 | 
			
		||||
// * ./gitea help -c /dev/null
 | 
			
		||||
// * ./gitea help -c /dev/null web
 | 
			
		||||
// * ./gitea help web -c /dev/null
 | 
			
		||||
// * ./gitea web help -c /dev/null
 | 
			
		||||
// * ./gitea web -h -c /dev/null
 | 
			
		||||
func cliHelpPrinterNew(out io.Writer, templ string, data any) {
 | 
			
		||||
	cmd, _ := data.(*cli.Command)
 | 
			
		||||
	if cmd != nil {
 | 
			
		||||
		prepareWorkPathAndCustomConf(cmd)
 | 
			
		||||
// cmdHelp is our own help subcommand with more information
 | 
			
		||||
// Keep in mind that the "./gitea help"(subcommand) is different from "./gitea --help"(flag), the flag doesn't parse the config or output "DEFAULT CONFIGURATION:" information
 | 
			
		||||
func cmdHelp() *cli.Command {
 | 
			
		||||
	c := &cli.Command{
 | 
			
		||||
		Name:      "help",
 | 
			
		||||
		Aliases:   []string{"h"},
 | 
			
		||||
		Usage:     "Shows a list of commands or help for one command",
 | 
			
		||||
		ArgsUsage: "[command]",
 | 
			
		||||
		Action: func(c *cli.Context) (err error) {
 | 
			
		||||
			lineage := c.Lineage() // The order is from child to parent: help, doctor, Gitea, {Command:nil}
 | 
			
		||||
			targetCmdIdx := 0
 | 
			
		||||
			if c.Command.Name == "help" {
 | 
			
		||||
				targetCmdIdx = 1
 | 
			
		||||
			}
 | 
			
		||||
	cliHelpPrinterOld(out, templ, data)
 | 
			
		||||
	if setting.CustomConf != "" {
 | 
			
		||||
		_, _ = fmt.Fprintf(out, `
 | 
			
		||||
			if lineage[targetCmdIdx+1].Command != nil {
 | 
			
		||||
				err = cli.ShowCommandHelp(lineage[targetCmdIdx+1], lineage[targetCmdIdx].Command.Name)
 | 
			
		||||
			} else {
 | 
			
		||||
				err = cli.ShowAppHelp(c)
 | 
			
		||||
			}
 | 
			
		||||
			_, _ = fmt.Fprintf(c.App.Writer, `
 | 
			
		||||
DEFAULT CONFIGURATION:
 | 
			
		||||
   AppPath:    %s
 | 
			
		||||
   WorkPath:   %s
 | 
			
		||||
@@ -45,38 +41,75 @@ DEFAULT CONFIGURATION:
 | 
			
		||||
   ConfigFile: %s
 | 
			
		||||
 | 
			
		||||
`, setting.AppPath, setting.AppWorkPath, setting.CustomPath, setting.CustomConf)
 | 
			
		||||
			return err
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
	return c
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func appGlobalFlags() []cli.Flag {
 | 
			
		||||
	return []cli.Flag{
 | 
			
		||||
		// make the builtin flags at the top
 | 
			
		||||
		cli.HelpFlag,
 | 
			
		||||
 | 
			
		||||
		// shared configuration flags, they are for global and for each sub-command at the same time
 | 
			
		||||
		// eg: such command is valid: "./gitea --config /tmp/app.ini web --config /tmp/app.ini", while it's discouraged indeed
 | 
			
		||||
		// keep in mind that the short flags like "-C", "-c" and "-w" are globally polluted, they can't be used for sub-commands anymore.
 | 
			
		||||
		&cli.StringFlag{
 | 
			
		||||
			Name:    "custom-path",
 | 
			
		||||
			Aliases: []string{"C"},
 | 
			
		||||
			Usage:   "Set custom path (defaults to '{WorkPath}/custom')",
 | 
			
		||||
		},
 | 
			
		||||
		&cli.StringFlag{
 | 
			
		||||
			Name:    "config",
 | 
			
		||||
			Aliases: []string{"c"},
 | 
			
		||||
			Value:   setting.CustomConf,
 | 
			
		||||
			Usage:   "Set custom config file (defaults to '{WorkPath}/custom/conf/app.ini')",
 | 
			
		||||
		},
 | 
			
		||||
		&cli.StringFlag{
 | 
			
		||||
			Name:    "work-path",
 | 
			
		||||
			Aliases: []string{"w"},
 | 
			
		||||
			Usage:   "Set Gitea's working path (defaults to the Gitea's binary directory)",
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func prepareSubcommandWithGlobalFlags(originCmd *cli.Command) {
 | 
			
		||||
	originBefore := originCmd.Before
 | 
			
		||||
	originCmd.Before = func(ctxOrig context.Context, cmd *cli.Command) (ctx context.Context, err error) {
 | 
			
		||||
		ctx = ctxOrig
 | 
			
		||||
		if originBefore != nil {
 | 
			
		||||
			ctx, err = originBefore(ctx, cmd)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return ctx, err
 | 
			
		||||
func prepareSubcommandWithConfig(command *cli.Command, globalFlags []cli.Flag) {
 | 
			
		||||
	command.Flags = append(append([]cli.Flag{}, globalFlags...), command.Flags...)
 | 
			
		||||
	command.Action = prepareWorkPathAndCustomConf(command.Action)
 | 
			
		||||
	command.HideHelp = true
 | 
			
		||||
	if command.Name != "help" {
 | 
			
		||||
		command.Subcommands = append(command.Subcommands, cmdHelp())
 | 
			
		||||
	}
 | 
			
		||||
		}
 | 
			
		||||
		prepareWorkPathAndCustomConf(cmd)
 | 
			
		||||
		return ctx, nil
 | 
			
		||||
	for i := range command.Subcommands {
 | 
			
		||||
		prepareSubcommandWithConfig(command.Subcommands[i], globalFlags)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// prepareWorkPathAndCustomConf tries to prepare the work path, custom path and custom config from various inputs:
 | 
			
		||||
// command line flags, environment variables, config file
 | 
			
		||||
func prepareWorkPathAndCustomConf(cmd *cli.Command) {
 | 
			
		||||
// prepareWorkPathAndCustomConf wraps the Action to prepare the work path and custom config
 | 
			
		||||
// It can't use "Before", because each level's sub-command's Before will be called one by one, so the "init" would be done multiple times
 | 
			
		||||
func prepareWorkPathAndCustomConf(action cli.ActionFunc) func(ctx *cli.Context) error {
 | 
			
		||||
	return func(ctx *cli.Context) error {
 | 
			
		||||
		var args setting.ArgWorkPathAndCustomConf
 | 
			
		||||
	if cmd.IsSet("work-path") {
 | 
			
		||||
		args.WorkPath = cmd.String("work-path")
 | 
			
		||||
		// from children to parent, check the global flags
 | 
			
		||||
		for _, curCtx := range ctx.Lineage() {
 | 
			
		||||
			if curCtx.IsSet("work-path") && args.WorkPath == "" {
 | 
			
		||||
				args.WorkPath = curCtx.String("work-path")
 | 
			
		||||
			}
 | 
			
		||||
	if cmd.IsSet("custom-path") {
 | 
			
		||||
		args.CustomPath = cmd.String("custom-path")
 | 
			
		||||
			if curCtx.IsSet("custom-path") && args.CustomPath == "" {
 | 
			
		||||
				args.CustomPath = curCtx.String("custom-path")
 | 
			
		||||
			}
 | 
			
		||||
			if curCtx.IsSet("config") && args.CustomConf == "" {
 | 
			
		||||
				args.CustomConf = curCtx.String("config")
 | 
			
		||||
			}
 | 
			
		||||
	if cmd.IsSet("config") {
 | 
			
		||||
		args.CustomConf = cmd.String("config")
 | 
			
		||||
		}
 | 
			
		||||
		setting.InitWorkPathAndCommonConfig(os.Getenv, args)
 | 
			
		||||
		if ctx.Bool("help") || action == nil {
 | 
			
		||||
			// the default behavior of "urfave/cli": "nil action" means "show help"
 | 
			
		||||
			return cmdHelp().Action(ctx)
 | 
			
		||||
		}
 | 
			
		||||
		return action(ctx)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type AppVersion struct {
 | 
			
		||||
@@ -84,36 +117,18 @@ type AppVersion struct {
 | 
			
		||||
	Extra   string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewMainApp(appVer AppVersion) *cli.Command {
 | 
			
		||||
	app := &cli.Command{}
 | 
			
		||||
	app.Name = "gitea" // must be lower-cased because it appears in the "USAGE" section like "gitea doctor [command [command options]]"
 | 
			
		||||
func NewMainApp(appVer AppVersion) *cli.App {
 | 
			
		||||
	app := cli.NewApp()
 | 
			
		||||
	app.Name = "Gitea"
 | 
			
		||||
	app.HelpName = "gitea"
 | 
			
		||||
	app.Usage = "A painless self-hosted Git service"
 | 
			
		||||
	app.Description = `Gitea program contains "web" and other subcommands. If no subcommand is given, it starts the web server by default. Use "web" subcommand for more web server arguments, use other subcommands for other purposes.`
 | 
			
		||||
	app.Version = appVer.Version + appVer.Extra
 | 
			
		||||
	app.EnableShellCompletion = true
 | 
			
		||||
	app.Flags = []cli.Flag{
 | 
			
		||||
		&cli.StringFlag{
 | 
			
		||||
			Name:      "work-path",
 | 
			
		||||
			Aliases:   []string{"w"},
 | 
			
		||||
			TakesFile: true,
 | 
			
		||||
			Usage:     "Set Gitea's working path (defaults to the Gitea's binary directory)",
 | 
			
		||||
		},
 | 
			
		||||
		&cli.StringFlag{
 | 
			
		||||
			Name:      "config",
 | 
			
		||||
			Aliases:   []string{"c"},
 | 
			
		||||
			TakesFile: true,
 | 
			
		||||
			Value:     setting.CustomConf,
 | 
			
		||||
			Usage:     "Set custom config file (defaults to '{WorkPath}/custom/conf/app.ini')",
 | 
			
		||||
		},
 | 
			
		||||
		&cli.StringFlag{
 | 
			
		||||
			Name:      "custom-path",
 | 
			
		||||
			Aliases:   []string{"C"},
 | 
			
		||||
			TakesFile: true,
 | 
			
		||||
			Usage:     "Set custom path (defaults to '{WorkPath}/custom')",
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
	// these sub-commands need to use a config file
 | 
			
		||||
	app.EnableBashCompletion = true
 | 
			
		||||
 | 
			
		||||
	// these sub-commands need to use config file
 | 
			
		||||
	subCmdWithConfig := []*cli.Command{
 | 
			
		||||
		cmdHelp(), // the "help" sub-command was used to show the more information for "work path" and "custom config"
 | 
			
		||||
		CmdWeb,
 | 
			
		||||
		CmdServ,
 | 
			
		||||
		CmdHook,
 | 
			
		||||
@@ -132,31 +147,29 @@ func NewMainApp(appVer AppVersion) *cli.Command {
 | 
			
		||||
 | 
			
		||||
	// these sub-commands do not need the config file, and they do not depend on any path or environment variable.
 | 
			
		||||
	subCmdStandalone := []*cli.Command{
 | 
			
		||||
		cmdConfig(),
 | 
			
		||||
		cmdCert(),
 | 
			
		||||
		CmdCert,
 | 
			
		||||
		CmdGenerate,
 | 
			
		||||
		CmdDocs,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// TODO: we should eventually drop the default command,
 | 
			
		||||
	// but not sure whether it would break Windows users who used to double-click the EXE to run.
 | 
			
		||||
	app.DefaultCommand = CmdWeb.Name
 | 
			
		||||
 | 
			
		||||
	globalFlags := appGlobalFlags()
 | 
			
		||||
	app.Flags = append(app.Flags, cli.VersionFlag)
 | 
			
		||||
	app.Flags = append(app.Flags, globalFlags...)
 | 
			
		||||
	app.HideHelp = true // use our own help action to show helps (with more information like default config)
 | 
			
		||||
	app.Before = PrepareConsoleLoggerLevel(log.INFO)
 | 
			
		||||
	for i := range subCmdWithConfig {
 | 
			
		||||
		prepareSubcommandWithGlobalFlags(subCmdWithConfig[i])
 | 
			
		||||
		prepareSubcommandWithConfig(subCmdWithConfig[i], globalFlags)
 | 
			
		||||
	}
 | 
			
		||||
	app.Commands = append(app.Commands, subCmdWithConfig...)
 | 
			
		||||
	app.Commands = append(app.Commands, subCmdStandalone...)
 | 
			
		||||
 | 
			
		||||
	setting.InitGiteaEnvVars()
 | 
			
		||||
	return app
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func RunMainApp(app *cli.Command, args ...string) error {
 | 
			
		||||
	ctx, cancel := installSignals()
 | 
			
		||||
	defer cancel()
 | 
			
		||||
	err := app.Run(ctx, args)
 | 
			
		||||
func RunMainApp(app *cli.App, args ...string) error {
 | 
			
		||||
	err := app.Run(args)
 | 
			
		||||
	if err == nil {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										141
									
								
								cmd/main_test.go
									
									
									
									
									
								
							
							
						
						
									
										141
									
								
								cmd/main_test.go
									
									
									
									
									
								
							@@ -4,10 +4,9 @@
 | 
			
		||||
package cmd
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"testing"
 | 
			
		||||
@@ -15,10 +14,9 @@ import (
 | 
			
		||||
	"code.gitea.io/gitea/models/unittest"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	"code.gitea.io/gitea/modules/test"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
 | 
			
		||||
	"github.com/stretchr/testify/assert"
 | 
			
		||||
	"github.com/urfave/cli/v3"
 | 
			
		||||
	"github.com/urfave/cli/v2"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestMain(m *testing.M) {
 | 
			
		||||
@@ -29,11 +27,11 @@ func makePathOutput(workPath, customPath, customConf string) string {
 | 
			
		||||
	return fmt.Sprintf("WorkPath=%s\nCustomPath=%s\nCustomConf=%s", workPath, customPath, customConf)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func newTestApp(testCmd cli.Command) *cli.Command {
 | 
			
		||||
func newTestApp(testCmdAction func(ctx *cli.Context) error) *cli.App {
 | 
			
		||||
	app := NewMainApp(AppVersion{})
 | 
			
		||||
	testCmd.Name = util.IfZero(testCmd.Name, "test-cmd")
 | 
			
		||||
	prepareSubcommandWithGlobalFlags(&testCmd)
 | 
			
		||||
	app.Commands = append(app.Commands, &testCmd)
 | 
			
		||||
	testCmd := &cli.Command{Name: "test-cmd", Action: testCmdAction}
 | 
			
		||||
	prepareSubcommandWithConfig(testCmd, appGlobalFlags())
 | 
			
		||||
	app.Commands = append(app.Commands, testCmd)
 | 
			
		||||
	app.DefaultCommand = testCmd.Name
 | 
			
		||||
	return app
 | 
			
		||||
}
 | 
			
		||||
@@ -44,7 +42,7 @@ type runResult struct {
 | 
			
		||||
	ExitCode int
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func runTestApp(app *cli.Command, args ...string) (runResult, error) {
 | 
			
		||||
func runTestApp(app *cli.App, args ...string) (runResult, error) {
 | 
			
		||||
	outBuf := new(strings.Builder)
 | 
			
		||||
	errBuf := new(strings.Builder)
 | 
			
		||||
	app.Writer = outBuf
 | 
			
		||||
@@ -67,7 +65,7 @@ func TestCliCmd(t *testing.T) {
 | 
			
		||||
	defaultCustomConf := filepath.Join(defaultCustomPath, "conf/app.ini")
 | 
			
		||||
 | 
			
		||||
	cli.CommandHelpTemplate = "(command help template)"
 | 
			
		||||
	cli.RootCommandHelpTemplate = "(app help template)"
 | 
			
		||||
	cli.AppHelpTemplate = "(app help template)"
 | 
			
		||||
	cli.SubcommandHelpTemplate = "(subcommand help template)"
 | 
			
		||||
 | 
			
		||||
	cases := []struct {
 | 
			
		||||
@@ -75,56 +73,12 @@ func TestCliCmd(t *testing.T) {
 | 
			
		||||
		cmd string
 | 
			
		||||
		exp string
 | 
			
		||||
	}{
 | 
			
		||||
		// help commands
 | 
			
		||||
		{
 | 
			
		||||
			cmd: "./gitea -h",
 | 
			
		||||
			exp: "DEFAULT CONFIGURATION:",
 | 
			
		||||
		},
 | 
			
		||||
		// main command help
 | 
			
		||||
		{
 | 
			
		||||
			cmd: "./gitea help",
 | 
			
		||||
			exp: "DEFAULT CONFIGURATION:",
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		{
 | 
			
		||||
			cmd: "./gitea -c /dev/null -h",
 | 
			
		||||
			exp: "ConfigFile: /dev/null",
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		{
 | 
			
		||||
			cmd: "./gitea -c /dev/null help",
 | 
			
		||||
			exp: "ConfigFile: /dev/null",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			cmd: "./gitea help -c /dev/null",
 | 
			
		||||
			exp: "ConfigFile: /dev/null",
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		{
 | 
			
		||||
			cmd: "./gitea -c /dev/null test-cmd -h",
 | 
			
		||||
			exp: "ConfigFile: /dev/null",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			cmd: "./gitea test-cmd -c /dev/null -h",
 | 
			
		||||
			exp: "ConfigFile: /dev/null",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			cmd: "./gitea test-cmd -h -c /dev/null",
 | 
			
		||||
			exp: "ConfigFile: /dev/null",
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		{
 | 
			
		||||
			cmd: "./gitea -c /dev/null test-cmd help",
 | 
			
		||||
			exp: "ConfigFile: /dev/null",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			cmd: "./gitea test-cmd -c /dev/null help",
 | 
			
		||||
			exp: "ConfigFile: /dev/null",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			cmd: "./gitea test-cmd help -c /dev/null",
 | 
			
		||||
			exp: "ConfigFile: /dev/null",
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		// parse paths
 | 
			
		||||
		{
 | 
			
		||||
			cmd: "./gitea test-cmd",
 | 
			
		||||
@@ -155,75 +109,70 @@ func TestCliCmd(t *testing.T) {
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, c := range cases {
 | 
			
		||||
		t.Run(c.cmd, func(t *testing.T) {
 | 
			
		||||
			app := newTestApp(cli.Command{
 | 
			
		||||
				Action: func(ctx context.Context, cmd *cli.Command) error {
 | 
			
		||||
					_, _ = fmt.Fprint(cmd.Root().Writer, makePathOutput(setting.AppWorkPath, setting.CustomPath, setting.CustomConf))
 | 
			
		||||
	app := newTestApp(func(ctx *cli.Context) error {
 | 
			
		||||
		_, _ = fmt.Fprint(ctx.App.Writer, makePathOutput(setting.AppWorkPath, setting.CustomPath, setting.CustomConf))
 | 
			
		||||
		return nil
 | 
			
		||||
				},
 | 
			
		||||
	})
 | 
			
		||||
	var envBackup []string
 | 
			
		||||
	for _, s := range os.Environ() {
 | 
			
		||||
		if strings.HasPrefix(s, "GITEA_") && strings.Contains(s, "=") {
 | 
			
		||||
			envBackup = append(envBackup, s)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	clearGiteaEnv := func() {
 | 
			
		||||
		for _, s := range os.Environ() {
 | 
			
		||||
			if strings.HasPrefix(s, "GITEA_") {
 | 
			
		||||
				_ = os.Unsetenv(s)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	defer func() {
 | 
			
		||||
		clearGiteaEnv()
 | 
			
		||||
		for _, s := range envBackup {
 | 
			
		||||
			k, v, _ := strings.Cut(s, "=")
 | 
			
		||||
			_ = os.Setenv(k, v)
 | 
			
		||||
		}
 | 
			
		||||
	}()
 | 
			
		||||
 | 
			
		||||
	for _, c := range cases {
 | 
			
		||||
		clearGiteaEnv()
 | 
			
		||||
		for k, v := range c.env {
 | 
			
		||||
				t.Setenv(k, v)
 | 
			
		||||
			_ = os.Setenv(k, v)
 | 
			
		||||
		}
 | 
			
		||||
		args := strings.Split(c.cmd, " ") // for test only, "split" is good enough
 | 
			
		||||
		r, err := runTestApp(app, args...)
 | 
			
		||||
		assert.NoError(t, err, c.cmd)
 | 
			
		||||
		assert.NotEmpty(t, c.exp, c.cmd)
 | 
			
		||||
		assert.Contains(t, r.Stdout, c.exp, c.cmd)
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestCliCmdError(t *testing.T) {
 | 
			
		||||
	app := newTestApp(cli.Command{Action: func(ctx context.Context, cmd *cli.Command) error { return errors.New("normal error") }})
 | 
			
		||||
	app := newTestApp(func(ctx *cli.Context) error { return fmt.Errorf("normal error") })
 | 
			
		||||
	r, err := runTestApp(app, "./gitea", "test-cmd")
 | 
			
		||||
	assert.Error(t, err)
 | 
			
		||||
	assert.Equal(t, 1, r.ExitCode)
 | 
			
		||||
	assert.Empty(t, r.Stdout)
 | 
			
		||||
	assert.Equal(t, "", r.Stdout)
 | 
			
		||||
	assert.Equal(t, "Command error: normal error\n", r.Stderr)
 | 
			
		||||
 | 
			
		||||
	app = newTestApp(cli.Command{Action: func(ctx context.Context, cmd *cli.Command) error { return cli.Exit("exit error", 2) }})
 | 
			
		||||
	app = newTestApp(func(ctx *cli.Context) error { return cli.Exit("exit error", 2) })
 | 
			
		||||
	r, err = runTestApp(app, "./gitea", "test-cmd")
 | 
			
		||||
	assert.Error(t, err)
 | 
			
		||||
	assert.Equal(t, 2, r.ExitCode)
 | 
			
		||||
	assert.Empty(t, r.Stdout)
 | 
			
		||||
	assert.Equal(t, "", r.Stdout)
 | 
			
		||||
	assert.Equal(t, "exit error\n", r.Stderr)
 | 
			
		||||
 | 
			
		||||
	app = newTestApp(cli.Command{Action: func(ctx context.Context, cmd *cli.Command) error { return nil }})
 | 
			
		||||
	app = newTestApp(func(ctx *cli.Context) error { return nil })
 | 
			
		||||
	r, err = runTestApp(app, "./gitea", "test-cmd", "--no-such")
 | 
			
		||||
	assert.Error(t, err)
 | 
			
		||||
	assert.Equal(t, 1, r.ExitCode)
 | 
			
		||||
	assert.Empty(t, r.Stdout)
 | 
			
		||||
	assert.Equal(t, "Incorrect Usage: flag provided but not defined: -no-such\n\n", r.Stderr)
 | 
			
		||||
	assert.Equal(t, "Incorrect Usage: flag provided but not defined: -no-such\n\n", r.Stdout)
 | 
			
		||||
	assert.Equal(t, "", r.Stderr) // the cli package's strange behavior, the error message is not in stderr ....
 | 
			
		||||
 | 
			
		||||
	app = newTestApp(cli.Command{Action: func(ctx context.Context, cmd *cli.Command) error { return nil }})
 | 
			
		||||
	app = newTestApp(func(ctx *cli.Context) error { return nil })
 | 
			
		||||
	r, err = runTestApp(app, "./gitea", "test-cmd")
 | 
			
		||||
	assert.NoError(t, err)
 | 
			
		||||
	assert.Equal(t, -1, r.ExitCode) // the cli.OsExiter is not called
 | 
			
		||||
	assert.Empty(t, r.Stdout)
 | 
			
		||||
	assert.Empty(t, r.Stderr)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestCliCmdBefore(t *testing.T) {
 | 
			
		||||
	ctxNew := context.WithValue(context.Background(), any("key"), "value")
 | 
			
		||||
	configValues := map[string]string{}
 | 
			
		||||
	setting.CustomConf = "/tmp/any.ini"
 | 
			
		||||
	var actionCtx context.Context
 | 
			
		||||
	app := newTestApp(cli.Command{
 | 
			
		||||
		Before: func(context.Context, *cli.Command) (context.Context, error) {
 | 
			
		||||
			configValues["before"] = setting.CustomConf
 | 
			
		||||
			return ctxNew, nil
 | 
			
		||||
		},
 | 
			
		||||
		Action: func(ctx context.Context, cmd *cli.Command) error {
 | 
			
		||||
			configValues["action"] = setting.CustomConf
 | 
			
		||||
			actionCtx = ctx
 | 
			
		||||
			return nil
 | 
			
		||||
		},
 | 
			
		||||
	})
 | 
			
		||||
	_, err := runTestApp(app, "./gitea", "--config", "/dev/null", "test-cmd")
 | 
			
		||||
	assert.NoError(t, err)
 | 
			
		||||
	assert.Equal(t, ctxNew, actionCtx)
 | 
			
		||||
	assert.Equal(t, "/tmp/any.ini", configValues["before"], "BeforeFunc must be called before preparing config")
 | 
			
		||||
	assert.Equal(t, "/dev/null", configValues["action"])
 | 
			
		||||
	assert.Equal(t, "", r.Stdout)
 | 
			
		||||
	assert.Equal(t, "", r.Stderr)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,13 +4,12 @@
 | 
			
		||||
package cmd
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"os"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/modules/private"
 | 
			
		||||
 | 
			
		||||
	"github.com/urfave/cli/v3"
 | 
			
		||||
	"github.com/urfave/cli/v2"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
@@ -19,7 +18,7 @@ var (
 | 
			
		||||
		Name:        "manager",
 | 
			
		||||
		Usage:       "Manage the running gitea process",
 | 
			
		||||
		Description: "This is a command for managing the running gitea process",
 | 
			
		||||
		Commands: []*cli.Command{
 | 
			
		||||
		Subcommands: []*cli.Command{
 | 
			
		||||
			subcmdShutdown,
 | 
			
		||||
			subcmdRestart,
 | 
			
		||||
			subcmdReloadTemplates,
 | 
			
		||||
@@ -109,31 +108,46 @@ var (
 | 
			
		||||
	}
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func runShutdown(ctx context.Context, c *cli.Command) error {
 | 
			
		||||
func runShutdown(c *cli.Context) error {
 | 
			
		||||
	ctx, cancel := installSignals()
 | 
			
		||||
	defer cancel()
 | 
			
		||||
 | 
			
		||||
	setup(ctx, c.Bool("debug"))
 | 
			
		||||
	extra := private.Shutdown(ctx)
 | 
			
		||||
	return handleCliResponseExtra(extra)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func runRestart(ctx context.Context, c *cli.Command) error {
 | 
			
		||||
func runRestart(c *cli.Context) error {
 | 
			
		||||
	ctx, cancel := installSignals()
 | 
			
		||||
	defer cancel()
 | 
			
		||||
 | 
			
		||||
	setup(ctx, c.Bool("debug"))
 | 
			
		||||
	extra := private.Restart(ctx)
 | 
			
		||||
	return handleCliResponseExtra(extra)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func runReloadTemplates(ctx context.Context, c *cli.Command) error {
 | 
			
		||||
func runReloadTemplates(c *cli.Context) error {
 | 
			
		||||
	ctx, cancel := installSignals()
 | 
			
		||||
	defer cancel()
 | 
			
		||||
 | 
			
		||||
	setup(ctx, c.Bool("debug"))
 | 
			
		||||
	extra := private.ReloadTemplates(ctx)
 | 
			
		||||
	return handleCliResponseExtra(extra)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func runFlushQueues(ctx context.Context, c *cli.Command) error {
 | 
			
		||||
func runFlushQueues(c *cli.Context) error {
 | 
			
		||||
	ctx, cancel := installSignals()
 | 
			
		||||
	defer cancel()
 | 
			
		||||
 | 
			
		||||
	setup(ctx, c.Bool("debug"))
 | 
			
		||||
	extra := private.FlushQueues(ctx, c.Duration("timeout"), c.Bool("non-blocking"))
 | 
			
		||||
	return handleCliResponseExtra(extra)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func runProcesses(ctx context.Context, c *cli.Command) error {
 | 
			
		||||
func runProcesses(c *cli.Context) error {
 | 
			
		||||
	ctx, cancel := installSignals()
 | 
			
		||||
	defer cancel()
 | 
			
		||||
 | 
			
		||||
	setup(ctx, c.Bool("debug"))
 | 
			
		||||
	extra := private.Processes(ctx, os.Stdout, c.Bool("flat"), c.Bool("no-system"), c.Bool("stacktraces"), c.Bool("json"), c.String("cancel"))
 | 
			
		||||
	return handleCliResponseExtra(extra)
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,6 @@
 | 
			
		||||
package cmd
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"os"
 | 
			
		||||
@@ -12,7 +11,7 @@ import (
 | 
			
		||||
	"code.gitea.io/gitea/modules/log"
 | 
			
		||||
	"code.gitea.io/gitea/modules/private"
 | 
			
		||||
 | 
			
		||||
	"github.com/urfave/cli/v3"
 | 
			
		||||
	"github.com/urfave/cli/v2"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
@@ -61,7 +60,7 @@ var (
 | 
			
		||||
	subcmdLogging = &cli.Command{
 | 
			
		||||
		Name:  "logging",
 | 
			
		||||
		Usage: "Adjust logging commands",
 | 
			
		||||
		Commands: []*cli.Command{
 | 
			
		||||
		Subcommands: []*cli.Command{
 | 
			
		||||
			{
 | 
			
		||||
				Name:  "pause",
 | 
			
		||||
				Usage: "Pause logging (Gitea will buffer logs up to a certain point and will drop them after that point)",
 | 
			
		||||
@@ -105,7 +104,7 @@ var (
 | 
			
		||||
			}, {
 | 
			
		||||
				Name:  "add",
 | 
			
		||||
				Usage: "Add a logger",
 | 
			
		||||
				Commands: []*cli.Command{
 | 
			
		||||
				Subcommands: []*cli.Command{
 | 
			
		||||
					{
 | 
			
		||||
						Name:  "file",
 | 
			
		||||
						Usage: "Add a file logger",
 | 
			
		||||
@@ -119,6 +118,7 @@ var (
 | 
			
		||||
								Name:    "rotate",
 | 
			
		||||
								Aliases: []string{"r"},
 | 
			
		||||
								Usage:   "Rotate logs",
 | 
			
		||||
								Value:   true,
 | 
			
		||||
							},
 | 
			
		||||
							&cli.Int64Flag{
 | 
			
		||||
								Name:    "max-size",
 | 
			
		||||
@@ -129,6 +129,7 @@ var (
 | 
			
		||||
								Name:    "daily",
 | 
			
		||||
								Aliases: []string{"d"},
 | 
			
		||||
								Usage:   "Rotate logs daily",
 | 
			
		||||
								Value:   true,
 | 
			
		||||
							},
 | 
			
		||||
							&cli.IntFlag{
 | 
			
		||||
								Name:    "max-days",
 | 
			
		||||
@@ -139,6 +140,7 @@ var (
 | 
			
		||||
								Name:    "compress",
 | 
			
		||||
								Aliases: []string{"z"},
 | 
			
		||||
								Usage:   "Compress rotated logs",
 | 
			
		||||
								Value:   true,
 | 
			
		||||
							},
 | 
			
		||||
							&cli.IntFlag{
 | 
			
		||||
								Name:    "compression-level",
 | 
			
		||||
@@ -193,7 +195,10 @@ var (
 | 
			
		||||
	}
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func runRemoveLogger(ctx context.Context, c *cli.Command) error {
 | 
			
		||||
func runRemoveLogger(c *cli.Context) error {
 | 
			
		||||
	ctx, cancel := installSignals()
 | 
			
		||||
	defer cancel()
 | 
			
		||||
 | 
			
		||||
	setup(ctx, c.Bool("debug"))
 | 
			
		||||
	logger := c.String("logger")
 | 
			
		||||
	if len(logger) == 0 {
 | 
			
		||||
@@ -205,7 +210,10 @@ func runRemoveLogger(ctx context.Context, c *cli.Command) error {
 | 
			
		||||
	return handleCliResponseExtra(extra)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func runAddConnLogger(ctx context.Context, c *cli.Command) error {
 | 
			
		||||
func runAddConnLogger(c *cli.Context) error {
 | 
			
		||||
	ctx, cancel := installSignals()
 | 
			
		||||
	defer cancel()
 | 
			
		||||
 | 
			
		||||
	setup(ctx, c.Bool("debug"))
 | 
			
		||||
	vals := map[string]any{}
 | 
			
		||||
	mode := "conn"
 | 
			
		||||
@@ -229,10 +237,13 @@ func runAddConnLogger(ctx context.Context, c *cli.Command) error {
 | 
			
		||||
	if c.IsSet("reconnect-on-message") {
 | 
			
		||||
		vals["reconnectOnMsg"] = c.Bool("reconnect-on-message")
 | 
			
		||||
	}
 | 
			
		||||
	return commonAddLogger(ctx, c, mode, vals)
 | 
			
		||||
	return commonAddLogger(c, mode, vals)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func runAddFileLogger(ctx context.Context, c *cli.Command) error {
 | 
			
		||||
func runAddFileLogger(c *cli.Context) error {
 | 
			
		||||
	ctx, cancel := installSignals()
 | 
			
		||||
	defer cancel()
 | 
			
		||||
 | 
			
		||||
	setup(ctx, c.Bool("debug"))
 | 
			
		||||
	vals := map[string]any{}
 | 
			
		||||
	mode := "file"
 | 
			
		||||
@@ -259,10 +270,10 @@ func runAddFileLogger(ctx context.Context, c *cli.Command) error {
 | 
			
		||||
	if c.IsSet("compression-level") {
 | 
			
		||||
		vals["compressionLevel"] = c.Int("compression-level")
 | 
			
		||||
	}
 | 
			
		||||
	return commonAddLogger(ctx, c, mode, vals)
 | 
			
		||||
	return commonAddLogger(c, mode, vals)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func commonAddLogger(ctx context.Context, c *cli.Command, mode string, vals map[string]any) error {
 | 
			
		||||
func commonAddLogger(c *cli.Context, mode string, vals map[string]any) error {
 | 
			
		||||
	if len(c.String("level")) > 0 {
 | 
			
		||||
		vals["level"] = log.LevelFromString(c.String("level")).String()
 | 
			
		||||
	}
 | 
			
		||||
@@ -289,33 +300,46 @@ func commonAddLogger(ctx context.Context, c *cli.Command, mode string, vals map[
 | 
			
		||||
	if c.IsSet("writer") {
 | 
			
		||||
		writer = c.String("writer")
 | 
			
		||||
	}
 | 
			
		||||
	ctx, cancel := installSignals()
 | 
			
		||||
	defer cancel()
 | 
			
		||||
 | 
			
		||||
	extra := private.AddLogger(ctx, logger, writer, mode, vals)
 | 
			
		||||
	return handleCliResponseExtra(extra)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func runPauseLogging(ctx context.Context, c *cli.Command) error {
 | 
			
		||||
func runPauseLogging(c *cli.Context) error {
 | 
			
		||||
	ctx, cancel := installSignals()
 | 
			
		||||
	defer cancel()
 | 
			
		||||
 | 
			
		||||
	setup(ctx, c.Bool("debug"))
 | 
			
		||||
	userMsg := private.PauseLogging(ctx)
 | 
			
		||||
	_, _ = fmt.Fprintln(os.Stdout, userMsg)
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func runResumeLogging(ctx context.Context, c *cli.Command) error {
 | 
			
		||||
func runResumeLogging(c *cli.Context) error {
 | 
			
		||||
	ctx, cancel := installSignals()
 | 
			
		||||
	defer cancel()
 | 
			
		||||
 | 
			
		||||
	setup(ctx, c.Bool("debug"))
 | 
			
		||||
	userMsg := private.ResumeLogging(ctx)
 | 
			
		||||
	_, _ = fmt.Fprintln(os.Stdout, userMsg)
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func runReleaseReopenLogging(ctx context.Context, c *cli.Command) error {
 | 
			
		||||
func runReleaseReopenLogging(c *cli.Context) error {
 | 
			
		||||
	ctx, cancel := installSignals()
 | 
			
		||||
	defer cancel()
 | 
			
		||||
 | 
			
		||||
	setup(ctx, c.Bool("debug"))
 | 
			
		||||
	userMsg := private.ReleaseReopenLogging(ctx)
 | 
			
		||||
	_, _ = fmt.Fprintln(os.Stdout, userMsg)
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func runSetLogSQL(ctx context.Context, c *cli.Command) error {
 | 
			
		||||
func runSetLogSQL(c *cli.Context) error {
 | 
			
		||||
	ctx, cancel := installSignals()
 | 
			
		||||
	defer cancel()
 | 
			
		||||
	setup(ctx, c.Bool("debug"))
 | 
			
		||||
 | 
			
		||||
	extra := private.SetLogSQL(ctx, !c.Bool("off"))
 | 
			
		||||
 
 | 
			
		||||
@@ -7,11 +7,11 @@ import (
 | 
			
		||||
	"context"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/models/db"
 | 
			
		||||
	"code.gitea.io/gitea/models/migrations"
 | 
			
		||||
	"code.gitea.io/gitea/modules/log"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	"code.gitea.io/gitea/services/versioned_migration"
 | 
			
		||||
 | 
			
		||||
	"github.com/urfave/cli/v3"
 | 
			
		||||
	"github.com/urfave/cli/v2"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// CmdMigrate represents the available migrate sub-command.
 | 
			
		||||
@@ -22,8 +22,11 @@ var CmdMigrate = &cli.Command{
 | 
			
		||||
	Action:      runMigrate,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func runMigrate(ctx context.Context, c *cli.Command) error {
 | 
			
		||||
	if err := initDB(ctx); err != nil {
 | 
			
		||||
func runMigrate(ctx *cli.Context) error {
 | 
			
		||||
	stdCtx, cancel := installSignals()
 | 
			
		||||
	defer cancel()
 | 
			
		||||
 | 
			
		||||
	if err := initDB(stdCtx); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -33,7 +36,7 @@ func runMigrate(ctx context.Context, c *cli.Command) error {
 | 
			
		||||
	log.Info("Log path: %s", setting.Log.RootPath)
 | 
			
		||||
	log.Info("Configuration file: %s", setting.CustomConf)
 | 
			
		||||
 | 
			
		||||
	if err := db.InitEngineWithMigration(context.Background(), versioned_migration.Migrate); err != nil {
 | 
			
		||||
	if err := db.InitEngineWithMigration(context.Background(), migrations.Migrate); err != nil {
 | 
			
		||||
		log.Fatal("Failed to initialize ORM engine: %v", err)
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -13,6 +13,7 @@ import (
 | 
			
		||||
	actions_model "code.gitea.io/gitea/models/actions"
 | 
			
		||||
	"code.gitea.io/gitea/models/db"
 | 
			
		||||
	git_model "code.gitea.io/gitea/models/git"
 | 
			
		||||
	"code.gitea.io/gitea/models/migrations"
 | 
			
		||||
	packages_model "code.gitea.io/gitea/models/packages"
 | 
			
		||||
	repo_model "code.gitea.io/gitea/models/repo"
 | 
			
		||||
	user_model "code.gitea.io/gitea/models/user"
 | 
			
		||||
@@ -20,9 +21,8 @@ import (
 | 
			
		||||
	packages_module "code.gitea.io/gitea/modules/packages"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	"code.gitea.io/gitea/modules/storage"
 | 
			
		||||
	"code.gitea.io/gitea/services/versioned_migration"
 | 
			
		||||
 | 
			
		||||
	"github.com/urfave/cli/v3"
 | 
			
		||||
	"github.com/urfave/cli/v2"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// CmdMigrateStorage represents the available migrate storage sub-command.
 | 
			
		||||
@@ -196,7 +196,7 @@ func migrateActionsLog(ctx context.Context, dstStorage storage.ObjectStorage) er
 | 
			
		||||
 | 
			
		||||
func migrateActionsArtifacts(ctx context.Context, dstStorage storage.ObjectStorage) error {
 | 
			
		||||
	return db.Iterate(ctx, nil, func(ctx context.Context, artifact *actions_model.ActionArtifact) error {
 | 
			
		||||
		if artifact.Status == actions_model.ArtifactStatusExpired {
 | 
			
		||||
		if artifact.Status == int64(actions_model.ArtifactStatusExpired) {
 | 
			
		||||
			return nil
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
@@ -213,8 +213,11 @@ func migrateActionsArtifacts(ctx context.Context, dstStorage storage.ObjectStora
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func runMigrateStorage(ctx context.Context, cmd *cli.Command) error {
 | 
			
		||||
	if err := initDB(ctx); err != nil {
 | 
			
		||||
func runMigrateStorage(ctx *cli.Context) error {
 | 
			
		||||
	stdCtx, cancel := installSignals()
 | 
			
		||||
	defer cancel()
 | 
			
		||||
 | 
			
		||||
	if err := initDB(stdCtx); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -224,7 +227,7 @@ func runMigrateStorage(ctx context.Context, cmd *cli.Command) error {
 | 
			
		||||
	log.Info("Log path: %s", setting.Log.RootPath)
 | 
			
		||||
	log.Info("Configuration file: %s", setting.CustomConf)
 | 
			
		||||
 | 
			
		||||
	if err := db.InitEngineWithMigration(context.Background(), versioned_migration.Migrate); err != nil {
 | 
			
		||||
	if err := db.InitEngineWithMigration(context.Background(), migrations.Migrate); err != nil {
 | 
			
		||||
		log.Fatal("Failed to initialize ORM engine: %v", err)
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
@@ -235,51 +238,51 @@ func runMigrateStorage(ctx context.Context, cmd *cli.Command) error {
 | 
			
		||||
 | 
			
		||||
	var dstStorage storage.ObjectStorage
 | 
			
		||||
	var err error
 | 
			
		||||
	switch strings.ToLower(cmd.String("storage")) {
 | 
			
		||||
	switch strings.ToLower(ctx.String("storage")) {
 | 
			
		||||
	case "":
 | 
			
		||||
		fallthrough
 | 
			
		||||
	case string(setting.LocalStorageType):
 | 
			
		||||
		p := cmd.String("path")
 | 
			
		||||
		p := ctx.String("path")
 | 
			
		||||
		if p == "" {
 | 
			
		||||
			log.Fatal("Path must be given when storage is local")
 | 
			
		||||
			return nil
 | 
			
		||||
		}
 | 
			
		||||
		dstStorage, err = storage.NewLocalStorage(
 | 
			
		||||
			ctx,
 | 
			
		||||
			stdCtx,
 | 
			
		||||
			&setting.Storage{
 | 
			
		||||
				Path: p,
 | 
			
		||||
			})
 | 
			
		||||
	case string(setting.MinioStorageType):
 | 
			
		||||
		dstStorage, err = storage.NewMinioStorage(
 | 
			
		||||
			ctx,
 | 
			
		||||
			stdCtx,
 | 
			
		||||
			&setting.Storage{
 | 
			
		||||
				MinioConfig: setting.MinioStorageConfig{
 | 
			
		||||
					Endpoint:           cmd.String("minio-endpoint"),
 | 
			
		||||
					AccessKeyID:        cmd.String("minio-access-key-id"),
 | 
			
		||||
					SecretAccessKey:    cmd.String("minio-secret-access-key"),
 | 
			
		||||
					Bucket:             cmd.String("minio-bucket"),
 | 
			
		||||
					Location:           cmd.String("minio-location"),
 | 
			
		||||
					BasePath:           cmd.String("minio-base-path"),
 | 
			
		||||
					UseSSL:             cmd.Bool("minio-use-ssl"),
 | 
			
		||||
					InsecureSkipVerify: cmd.Bool("minio-insecure-skip-verify"),
 | 
			
		||||
					ChecksumAlgorithm:  cmd.String("minio-checksum-algorithm"),
 | 
			
		||||
					BucketLookUpType:   cmd.String("minio-bucket-lookup-type"),
 | 
			
		||||
					Endpoint:           ctx.String("minio-endpoint"),
 | 
			
		||||
					AccessKeyID:        ctx.String("minio-access-key-id"),
 | 
			
		||||
					SecretAccessKey:    ctx.String("minio-secret-access-key"),
 | 
			
		||||
					Bucket:             ctx.String("minio-bucket"),
 | 
			
		||||
					Location:           ctx.String("minio-location"),
 | 
			
		||||
					BasePath:           ctx.String("minio-base-path"),
 | 
			
		||||
					UseSSL:             ctx.Bool("minio-use-ssl"),
 | 
			
		||||
					InsecureSkipVerify: ctx.Bool("minio-insecure-skip-verify"),
 | 
			
		||||
					ChecksumAlgorithm:  ctx.String("minio-checksum-algorithm"),
 | 
			
		||||
					BucketLookUpType:   ctx.String("minio-bucket-lookup-type"),
 | 
			
		||||
				},
 | 
			
		||||
			})
 | 
			
		||||
	case string(setting.AzureBlobStorageType):
 | 
			
		||||
		dstStorage, err = storage.NewAzureBlobStorage(
 | 
			
		||||
			ctx,
 | 
			
		||||
			stdCtx,
 | 
			
		||||
			&setting.Storage{
 | 
			
		||||
				AzureBlobConfig: setting.AzureBlobStorageConfig{
 | 
			
		||||
					Endpoint:    cmd.String("azureblob-endpoint"),
 | 
			
		||||
					AccountName: cmd.String("azureblob-account-name"),
 | 
			
		||||
					AccountKey:  cmd.String("azureblob-account-key"),
 | 
			
		||||
					Container:   cmd.String("azureblob-container"),
 | 
			
		||||
					BasePath:    cmd.String("azureblob-base-path"),
 | 
			
		||||
					Endpoint:    ctx.String("azureblob-endpoint"),
 | 
			
		||||
					AccountName: ctx.String("azureblob-account-name"),
 | 
			
		||||
					AccountKey:  ctx.String("azureblob-account-key"),
 | 
			
		||||
					Container:   ctx.String("azureblob-container"),
 | 
			
		||||
					BasePath:    ctx.String("azureblob-base-path"),
 | 
			
		||||
				},
 | 
			
		||||
			})
 | 
			
		||||
	default:
 | 
			
		||||
		return fmt.Errorf("unsupported storage type: %s", cmd.String("storage"))
 | 
			
		||||
		return fmt.Errorf("unsupported storage type: %s", ctx.String("storage"))
 | 
			
		||||
	}
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
@@ -296,14 +299,14 @@ func runMigrateStorage(ctx context.Context, cmd *cli.Command) error {
 | 
			
		||||
		"actions-artifacts": migrateActionsArtifacts,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	tp := strings.ToLower(cmd.String("type"))
 | 
			
		||||
	tp := strings.ToLower(ctx.String("type"))
 | 
			
		||||
	if m, ok := migratedMethods[tp]; ok {
 | 
			
		||||
		if err := m(ctx, dstStorage); err != nil {
 | 
			
		||||
		if err := m(stdCtx, dstStorage); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		log.Info("%s files have successfully been copied to the new storage.", tp)
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return fmt.Errorf("unsupported storage: %s", cmd.String("type"))
 | 
			
		||||
	return fmt.Errorf("unsupported storage: %s", ctx.String("type"))
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,10 +4,12 @@
 | 
			
		||||
package cmd
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"os"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/models/db"
 | 
			
		||||
	"code.gitea.io/gitea/models/packages"
 | 
			
		||||
	"code.gitea.io/gitea/models/unittest"
 | 
			
		||||
	user_model "code.gitea.io/gitea/models/user"
 | 
			
		||||
@@ -29,7 +31,7 @@ func TestMigratePackages(t *testing.T) {
 | 
			
		||||
	assert.NoError(t, err)
 | 
			
		||||
	defer buf.Close()
 | 
			
		||||
 | 
			
		||||
	v, f, err := packages_service.CreatePackageAndAddFile(t.Context(), &packages_service.PackageCreationInfo{
 | 
			
		||||
	v, f, err := packages_service.CreatePackageAndAddFile(db.DefaultContext, &packages_service.PackageCreationInfo{
 | 
			
		||||
		PackageInfo: packages_service.PackageInfo{
 | 
			
		||||
			Owner:       creator,
 | 
			
		||||
			PackageType: packages.TypeGeneric,
 | 
			
		||||
@@ -51,7 +53,7 @@ func TestMigratePackages(t *testing.T) {
 | 
			
		||||
	assert.NotNil(t, v)
 | 
			
		||||
	assert.NotNil(t, f)
 | 
			
		||||
 | 
			
		||||
	ctx := t.Context()
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
 | 
			
		||||
	p := t.TempDir()
 | 
			
		||||
 | 
			
		||||
@@ -68,6 +70,6 @@ func TestMigratePackages(t *testing.T) {
 | 
			
		||||
	entries, err := os.ReadDir(p)
 | 
			
		||||
	assert.NoError(t, err)
 | 
			
		||||
	assert.Len(t, entries, 2)
 | 
			
		||||
	assert.Equal(t, "01", entries[0].Name())
 | 
			
		||||
	assert.Equal(t, "tmp", entries[1].Name())
 | 
			
		||||
	assert.EqualValues(t, "01", entries[0].Name())
 | 
			
		||||
	assert.EqualValues(t, "tmp", entries[1].Name())
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,13 +4,12 @@
 | 
			
		||||
package cmd
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/modules/private"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
 | 
			
		||||
	"github.com/urfave/cli/v3"
 | 
			
		||||
	"github.com/urfave/cli/v2"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// CmdRestoreRepository represents the available restore a repository sub-command.
 | 
			
		||||
@@ -49,7 +48,10 @@ wiki, issues, labels, releases, release_assets, milestones, pull_requests, comme
 | 
			
		||||
	},
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func runRestoreRepository(ctx context.Context, c *cli.Command) error {
 | 
			
		||||
func runRestoreRepository(c *cli.Context) error {
 | 
			
		||||
	ctx, cancel := installSignals()
 | 
			
		||||
	defer cancel()
 | 
			
		||||
 | 
			
		||||
	setting.MustInstalled()
 | 
			
		||||
	var units []string
 | 
			
		||||
	if s := c.String("units"); s != "" {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										170
									
								
								cmd/serv.go
									
									
									
									
									
								
							
							
						
						
									
										170
									
								
								cmd/serv.go
									
									
									
									
									
								
							@@ -11,16 +11,17 @@ import (
 | 
			
		||||
	"os"
 | 
			
		||||
	"os/exec"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"regexp"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
	"unicode"
 | 
			
		||||
 | 
			
		||||
	asymkey_model "code.gitea.io/gitea/models/asymkey"
 | 
			
		||||
	git_model "code.gitea.io/gitea/models/git"
 | 
			
		||||
	"code.gitea.io/gitea/models/perm"
 | 
			
		||||
	repo_model "code.gitea.io/gitea/models/repo"
 | 
			
		||||
	"code.gitea.io/gitea/modules/container"
 | 
			
		||||
	"code.gitea.io/gitea/modules/git"
 | 
			
		||||
	"code.gitea.io/gitea/modules/git/gitcmd"
 | 
			
		||||
	"code.gitea.io/gitea/modules/json"
 | 
			
		||||
	"code.gitea.io/gitea/modules/lfstransfer"
 | 
			
		||||
	"code.gitea.io/gitea/modules/log"
 | 
			
		||||
@@ -31,8 +32,17 @@ import (
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	"code.gitea.io/gitea/services/lfs"
 | 
			
		||||
 | 
			
		||||
	"github.com/golang-jwt/jwt/v5"
 | 
			
		||||
	"github.com/kballard/go-shellquote"
 | 
			
		||||
	"github.com/urfave/cli/v3"
 | 
			
		||||
	"github.com/urfave/cli/v2"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	verbUploadPack      = "git-upload-pack"
 | 
			
		||||
	verbUploadArchive   = "git-upload-archive"
 | 
			
		||||
	verbReceivePack     = "git-receive-pack"
 | 
			
		||||
	verbLfsAuthenticate = "git-lfs-authenticate"
 | 
			
		||||
	verbLfsTransfer     = "git-lfs-transfer"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// CmdServ represents the available serv sub-command.
 | 
			
		||||
@@ -40,7 +50,6 @@ var CmdServ = &cli.Command{
 | 
			
		||||
	Name:        "serv",
 | 
			
		||||
	Usage:       "(internal) Should only be called by SSH shell",
 | 
			
		||||
	Description: "Serv provides access auth for repositories",
 | 
			
		||||
	Hidden:      true, // Internal commands shouldn't be visible in help
 | 
			
		||||
	Before:      PrepareConsoleLoggerLevel(log.FATAL),
 | 
			
		||||
	Action:      runServ,
 | 
			
		||||
	Flags: []cli.Flag{
 | 
			
		||||
@@ -64,11 +73,27 @@ func setup(ctx context.Context, debug bool) {
 | 
			
		||||
		_ = fail(ctx, "Unable to access repository path", "Unable to access repository path %q, err: %v", setting.RepoRootPath, err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	if err := git.InitSimple(); err != nil {
 | 
			
		||||
	if err := git.InitSimple(context.Background()); err != nil {
 | 
			
		||||
		_ = fail(ctx, "Failed to init git", "Failed to init git, err: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	// keep getAccessMode() in sync
 | 
			
		||||
	allowedCommands = container.SetOf(
 | 
			
		||||
		verbUploadPack,
 | 
			
		||||
		verbUploadArchive,
 | 
			
		||||
		verbReceivePack,
 | 
			
		||||
		verbLfsAuthenticate,
 | 
			
		||||
		verbLfsTransfer,
 | 
			
		||||
	)
 | 
			
		||||
	allowedCommandsLfs = container.SetOf(
 | 
			
		||||
		verbLfsAuthenticate,
 | 
			
		||||
		verbLfsTransfer,
 | 
			
		||||
	)
 | 
			
		||||
	alphaDashDotPattern = regexp.MustCompile(`[^\w-\.]`)
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// fail prints message to stdout, it's mainly used for git serv and git hook commands.
 | 
			
		||||
// The output will be passed to git client and shown to user.
 | 
			
		||||
func fail(ctx context.Context, userMessage, logMsgFmt string, args ...any) error {
 | 
			
		||||
@@ -79,10 +104,7 @@ func fail(ctx context.Context, userMessage, logMsgFmt string, args ...any) error
 | 
			
		||||
	// There appears to be a chance to cause a zombie process and failure to read the Exit status
 | 
			
		||||
	// if nothing is outputted on stdout.
 | 
			
		||||
	_, _ = fmt.Fprintln(os.Stdout, "")
 | 
			
		||||
	// 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:")
 | 
			
		||||
	_, _ = fmt.Fprintln(os.Stderr, "Gitea:", userMessage)
 | 
			
		||||
 | 
			
		||||
	if logMsgFmt != "" {
 | 
			
		||||
		logMsg := fmt.Sprintf(logMsgFmt, args...)
 | 
			
		||||
@@ -114,24 +136,47 @@ func handleCliResponseExtra(extra private.ResponseExtra) error {
 | 
			
		||||
 | 
			
		||||
func getAccessMode(verb, lfsVerb string) perm.AccessMode {
 | 
			
		||||
	switch verb {
 | 
			
		||||
	case git.CmdVerbUploadPack, git.CmdVerbUploadArchive:
 | 
			
		||||
	case verbUploadPack, verbUploadArchive:
 | 
			
		||||
		return perm.AccessModeRead
 | 
			
		||||
	case git.CmdVerbReceivePack:
 | 
			
		||||
	case verbReceivePack:
 | 
			
		||||
		return perm.AccessModeWrite
 | 
			
		||||
	case git.CmdVerbLfsAuthenticate, git.CmdVerbLfsTransfer:
 | 
			
		||||
	case verbLfsAuthenticate, verbLfsTransfer:
 | 
			
		||||
		switch lfsVerb {
 | 
			
		||||
		case git.CmdSubVerbLfsUpload:
 | 
			
		||||
		case "upload":
 | 
			
		||||
			return perm.AccessModeWrite
 | 
			
		||||
		case git.CmdSubVerbLfsDownload:
 | 
			
		||||
		case "download":
 | 
			
		||||
			return perm.AccessModeRead
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	// should be unreachable
 | 
			
		||||
	setting.PanicInDevOrTesting("unknown verb: %s %s", verb, lfsVerb)
 | 
			
		||||
	return perm.AccessModeNone
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func runServ(ctx context.Context, c *cli.Command) error {
 | 
			
		||||
func getLFSAuthToken(ctx context.Context, lfsVerb string, results *private.ServCommandResults) (string, error) {
 | 
			
		||||
	now := time.Now()
 | 
			
		||||
	claims := lfs.Claims{
 | 
			
		||||
		RegisteredClaims: jwt.RegisteredClaims{
 | 
			
		||||
			ExpiresAt: jwt.NewNumericDate(now.Add(setting.LFS.HTTPAuthExpiry)),
 | 
			
		||||
			NotBefore: jwt.NewNumericDate(now),
 | 
			
		||||
		},
 | 
			
		||||
		RepoID: results.RepoID,
 | 
			
		||||
		Op:     lfsVerb,
 | 
			
		||||
		UserID: results.UserID,
 | 
			
		||||
	}
 | 
			
		||||
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
 | 
			
		||||
 | 
			
		||||
	// Sign and get the complete encoded token as a string using the secret
 | 
			
		||||
	tokenString, err := token.SignedString(setting.LFS.JWTSecretBytes)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", fail(ctx, "Failed to sign JWT Token", "Failed to sign JWT token: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
	return fmt.Sprintf("Bearer %s", tokenString), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func runServ(c *cli.Context) error {
 | 
			
		||||
	ctx, cancel := installSignals()
 | 
			
		||||
	defer cancel()
 | 
			
		||||
 | 
			
		||||
	// FIXME: This needs to internationalised
 | 
			
		||||
	setup(ctx, c.Bool("debug"))
 | 
			
		||||
 | 
			
		||||
@@ -182,32 +227,41 @@ func runServ(ctx context.Context, c *cli.Command) error {
 | 
			
		||||
		log.Debug("SSH_ORIGINAL_COMMAND: %s", os.Getenv("SSH_ORIGINAL_COMMAND"))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	sshCmdArgs, err := shellquote.Split(cmd)
 | 
			
		||||
	words, err := shellquote.Split(cmd)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fail(ctx, "Error parsing arguments", "Failed to parse arguments: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(sshCmdArgs) < 2 {
 | 
			
		||||
	if len(words) < 2 {
 | 
			
		||||
		if git.DefaultFeatures().SupportProcReceive {
 | 
			
		||||
			// for AGit Flow
 | 
			
		||||
			if cmd == "ssh_info" {
 | 
			
		||||
				fmt.Print(`{"type":"agit","version":1}`)
 | 
			
		||||
				fmt.Print(`{"type":"gitea","version":1}`)
 | 
			
		||||
				return nil
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		return fail(ctx, "Too few arguments", "Too few arguments in cmd: %s", cmd)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	repoPath := strings.TrimPrefix(sshCmdArgs[1], "/")
 | 
			
		||||
	repoPathFields := strings.SplitN(repoPath, "/", 2)
 | 
			
		||||
	if len(repoPathFields) != 2 {
 | 
			
		||||
	verb := words[0]
 | 
			
		||||
	repoPath := strings.TrimPrefix(words[1], "/")
 | 
			
		||||
 | 
			
		||||
	var lfsVerb string
 | 
			
		||||
 | 
			
		||||
	rr := strings.SplitN(repoPath, "/", 2)
 | 
			
		||||
	if len(rr) != 2 {
 | 
			
		||||
		return fail(ctx, "Invalid repository path", "Invalid repository path: %v", repoPath)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	username := repoPathFields[0]
 | 
			
		||||
	reponame := strings.TrimSuffix(repoPathFields[1], ".git") // “the-repo-name" or "the-repo-name.wiki"
 | 
			
		||||
	username := rr[0]
 | 
			
		||||
	reponame := strings.TrimSuffix(rr[1], ".git")
 | 
			
		||||
 | 
			
		||||
	if !repo_model.IsValidSSHAccessRepoName(reponame) {
 | 
			
		||||
	// LowerCase and trim the repoPath as that's how they are stored.
 | 
			
		||||
	// This should be done after splitting the repoPath into username and reponame
 | 
			
		||||
	// so that username and reponame are not affected.
 | 
			
		||||
	repoPath = strings.ToLower(strings.TrimSpace(repoPath))
 | 
			
		||||
 | 
			
		||||
	if alphaDashDotPattern.MatchString(reponame) {
 | 
			
		||||
		return fail(ctx, "Invalid repo name", "Invalid repo name: %s", reponame)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -229,22 +283,21 @@ func runServ(ctx context.Context, c *cli.Command) error {
 | 
			
		||||
		}()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	verb, lfsVerb := sshCmdArgs[0], ""
 | 
			
		||||
	if !git.IsAllowedVerbForServe(verb) {
 | 
			
		||||
		return fail(ctx, "Unknown git command", "Unknown git command %s", verb)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if git.IsAllowedVerbForServeLfs(verb) {
 | 
			
		||||
	if allowedCommands.Contains(verb) {
 | 
			
		||||
		if allowedCommandsLfs.Contains(verb) {
 | 
			
		||||
			if !setting.LFS.StartServer {
 | 
			
		||||
				return fail(ctx, "LFS Server is not enabled", "")
 | 
			
		||||
			}
 | 
			
		||||
		if verb == git.CmdVerbLfsTransfer && !setting.LFS.AllowPureSSH {
 | 
			
		||||
			if verb == verbLfsTransfer && !setting.LFS.AllowPureSSH {
 | 
			
		||||
				return fail(ctx, "LFS SSH transfer is not enabled", "")
 | 
			
		||||
			}
 | 
			
		||||
		if len(sshCmdArgs) > 2 {
 | 
			
		||||
			lfsVerb = sshCmdArgs[2]
 | 
			
		||||
			if len(words) > 2 {
 | 
			
		||||
				lfsVerb = words[2]
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		return fail(ctx, "Unknown git command", "Unknown git command %s", verb)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	requestedMode := getAccessMode(verb, lfsVerb)
 | 
			
		||||
 | 
			
		||||
@@ -253,16 +306,9 @@ func runServ(ctx context.Context, c *cli.Command) error {
 | 
			
		||||
		return fail(ctx, extra.UserMsg, "ServCommand failed: %s", extra.Error)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// because the original repoPath maybe redirected, we need to use the returned actual repository information
 | 
			
		||||
	if results.IsWiki {
 | 
			
		||||
		repoPath = repo_model.RelativeWikiPath(results.OwnerName, results.RepoName)
 | 
			
		||||
	} else {
 | 
			
		||||
		repoPath = repo_model.RelativePath(results.OwnerName, results.RepoName)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// LFS SSH protocol
 | 
			
		||||
	if verb == git.CmdVerbLfsTransfer {
 | 
			
		||||
		token, err := lfs.GetLFSAuthTokenWithBearer(lfs.AuthTokenOptions{Op: lfsVerb, UserID: results.UserID, RepoID: results.RepoID})
 | 
			
		||||
	if verb == verbLfsTransfer {
 | 
			
		||||
		token, err := getLFSAuthToken(ctx, lfsVerb, results)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
@@ -270,10 +316,10 @@ func runServ(ctx context.Context, c *cli.Command) error {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// LFS token authentication
 | 
			
		||||
	if verb == git.CmdVerbLfsAuthenticate {
 | 
			
		||||
	if verb == verbLfsAuthenticate {
 | 
			
		||||
		url := fmt.Sprintf("%s%s/%s.git/info/lfs", setting.AppURL, url.PathEscape(results.OwnerName), url.PathEscape(results.RepoName))
 | 
			
		||||
 | 
			
		||||
		token, err := lfs.GetLFSAuthTokenWithBearer(lfs.AuthTokenOptions{Op: lfsVerb, UserID: results.UserID, RepoID: results.RepoID})
 | 
			
		||||
		token, err := getLFSAuthToken(ctx, lfsVerb, results)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
@@ -292,8 +338,8 @@ func runServ(ctx context.Context, c *cli.Command) error {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var command *exec.Cmd
 | 
			
		||||
	gitBinPath := filepath.Dir(gitcmd.GitExecutable) // e.g. /usr/bin
 | 
			
		||||
	var gitcmd *exec.Cmd
 | 
			
		||||
	gitBinPath := filepath.Dir(git.GitExecutable) // e.g. /usr/bin
 | 
			
		||||
	gitBinVerb := filepath.Join(gitBinPath, verb) // e.g. /usr/bin/git-upload-pack
 | 
			
		||||
	if _, err := os.Stat(gitBinVerb); err != nil {
 | 
			
		||||
		// if the command "git-upload-pack" doesn't exist, try to split "git-upload-pack" to use the sub-command with git
 | 
			
		||||
@@ -301,21 +347,21 @@ func runServ(ctx context.Context, c *cli.Command) error {
 | 
			
		||||
		verbFields := strings.SplitN(verb, "-", 2)
 | 
			
		||||
		if len(verbFields) == 2 {
 | 
			
		||||
			// use git binary with the sub-command part: "C:\...\bin\git.exe", "upload-pack", ...
 | 
			
		||||
			command = exec.CommandContext(ctx, gitcmd.GitExecutable, verbFields[1], repoPath)
 | 
			
		||||
			gitcmd = exec.CommandContext(ctx, git.GitExecutable, verbFields[1], repoPath)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	if command == nil {
 | 
			
		||||
	if gitcmd == nil {
 | 
			
		||||
		// by default, use the verb (it has been checked above by allowedCommands)
 | 
			
		||||
		command = exec.CommandContext(ctx, gitBinVerb, repoPath)
 | 
			
		||||
		gitcmd = exec.CommandContext(ctx, gitBinVerb, repoPath)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	process.SetSysProcAttribute(command)
 | 
			
		||||
	command.Dir = setting.RepoRootPath
 | 
			
		||||
	command.Stdout = os.Stdout
 | 
			
		||||
	command.Stdin = os.Stdin
 | 
			
		||||
	command.Stderr = os.Stderr
 | 
			
		||||
	command.Env = append(command.Env, os.Environ()...)
 | 
			
		||||
	command.Env = append(command.Env,
 | 
			
		||||
	process.SetSysProcAttribute(gitcmd)
 | 
			
		||||
	gitcmd.Dir = setting.RepoRootPath
 | 
			
		||||
	gitcmd.Stdout = os.Stdout
 | 
			
		||||
	gitcmd.Stdin = os.Stdin
 | 
			
		||||
	gitcmd.Stderr = os.Stderr
 | 
			
		||||
	gitcmd.Env = append(gitcmd.Env, os.Environ()...)
 | 
			
		||||
	gitcmd.Env = append(gitcmd.Env,
 | 
			
		||||
		repo_module.EnvRepoIsWiki+"="+strconv.FormatBool(results.IsWiki),
 | 
			
		||||
		repo_module.EnvRepoName+"="+results.RepoName,
 | 
			
		||||
		repo_module.EnvRepoUsername+"="+results.OwnerName,
 | 
			
		||||
@@ -323,16 +369,16 @@ func runServ(ctx context.Context, c *cli.Command) error {
 | 
			
		||||
		repo_module.EnvPusherEmail+"="+results.UserEmail,
 | 
			
		||||
		repo_module.EnvPusherID+"="+strconv.FormatInt(results.UserID, 10),
 | 
			
		||||
		repo_module.EnvRepoID+"="+strconv.FormatInt(results.RepoID, 10),
 | 
			
		||||
		repo_module.EnvPRID+"="+strconv.Itoa(0),
 | 
			
		||||
		repo_module.EnvDeployKeyID+"="+strconv.FormatInt(results.DeployKeyID, 10),
 | 
			
		||||
		repo_module.EnvKeyID+"="+strconv.FormatInt(results.KeyID, 10),
 | 
			
		||||
		repo_module.EnvPRID+"="+fmt.Sprintf("%d", 0),
 | 
			
		||||
		repo_module.EnvDeployKeyID+"="+fmt.Sprintf("%d", results.DeployKeyID),
 | 
			
		||||
		repo_module.EnvKeyID+"="+fmt.Sprintf("%d", results.KeyID),
 | 
			
		||||
		repo_module.EnvAppURL+"="+setting.AppURL,
 | 
			
		||||
	)
 | 
			
		||||
	// to avoid breaking, here only use the minimal environment variables for the "gitea serv" command.
 | 
			
		||||
	// it could be re-considered whether to use the same git.CommonGitCmdEnvs() as "git" command later.
 | 
			
		||||
	command.Env = append(command.Env, gitcmd.CommonCmdServEnvs()...)
 | 
			
		||||
	gitcmd.Env = append(gitcmd.Env, git.CommonCmdServEnvs()...)
 | 
			
		||||
 | 
			
		||||
	if err = command.Run(); err != nil {
 | 
			
		||||
	if err = gitcmd.Run(); err != nil {
 | 
			
		||||
		return fail(ctx, "Failed to execute git command", "Failed to execute git command: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										49
									
								
								cmd/web.go
									
									
									
									
									
								
							
							
						
						
									
										49
									
								
								cmd/web.go
									
									
									
									
									
								
							@@ -18,17 +18,15 @@ import (
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/modules/container"
 | 
			
		||||
	"code.gitea.io/gitea/modules/graceful"
 | 
			
		||||
	"code.gitea.io/gitea/modules/gtprof"
 | 
			
		||||
	"code.gitea.io/gitea/modules/log"
 | 
			
		||||
	"code.gitea.io/gitea/modules/process"
 | 
			
		||||
	"code.gitea.io/gitea/modules/public"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
	"code.gitea.io/gitea/routers"
 | 
			
		||||
	"code.gitea.io/gitea/routers/install"
 | 
			
		||||
 | 
			
		||||
	"github.com/felixge/fgprof"
 | 
			
		||||
	"github.com/urfave/cli/v3"
 | 
			
		||||
	"github.com/urfave/cli/v2"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// PIDFile could be set from build tag
 | 
			
		||||
@@ -130,19 +128,19 @@ func showWebStartupMessage(msg string) {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func serveInstall(cmd *cli.Command) error {
 | 
			
		||||
func serveInstall(ctx *cli.Context) error {
 | 
			
		||||
	showWebStartupMessage("Prepare to run install page")
 | 
			
		||||
 | 
			
		||||
	routers.InitWebInstallPage(graceful.GetManager().HammerContext())
 | 
			
		||||
 | 
			
		||||
	// Flag for port number in case first time run conflict
 | 
			
		||||
	if cmd.IsSet("port") {
 | 
			
		||||
		if err := setPort(cmd.String("port")); err != nil {
 | 
			
		||||
	if ctx.IsSet("port") {
 | 
			
		||||
		if err := setPort(ctx.String("port")); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	if cmd.IsSet("install-port") {
 | 
			
		||||
		if err := setPort(cmd.String("install-port")); err != nil {
 | 
			
		||||
	if ctx.IsSet("install-port") {
 | 
			
		||||
		if err := setPort(ctx.String("install-port")); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
@@ -156,13 +154,14 @@ func serveInstall(cmd *cli.Command) error {
 | 
			
		||||
	case <-graceful.GetManager().IsShutdown():
 | 
			
		||||
		<-graceful.GetManager().Done()
 | 
			
		||||
		log.Info("PID: %d Gitea Web Finished", os.Getpid())
 | 
			
		||||
		log.GetManager().Close()
 | 
			
		||||
		return err
 | 
			
		||||
	default:
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func serveInstalled(c *cli.Command) error {
 | 
			
		||||
func serveInstalled(ctx *cli.Context) error {
 | 
			
		||||
	setting.InitCfgProvider(setting.CustomConf)
 | 
			
		||||
	setting.LoadCommonSettings()
 | 
			
		||||
	setting.MustInstalled()
 | 
			
		||||
@@ -212,49 +211,39 @@ func serveInstalled(c *cli.Command) error {
 | 
			
		||||
		log.Fatal("Can not find APP_DATA_PATH %q", setting.AppDataPath)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// the AppDataTempDir is fully managed by us with a safe sub-path
 | 
			
		||||
	// so it's safe to automatically remove the outdated files
 | 
			
		||||
	setting.AppDataTempDir("").RemoveOutdated(3 * 24 * time.Hour)
 | 
			
		||||
 | 
			
		||||
	// Override the provided port number within the configuration
 | 
			
		||||
	if c.IsSet("port") {
 | 
			
		||||
		if err := setPort(c.String("port")); err != nil {
 | 
			
		||||
	if ctx.IsSet("port") {
 | 
			
		||||
		if err := setPort(ctx.String("port")); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	gtprof.EnableBuiltinTracer(util.Iif(setting.IsProd, 2000*time.Millisecond, 100*time.Millisecond))
 | 
			
		||||
 | 
			
		||||
	// Set up Chi routes
 | 
			
		||||
	webRoutes := routers.NormalRoutes()
 | 
			
		||||
	err := listen(webRoutes, true)
 | 
			
		||||
	<-graceful.GetManager().Done()
 | 
			
		||||
	log.Info("PID: %d Gitea Web Finished", os.Getpid())
 | 
			
		||||
	log.GetManager().Close()
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func servePprof() {
 | 
			
		||||
	// FIXME: it shouldn't use the global DefaultServeMux, and it should use a proper context
 | 
			
		||||
	http.DefaultServeMux.Handle("/debug/fgprof", fgprof.Handler())
 | 
			
		||||
	_, _, finished := process.GetManager().AddTypedContext(context.TODO(), "Web: PProf Server", process.SystemProcessType, true)
 | 
			
		||||
	// The pprof server is for debug purpose only, it shouldn't be exposed on public network. At the moment, it's not worth introducing a configurable option for it.
 | 
			
		||||
	_, _, finished := process.GetManager().AddTypedContext(context.Background(), "Web: PProf Server", process.SystemProcessType, true)
 | 
			
		||||
	// The pprof server is for debug purpose only, it shouldn't be exposed on public network. At the moment it's not worth to introduce a configurable option for it.
 | 
			
		||||
	log.Info("Starting pprof server on localhost:6060")
 | 
			
		||||
	log.Info("Stopped pprof server: %v", http.ListenAndServe("localhost:6060", nil))
 | 
			
		||||
	finished()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func runWeb(ctx context.Context, cmd *cli.Command) error {
 | 
			
		||||
func runWeb(ctx *cli.Context) error {
 | 
			
		||||
	defer func() {
 | 
			
		||||
		if panicked := recover(); panicked != nil {
 | 
			
		||||
			log.Fatal("PANIC: %v\n%s", panicked, log.Stack(2))
 | 
			
		||||
		}
 | 
			
		||||
	}()
 | 
			
		||||
 | 
			
		||||
	if subCmdName, valid := isValidDefaultSubCommand(cmd); !valid {
 | 
			
		||||
		return fmt.Errorf("unknown command: %s", subCmdName)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	managerCtx, cancel := context.WithCancel(ctx)
 | 
			
		||||
	managerCtx, cancel := context.WithCancel(context.Background())
 | 
			
		||||
	graceful.InitManager(managerCtx)
 | 
			
		||||
	defer cancel()
 | 
			
		||||
 | 
			
		||||
@@ -265,12 +254,12 @@ func runWeb(ctx context.Context, cmd *cli.Command) error {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Set pid file setting
 | 
			
		||||
	if cmd.IsSet("pid") {
 | 
			
		||||
		createPIDFile(cmd.String("pid"))
 | 
			
		||||
	if ctx.IsSet("pid") {
 | 
			
		||||
		createPIDFile(ctx.String("pid"))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !setting.InstallLock {
 | 
			
		||||
		if err := serveInstall(cmd); err != nil {
 | 
			
		||||
		if err := serveInstall(ctx); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
@@ -281,7 +270,7 @@ func runWeb(ctx context.Context, cmd *cli.Command) error {
 | 
			
		||||
		go servePprof()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return serveInstalled(cmd)
 | 
			
		||||
	return serveInstalled(ctx)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func setPort(port string) error {
 | 
			
		||||
 
 | 
			
		||||
@@ -16,7 +16,6 @@ import (
 | 
			
		||||
	"code.gitea.io/gitea/modules/log"
 | 
			
		||||
	"code.gitea.io/gitea/modules/process"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
 | 
			
		||||
	"github.com/caddyserver/certmagic"
 | 
			
		||||
)
 | 
			
		||||
@@ -55,6 +54,10 @@ func runACME(listenAddr string, m http.Handler) error {
 | 
			
		||||
		altTLSALPNPort = p
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// FIXME: this path is not right, it uses "AppWorkPath" incorrectly, and writes the data into "AppWorkPath/https"
 | 
			
		||||
	// Ideally it should migrate to AppDataPath write to "AppDataPath/https"
 | 
			
		||||
	certmagic.Default.Storage = &certmagic.FileStorage{Path: setting.AcmeLiveDirectory}
 | 
			
		||||
	magic := certmagic.NewDefault()
 | 
			
		||||
	// Try to use private CA root if provided, otherwise defaults to system's trust
 | 
			
		||||
	var certPool *x509.CertPool
 | 
			
		||||
	if setting.AcmeCARoot != "" {
 | 
			
		||||
@@ -64,20 +67,8 @@ func runACME(listenAddr string, m http.Handler) error {
 | 
			
		||||
			log.Warn("Failed to parse CA Root certificate, using default CA trust: %v", err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	// FIXME: this path is not right, it uses "AppWorkPath" incorrectly, and writes the data into "AppWorkPath/https"
 | 
			
		||||
	// Ideally it should migrate to AppDataPath write to "AppDataPath/https"
 | 
			
		||||
	// And one more thing, no idea why we should set the global default variables here
 | 
			
		||||
	// But it seems that the current ACME code needs these global variables to make renew work.
 | 
			
		||||
	// Otherwise, "renew" will use incorrect storage path
 | 
			
		||||
	oldDefaultACME := certmagic.DefaultACME
 | 
			
		||||
	certmagic.Default.Storage = &certmagic.FileStorage{Path: setting.AcmeLiveDirectory}
 | 
			
		||||
	certmagic.DefaultACME = certmagic.ACMEIssuer{
 | 
			
		||||
		// try to use the default values provided by DefaultACME
 | 
			
		||||
		CA:        util.IfZero(setting.AcmeURL, oldDefaultACME.CA),
 | 
			
		||||
		TestCA:    oldDefaultACME.TestCA,
 | 
			
		||||
		Logger:    oldDefaultACME.Logger,
 | 
			
		||||
		HTTPProxy: oldDefaultACME.HTTPProxy,
 | 
			
		||||
 | 
			
		||||
	myACME := certmagic.NewACMEIssuer(magic, certmagic.ACMEIssuer{
 | 
			
		||||
		CA:                      setting.AcmeURL,
 | 
			
		||||
		TrustedRoots:            certPool,
 | 
			
		||||
		Email:                   setting.AcmeEmail,
 | 
			
		||||
		Agreed:                  setting.AcmeTOS,
 | 
			
		||||
@@ -86,10 +77,8 @@ func runACME(listenAddr string, m http.Handler) error {
 | 
			
		||||
		ListenHost:              setting.HTTPAddr,
 | 
			
		||||
		AltTLSALPNPort:          altTLSALPNPort,
 | 
			
		||||
		AltHTTPPort:             altHTTPPort,
 | 
			
		||||
	}
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	magic := certmagic.NewDefault()
 | 
			
		||||
	myACME := certmagic.NewACMEIssuer(magic, certmagic.DefaultACME)
 | 
			
		||||
	magic.Issuers = []certmagic.Issuer{myACME}
 | 
			
		||||
 | 
			
		||||
	// this obtains certificates or renews them if necessary
 | 
			
		||||
@@ -136,7 +125,7 @@ func runACME(listenAddr string, m http.Handler) error {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func runLetsEncryptFallbackHandler(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	if r.Method != http.MethodGet && r.Method != http.MethodHead {
 | 
			
		||||
	if r.Method != "GET" && r.Method != "HEAD" {
 | 
			
		||||
		http.Error(w, "Use HTTPS", http.StatusBadRequest)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -23,6 +23,12 @@ func NoHTTPRedirector() {
 | 
			
		||||
	graceful.GetManager().InformCleanup()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NoMainListener tells our cleanup routine that we will not be using a possibly provided listener
 | 
			
		||||
// for our main HTTP/HTTPS service
 | 
			
		||||
func NoMainListener() {
 | 
			
		||||
	graceful.GetManager().InformCleanup()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NoInstallListener tells our cleanup routine that we will not be using a possibly provided listener
 | 
			
		||||
// for our install HTTP/HTTPS service
 | 
			
		||||
func NoInstallListener() {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										17
									
								
								contrib/autocompletion/README
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								contrib/autocompletion/README
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,17 @@
 | 
			
		||||
Bash and Zsh completion
 | 
			
		||||
=======================
 | 
			
		||||
 | 
			
		||||
From within the gitea root run:
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
source contrib/autocompletion/bash_autocomplete
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
or for zsh run:
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
source contrib/autocompletion/zsh_autocomplete
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
These scripts will check if gitea is on the path and if so add autocompletion for `gitea`. Or if not autocompletion will work for `./gitea`.
 | 
			
		||||
If gitea has been installed as a different program pass in the `PROG` environment variable to set the correct program name.
 | 
			
		||||
							
								
								
									
										30
									
								
								contrib/autocompletion/bash_autocomplete
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										30
									
								
								contrib/autocompletion/bash_autocomplete
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,30 @@
 | 
			
		||||
#! /bin/bash
 | 
			
		||||
# Heavily inspired by https://github.com/urfave/cli
 | 
			
		||||
 | 
			
		||||
_cli_bash_autocomplete() {
 | 
			
		||||
  if [[ "${COMP_WORDS[0]}" != "source" ]]; then
 | 
			
		||||
    local cur opts base
 | 
			
		||||
    COMPREPLY=()
 | 
			
		||||
    cur="${COMP_WORDS[COMP_CWORD]}"
 | 
			
		||||
    if [[ "$cur" == "-"* ]]; then
 | 
			
		||||
      opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} ${cur} --generate-bash-completion )
 | 
			
		||||
    else
 | 
			
		||||
      opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} --generate-bash-completion )
 | 
			
		||||
    fi
 | 
			
		||||
    COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
 | 
			
		||||
    return 0
 | 
			
		||||
  fi
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
if [ -z "$PROG" ] && [ ! "$(command -v gitea &> /dev/null)" ] ; then
 | 
			
		||||
  complete -o bashdefault -o default -o nospace -F _cli_bash_autocomplete gitea
 | 
			
		||||
elif [ -z "$PROG" ]; then
 | 
			
		||||
  complete -o bashdefault -o default -o nospace -F _cli_bash_autocomplete ./gitea
 | 
			
		||||
  complete -o bashdefault -o default -o nospace -F _cli_bash_autocomplete "$PWD/gitea"
 | 
			
		||||
else
 | 
			
		||||
  complete -o bashdefault -o default -o nospace -F _cli_bash_autocomplete "$PROG"
 | 
			
		||||
  unset PROG
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user