瀏覽代碼

add gitea code

刘清 4 月之前
當前提交
61128e8e94
共有 100 個文件被更改,包括 23454 次插入0 次删除
  1. 26
    0
      .air.toml
  2. 62
    0
      .changelog.yml
  3. 46
    0
      .devcontainer/devcontainer.json
  4. 95
    0
      .dockerignore
  5. 32
    0
      .editorconfig
  6. 1
    0
      .envrc
  7. 10
    0
      .gitattributes
  8. 42
    0
      .gitea/issue_template.md
  9. 1
    0
      .github/FUNDING.yml
  10. 91
    0
      .github/ISSUE_TEMPLATE/bug-report.yaml
  11. 17
    0
      .github/ISSUE_TEMPLATE/config.yml
  12. 24
    0
      .github/ISSUE_TEMPLATE/feature-request.yaml
  13. 66
    0
      .github/ISSUE_TEMPLATE/ui.bug-report.yaml
  14. 7
    0
      .github/actionlint.yaml
  15. 93
    0
      .github/labeler.yml
  16. 10
    0
      .github/pull_request_template.md
  17. 29
    0
      .github/workflows/cron-licenses.yml
  18. 38
    0
      .github/workflows/cron-translations.yml
  19. 100
    0
      .github/workflows/files-changed.yml
  20. 197
    0
      .github/workflows/pull-compliance.yml
  21. 237
    0
      .github/workflows/pull-db-tests.yml
  22. 35
    0
      .github/workflows/pull-docker-dryrun.yml
  23. 35
    0
      .github/workflows/pull-e2e-tests.yml
  24. 20
    0
      .github/workflows/pull-labeler.yml
  25. 143
    0
      .github/workflows/release-nightly.yml
  26. 153
    0
      .github/workflows/release-tag-rc.yml
  27. 164
    0
      .github/workflows/release-tag-version.yml
  28. 124
    0
      .gitignore
  29. 51
    0
      .gitpod.yml
  30. 182
    0
      .golangci.yml
  31. 8
    0
      .ignore
  32. 2
    0
      .mailmap
  33. 15
    0
      .markdownlint.yaml
  34. 7
    0
      .npmrc
  35. 12
    0
      .spectral.yaml
  36. 44
    0
      .yamllint.yaml
  37. 58
    0
      BSDmakefile
  38. 5223
    0
      CHANGELOG-archived.md
  39. 4886
    0
      CHANGELOG.md
  40. 96
    0
      CODE_OF_CONDUCT.md
  41. 606
    0
      CONTRIBUTING.md
  42. 34
    0
      DCO
  43. 85
    0
      Dockerfile
  44. 90
    0
      Dockerfile.rootless
  45. 20
    0
      LICENSE
  46. 66
    0
      MAINTAINERS
  47. 954
    0
      Makefile
  48. 213
    0
      README.md
  49. 206
    0
      README.zh-cn.md
  50. 206
    0
      README.zh-tw.md
  51. 85
    0
      SECURITY.md
  52. 1
    0
      assets/emoji.json
  53. 31
    0
      assets/favicon.svg
  54. 1332
    0
      assets/go-licenses.json
  55. 31
    0
      assets/logo.svg
  56. 14
    0
      build.go
  57. 115
    0
      build/backport-locales.go
  58. 281
    0
      build/code-batch-process.go
  59. 195
    0
      build/codeformat/formatimports.go
  60. 124
    0
      build/codeformat/formatimports_test.go
  61. 27
    0
      build/generate-bindata.go
  62. 219
    0
      build/generate-emoji.go
  63. 126
    0
      build/generate-gitignores.go
  64. 118
    0
      build/generate-go-licenses.go
  65. 118
    0
      build/gocovmerge.go
  66. 20
    0
      build/test-echo.go
  67. 24
    0
      build/test-env-check.sh
  68. 11
    0
      build/test-env-prepare.sh
  69. 52
    0
      build/update-locales.sh
  70. 53
    0
      cmd/actions.go
  71. 167
    0
      cmd/admin.go
  72. 106
    0
      cmd/admin_auth.go
  73. 455
    0
      cmd/admin_auth_ldap.go
  74. 1348
    0
      cmd/admin_auth_ldap_test.go
  75. 322
    0
      cmd/admin_auth_oauth.go
  76. 343
    0
      cmd/admin_auth_oauth_test.go
  77. 200
    0
      cmd/admin_auth_smtp.go
  78. 271
    0
      cmd/admin_auth_smtp_test.go
  79. 42
    0
      cmd/admin_regenerate.go
  80. 21
    0
      cmd/admin_user.go
  81. 78
    0
      cmd/admin_user_change_password.go
  82. 91
    0
      cmd/admin_user_change_password_test.go
  83. 240
    0
      cmd/admin_user_create.go
  84. 134
    0
      cmd/admin_user_create_test.go
  85. 84
    0
      cmd/admin_user_delete.go
  86. 111
    0
      cmd/admin_user_delete_test.go
  87. 95
    0
      cmd/admin_user_generate_access_token.go
  88. 58
    0
      cmd/admin_user_list.go
  89. 63
    0
      cmd/admin_user_must_change_password.go
  90. 78
    0
      cmd/admin_user_must_change_password_test.go
  91. 206
    0
      cmd/cert.go
  92. 123
    0
      cmd/cert_test.go
  93. 144
    0
      cmd/cmd.go
  94. 38
    0
      cmd/cmd_test.go
  95. 67
    0
      cmd/docs.go
  96. 215
    0
      cmd/doctor.go
  97. 54
    0
      cmd/doctor_convert.go
  98. 34
    0
      cmd/doctor_test.go
  99. 327
    0
      cmd/dump.go
  100. 0
    0
      cmd/dump_repo.go

+ 26
- 0
.air.toml 查看文件

@@ -0,0 +1,26 @@
1
+root = "."
2
+tmp_dir = ".air"
3
+
4
+[build]
5
+pre_cmd = ["killall -9 gitea 2>/dev/null || true"] # kill off potential zombie processes from previous runs
6
+cmd = "make --no-print-directory backend"
7
+bin = "gitea"
8
+delay = 2000
9
+include_ext = ["go", "tmpl"]
10
+include_file = ["main.go"]
11
+include_dir = ["cmd", "models", "modules", "options", "routers", "services"]
12
+exclude_dir = [
13
+  "models/fixtures",
14
+  "models/migrations/fixtures",
15
+  "modules/avatar/identicon/testdata",
16
+  "modules/avatar/testdata",
17
+  "modules/git/tests",
18
+  "modules/migration/file_format_testdata",
19
+  "routers/private/tests",
20
+  "services/gitdiff/testdata",
21
+]
22
+exclude_regex = ["_test.go$", "_gen.go$"]
23
+stop_on_error = true
24
+
25
+[log]
26
+main_only = true

+ 62
- 0
.changelog.yml 查看文件

@@ -0,0 +1,62 @@
1
+# The full repository name
2
+repo: go-gitea/gitea
3
+
4
+# Service type (gitea or github)
5
+service: github
6
+
7
+# Base URL for Gitea instance if using gitea service type (optional)
8
+# Default: https://gitea.com
9
+base-url:
10
+
11
+# Changelog groups and which labeled PRs to add to each group
12
+groups:
13
+  -
14
+    name: BREAKING
15
+    labels:
16
+      - pr/breaking
17
+  -
18
+    name: SECURITY
19
+    labels:
20
+      - topic/security
21
+  -
22
+    name: FEATURES
23
+    labels:
24
+      - type/feature
25
+  -
26
+    name: ENHANCEMENTS
27
+    labels:
28
+      - type/enhancement
29
+  -
30
+    name: PERFORMANCE
31
+    labels:
32
+      - performance/memory
33
+      - performance/speed
34
+      - performance/bigrepo
35
+      - performance/cpu
36
+  -
37
+    name: BUGFIXES
38
+    labels:
39
+      - type/bug
40
+  -
41
+    name: API
42
+    labels:
43
+      - modifies/api
44
+  -
45
+    name: TESTING
46
+    labels:
47
+      - type/testing
48
+  -
49
+    name: BUILD
50
+    labels:
51
+      - topic/build
52
+      - topic/code-linting
53
+  -
54
+    name: DOCS
55
+    labels:
56
+      - type/docs
57
+  -
58
+    name: MISC
59
+    default: true
60
+
61
+# regex indicating which labels to skip for the changelog
62
+skip-labels: skip-changelog|backport\/.+

+ 46
- 0
.devcontainer/devcontainer.json 查看文件

@@ -0,0 +1,46 @@
1
+{
2
+  "name": "Gitea DevContainer",
3
+  "image": "mcr.microsoft.com/devcontainers/go:1.25-trixie",
4
+  "containerEnv": {
5
+    // override "local" from packaged version
6
+    "GOTOOLCHAIN": "auto"
7
+  },
8
+  "features": {
9
+    // installs nodejs into container
10
+    "ghcr.io/devcontainers/features/node:1": {
11
+      "version": "latest"
12
+    },
13
+    "ghcr.io/devcontainers/features/git-lfs:1.2.5": {},
14
+    "ghcr.io/jsburckhardt/devcontainer-features/uv:1": {},
15
+    "ghcr.io/devcontainers/features/python:1": {
16
+      "version": "3.13"
17
+    },
18
+    "ghcr.io/warrenbuckley/codespace-features/sqlite:1": {}
19
+  },
20
+  "customizations": {
21
+    "vscode": {
22
+      "settings": {},
23
+      // same extensions as Gitpod, should match /.gitpod.yml
24
+      "extensions": [
25
+        "editorconfig.editorconfig",
26
+        "dbaeumer.vscode-eslint",
27
+        "golang.go",
28
+        "stylelint.vscode-stylelint",
29
+        "DavidAnson.vscode-markdownlint",
30
+        "Vue.volar",
31
+        "ms-azuretools.vscode-docker",
32
+        "vitest.explorer",
33
+        "cweijan.vscode-database-client2",
34
+        "GitHub.vscode-pull-request-github",
35
+        "Azurite.azurite"
36
+      ]
37
+    }
38
+  },
39
+  "portsAttributes": {
40
+    "3000": {
41
+      "label": "Gitea Web",
42
+      "onAutoForward": "notify"
43
+    }
44
+  },
45
+  "postCreateCommand": "make deps"
46
+}

+ 95
- 0
.dockerignore 查看文件

@@ -0,0 +1,95 @@
1
+# Compiled Object files, Static and Dynamic libs (Shared Objects)
2
+*.o
3
+*.a
4
+*.so
5
+
6
+# Folders
7
+_obj
8
+_test
9
+
10
+# IntelliJ
11
+.idea
12
+# Goland's output filename can not be set manually
13
+/go_build_*
14
+
15
+# MS VSCode
16
+.vscode
17
+__debug_bin*
18
+
19
+# Architecture specific extensions/prefixes
20
+*.[568vq]
21
+[568vq].out
22
+
23
+*.cgo1.go
24
+*.cgo2.c
25
+_cgo_defun.c
26
+_cgo_gotypes.go
27
+_cgo_export.*
28
+
29
+_testmain.go
30
+
31
+*.exe
32
+*.test
33
+*.prof
34
+
35
+*coverage.out
36
+coverage.all
37
+cpu.out
38
+
39
+*.db
40
+*.log
41
+
42
+/gitea
43
+/gitea-vet
44
+/debug
45
+/integrations.test
46
+
47
+/bin
48
+/dist
49
+/custom/*
50
+!/custom/conf
51
+/custom/conf/*
52
+!/custom/conf/app.example.ini
53
+/data
54
+/indexers
55
+/log
56
+/tests/integration/gitea-integration-*
57
+/tests/integration/indexers-*
58
+/tests/e2e/gitea-e2e-*
59
+/tests/e2e/indexers-*
60
+/tests/e2e/reports
61
+/tests/e2e/test-artifacts
62
+/tests/e2e/test-snapshots
63
+/tests/*.ini
64
+/node_modules
65
+/yarn.lock
66
+/yarn-error.log
67
+/npm-debug.log*
68
+/pnpm-debug.log*
69
+/public/assets/js
70
+/public/assets/css
71
+/public/assets/fonts
72
+/public/assets/img/avatar
73
+/vendor
74
+/VERSION
75
+/.air
76
+/.go-licenses
77
+
78
+# Files and folders that were previously generated
79
+/public/assets/img/webpack
80
+
81
+# Snapcraft
82
+snap/.snapcraft/
83
+parts/
84
+stage/
85
+prime/
86
+*.snap
87
+*.snap-build
88
+*_source.tar.bz2
89
+.DS_Store
90
+
91
+# Make evidence files
92
+/.make_evidence
93
+
94
+# Manpage
95
+/man

+ 32
- 0
.editorconfig 查看文件

@@ -0,0 +1,32 @@
1
+root = true
2
+
3
+[*]
4
+indent_style = space
5
+indent_size = 2
6
+tab_width = 2
7
+end_of_line = lf
8
+charset = utf-8
9
+trim_trailing_whitespace = true
10
+insert_final_newline = true
11
+
12
+[*.{go,tmpl,html}]
13
+indent_style = tab
14
+
15
+[go.*]
16
+indent_style = tab
17
+
18
+[templates/custom/*.tmpl]
19
+insert_final_newline = false
20
+
21
+[templates/swagger/v1_json.tmpl]
22
+indent_style = space
23
+insert_final_newline = false
24
+
25
+[templates/user/auth/oidc_wellknown.tmpl]
26
+indent_style = space
27
+
28
+[Makefile]
29
+indent_style = tab
30
+
31
+[*.svg]
32
+insert_final_newline = false

+ 1
- 0
.envrc 查看文件

@@ -0,0 +1 @@
1
+use flake

+ 10
- 0
.gitattributes 查看文件

@@ -0,0 +1,10 @@
1
+* text=auto eol=lf
2
+*.tmpl linguist-language=Handlebars
3
+*.pb.go linguist-generated
4
+/assets/*.json linguist-generated
5
+/public/assets/img/svg/*.svg linguist-generated
6
+/templates/swagger/v1_json.tmpl linguist-generated
7
+/options/fileicon/** linguist-generated
8
+/vendor/** -text -eol linguist-vendored
9
+/web_src/js/vendor/** -text -eol linguist-vendored
10
+Dockerfile.* linguist-language=Dockerfile

+ 42
- 0
.gitea/issue_template.md 查看文件

@@ -0,0 +1,42 @@
1
+<!-- NOTE: If your issue is a security concern, please send an email to security@gitea.io instead of opening a public issue -->
2
+
3
+<!--
4
+    1. Please speak English, this is the language all maintainers can speak and write.
5
+    2. Please ask questions or configuration/deploy problems on our Discord
6
+       server (https://discord.gg/gitea) or forum (https://forum.gitea.com).
7
+    3. Please take a moment to check that your issue doesn't already exist.
8
+    4. Make sure it's not mentioned in the FAQ (https://docs.gitea.com/help/faq)
9
+    5. Please give all relevant information below for bug reports, because
10
+       incomplete details will be handled as an invalid report.
11
+-->
12
+
13
+- Gitea version (or commit ref):
14
+- Git version:
15
+- Operating system:
16
+  <!-- Please include information on whether you built gitea yourself, used one of our downloads or are using some other package -->
17
+  <!-- Please also tell us how you are running gitea, e.g. if it is being run from docker, a command-line, systemd etc. --->
18
+  <!-- If you are using a package or systemd tell us what distribution you are using -->
19
+- Database (use `[x]`):
20
+  - [ ] PostgreSQL
21
+  - [ ] MySQL
22
+  - [ ] MSSQL
23
+  - [ ] SQLite
24
+- Can you reproduce the bug at https://demo.gitea.com:
25
+  - [ ] Yes (provide example URL)
26
+  - [ ] No
27
+- Log gist:
28
+<!-- It really is important to provide pertinent logs -->
29
+<!-- Please read https://docs.gitea.com/administration/logging-config#collecting-logs-for-help -->
30
+<!-- In addition, if your problem relates to git commands set `RUN_MODE=dev` at the top of app.ini -->
31
+
32
+## Description
33
+<!-- If using a proxy or a CDN (e.g. CloudFlare) in front of gitea, please
34
+     disable the proxy/CDN fully and connect to gitea directly to confirm
35
+     the issue still persists without those services. -->
36
+
37
+...
38
+
39
+
40
+## Screenshots
41
+
42
+<!-- **If this issue involves the Web Interface, please include a screenshot** -->

+ 1
- 0
.github/FUNDING.yml 查看文件

@@ -0,0 +1 @@
1
+open_collective: gitea

+ 91
- 0
.github/ISSUE_TEMPLATE/bug-report.yaml 查看文件

@@ -0,0 +1,91 @@
1
+name: Bug Report
2
+description: Found something you weren't expecting? Report it here!
3
+labels: ["type/bug"]
4
+body:
5
+  - type: markdown
6
+    attributes:
7
+      value: |
8
+        NOTE: If your issue is a security concern, please send an email to security@gitea.io instead of opening a public issue.
9
+  - type: markdown
10
+    attributes:
11
+      value: |
12
+        1. Please speak English, this is the language all maintainers can speak and write.
13
+        2. Please ask questions or configuration/deploy problems on our Discord
14
+           server (https://discord.gg/gitea) or forum (https://forum.gitea.com).
15
+        3. Make sure you are using the latest release and
16
+           take a moment to check that your issue hasn't been reported before.
17
+        4. Make sure it's not mentioned in the FAQ (https://docs.gitea.com/help/faq)
18
+        5. It's really important to provide pertinent details and logs (https://docs.gitea.com/help/support),
19
+           incomplete details will be handled as an invalid report.
20
+  - type: textarea
21
+    id: description
22
+    attributes:
23
+      label: Description
24
+      description: |
25
+        Please provide a description of your issue here, with a URL if you were able to reproduce the issue (see below)
26
+        If you are using a proxy or a CDN (e.g. Cloudflare) in front of Gitea, please disable the proxy/CDN fully and access Gitea directly to confirm the issue still persists without those services.
27
+  - type: input
28
+    id: gitea-ver
29
+    attributes:
30
+      label: Gitea Version
31
+      description: Gitea version (or commit reference) of your instance
32
+    validations:
33
+      required: true
34
+  - type: dropdown
35
+    id: can-reproduce
36
+    attributes:
37
+      label: Can you reproduce the bug on the Gitea demo site?
38
+      description: |
39
+        If so, please provide a URL in the Description field
40
+        URL of Gitea demo: https://demo.gitea.com
41
+      options:
42
+        - "Yes"
43
+        - "No"
44
+    validations:
45
+      required: true
46
+  - type: markdown
47
+    attributes:
48
+      value: |
49
+        It's really important to provide pertinent logs
50
+        Please read https://docs.gitea.com/administration/logging-config#collecting-logs-for-help
51
+        In addition, if your problem relates to git commands set `RUN_MODE=dev` at the top of app.ini
52
+  - type: input
53
+    id: logs
54
+    attributes:
55
+      label: Log Gist
56
+      description: Please provide a gist URL of your logs, with any sensitive information (e.g. API keys) removed/hidden
57
+  - type: textarea
58
+    id: screenshots
59
+    attributes:
60
+      label: Screenshots
61
+      description: If this issue involves the Web Interface, please provide one or more screenshots
62
+  - type: input
63
+    id: git-ver
64
+    attributes:
65
+      label: Git Version
66
+      description: The version of git running on the server
67
+  - type: input
68
+    id: os-ver
69
+    attributes:
70
+      label: Operating System
71
+      description: The operating system you are using to run Gitea
72
+  - type: textarea
73
+    id: run-info
74
+    attributes:
75
+      label: How are you running Gitea?
76
+      description: |
77
+        Please include information on whether you built Gitea yourself, used one of our downloads, are using https://demo.gitea.com or are using some other package
78
+        Please also tell us how you are running Gitea, e.g. if it is being run from docker, a command-line, systemd etc.
79
+        If you are using a package or systemd tell us what distribution you are using
80
+    validations:
81
+      required: true
82
+  - type: dropdown
83
+    id: database
84
+    attributes:
85
+      label: Database
86
+      description: What database system are you running?
87
+      options:
88
+        - PostgreSQL
89
+        - MySQL/MariaDB
90
+        - MSSQL
91
+        - SQLite

+ 17
- 0
.github/ISSUE_TEMPLATE/config.yml 查看文件

@@ -0,0 +1,17 @@
1
+blank_issues_enabled: false
2
+contact_links:
3
+  - name: Security Concern
4
+    url: https://tinyurl.com/security-gitea
5
+    about: For security concerns, please send a mail to security@gitea.io instead of opening a public issue.
6
+  - name: Discord Server
7
+    url: https://discord.gg/Gitea
8
+    about: Please ask questions and discuss configuration or deployment problems here.
9
+  - name: Discourse Forum
10
+    url: https://forum.gitea.com
11
+    about: Questions and configuration or deployment problems can also be discussed on our forum.
12
+  - name: Frequently Asked Questions
13
+    url: https://docs.gitea.com/help/faq
14
+    about: Please check if your question isn't mentioned here.
15
+  - name: Crowdin Translations
16
+    url: https://translate.gitea.com
17
+    about: Translations are managed here.

+ 24
- 0
.github/ISSUE_TEMPLATE/feature-request.yaml 查看文件

@@ -0,0 +1,24 @@
1
+name: Feature Request
2
+description: Got an idea for a feature that Gitea doesn't have currently?  Submit your idea here!
3
+labels: ["type/proposal"]
4
+body:
5
+  - type: markdown
6
+    attributes:
7
+      value: |
8
+        1. Please speak English, this is the language all maintainers can speak and write.
9
+        2. Please ask questions or configuration/deploy problems on our Discord
10
+           server (https://discord.gg/gitea) or forum (https://forum.gitea.com).
11
+        3. Please take a moment to check that your feature hasn't already been suggested.
12
+  - type: textarea
13
+    id: description
14
+    attributes:
15
+      label: Feature Description
16
+      placeholder: |
17
+        I think it would be great if Gitea had...
18
+    validations:
19
+      required: true
20
+  - type: textarea
21
+    id: screenshots
22
+    attributes:
23
+      label: Screenshots
24
+      description: If you can, provide screenshots of an implementation on another site e.g. GitHub

+ 66
- 0
.github/ISSUE_TEMPLATE/ui.bug-report.yaml 查看文件

@@ -0,0 +1,66 @@
1
+name: Web Interface Bug Report
2
+description: Something doesn't look quite as it should?  Report it here!
3
+labels: ["type/bug", "topic/ui"]
4
+body:
5
+  - type: markdown
6
+    attributes:
7
+      value: |
8
+        NOTE: If your issue is a security concern, please send an email to security@gitea.io instead of opening a public issue.
9
+  - type: markdown
10
+    attributes:
11
+      value: |
12
+        1. Please speak English, this is the language all maintainers can speak and write.
13
+        2. Please ask questions or configuration/deploy problems on our Discord
14
+           server (https://discord.gg/gitea) or forum (https://forum.gitea.com).
15
+        3. Please take a moment to check that your issue doesn't already exist.
16
+        4. Make sure it's not mentioned in the FAQ (https://docs.gitea.com/help/faq)
17
+        5. Please give all relevant information below for bug reports, because
18
+           incomplete details will be handled as an invalid report.
19
+        6. In particular it's really important to provide pertinent logs. If you are certain that this is a javascript
20
+           error, show us the javascript console. If the error appears to relate to Gitea the server you must also give us
21
+           DEBUG level logs. (See https://docs.gitea.com/administration/logging-config#collecting-logs-for-help)
22
+  - type: textarea
23
+    id: description
24
+    attributes:
25
+      label: Description
26
+      description: |
27
+        Please provide a description of your issue here, with a URL if you were able to reproduce the issue (see below)
28
+        If using a proxy or a CDN (e.g. CloudFlare) in front of gitea, please disable the proxy/CDN fully and connect to gitea directly to confirm the issue still persists without those services.
29
+  - type: textarea
30
+    id: screenshots
31
+    attributes:
32
+      label: Screenshots
33
+      description: Please provide at least 1 screenshot showing the issue.
34
+    validations:
35
+      required: true
36
+  - type: input
37
+    id: gitea-ver
38
+    attributes:
39
+      label: Gitea Version
40
+      description: Gitea version (or commit reference) your instance is running
41
+    validations:
42
+      required: true
43
+  - type: dropdown
44
+    id: can-reproduce
45
+    attributes:
46
+      label: Can you reproduce the bug on the Gitea demo site?
47
+      description: |
48
+        If so, please provide a URL in the Description field
49
+        URL of Gitea demo: https://demo.gitea.com
50
+      options:
51
+        - "Yes"
52
+        - "No"
53
+    validations:
54
+      required: true
55
+  - type: input
56
+    id: os-ver
57
+    attributes:
58
+      label: Operating System
59
+      description: The operating system you are using to access Gitea
60
+  - type: input
61
+    id: browser-ver
62
+    attributes:
63
+      label: Browser Version
64
+      description: The browser and version that you are using to access Gitea
65
+    validations:
66
+      required: true

+ 7
- 0
.github/actionlint.yaml 查看文件

@@ -0,0 +1,7 @@
1
+self-hosted-runner:
2
+  labels:
3
+    - actuated-4cpu-8gb
4
+    - actuated-4cpu-16gb
5
+    - nscloud
6
+    - namespace-profile-gitea-release-docker
7
+    - namespace-profile-gitea-release-binary

+ 93
- 0
.github/labeler.yml 查看文件

@@ -0,0 +1,93 @@
1
+modifies/docs:
2
+  - changed-files:
3
+      - any-glob-to-any-file:
4
+          - "**/*.md"
5
+          - "docs/**"
6
+
7
+modifies/templates:
8
+  - changed-files:
9
+      - all-globs-to-any-file:
10
+          - "templates/**"
11
+          - "!templates/swagger/v1_json.tmpl"
12
+
13
+modifies/api:
14
+  - changed-files:
15
+      - any-glob-to-any-file:
16
+          - "routers/api/**"
17
+          - "templates/swagger/v1_json.tmpl"
18
+
19
+modifies/cli:
20
+  - changed-files:
21
+      - any-glob-to-any-file:
22
+          - "cmd/**"
23
+
24
+modifies/translation:
25
+  - changed-files:
26
+      - any-glob-to-any-file:
27
+          - "options/locale/*.ini"
28
+
29
+modifies/migrations:
30
+  - changed-files:
31
+      - any-glob-to-any-file:
32
+          - "models/migrations/**"
33
+
34
+modifies/internal:
35
+  - changed-files:
36
+      - any-glob-to-any-file:
37
+          - ".air.toml"
38
+          - "Makefile"
39
+          - "Dockerfile"
40
+          - "Dockerfile.rootless"
41
+          - ".dockerignore"
42
+          - "docker/**"
43
+          - ".editorconfig"
44
+          - ".eslintrc.cjs"
45
+          - ".golangci.yml"
46
+          - ".gitpod.yml"
47
+          - ".markdownlint.yaml"
48
+          - ".spectral.yaml"
49
+          - "stylelint.config.js"
50
+          - ".yamllint.yaml"
51
+          - ".github/**"
52
+          - ".gitea/**"
53
+          - ".devcontainer/**"
54
+          - "build.go"
55
+          - "build/**"
56
+          - "contrib/**"
57
+
58
+modifies/dependencies:
59
+  - changed-files:
60
+      - any-glob-to-any-file:
61
+          - "package.json"
62
+          - "pnpm-lock.yaml"
63
+          - "pyproject.toml"
64
+          - "uv.lock"
65
+          - "go.mod"
66
+          - "go.sum"
67
+
68
+modifies/go:
69
+  - changed-files:
70
+      - any-glob-to-any-file:
71
+          - "**/*.go"
72
+
73
+modifies/frontend:
74
+  - changed-files:
75
+      - any-glob-to-any-file:
76
+          - "*.js"
77
+          - "*.ts"
78
+          - "web_src/**"
79
+
80
+docs-update-needed:
81
+  - changed-files:
82
+      - any-glob-to-any-file:
83
+          - "custom/conf/app.example.ini"
84
+
85
+topic/code-linting:
86
+  - changed-files:
87
+      - any-glob-to-any-file:
88
+          - ".eslintrc.cjs"
89
+          - ".golangci.yml"
90
+          - ".markdownlint.yaml"
91
+          - ".spectral.yaml"
92
+          - ".yamllint.yaml"
93
+          - "stylelint.config.js"

+ 10
- 0
.github/pull_request_template.md 查看文件

@@ -0,0 +1,10 @@
1
+<!-- start tips -->
2
+Please check the following:
3
+1. Make sure you are targeting the `main` branch, pull requests on release branches are only allowed for backports.
4
+2. Make sure you have read contributing guidelines: https://github.com/go-gitea/gitea/blob/main/CONTRIBUTING.md .
5
+3. For documentations contribution, please go to https://gitea.com/gitea/docs
6
+4. Describe what your pull request does and which issue you're targeting (if any).
7
+5. It is recommended to enable "Allow edits by maintainers", so maintainers can help more easily.
8
+6. Your input here will be included in the commit message when this PR has been merged. If you don't want some content to be included, please separate them with a line like `---`.
9
+7. Delete all these tips before posting.
10
+<!-- end tips -->

+ 29
- 0
.github/workflows/cron-licenses.yml 查看文件

@@ -0,0 +1,29 @@
1
+name: cron-licenses
2
+
3
+on:
4
+  # schedule:
5
+  #   - cron: "7 0 * * 1" # every Monday at 00:07 UTC
6
+  workflow_dispatch:
7
+
8
+jobs:
9
+  cron-licenses:
10
+    runs-on: ubuntu-latest
11
+    if: github.repository == 'go-gitea/gitea'
12
+    steps:
13
+      - uses: actions/checkout@v4
14
+      - uses: actions/setup-go@v5
15
+        with:
16
+          go-version-file: go.mod
17
+          check-latest: true
18
+      - run: make generate-gitignore
19
+        timeout-minutes: 40
20
+      - name: push translations to repo
21
+        uses: appleboy/git-push-action@v0.0.3
22
+        with:
23
+          author_email: "teabot@gitea.io"
24
+          author_name: GiteaBot
25
+          branch: main
26
+          commit: true
27
+          commit_message: "[skip ci] Updated licenses and gitignores"
28
+          remote: "git@github.com:go-gitea/gitea.git"
29
+          ssh_key: ${{ secrets.DEPLOY_KEY }}

+ 38
- 0
.github/workflows/cron-translations.yml 查看文件

@@ -0,0 +1,38 @@
1
+name: cron-translations
2
+
3
+on:
4
+  schedule:
5
+    - cron: "7 0 * * *" # every day at 00:07 UTC
6
+  workflow_dispatch:
7
+
8
+jobs:
9
+  crowdin-pull:
10
+    runs-on: ubuntu-latest
11
+    if: github.repository == 'go-gitea/gitea'
12
+    steps:
13
+      - uses: actions/checkout@v4
14
+      - uses: crowdin/github-action@v1
15
+        with:
16
+          upload_sources: true
17
+          upload_translations: false
18
+          download_sources: false
19
+          download_translations: true
20
+          push_translations: false
21
+          push_sources: false
22
+          create_pull_request: false
23
+          config: crowdin.yml
24
+        env:
25
+          CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
26
+          CROWDIN_KEY: ${{ secrets.CROWDIN_KEY }}
27
+      - name: update locales
28
+        run: ./build/update-locales.sh
29
+      - name: push translations to repo
30
+        uses: appleboy/git-push-action@v0.0.3
31
+        with:
32
+          author_email: "teabot@gitea.io"
33
+          author_name: GiteaBot
34
+          branch: main
35
+          commit: true
36
+          commit_message: "[skip ci] Updated translations via Crowdin"
37
+          remote: "git@github.com:go-gitea/gitea.git"
38
+          ssh_key: ${{ secrets.DEPLOY_KEY }}

+ 100
- 0
.github/workflows/files-changed.yml 查看文件

@@ -0,0 +1,100 @@
1
+name: files-changed
2
+
3
+on:
4
+  workflow_call:
5
+    outputs:
6
+      backend:
7
+        value: ${{ jobs.detect.outputs.backend }}
8
+      frontend:
9
+        value: ${{ jobs.detect.outputs.frontend }}
10
+      docs:
11
+        value: ${{ jobs.detect.outputs.docs }}
12
+      actions:
13
+        value: ${{ jobs.detect.outputs.actions }}
14
+      templates:
15
+        value: ${{ jobs.detect.outputs.templates }}
16
+      docker:
17
+        value: ${{ jobs.detect.outputs.docker }}
18
+      swagger:
19
+        value: ${{ jobs.detect.outputs.swagger }}
20
+      yaml:
21
+        value: ${{ jobs.detect.outputs.yaml }}
22
+
23
+jobs:
24
+  detect:
25
+    runs-on: ubuntu-latest
26
+    timeout-minutes: 3
27
+    outputs:
28
+      backend: ${{ steps.changes.outputs.backend }}
29
+      frontend: ${{ steps.changes.outputs.frontend }}
30
+      docs: ${{ steps.changes.outputs.docs }}
31
+      actions: ${{ steps.changes.outputs.actions }}
32
+      templates: ${{ steps.changes.outputs.templates }}
33
+      docker: ${{ steps.changes.outputs.docker }}
34
+      swagger: ${{ steps.changes.outputs.swagger }}
35
+      yaml: ${{ steps.changes.outputs.yaml }}
36
+    steps:
37
+      - uses: actions/checkout@v4
38
+      - uses: dorny/paths-filter@v3
39
+        id: changes
40
+        with:
41
+          filters: |
42
+            backend:
43
+              - "**/*.go"
44
+              - "templates/**/*.tmpl"
45
+              - "assets/emoji.json"
46
+              - "go.mod"
47
+              - "go.sum"
48
+              - "Makefile"
49
+              - ".golangci.yml"
50
+              - ".editorconfig"
51
+              - "options/locale/locale_en-US.ini"
52
+
53
+            frontend:
54
+              - "*.js"
55
+              - "*.ts"
56
+              - "web_src/**"
57
+              - "tools/*.js"
58
+              - "tools/*.ts"
59
+              - "assets/emoji.json"
60
+              - "package.json"
61
+              - "pnpm-lock.yaml"
62
+              - "Makefile"
63
+              - ".eslintrc.cjs"
64
+              - ".npmrc"
65
+
66
+            docs:
67
+              - "**/*.md"
68
+              - ".markdownlint.yaml"
69
+              - "package.json"
70
+              - "pnpm-lock.yaml"
71
+
72
+            actions:
73
+              - ".github/workflows/*"
74
+              - "Makefile"
75
+
76
+            templates:
77
+              - "tools/lint-templates-*.js"
78
+              - "templates/**/*.tmpl"
79
+              - "pyproject.toml"
80
+              - "uv.lock"
81
+
82
+            docker:
83
+              - "Dockerfile"
84
+              - "Dockerfile.rootless"
85
+              - "docker/**"
86
+              - "Makefile"
87
+
88
+            swagger:
89
+              - "templates/swagger/v1_json.tmpl"
90
+              - "templates/swagger/v1_input.json"
91
+              - "Makefile"
92
+              - "package.json"
93
+              - "pnpm-lock.yaml"
94
+              - ".spectral.yaml"
95
+
96
+            yaml:
97
+              - "**/*.yml"
98
+              - "**/*.yaml"
99
+              - ".yamllint.yaml"
100
+              - "pyproject.toml"

+ 197
- 0
.github/workflows/pull-compliance.yml 查看文件

@@ -0,0 +1,197 @@
1
+name: compliance
2
+
3
+on:
4
+  pull_request:
5
+
6
+concurrency:
7
+  group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
8
+  cancel-in-progress: true
9
+
10
+jobs:
11
+  files-changed:
12
+    uses: ./.github/workflows/files-changed.yml
13
+
14
+  lint-backend:
15
+    if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
16
+    needs: files-changed
17
+    runs-on: ubuntu-latest
18
+    steps:
19
+      - uses: actions/checkout@v4
20
+      - uses: actions/setup-go@v5
21
+        with:
22
+          go-version-file: go.mod
23
+          check-latest: true
24
+      - run: make deps-backend deps-tools
25
+      - run: make lint-backend
26
+        env:
27
+          TAGS: bindata sqlite sqlite_unlock_notify
28
+
29
+  lint-templates:
30
+    if: needs.files-changed.outputs.templates == 'true'
31
+    needs: files-changed
32
+    runs-on: ubuntu-latest
33
+    steps:
34
+      - uses: actions/checkout@v4
35
+      - uses: astral-sh/setup-uv@v6
36
+      - run: uv python install 3.12
37
+      - uses: pnpm/action-setup@v4
38
+      - uses: actions/setup-node@v5
39
+        with:
40
+          node-version: 24
41
+      - run: make deps-py
42
+      - run: make deps-frontend
43
+      - run: make lint-templates
44
+
45
+  lint-yaml:
46
+    if: needs.files-changed.outputs.yaml == 'true'
47
+    needs: files-changed
48
+    runs-on: ubuntu-latest
49
+    steps:
50
+      - uses: actions/checkout@v4
51
+      - uses: astral-sh/setup-uv@v6
52
+      - run: uv python install 3.12
53
+      - run: make deps-py
54
+      - run: make lint-yaml
55
+
56
+  lint-swagger:
57
+    if: needs.files-changed.outputs.swagger == 'true'
58
+    needs: files-changed
59
+    runs-on: ubuntu-latest
60
+    steps:
61
+      - uses: actions/checkout@v4
62
+      - uses: pnpm/action-setup@v4
63
+      - uses: actions/setup-node@v5
64
+        with:
65
+          node-version: 24
66
+      - run: make deps-frontend
67
+      - run: make lint-swagger
68
+
69
+  lint-spell:
70
+    if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.frontend == 'true' || needs.files-changed.outputs.actions == 'true' || needs.files-changed.outputs.docs == 'true' || needs.files-changed.outputs.templates == 'true'
71
+    needs: files-changed
72
+    runs-on: ubuntu-latest
73
+    steps:
74
+      - uses: actions/checkout@v4
75
+      - uses: actions/setup-go@v5
76
+        with:
77
+          go-version-file: go.mod
78
+          check-latest: true
79
+      - run: make lint-spell
80
+
81
+  lint-go-windows:
82
+    if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
83
+    needs: files-changed
84
+    runs-on: ubuntu-latest
85
+    steps:
86
+      - uses: actions/checkout@v4
87
+      - uses: actions/setup-go@v5
88
+        with:
89
+          go-version-file: go.mod
90
+          check-latest: true
91
+      - run: make deps-backend deps-tools
92
+      - run: make lint-go-windows lint-go-gitea-vet
93
+        env:
94
+          TAGS: bindata sqlite sqlite_unlock_notify
95
+          GOOS: windows
96
+          GOARCH: amd64
97
+
98
+  lint-go-gogit:
99
+    if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
100
+    needs: files-changed
101
+    runs-on: ubuntu-latest
102
+    steps:
103
+      - uses: actions/checkout@v4
104
+      - uses: actions/setup-go@v5
105
+        with:
106
+          go-version-file: go.mod
107
+          check-latest: true
108
+      - run: make deps-backend deps-tools
109
+      - run: make lint-go
110
+        env:
111
+          TAGS: bindata gogit sqlite sqlite_unlock_notify
112
+
113
+  checks-backend:
114
+    if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
115
+    needs: files-changed
116
+    runs-on: ubuntu-latest
117
+    steps:
118
+      - uses: actions/checkout@v4
119
+      - uses: actions/setup-go@v5
120
+        with:
121
+          go-version-file: go.mod
122
+          check-latest: true
123
+      - run: make deps-backend deps-tools
124
+      - run: make --always-make checks-backend # ensure the "go-licenses" make target runs
125
+
126
+  frontend:
127
+    if: needs.files-changed.outputs.frontend == 'true' || needs.files-changed.outputs.actions == 'true'
128
+    needs: files-changed
129
+    runs-on: ubuntu-latest
130
+    steps:
131
+      - uses: actions/checkout@v4
132
+      - uses: pnpm/action-setup@v4
133
+      - uses: actions/setup-node@v5
134
+        with:
135
+          node-version: 24
136
+      - run: make deps-frontend
137
+      - run: make lint-frontend
138
+      - run: make checks-frontend
139
+      - run: make test-frontend
140
+      - run: make frontend
141
+
142
+  backend:
143
+    if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
144
+    needs: files-changed
145
+    runs-on: ubuntu-latest
146
+    steps:
147
+      - uses: actions/checkout@v4
148
+      - uses: actions/setup-go@v5
149
+        with:
150
+          go-version-file: go.mod
151
+          check-latest: true
152
+      # no frontend build here as backend should be able to build
153
+      # even without any frontend files
154
+      - run: make deps-backend
155
+      - run: go build -o gitea_no_gcc # test if build succeeds without the sqlite tag
156
+      - name: build-backend-arm64
157
+        run: make backend # test cross compile
158
+        env:
159
+          GOOS: linux
160
+          GOARCH: arm64
161
+          TAGS: bindata gogit
162
+      - name: build-backend-windows
163
+        run: go build -o gitea_windows
164
+        env:
165
+          GOOS: windows
166
+          GOARCH: amd64
167
+          TAGS: bindata gogit
168
+      - name: build-backend-386
169
+        run: go build -o gitea_linux_386 # test if compatible with 32 bit
170
+        env:
171
+          GOOS: linux
172
+          GOARCH: 386
173
+
174
+  docs:
175
+    if: needs.files-changed.outputs.docs == 'true' || needs.files-changed.outputs.actions == 'true'
176
+    needs: files-changed
177
+    runs-on: ubuntu-latest
178
+    steps:
179
+      - uses: actions/checkout@v4
180
+      - uses: pnpm/action-setup@v4
181
+      - uses: actions/setup-node@v5
182
+        with:
183
+          node-version: 24
184
+      - run: make deps-frontend
185
+      - run: make lint-md
186
+
187
+  actions:
188
+    if: needs.files-changed.outputs.actions == 'true' || needs.files-changed.outputs.actions == 'true'
189
+    needs: files-changed
190
+    runs-on: ubuntu-latest
191
+    steps:
192
+      - uses: actions/checkout@v4
193
+      - uses: actions/setup-go@v5
194
+        with:
195
+          go-version-file: go.mod
196
+          check-latest: true
197
+      - run: make lint-actions

+ 237
- 0
.github/workflows/pull-db-tests.yml 查看文件

@@ -0,0 +1,237 @@
1
+name: db-tests
2
+
3
+on:
4
+  pull_request:
5
+
6
+concurrency:
7
+  group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
8
+  cancel-in-progress: true
9
+
10
+jobs:
11
+  files-changed:
12
+    uses: ./.github/workflows/files-changed.yml
13
+
14
+  test-pgsql:
15
+    if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
16
+    needs: files-changed
17
+    runs-on: ubuntu-latest
18
+    services:
19
+      pgsql:
20
+        image: postgres:14
21
+        env:
22
+          POSTGRES_DB: test
23
+          POSTGRES_PASSWORD: postgres
24
+        ports:
25
+          - "5432:5432"
26
+      ldap:
27
+        image: gitea/test-openldap:latest
28
+        ports:
29
+          - "389:389"
30
+          - "636:636"
31
+      minio:
32
+        # as github actions doesn't support "entrypoint", we need to use a non-official image
33
+        # that has a custom entrypoint set to "minio server /data"
34
+        image: bitnamilegacy/minio:2023.8.31
35
+        env:
36
+          MINIO_ROOT_USER: 123456
37
+          MINIO_ROOT_PASSWORD: 12345678
38
+        ports:
39
+          - "9000:9000"
40
+    steps:
41
+      - uses: actions/checkout@v4
42
+      - uses: actions/setup-go@v5
43
+        with:
44
+          go-version-file: go.mod
45
+          check-latest: true
46
+      - name: Add hosts to /etc/hosts
47
+        run: '[ -e "/.dockerenv" ] || [ -e "/run/.containerenv" ] || echo "127.0.0.1 pgsql ldap minio" | sudo tee -a /etc/hosts'
48
+      - run: make deps-backend
49
+      - run: make backend
50
+        env:
51
+          TAGS: bindata
52
+      - name: run migration tests
53
+        run: make test-pgsql-migration
54
+      - name: run tests
55
+        run: make test-pgsql
56
+        timeout-minutes: 50
57
+        env:
58
+          TAGS: bindata gogit
59
+          RACE_ENABLED: true
60
+          TEST_TAGS: gogit
61
+          TEST_LDAP: 1
62
+          USE_REPO_TEST_DIR: 1
63
+
64
+  test-sqlite:
65
+    if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
66
+    needs: files-changed
67
+    runs-on: ubuntu-latest
68
+    steps:
69
+      - uses: actions/checkout@v4
70
+      - uses: actions/setup-go@v5
71
+        with:
72
+          go-version-file: go.mod
73
+          check-latest: true
74
+      - run: make deps-backend
75
+      - run: make backend
76
+        env:
77
+          TAGS: bindata gogit sqlite sqlite_unlock_notify
78
+      - name: run migration tests
79
+        run: make test-sqlite-migration
80
+      - name: run tests
81
+        run: make test-sqlite
82
+        timeout-minutes: 50
83
+        env:
84
+          TAGS: bindata gogit sqlite sqlite_unlock_notify
85
+          RACE_ENABLED: true
86
+          TEST_TAGS: gogit sqlite sqlite_unlock_notify
87
+          USE_REPO_TEST_DIR: 1
88
+
89
+  test-unit:
90
+    if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
91
+    needs: files-changed
92
+    runs-on: ubuntu-latest
93
+    services:
94
+      elasticsearch:
95
+        image: elasticsearch:7.5.0
96
+        env:
97
+          discovery.type: single-node
98
+        ports:
99
+          - "9200:9200"
100
+      meilisearch:
101
+        image: getmeili/meilisearch:v1
102
+        env:
103
+          MEILI_ENV: development # disable auth
104
+        ports:
105
+          - "7700:7700"
106
+      redis:
107
+        image: redis
108
+        options: >- # wait until redis has started
109
+          --health-cmd "redis-cli ping"
110
+          --health-interval 5s
111
+          --health-timeout 3s
112
+          --health-retries 10
113
+        ports:
114
+          - 6379:6379
115
+      minio:
116
+        image: bitnamilegacy/minio:2021.3.17
117
+        env:
118
+          MINIO_ACCESS_KEY: 123456
119
+          MINIO_SECRET_KEY: 12345678
120
+        ports:
121
+          - "9000:9000"
122
+      devstoreaccount1.azurite.local: # https://github.com/Azure/Azurite/issues/1583
123
+        image: mcr.microsoft.com/azure-storage/azurite:latest
124
+        ports:
125
+          - 10000:10000
126
+    steps:
127
+      - uses: actions/checkout@v4
128
+      - uses: actions/setup-go@v5
129
+        with:
130
+          go-version-file: go.mod
131
+          check-latest: true
132
+      - name: Add hosts to /etc/hosts
133
+        run: '[ -e "/.dockerenv" ] || [ -e "/run/.containerenv" ] || echo "127.0.0.1 minio devstoreaccount1.azurite.local mysql elasticsearch meilisearch smtpimap" | sudo tee -a /etc/hosts'
134
+      - run: make deps-backend
135
+      - run: make backend
136
+        env:
137
+          TAGS: bindata
138
+      - name: unit-tests
139
+        run: make unit-test-coverage test-check
140
+        env:
141
+          TAGS: bindata
142
+          RACE_ENABLED: true
143
+          GITHUB_READ_TOKEN: ${{ secrets.GITHUB_READ_TOKEN }}
144
+      - name: unit-tests-gogit
145
+        run: make unit-test-coverage test-check
146
+        env:
147
+          TAGS: bindata gogit
148
+          RACE_ENABLED: true
149
+          GITHUB_READ_TOKEN: ${{ secrets.GITHUB_READ_TOKEN }}
150
+
151
+  test-mysql:
152
+    if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
153
+    needs: files-changed
154
+    runs-on: ubuntu-latest
155
+    services:
156
+      mysql:
157
+        # the bitnami mysql image has more options than the official one, it's easier to customize
158
+        image: bitnamilegacy/mysql:8.0
159
+        env:
160
+          ALLOW_EMPTY_PASSWORD: true
161
+          MYSQL_DATABASE: testgitea
162
+        ports:
163
+          - "3306:3306"
164
+        options: >-
165
+          --mount type=tmpfs,destination=/bitnami/mysql/data
166
+      elasticsearch:
167
+        image: elasticsearch:7.5.0
168
+        env:
169
+          discovery.type: single-node
170
+        ports:
171
+          - "9200:9200"
172
+      smtpimap:
173
+        image: tabascoterrier/docker-imap-devel:latest
174
+        ports:
175
+          - "25:25"
176
+          - "143:143"
177
+          - "587:587"
178
+          - "993:993"
179
+    steps:
180
+      - uses: actions/checkout@v4
181
+      - uses: actions/setup-go@v5
182
+        with:
183
+          go-version-file: go.mod
184
+          check-latest: true
185
+      - name: Add hosts to /etc/hosts
186
+        run: '[ -e "/.dockerenv" ] || [ -e "/run/.containerenv" ] || echo "127.0.0.1 mysql elasticsearch smtpimap" | sudo tee -a /etc/hosts'
187
+      - run: make deps-backend
188
+      - run: make backend
189
+        env:
190
+          TAGS: bindata
191
+      - name: run migration tests
192
+        run: make test-mysql-migration
193
+      - name: run tests
194
+        # run: make integration-test-coverage (at the moment, no coverage is really handled)
195
+        run: make test-mysql
196
+        env:
197
+          TAGS: bindata
198
+          RACE_ENABLED: true
199
+          USE_REPO_TEST_DIR: 1
200
+          TEST_INDEXER_CODE_ES_URL: "http://elastic:changeme@elasticsearch:9200"
201
+
202
+  test-mssql:
203
+    if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
204
+    needs: files-changed
205
+    runs-on: ubuntu-latest
206
+    services:
207
+      mssql:
208
+        image: mcr.microsoft.com/mssql/server:2019-latest
209
+        env:
210
+          ACCEPT_EULA: Y
211
+          MSSQL_PID: Standard
212
+          SA_PASSWORD: MwantsaSecurePassword1
213
+        ports:
214
+          - "1433:1433"
215
+      devstoreaccount1.azurite.local: # https://github.com/Azure/Azurite/issues/1583
216
+        image: mcr.microsoft.com/azure-storage/azurite:latest
217
+        ports:
218
+          - 10000:10000
219
+    steps:
220
+      - uses: actions/checkout@v4
221
+      - uses: actions/setup-go@v5
222
+        with:
223
+          go-version-file: go.mod
224
+          check-latest: true
225
+      - name: Add hosts to /etc/hosts
226
+        run: '[ -e "/.dockerenv" ] || [ -e "/run/.containerenv" ] || echo "127.0.0.1 mssql devstoreaccount1.azurite.local" | sudo tee -a /etc/hosts'
227
+      - run: make deps-backend
228
+      - run: make backend
229
+        env:
230
+          TAGS: bindata
231
+      - run: make test-mssql-migration
232
+      - name: run tests
233
+        run: make test-mssql
234
+        timeout-minutes: 50
235
+        env:
236
+          TAGS: bindata
237
+          USE_REPO_TEST_DIR: 1

+ 35
- 0
.github/workflows/pull-docker-dryrun.yml 查看文件

@@ -0,0 +1,35 @@
1
+name: docker-dryrun
2
+
3
+on:
4
+  pull_request:
5
+
6
+concurrency:
7
+  group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
8
+  cancel-in-progress: true
9
+
10
+jobs:
11
+  files-changed:
12
+    uses: ./.github/workflows/files-changed.yml
13
+
14
+  regular:
15
+    if: needs.files-changed.outputs.docker == 'true' || needs.files-changed.outputs.actions == 'true'
16
+    needs: files-changed
17
+    runs-on: ubuntu-latest
18
+    steps:
19
+      - uses: docker/setup-buildx-action@v3
20
+      - uses: docker/build-push-action@v5
21
+        with:
22
+          push: false
23
+          tags: gitea/gitea:linux-amd64
24
+
25
+  rootless:
26
+    if: needs.files-changed.outputs.docker == 'true' || needs.files-changed.outputs.actions == 'true'
27
+    needs: files-changed
28
+    runs-on: ubuntu-latest
29
+    steps:
30
+      - uses: docker/setup-buildx-action@v3
31
+      - uses: docker/build-push-action@v5
32
+        with:
33
+          push: false
34
+          file: Dockerfile.rootless
35
+          tags: gitea/gitea:linux-amd64

+ 35
- 0
.github/workflows/pull-e2e-tests.yml 查看文件

@@ -0,0 +1,35 @@
1
+name: e2e-tests
2
+
3
+on:
4
+  pull_request:
5
+
6
+concurrency:
7
+  group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
8
+  cancel-in-progress: true
9
+
10
+jobs:
11
+  files-changed:
12
+    uses: ./.github/workflows/files-changed.yml
13
+
14
+  test-e2e:
15
+    # the "test-e2e" won't pass, and it seems that there is no useful test, so skip
16
+    # if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.frontend == 'true' || needs.files-changed.outputs.actions == 'true'
17
+    if: false
18
+    needs: files-changed
19
+    runs-on: ubuntu-latest
20
+    steps:
21
+      - uses: actions/checkout@v4
22
+      - uses: actions/setup-go@v5
23
+        with:
24
+          go-version-file: go.mod
25
+          check-latest: true
26
+      - uses: pnpm/action-setup@v4
27
+      - uses: actions/setup-node@v5
28
+        with:
29
+          node-version: 24
30
+      - run: make deps-frontend frontend deps-backend
31
+      - run: pnpm exec playwright install --with-deps
32
+      - run: make test-e2e-sqlite
33
+        timeout-minutes: 40
34
+        env:
35
+          USE_REPO_TEST_DIR: 1

+ 20
- 0
.github/workflows/pull-labeler.yml 查看文件

@@ -0,0 +1,20 @@
1
+name: labeler
2
+
3
+on:
4
+  pull_request_target:
5
+    types: [opened, synchronize, reopened]
6
+
7
+concurrency:
8
+  group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
9
+  cancel-in-progress: true
10
+
11
+jobs:
12
+  labeler:
13
+    runs-on: ubuntu-latest
14
+    permissions:
15
+      contents: read
16
+      pull-requests: write
17
+    steps:
18
+      - uses: actions/labeler@v5
19
+        with:
20
+          sync-labels: true

+ 143
- 0
.github/workflows/release-nightly.yml 查看文件

@@ -0,0 +1,143 @@
1
+name: release-nightly
2
+
3
+on:
4
+  push:
5
+    branches: [main, release/v*]
6
+
7
+concurrency:
8
+  group: ${{ github.workflow }}-${{ github.ref }}
9
+  cancel-in-progress: true
10
+
11
+jobs:
12
+  nightly-binary:
13
+    runs-on: namespace-profile-gitea-release-binary
14
+    steps:
15
+      - uses: actions/checkout@v4
16
+      # fetch all commits instead of only the last as some branches are long lived and could have many between versions
17
+      # fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
18
+      - run: git fetch --unshallow --quiet --tags --force
19
+      - uses: actions/setup-go@v5
20
+        with:
21
+          go-version-file: go.mod
22
+          check-latest: true
23
+      - uses: pnpm/action-setup@v4
24
+      - uses: actions/setup-node@v5
25
+        with:
26
+          node-version: 24
27
+      - run: make deps-frontend deps-backend
28
+      # xgo build
29
+      - run: make release
30
+        env:
31
+          TAGS: bindata sqlite sqlite_unlock_notify
32
+      - name: import gpg key
33
+        id: import_gpg
34
+        uses: crazy-max/ghaction-import-gpg@v6
35
+        with:
36
+          gpg_private_key: ${{ secrets.GPGSIGN_KEY }}
37
+          passphrase: ${{ secrets.GPGSIGN_PASSPHRASE }}
38
+      - name: sign binaries
39
+        run: |
40
+          for f in dist/release/*; do
41
+            echo '${{ secrets.GPGSIGN_PASSPHRASE }}' | gpg --pinentry-mode loopback --passphrase-fd 0 --batch --yes --detach-sign -u ${{ steps.import_gpg.outputs.fingerprint }} --output "$f.asc" "$f"
42
+          done
43
+      # clean branch name to get the folder name in S3
44
+      - name: Get cleaned branch name
45
+        id: clean_name
46
+        run: |
47
+          REF_NAME=$(echo "${{ github.ref }}" | sed -e 's/refs\/heads\///' -e 's/refs\/tags\///' -e 's/release\/v//')
48
+          echo "Cleaned name is ${REF_NAME}"
49
+          echo "branch=${REF_NAME}-nightly" >> "$GITHUB_OUTPUT"
50
+      - name: configure aws
51
+        uses: aws-actions/configure-aws-credentials@v4
52
+        with:
53
+          aws-region: ${{ secrets.AWS_REGION }}
54
+          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
55
+          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
56
+      - name: upload binaries to s3
57
+        run: |
58
+          aws s3 sync dist/release s3://${{ secrets.AWS_S3_BUCKET }}/gitea/${{ steps.clean_name.outputs.branch }} --no-progress
59
+  nightly-docker-rootful:
60
+    runs-on: namespace-profile-gitea-release-docker
61
+    permissions:
62
+      packages: write # to publish to ghcr.io
63
+    steps:
64
+      - uses: actions/checkout@v4
65
+      # fetch all commits instead of only the last as some branches are long lived and could have many between versions
66
+      # fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
67
+      - run: git fetch --unshallow --quiet --tags --force
68
+      - uses: actions/setup-go@v5
69
+        with:
70
+          go-version-file: go.mod
71
+          check-latest: true
72
+      - uses: docker/setup-qemu-action@v3
73
+      - uses: docker/setup-buildx-action@v3
74
+      - name: Get cleaned branch name
75
+        id: clean_name
76
+        run: |
77
+          REF_NAME=$(echo "${{ github.ref }}" | sed -e 's/refs\/heads\///' -e 's/refs\/tags\///' -e 's/release\/v//')
78
+          echo "branch=${REF_NAME}-nightly" >> "$GITHUB_OUTPUT"
79
+      - name: Login to Docker Hub
80
+        uses: docker/login-action@v3
81
+        with:
82
+          username: ${{ secrets.DOCKERHUB_USERNAME }}
83
+          password: ${{ secrets.DOCKERHUB_TOKEN }}
84
+      - name: Login to GHCR using PAT
85
+        uses: docker/login-action@v3
86
+        with:
87
+          registry: ghcr.io
88
+          username: ${{ github.repository_owner }}
89
+          password: ${{ secrets.GITHUB_TOKEN }}
90
+      - name: fetch go modules
91
+        run: make vendor
92
+      - name: build rootful docker image
93
+        uses: docker/build-push-action@v5
94
+        with:
95
+          context: .
96
+          platforms: linux/amd64,linux/arm64,linux/riscv64
97
+          push: true
98
+          tags: |-
99
+            gitea/gitea:${{ steps.clean_name.outputs.branch }}
100
+            ghcr.io/go-gitea/gitea:${{ steps.clean_name.outputs.branch }}
101
+  nightly-docker-rootless:
102
+    runs-on: namespace-profile-gitea-release-docker
103
+    permissions:
104
+      packages: write # to publish to ghcr.io
105
+    steps:
106
+      - uses: actions/checkout@v4
107
+      # fetch all commits instead of only the last as some branches are long lived and could have many between versions
108
+      # fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
109
+      - run: git fetch --unshallow --quiet --tags --force
110
+      - uses: actions/setup-go@v5
111
+        with:
112
+          go-version-file: go.mod
113
+          check-latest: true
114
+      - uses: docker/setup-qemu-action@v3
115
+      - uses: docker/setup-buildx-action@v3
116
+      - name: Get cleaned branch name
117
+        id: clean_name
118
+        run: |
119
+          REF_NAME=$(echo "${{ github.ref }}" | sed -e 's/refs\/heads\///' -e 's/refs\/tags\///' -e 's/release\/v//')
120
+          echo "branch=${REF_NAME}-nightly" >> "$GITHUB_OUTPUT"
121
+      - name: Login to Docker Hub
122
+        uses: docker/login-action@v3
123
+        with:
124
+          username: ${{ secrets.DOCKERHUB_USERNAME }}
125
+          password: ${{ secrets.DOCKERHUB_TOKEN }}
126
+      - name: Login to GHCR using PAT
127
+        uses: docker/login-action@v3
128
+        with:
129
+          registry: ghcr.io
130
+          username: ${{ github.repository_owner }}
131
+          password: ${{ secrets.GITHUB_TOKEN }}
132
+      - name: fetch go modules
133
+        run: make vendor
134
+      - name: build rootless docker image
135
+        uses: docker/build-push-action@v5
136
+        with:
137
+          context: .
138
+          platforms: linux/amd64,linux/arm64
139
+          push: true
140
+          file: Dockerfile.rootless
141
+          tags: |-
142
+            gitea/gitea:${{ steps.clean_name.outputs.branch }}-rootless
143
+            ghcr.io/go-gitea/gitea:${{ steps.clean_name.outputs.branch }}-rootless

+ 153
- 0
.github/workflows/release-tag-rc.yml 查看文件

@@ -0,0 +1,153 @@
1
+name: release-tag-rc
2
+
3
+on:
4
+  push:
5
+    tags:
6
+      - "v1*-rc*"
7
+
8
+concurrency:
9
+  group: ${{ github.workflow }}-${{ github.ref }}
10
+  cancel-in-progress: false
11
+
12
+jobs:
13
+  binary:
14
+    runs-on: namespace-profile-gitea-release-binary
15
+    steps:
16
+      - uses: actions/checkout@v4
17
+      # fetch all commits instead of only the last as some branches are long lived and could have many between versions
18
+      # fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
19
+      - run: git fetch --unshallow --quiet --tags --force
20
+      - uses: actions/setup-go@v5
21
+        with:
22
+          go-version-file: go.mod
23
+          check-latest: true
24
+      - uses: pnpm/action-setup@v4
25
+      - uses: actions/setup-node@v5
26
+        with:
27
+          node-version: 24
28
+      - run: make deps-frontend deps-backend
29
+      # xgo build
30
+      - run: make release
31
+        env:
32
+          TAGS: bindata sqlite sqlite_unlock_notify
33
+      - name: import gpg key
34
+        id: import_gpg
35
+        uses: crazy-max/ghaction-import-gpg@v6
36
+        with:
37
+          gpg_private_key: ${{ secrets.GPGSIGN_KEY }}
38
+          passphrase: ${{ secrets.GPGSIGN_PASSPHRASE }}
39
+      - name: sign binaries
40
+        run: |
41
+          for f in dist/release/*; do
42
+            echo '${{ secrets.GPGSIGN_PASSPHRASE }}' | gpg --pinentry-mode loopback --passphrase-fd 0 --batch --yes --detach-sign -u ${{ steps.import_gpg.outputs.fingerprint }} --output "$f.asc" "$f"
43
+          done
44
+      # clean branch name to get the folder name in S3
45
+      - name: Get cleaned branch name
46
+        id: clean_name
47
+        run: |
48
+          REF_NAME=$(echo "${{ github.ref }}" | sed -e 's/refs\/heads\///' -e 's/refs\/tags\/v//' -e 's/release\/v//')
49
+          echo "Cleaned name is ${REF_NAME}"
50
+          echo "branch=${REF_NAME}" >> "$GITHUB_OUTPUT"
51
+      - name: configure aws
52
+        uses: aws-actions/configure-aws-credentials@v4
53
+        with:
54
+          aws-region: ${{ secrets.AWS_REGION }}
55
+          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
56
+          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
57
+      - name: upload binaries to s3
58
+        run: |
59
+          aws s3 sync dist/release s3://${{ secrets.AWS_S3_BUCKET }}/gitea/${{ steps.clean_name.outputs.branch }} --no-progress
60
+      - name: Install GH CLI
61
+        uses: dev-hanz-ops/install-gh-cli-action@v0.1.0
62
+        with:
63
+          gh-cli-version: 2.39.1
64
+      - name: create github release
65
+        run: |
66
+          gh release create ${{ github.ref_name }} --title ${{ github.ref_name }} --draft --notes-from-tag dist/release/*
67
+        env:
68
+          GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
69
+  docker-rootful:
70
+    runs-on: namespace-profile-gitea-release-docker
71
+    permissions:
72
+      packages: write # to publish to ghcr.io
73
+    steps:
74
+      - uses: actions/checkout@v4
75
+      # fetch all commits instead of only the last as some branches are long lived and could have many between versions
76
+      # fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
77
+      - run: git fetch --unshallow --quiet --tags --force
78
+      - uses: docker/setup-qemu-action@v3
79
+      - uses: docker/setup-buildx-action@v3
80
+      - uses: docker/metadata-action@v5
81
+        id: meta
82
+        with:
83
+          images: |-
84
+            gitea/gitea
85
+            ghcr.io/go-gitea/gitea
86
+          flavor: |
87
+            latest=false
88
+          # 1.2.3-rc0
89
+          tags: |
90
+            type=semver,pattern={{version}}
91
+      - name: Login to Docker Hub
92
+        uses: docker/login-action@v3
93
+        with:
94
+          username: ${{ secrets.DOCKERHUB_USERNAME }}
95
+          password: ${{ secrets.DOCKERHUB_TOKEN }}
96
+      - name: Login to GHCR using PAT
97
+        uses: docker/login-action@v3
98
+        with:
99
+          registry: ghcr.io
100
+          username: ${{ github.repository_owner }}
101
+          password: ${{ secrets.GITHUB_TOKEN }}
102
+      - name: build rootful docker image
103
+        uses: docker/build-push-action@v5
104
+        with:
105
+          context: .
106
+          platforms: linux/amd64,linux/arm64,linux/riscv64
107
+          push: true
108
+          tags: ${{ steps.meta.outputs.tags }}
109
+          labels: ${{ steps.meta.outputs.labels }}
110
+  docker-rootless:
111
+    runs-on: namespace-profile-gitea-release-docker
112
+    permissions:
113
+      packages: write # to publish to ghcr.io
114
+    steps:
115
+      - uses: actions/checkout@v4
116
+      # fetch all commits instead of only the last as some branches are long lived and could have many between versions
117
+      # fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
118
+      - run: git fetch --unshallow --quiet --tags --force
119
+      - uses: docker/setup-qemu-action@v3
120
+      - uses: docker/setup-buildx-action@v3
121
+      - uses: docker/metadata-action@v5
122
+        id: meta
123
+        with:
124
+          images: |-
125
+            gitea/gitea
126
+            ghcr.io/go-gitea/gitea
127
+          # each tag below will have the suffix of -rootless
128
+          flavor: |
129
+            latest=false
130
+            suffix=-rootless
131
+          # 1.2.3-rc0
132
+          tags: |
133
+            type=semver,pattern={{version}}
134
+      - name: Login to Docker Hub
135
+        uses: docker/login-action@v3
136
+        with:
137
+          username: ${{ secrets.DOCKERHUB_USERNAME }}
138
+          password: ${{ secrets.DOCKERHUB_TOKEN }}
139
+      - name: Login to GHCR using PAT
140
+        uses: docker/login-action@v3
141
+        with:
142
+          registry: ghcr.io
143
+          username: ${{ github.repository_owner }}
144
+          password: ${{ secrets.GITHUB_TOKEN }}
145
+      - name: build rootless docker image
146
+        uses: docker/build-push-action@v5
147
+        with:
148
+          context: .
149
+          platforms: linux/amd64,linux/arm64,linux/riscv64
150
+          push: true
151
+          file: Dockerfile.rootless
152
+          tags: ${{ steps.meta.outputs.tags }}
153
+          labels: ${{ steps.meta.outputs.labels }}

+ 164
- 0
.github/workflows/release-tag-version.yml 查看文件

@@ -0,0 +1,164 @@
1
+name: release-tag-version
2
+
3
+on:
4
+  push:
5
+    tags:
6
+      - "v1.*"
7
+      - "!v1*-rc*"
8
+      - "!v1*-dev"
9
+
10
+concurrency:
11
+  group: ${{ github.workflow }}-${{ github.ref }}
12
+  cancel-in-progress: false
13
+
14
+jobs:
15
+  binary:
16
+    runs-on: namespace-profile-gitea-release-binary
17
+    permissions:
18
+      packages: write # to publish to ghcr.io
19
+    steps:
20
+      - uses: actions/checkout@v4
21
+      # fetch all commits instead of only the last as some branches are long lived and could have many between versions
22
+      # fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
23
+      - run: git fetch --unshallow --quiet --tags --force
24
+      - uses: actions/setup-go@v5
25
+        with:
26
+          go-version-file: go.mod
27
+          check-latest: true
28
+      - uses: pnpm/action-setup@v4
29
+      - uses: actions/setup-node@v5
30
+        with:
31
+          node-version: 24
32
+      - run: make deps-frontend deps-backend
33
+      # xgo build
34
+      - run: make release
35
+        env:
36
+          TAGS: bindata sqlite sqlite_unlock_notify
37
+      - name: import gpg key
38
+        id: import_gpg
39
+        uses: crazy-max/ghaction-import-gpg@v6
40
+        with:
41
+          gpg_private_key: ${{ secrets.GPGSIGN_KEY }}
42
+          passphrase: ${{ secrets.GPGSIGN_PASSPHRASE }}
43
+      - name: sign binaries
44
+        run: |
45
+          for f in dist/release/*; do
46
+            echo '${{ secrets.GPGSIGN_PASSPHRASE }}' | gpg --pinentry-mode loopback --passphrase-fd 0 --batch --yes --detach-sign -u ${{ steps.import_gpg.outputs.fingerprint }} --output "$f.asc" "$f"
47
+          done
48
+      # clean branch name to get the folder name in S3
49
+      - name: Get cleaned branch name
50
+        id: clean_name
51
+        run: |
52
+          REF_NAME=$(echo "${{ github.ref }}" | sed -e 's/refs\/heads\///' -e 's/refs\/tags\/v//' -e 's/release\/v//')
53
+          echo "Cleaned name is ${REF_NAME}"
54
+          echo "branch=${REF_NAME}" >> "$GITHUB_OUTPUT"
55
+      - name: configure aws
56
+        uses: aws-actions/configure-aws-credentials@v4
57
+        with:
58
+          aws-region: ${{ secrets.AWS_REGION }}
59
+          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
60
+          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
61
+      - name: upload binaries to s3
62
+        run: |
63
+          aws s3 sync dist/release s3://${{ secrets.AWS_S3_BUCKET }}/gitea/${{ steps.clean_name.outputs.branch }} --no-progress
64
+      - name: Install GH CLI
65
+        uses: dev-hanz-ops/install-gh-cli-action@v0.1.0
66
+        with:
67
+          gh-cli-version: 2.39.1
68
+      - name: create github release
69
+        run: |
70
+          gh release create ${{ github.ref_name }} --title ${{ github.ref_name }} --notes-from-tag dist/release/*
71
+        env:
72
+          GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
73
+  docker-rootful:
74
+    runs-on: namespace-profile-gitea-release-docker
75
+    permissions:
76
+      packages: write # to publish to ghcr.io
77
+    steps:
78
+      - uses: actions/checkout@v4
79
+      # fetch all commits instead of only the last as some branches are long lived and could have many between versions
80
+      # fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
81
+      - run: git fetch --unshallow --quiet --tags --force
82
+      - uses: docker/setup-qemu-action@v3
83
+      - uses: docker/setup-buildx-action@v3
84
+      - uses: docker/metadata-action@v5
85
+        id: meta
86
+        with:
87
+          images: |-
88
+            gitea/gitea
89
+            ghcr.io/go-gitea/gitea
90
+          # this will generate tags in the following format:
91
+          # latest
92
+          # 1
93
+          # 1.2
94
+          # 1.2.3
95
+          tags: |
96
+            type=semver,pattern={{version}}
97
+            type=semver,pattern={{major}}
98
+            type=semver,pattern={{major}}.{{minor}}
99
+      - name: Login to Docker Hub
100
+        uses: docker/login-action@v3
101
+        with:
102
+          username: ${{ secrets.DOCKERHUB_USERNAME }}
103
+          password: ${{ secrets.DOCKERHUB_TOKEN }}
104
+      - name: Login to GHCR using PAT
105
+        uses: docker/login-action@v3
106
+        with:
107
+          registry: ghcr.io
108
+          username: ${{ github.repository_owner }}
109
+          password: ${{ secrets.GITHUB_TOKEN }}
110
+      - name: build rootful docker image
111
+        uses: docker/build-push-action@v5
112
+        with:
113
+          context: .
114
+          platforms: linux/amd64,linux/arm64,linux/riscv64
115
+          push: true
116
+          tags: ${{ steps.meta.outputs.tags }}
117
+          labels: ${{ steps.meta.outputs.labels }}
118
+  docker-rootless:
119
+    runs-on: namespace-profile-gitea-release-docker
120
+    steps:
121
+      - uses: actions/checkout@v4
122
+      # fetch all commits instead of only the last as some branches are long lived and could have many between versions
123
+      # fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
124
+      - run: git fetch --unshallow --quiet --tags --force
125
+      - uses: docker/setup-qemu-action@v3
126
+      - uses: docker/setup-buildx-action@v3
127
+      - uses: docker/metadata-action@v5
128
+        id: meta
129
+        with:
130
+          images: |-
131
+            gitea/gitea
132
+            ghcr.io/go-gitea/gitea
133
+          # each tag below will have the suffix of -rootless
134
+          flavor: |
135
+            suffix=-rootless,onlatest=true
136
+          # this will generate tags in the following format (with -rootless suffix added):
137
+          # latest
138
+          # 1
139
+          # 1.2
140
+          # 1.2.3
141
+          tags: |
142
+            type=semver,pattern={{version}}
143
+            type=semver,pattern={{major}}
144
+            type=semver,pattern={{major}}.{{minor}}
145
+      - name: Login to Docker Hub
146
+        uses: docker/login-action@v3
147
+        with:
148
+          username: ${{ secrets.DOCKERHUB_USERNAME }}
149
+          password: ${{ secrets.DOCKERHUB_TOKEN }}
150
+      - name: Login to GHCR using PAT
151
+        uses: docker/login-action@v3
152
+        with:
153
+          registry: ghcr.io
154
+          username: ${{ github.repository_owner }}
155
+          password: ${{ secrets.GITHUB_TOKEN }}
156
+      - name: build rootless docker image
157
+        uses: docker/build-push-action@v5
158
+        with:
159
+          context: .
160
+          platforms: linux/amd64,linux/arm64,linux/riscv64
161
+          push: true
162
+          file: Dockerfile.rootless
163
+          tags: ${{ steps.meta.outputs.tags }}
164
+          labels: ${{ steps.meta.outputs.labels }}

+ 124
- 0
.gitignore 查看文件

@@ -0,0 +1,124 @@
1
+# Compiled Object files, Static and Dynamic libs (Shared Objects)
2
+*.o
3
+*.a
4
+*.so
5
+
6
+# Folders
7
+_obj
8
+_test
9
+
10
+# IntelliJ
11
+.idea
12
+.run
13
+
14
+# IntelliJ Gateway
15
+.uuid
16
+
17
+# Goland's output filename can not be set manually
18
+/go_build_*
19
+/gitea_*
20
+
21
+# MS VSCode
22
+.vscode
23
+__debug_bin*
24
+
25
+# Visual Studio
26
+/.vs/
27
+
28
+*.cgo1.go
29
+*.cgo2.c
30
+_cgo_defun.c
31
+_cgo_gotypes.go
32
+_cgo_export.*
33
+
34
+_testmain.go
35
+
36
+*.exe
37
+*.test
38
+*.prof
39
+*.tsbuildinfo
40
+
41
+*coverage.out
42
+coverage.all
43
+cpu.out
44
+
45
+/modules/migration/bindata.*
46
+/modules/options/bindata.*
47
+/modules/public/bindata.*
48
+/modules/templates/bindata.*
49
+
50
+*.db
51
+*.log
52
+*.log.*.gz
53
+
54
+/gitea
55
+/gitea-vet
56
+/debug
57
+/integrations.test
58
+
59
+/bin
60
+/dist
61
+/custom/*
62
+!/custom/conf/app.example.ini
63
+/data
64
+/indexers
65
+/log
66
+/public/assets/img/avatar
67
+/tests/integration/gitea-integration-*
68
+/tests/integration/indexers-*
69
+/tests/e2e/gitea-e2e-*
70
+/tests/e2e/indexers-*
71
+/tests/e2e/reports
72
+/tests/e2e/test-artifacts
73
+/tests/e2e/test-snapshots
74
+/tests/*.ini
75
+/tests/**/*.git/**/*.sample
76
+/node_modules
77
+/.venv
78
+/yarn.lock
79
+/yarn-error.log
80
+/npm-debug.log*
81
+/.pnpm-store
82
+/public/assets/js
83
+/public/assets/css
84
+/public/assets/fonts
85
+/public/assets/licenses.txt
86
+/vendor
87
+/VERSION
88
+/.air
89
+/.go-licenses
90
+
91
+# Files and folders that were previously generated
92
+/public/assets/img/webpack
93
+
94
+# Snapcraft
95
+/gitea_a*.txt
96
+snap/.snapcraft/
97
+parts/
98
+stage/
99
+prime/
100
+*.snap
101
+*.snap-build
102
+*_source.tar.bz2
103
+.DS_Store
104
+
105
+# nix-direnv generated files
106
+.direnv/
107
+
108
+# Make evidence files
109
+/.make_evidence
110
+
111
+# Manpage
112
+/man
113
+
114
+# Ignore AI/LLM instruction files
115
+/.claude/
116
+/.cursorrules
117
+/.cursor/
118
+/.goosehints
119
+/.windsurfrules
120
+/.github/copilot-instructions.md
121
+/AGENT.md
122
+/CLAUDE.md
123
+/llms.txt
124
+

+ 51
- 0
.gitpod.yml 查看文件

@@ -0,0 +1,51 @@
1
+tasks:
2
+  - name: Setup
3
+    init: |
4
+      cp -r contrib/ide/vscode .vscode
5
+      make deps
6
+      make build
7
+    command: |
8
+      gp sync-done setup
9
+      exit 0
10
+  - name: Run backend
11
+    command: |
12
+      gp sync-await setup
13
+
14
+      # Get the URL and extract the domain
15
+      url=$(gp url 3000)
16
+      domain=$(echo $url | awk -F[/:] '{print $4}')
17
+
18
+      if [ -f custom/conf/app.ini ]; then
19
+        sed -i "s|^ROOT_URL =.*|ROOT_URL = ${url}/|" custom/conf/app.ini
20
+        sed -i "s|^DOMAIN =.*|DOMAIN = ${domain}|" custom/conf/app.ini
21
+        sed -i "s|^SSH_DOMAIN =.*|SSH_DOMAIN = ${domain}|" custom/conf/app.ini
22
+        sed -i "s|^NO_REPLY_ADDRESS =.*|SSH_DOMAIN = noreply.${domain}|" custom/conf/app.ini
23
+      else
24
+        mkdir -p custom/conf/
25
+        echo -e "[server]\nROOT_URL = ${url}/" > custom/conf/app.ini
26
+        echo -e "\n[database]\nDB_TYPE = sqlite3\nPATH = $GITPOD_REPO_ROOT/data/gitea.db" >> custom/conf/app.ini
27
+      fi
28
+      export TAGS="sqlite sqlite_unlock_notify"
29
+      make watch-backend
30
+  - name: Run frontend
31
+    command: |
32
+      gp sync-await setup
33
+      make watch-frontend
34
+    openMode: split-right
35
+
36
+vscode:
37
+  extensions:
38
+    - editorconfig.editorconfig
39
+    - dbaeumer.vscode-eslint
40
+    - golang.go
41
+    - stylelint.vscode-stylelint
42
+    - DavidAnson.vscode-markdownlint
43
+    - Vue.volar
44
+    - ms-azuretools.vscode-docker
45
+    - vitest.explorer
46
+    - cweijan.vscode-database-client2
47
+    - GitHub.vscode-pull-request-github
48
+
49
+ports:
50
+  - name: Gitea
51
+    port: 3000

+ 182
- 0
.golangci.yml 查看文件

@@ -0,0 +1,182 @@
1
+version: "2"
2
+output:
3
+  sort-order:
4
+    - file
5
+linters:
6
+  default: none
7
+  enable:
8
+    - bidichk
9
+    - depguard
10
+    - dupl
11
+    - errcheck
12
+    - forbidigo
13
+    - gocritic
14
+    - govet
15
+    - ineffassign
16
+    - mirror
17
+    - nakedret
18
+    - nolintlint
19
+    - perfsprint
20
+    - revive
21
+    - staticcheck
22
+    - testifylint
23
+    - unconvert
24
+    - unparam
25
+    - unused
26
+    - usestdlibvars
27
+    - usetesting
28
+    - wastedassign
29
+  settings:
30
+    depguard:
31
+      rules:
32
+        main:
33
+          deny:
34
+            - pkg: encoding/json
35
+              desc: use gitea's modules/json instead of encoding/json
36
+            - pkg: github.com/unknwon/com
37
+              desc: use gitea's util and replacements
38
+            - pkg: io/ioutil
39
+              desc: use os or io instead
40
+            - pkg: golang.org/x/exp
41
+              desc: it's experimental and unreliable
42
+            - pkg: code.gitea.io/gitea/modules/git/internal
43
+              desc: do not use the internal package, use AddXxx function instead
44
+            - pkg: gopkg.in/ini.v1
45
+              desc: do not use the ini package, use gitea's config system instead
46
+            - pkg: gitea.com/go-chi/cache
47
+              desc: do not use the go-chi cache package, use gitea's cache system
48
+    nolintlint:
49
+      allow-unused: false
50
+      require-explanation: true
51
+      require-specific: true
52
+    gocritic:
53
+      enabled-checks:
54
+        - equalFold
55
+      disabled-checks:
56
+        - ifElseChain
57
+        - singleCaseSwitch # Every time this occurred in the code, there was no other way.
58
+    revive:
59
+      severity: error
60
+      rules:
61
+        - name: atomic
62
+        - name: bare-return
63
+        - name: blank-imports
64
+        - name: constant-logical-expr
65
+        - name: context-as-argument
66
+        - name: context-keys-type
67
+        - name: dot-imports
68
+        - name: duplicated-imports
69
+        - name: empty-lines
70
+        - name: error-naming
71
+        - name: error-return
72
+        - name: error-strings
73
+        - name: errorf
74
+        - name: exported
75
+        - name: identical-branches
76
+        - name: if-return
77
+        - name: increment-decrement
78
+        - name: indent-error-flow
79
+        - name: modifies-value-receiver
80
+        - name: package-comments
81
+        - name: range
82
+        - name: receiver-naming
83
+        - name: redefines-builtin-id
84
+        - name: string-of-int
85
+        - name: superfluous-else
86
+        - name: time-naming
87
+        - name: unconditional-recursion
88
+        - name: unexported-return
89
+        - name: unreachable-code
90
+        - name: var-declaration
91
+        - name: var-naming
92
+          arguments:
93
+            - [] # AllowList - do not remove as args for the rule are positional and won't work without lists first
94
+            - [] # DenyList
95
+            - - skip-package-name-checks: true # supress errors from underscore in migration packages
96
+    staticcheck:
97
+      checks:
98
+        - all
99
+        - -ST1003
100
+        - -ST1005
101
+        - -QF1001
102
+        - -QF1006
103
+        - -QF1008
104
+    testifylint:
105
+      disable:
106
+        - go-require
107
+        - require-error
108
+    usetesting:
109
+      os-temp-dir: true
110
+  exclusions:
111
+    generated: lax
112
+    presets:
113
+      - comments
114
+      - common-false-positives
115
+      - legacy
116
+      - std-error-handling
117
+    rules:
118
+      - linters:
119
+          - dupl
120
+          - errcheck
121
+          - gocyclo
122
+          - gosec
123
+          - staticcheck
124
+          - unparam
125
+        path: _test\.go
126
+      - linters:
127
+          - dupl
128
+          - errcheck
129
+          - gocyclo
130
+          - gosec
131
+        path: models/migrations/v
132
+      - linters:
133
+          - forbidigo
134
+        path: cmd
135
+      - linters:
136
+          - dupl
137
+        text: (?i)webhook
138
+      - linters:
139
+          - gocritic
140
+        text: (?i)`ID' should not be capitalized
141
+      - linters:
142
+          - deadcode
143
+          - unused
144
+        text: (?i)swagger
145
+      - linters:
146
+          - staticcheck
147
+        text: (?i)argument x is overwritten before first use
148
+      - linters:
149
+          - gocritic
150
+        text: '(?i)commentFormatting: put a space between `//` and comment text'
151
+      - linters:
152
+          - gocritic
153
+        text: '(?i)exitAfterDefer:'
154
+    paths:
155
+      - node_modules
156
+      - public
157
+      - web_src
158
+      - third_party$
159
+      - builtin$
160
+      - examples$
161
+issues:
162
+  max-issues-per-linter: 0
163
+  max-same-issues: 0
164
+formatters:
165
+  enable:
166
+    - gofmt
167
+    - gofumpt
168
+  settings:
169
+    gofumpt:
170
+      extra-rules: true
171
+  exclusions:
172
+    generated: lax
173
+    paths:
174
+      - node_modules
175
+      - public
176
+      - web_src
177
+      - third_party$
178
+      - builtin$
179
+      - examples$
180
+
181
+run:
182
+  timeout: 10m

+ 8
- 0
.ignore 查看文件

@@ -0,0 +1,8 @@
1
+*.min.css
2
+*.min.js
3
+/assets/*.json
4
+/options/gitignore
5
+/options/license
6
+/public/assets
7
+/vendor
8
+node_modules

+ 2
- 0
.mailmap 查看文件

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

+ 15
- 0
.markdownlint.yaml 查看文件

@@ -0,0 +1,15 @@
1
+commands-show-output: false
2
+fenced-code-language: false
3
+first-line-h1: false
4
+heading-increment: false
5
+line-length: {code_blocks: false, tables: false, stern: true, line_length: -1}
6
+no-alt-text: false
7
+no-bare-urls: false
8
+no-emphasis-as-heading: false
9
+no-empty-links: false
10
+no-hard-tabs: {code_blocks: false}
11
+no-inline-html: false
12
+no-space-in-code: false
13
+no-space-in-emphasis: false
14
+no-trailing-spaces: {br_spaces: 0}
15
+single-h1: false

+ 7
- 0
.npmrc 查看文件

@@ -0,0 +1,7 @@
1
+audit=false
2
+fund=false
3
+update-notifier=false
4
+save-exact=true
5
+auto-install-peers=true
6
+dedupe-peer-dependents=false
7
+enable-pre-post-scripts=true

+ 12
- 0
.spectral.yaml 查看文件

@@ -0,0 +1,12 @@
1
+extends: [[spectral:oas, all]]
2
+
3
+rules:
4
+  info-contact: off
5
+  oas2-api-host: off
6
+  oas2-parameter-description: off
7
+  oas2-schema: off
8
+  oas2-valid-schema-example: off
9
+  openapi-tags: off
10
+  operation-description: off
11
+  operation-singular-tag: off
12
+  operation-tag-defined: off

+ 44
- 0
.yamllint.yaml 查看文件

@@ -0,0 +1,44 @@
1
+extends: default
2
+
3
+rules:
4
+  braces:
5
+    min-spaces-inside: 0
6
+    max-spaces-inside: 1
7
+    min-spaces-inside-empty: 0
8
+    max-spaces-inside-empty: 0
9
+
10
+  brackets:
11
+    min-spaces-inside: 0
12
+    max-spaces-inside: 1
13
+    min-spaces-inside-empty: 0
14
+    max-spaces-inside-empty: 0
15
+
16
+  comments:
17
+    require-starting-space: true
18
+    ignore-shebangs: true
19
+    min-spaces-from-content: 1
20
+
21
+  comments-indentation:
22
+    level: error
23
+
24
+  document-start:
25
+    level: error
26
+    present: false
27
+
28
+  document-end:
29
+    present: false
30
+
31
+  empty-lines:
32
+    max: 1
33
+
34
+  indentation:
35
+    spaces: 2
36
+
37
+  line-length: disable
38
+
39
+  truthy:
40
+    allowed-values: ["true", "false", "on", "off"]
41
+
42
+ignore: |
43
+  .venv
44
+  node_modules

+ 58
- 0
BSDmakefile 查看文件

@@ -0,0 +1,58 @@
1
+# GNU makefile proxy script for BSD make
2
+#
3
+# Written and maintained by Mahmoud Al-Qudsi <mqudsi@neosmart.net>
4
+# Copyright NeoSmart Technologies <https://neosmart.net/> 2014-2019
5
+# Obtain updates from <https://github.com/neosmart/gmake-proxy>
6
+#
7
+# Redistribution and use in source and binary forms, with or without
8
+# modification, are permitted provided that the following conditions are met:
9
+#
10
+# 1. Redistributions of source code must retain the above copyright notice, this
11
+# list of conditions and the following disclaimer.
12
+#
13
+# 2. Redistributions in binary form must reproduce the above copyright notice,
14
+# this list of conditions and the following disclaimer in the documentation
15
+# and/or other materials provided with the distribution.
16
+#
17
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
21
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
22
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
23
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
24
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
25
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
+
28
+JARG =
29
+GMAKE = "gmake"
30
+# When gmake is called from another make instance, -w is automatically added
31
+# which causes extraneous messages about directory changes to be emitted.
32
+# Running with --no-print-directory silences these messages.
33
+GARGS = "--no-print-directory"
34
+
35
+.if "$(.MAKE.JOBS)" != ""
36
+    JARG = -j$(.MAKE.JOBS)
37
+.endif
38
+
39
+# bmake prefers out-of-source builds and tries to cd into ./obj (among others)
40
+# where possible. GNU Make doesn't, so override that value.
41
+.OBJDIR: ./
42
+
43
+# The GNU convention is to use the lowercased `prefix` variable/macro to
44
+# specify the installation directory. Humor them.
45
+GPREFIX =
46
+.if defined(PREFIX) && ! defined(prefix)
47
+    GPREFIX = 'prefix = "$(PREFIX)"'
48
+.endif
49
+
50
+.BEGIN: .SILENT
51
+	which $(GMAKE) || (printf "Error: GNU Make is required!\n\n" 1>&2 && false)
52
+
53
+.PHONY: FRC
54
+$(.TARGETS): FRC
55
+	$(GMAKE) $(GPREFIX) $(GARGS) $(.TARGETS:S,.DONE,,) $(JARG)
56
+
57
+.DONE .DEFAULT: .SILENT
58
+	$(GMAKE) $(GPREFIX) $(GARGS) $(.TARGETS:S,.DONE,,) $(JARG)

+ 5223
- 0
CHANGELOG-archived.md
文件差異過大導致無法顯示
查看文件


+ 4886
- 0
CHANGELOG.md
文件差異過大導致無法顯示
查看文件


+ 96
- 0
CODE_OF_CONDUCT.md 查看文件

@@ -0,0 +1,96 @@
1
+# Gitea Community Code of Conduct
2
+
3
+## About
4
+
5
+Online communities include people from many different backgrounds. The Gitea contributors are committed to providing a friendly, safe and welcoming environment for all, regardless of gender identity and expression, sexual orientation, disabilities, neurodiversity, physical appearance, body size, ethnicity, nationality, race, age, religion, or similar personal characteristics.
6
+
7
+The first goal of the Code of Conduct is to specify a baseline standard of behavior so that people with different social values and communication styles can talk about Gitea effectively, productively, and respectfully.
8
+
9
+The second goal is to provide a mechanism for resolving conflicts in the community when they arise.
10
+
11
+The third goal of the Code of Conduct is to make our community welcoming to people from different backgrounds. Diversity is critical to the project; for Gitea to be successful, it needs contributors and users from all backgrounds.
12
+
13
+We believe that healthy debate and disagreement are essential to a healthy project and community. However, it is never ok to be disrespectful. We value diverse opinions, but we value respectful behavior more.
14
+
15
+## Community values
16
+
17
+These are the values to which people in the Gitea community should aspire.
18
+
19
+- **Be friendly and welcoming.**
20
+- **Be patient.**
21
+  - Remember that people have varying communication styles and that not everyone is using their native language. (Meaning and tone can be lost in translation.)
22
+- **Be thoughtful.**
23
+  - Productive communication requires effort. Think about how your words will be interpreted.
24
+  - Remember that sometimes it is best to refrain entirely from commenting.
25
+- **Be respectful.**
26
+  - In particular, respect differences of opinion.
27
+- **Be charitable.**
28
+  - Interpret the arguments of others in good faith, do not seek to disagree.
29
+  - When we do disagree, try to understand why.
30
+- **Be constructive.**
31
+  - Avoid derailing: stay on topic; if you want to talk about something else, start a new conversation.
32
+  - 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.
33
+  - Avoid snarking (pithy, unproductive, sniping comments).
34
+  - Avoid discussing potentially offensive or sensitive issues; this all too often leads to unnecessary conflict.
35
+  - Avoid microaggressions (brief and commonplace verbal, behavioral and environmental indignities that communicate hostile, derogatory or negative slights and insults to a person or group).
36
+- **Be responsible.**
37
+  - What you say and do matters. Take responsibility for your words and actions, including their consequences, whether intended or otherwise.
38
+
39
+People are complicated. You should expect to be misunderstood and to misunderstand others; when this inevitably occurs, resist the urge to be defensive or assign blame. Try not to take offense where no offense was intended. Give people the benefit of the doubt. Even if the intent was to provoke, do not rise to it. It is the responsibility of all parties to de-escalate conflict when it arises.
40
+
41
+## Code of Conduct
42
+
43
+### Our Pledge
44
+
45
+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.
46
+
47
+### Our Standards
48
+
49
+Examples of behavior that contributes to creating a positive environment include:
50
+
51
+- Using welcoming and inclusive language
52
+- Being respectful of differing viewpoints and experiences
53
+- Gracefully accepting constructive criticism
54
+- Focusing on what is best for the community
55
+- Showing empathy towards other community members
56
+
57
+Examples of unacceptable behavior by participants include:
58
+
59
+- The use of sexualized language or imagery and unwelcome sexual attention or advances
60
+- Trolling, insulting/derogatory comments, and personal or political attacks
61
+- Public or private harassment
62
+- Publishing others’ private information, such as a physical or electronic address, without explicit permission
63
+- Other conduct which could reasonably be considered inappropriate in a professional setting
64
+
65
+### Our Responsibilities
66
+
67
+Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
68
+
69
+Project maintainers have the right and responsibility to remove, edit, or reject: comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, as well as to ban (temporarily or permanently) any contributor for behaviors that they deem inappropriate, threatening, offensive, or harmful.
70
+
71
+### Scope
72
+
73
+This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
74
+
75
+This Code of Conduct also applies outside the project spaces when the Project Stewards have a reasonable belief that an individual’s behavior may have a negative impact on the project or its community.
76
+
77
+### Conflict Resolution
78
+
79
+We do not believe that all conflict is bad; healthy debate and disagreement often yield positive results. However, it is never okay to be disrespectful or to engage in behavior that violates the project’s code of conduct.
80
+
81
+If you see someone violating the code of conduct, you are encouraged to address the behavior directly with those involved. Many issues can be resolved quickly and easily, and this gives people more control over the outcome of their dispute. If you are unable to resolve the matter for any reason, or if the behavior is threatening or harassing, report it. We are dedicated to providing an environment where participants feel welcome and safe.
82
+
83
+Reports should be directed to the Gitea Project Stewards at conduct@gitea.com. It is the Project Stewards’ duty to receive and address reported violations of the code of conduct. They will then work with a committee consisting of representatives from the technical-oversight-committee.
84
+
85
+We will investigate every complaint, but you may not receive a direct response. We will use our discretion in determining when and how to follow up on reported incidents, which may range from not taking action to permanent expulsion from the project and project-sponsored spaces. Under normal circumstances, we will notify the accused of the report and provide them an opportunity to discuss it before any action is taken. If there is a consensus between maintainers that such an endeavor would be useless (i.e. in case of an obvious spammer), we reserve the right to take action without notifying the accused first. The identity of the reporter will be omitted from the details of the report supplied to the accused. In potentially harmful situations, such as ongoing harassment or threats to anyone’s safety, we may take action without notice.
86
+
87
+### Attribution
88
+
89
+This Code of Conduct is adapted from the Contributor Covenant, version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
90
+
91
+## Summary
92
+
93
+- Treat everyone with respect and kindness.
94
+- Be thoughtful in how you communicate.
95
+- Don’t be destructive or inflammatory.
96
+- If you encounter an issue, please mail conduct@gitea.com.

+ 606
- 0
CONTRIBUTING.md 查看文件

@@ -0,0 +1,606 @@
1
+# Contribution Guidelines
2
+
3
+<details><summary>Table of Contents</summary>
4
+
5
+- [Contribution Guidelines](#contribution-guidelines)
6
+  - [Introduction](#introduction)
7
+  - [Issues](#issues)
8
+    - [How to report issues](#how-to-report-issues)
9
+    - [Types of issues](#types-of-issues)
10
+    - [Discuss your design before the implementation](#discuss-your-design-before-the-implementation)
11
+    - [Issue locking](#issue-locking)
12
+  - [Building Gitea](#building-gitea)
13
+  - [Dependencies](#dependencies)
14
+    - [Backend](#backend)
15
+    - [Frontend](#frontend)
16
+  - [Design guideline](#design-guideline)
17
+  - [Styleguide](#styleguide)
18
+  - [Copyright](#copyright)
19
+  - [Testing](#testing)
20
+  - [Translation](#translation)
21
+  - [Code review](#code-review)
22
+    - [Pull request format](#pull-request-format)
23
+    - [PR title and summary](#pr-title-and-summary)
24
+    - [Milestone](#milestone)
25
+    - [Labels](#labels)
26
+    - [Breaking PRs](#breaking-prs)
27
+      - [What is a breaking PR?](#what-is-a-breaking-pr)
28
+      - [How to handle breaking PRs?](#how-to-handle-breaking-prs)
29
+    - [Maintaining open PRs](#maintaining-open-prs)
30
+    - [Getting PRs merged](#getting-prs-merged)
31
+    - [Final call](#final-call)
32
+    - [Commit messages](#commit-messages)
33
+      - [PR Co-authors](#pr-co-authors)
34
+      - [PRs targeting `main`](#prs-targeting-main)
35
+      - [Backport PRs](#backport-prs)
36
+  - [Documentation](#documentation)
37
+  - [API v1](#api-v1)
38
+    - [GitHub API compatibility](#github-api-compatibility)
39
+    - [Adding/Maintaining API routes](#addingmaintaining-api-routes)
40
+    - [When to use what HTTP method](#when-to-use-what-http-method)
41
+    - [Requirements for API routes](#requirements-for-api-routes)
42
+  - [Backports and Frontports](#backports-and-frontports)
43
+    - [What is backported?](#what-is-backported)
44
+    - [How to backport?](#how-to-backport)
45
+    - [Format of backport PRs](#format-of-backport-prs)
46
+    - [Frontports](#frontports)
47
+  - [Developer Certificate of Origin (DCO)](#developer-certificate-of-origin-dco)
48
+  - [Release Cycle](#release-cycle)
49
+  - [Maintainers](#maintainers)
50
+  - [Technical Oversight Committee (TOC)](#technical-oversight-committee-toc)
51
+    - [TOC election process](#toc-election-process)
52
+    - [Current TOC members](#current-toc-members)
53
+    - [Previous TOC/owners members](#previous-tocowners-members)
54
+  - [Governance Compensation](#governance-compensation)
55
+  - [TOC \& Working groups](#toc--working-groups)
56
+  - [Roadmap](#roadmap)
57
+  - [Versions](#versions)
58
+  - [Releasing Gitea](#releasing-gitea)
59
+
60
+</details>
61
+
62
+## Introduction
63
+
64
+This document explains how to contribute changes to the Gitea project. \
65
+It assumes you have followed the [installation instructions](https://docs.gitea.com/category/installation). \
66
+Sensitive security-related issues should be reported to [security@gitea.io](mailto:security@gitea.io).
67
+
68
+For configuring IDEs for Gitea development, see the [contributed IDE configurations](contrib/ide/).
69
+
70
+## Issues
71
+
72
+### How to report issues
73
+
74
+Please search the issues on the issue tracker with a variety of related keywords to ensure that your issue has not already been reported.
75
+
76
+If your issue has not been reported yet, [open an issue](https://github.com/go-gitea/gitea/issues/new)
77
+and answer the questions so we can understand and reproduce the problematic behavior. \
78
+Please write clear and concise instructions so that we can reproduce the behavior — even if it seems obvious. \
79
+The more detailed and specific you are, the faster we can fix the issue. \
80
+It is really helpful if you can reproduce your problem on a site running on the latest commits, i.e. <https://demo.gitea.com>, as perhaps your problem has already been fixed on a current version. \
81
+Please follow the guidelines described in [How to Report Bugs Effectively](http://www.chiark.greenend.org.uk/~sgtatham/bugs.html) for your report.
82
+
83
+Please be kind, remember that Gitea comes at no cost to you, and you're getting free help.
84
+
85
+### Types of issues
86
+
87
+Typically, issues fall in one of the following categories:
88
+
89
+- `bug`: Something in the frontend or backend behaves unexpectedly
90
+- `security issue`: bug that has serious implications such as leaking another users data. Please do not file such issues on the public tracker and send a mail to security@gitea.io instead
91
+- `feature`: Completely new functionality. You should describe this feature in enough detail that anyone who reads the issue can understand how it is supposed to be implemented
92
+- `enhancement`: An existing feature should get an upgrade
93
+- `refactoring`: Parts of the code base don't conform with other parts and should be changed to improve Gitea's maintainability
94
+
95
+### Discuss your design before the implementation
96
+
97
+We welcome submissions. \
98
+If you want to change or add something, please let everyone know what you're working on — [file an issue](https://github.com/go-gitea/gitea/issues/new) or comment on an existing one before starting your work!
99
+
100
+Significant changes such as new features must go through the change proposal process before they can be accepted. \
101
+This is mainly to save yourself the trouble of implementing it, only to find out that your proposed implementation has some potential problems. \
102
+Furthermore, this process gives everyone a chance to validate the design, helps prevent duplication of effort, and ensures that the idea fits inside
103
+the goals for the project and tools.
104
+
105
+Pull requests should not be the place for architecture discussions.
106
+
107
+### Issue locking
108
+
109
+Commenting on closed or merged issues/PRs is strongly discouraged.
110
+Such comments will likely be overlooked as some maintainers may not view notifications on closed issues, thinking that the item is resolved.
111
+As such, commenting on closed/merged issues/PRs may be disabled prior to the scheduled auto-locking if a discussion starts or if unrelated comments are posted.
112
+If further discussion is needed, we encourage you to open a new issue instead and we recommend linking to the issue/PR in question for context.
113
+
114
+## Building Gitea
115
+
116
+See the [development setup instructions](https://docs.gitea.com/development/hacking-on-gitea).
117
+
118
+## Dependencies
119
+
120
+### Backend
121
+
122
+Go dependencies are managed using [Go Modules](https://go.dev/cmd/go/#hdr-Module_maintenance). \
123
+You can find more details in the [go mod documentation](https://go.dev/ref/mod) and the [Go Modules Wiki](https://github.com/golang/go/wiki/Modules).
124
+
125
+Pull requests should only modify `go.mod` and `go.sum` where it is related to your change, be it a bugfix or a new feature. \
126
+Apart from that, these files should only be modified by Pull Requests whose only purpose is to update dependencies.
127
+
128
+The `go.mod`, `go.sum` update needs to be justified as part of the PR description,
129
+and must be verified by the reviewers and/or merger to always reference
130
+an existing upstream commit.
131
+
132
+### Frontend
133
+
134
+For the frontend, we use [npm](https://www.npmjs.com/).
135
+
136
+The same restrictions apply for frontend dependencies as for backend dependencies, with the exceptions that the files for it are `package.json` and `package-lock.json`, and that new versions must always reference an existing version.
137
+
138
+## Design guideline
139
+
140
+Depending on your change, please read the
141
+
142
+- [backend development guideline](https://docs.gitea.com/contributing/guidelines-backend)
143
+- [frontend development guideline](https://docs.gitea.com/contributing/guidelines-frontend)
144
+- [refactoring guideline](https://docs.gitea.com/contributing/guidelines-refactoring)
145
+
146
+## Styleguide
147
+
148
+You should always run `make fmt` before committing to conform to Gitea's styleguide.
149
+
150
+## Copyright
151
+
152
+New code files that you contribute should use the standard copyright header:
153
+
154
+```
155
+// Copyright <current year> The Gitea Authors. All rights reserved.
156
+// SPDX-License-Identifier: MIT
157
+```
158
+
159
+Afterwards, copyright should only be modified when the copyright author changes.
160
+
161
+## Testing
162
+
163
+Before submitting a pull request, run all tests to make sure your changes don't cause a regression elsewhere.
164
+
165
+Here's how to run the test suite:
166
+
167
+- code lint
168
+
169
+|                       |                                                                   |
170
+| :-------------------- | :---------------------------------------------------------------- |
171
+|``make lint``          | lint everything (not needed if you only change the front- **or** backend)    |
172
+|``make lint-frontend`` | lint frontend files  |
173
+|``make lint-backend``  | lint backend files   |
174
+
175
+- run tests (we suggest running them on Linux)
176
+
177
+|  Command                               | Action                                           |              |
178
+| :------------------------------------- | :----------------------------------------------- | ------------ |
179
+|``make test[\#SpecificTestName]``       |  run unit test(s)  | |
180
+|``make test-sqlite[\#SpecificTestName]``|  run [integration](tests/integration) test(s) for SQLite |[More details](tests/integration/README.md)  |
181
+|``make test-e2e-sqlite[\#SpecificTestName]``|  run [end-to-end](tests/e2e) test(s) for SQLite |[More details](tests/e2e/README.md)  |
182
+
183
+## Translation
184
+
185
+All translation work happens on [Crowdin](https://translate.gitea.com).
186
+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).
187
+It is synced regularly with Crowdin. \
188
+Other locales on main branch **should not** be updated manually as they will be overwritten with each sync. \
189
+Once a language has reached a **satisfactory percentage** of translated keys (~25%), it will be synced back into this repo and included in the next released version.
190
+
191
+The tool `go run build/backport-locale.go` can be used to backport locales from the main branch to release branches that were missed.
192
+
193
+## Code review
194
+
195
+### Pull request format
196
+
197
+Please try to make your pull request easy to review for us. \
198
+For that, please read the [*Best Practices for Faster Reviews*](https://github.com/kubernetes/community/blob/261cb0fd089b64002c91e8eddceebf032462ccd6/contributors/guide/pull-requests.md#best-practices-for-faster-reviews) guide. \
199
+It has lots of useful tips for any project you may want to contribute to. \
200
+Some of the key points:
201
+
202
+- Make small pull requests. \
203
+  The smaller, the faster to review and the more likely it will be merged soon.
204
+- Don't make changes unrelated to your PR. \
205
+  Maybe there are typos on some comments, maybe refactoring would be welcome on a function... \
206
+  but if that is not related to your PR, please make *another* PR for that.
207
+- Split big pull requests into multiple small ones. \
208
+  An incremental change will be faster to review than a huge PR.
209
+- Allow edits by maintainers. This way, the maintainers will take care of merging the PR later on instead of you.
210
+
211
+### PR title and summary
212
+
213
+In the PR title, describe the problem you are fixing, not how you are fixing it. \
214
+Use the first comment as a summary of your PR. \
215
+In the PR summary, you can describe exactly how you are fixing this problem.
216
+
217
+Keep this summary up-to-date as the PR evolves. \
218
+If your PR changes the UI, you must add **after** screenshots in the PR summary. \
219
+If you are not implementing a new feature, you should also post **before** screenshots for comparison.
220
+
221
+If you are implementing a new feature, your PR will only be merged if your screenshots are up to date.\
222
+Furthermore, feature PRs will only be merged if their summary contains a clear usage description (understandable for users) and testing description (understandable for reviewers).
223
+You should strive to combine both into a single description.
224
+
225
+Another requirement for merging PRs is that the PR is labeled correctly.\
226
+However, this is not your job as a contributor, but the job of the person merging your PR.\
227
+If you think that your PR was labeled incorrectly, or notice that it was merged without labels, please let us know.
228
+
229
+If your PR closes some issues, you must note that in a way that both GitHub and Gitea understand, i.e. by appending a paragraph like
230
+
231
+```text
232
+Fixes/Closes/Resolves #<ISSUE_NR_X>.
233
+Fixes/Closes/Resolves #<ISSUE_NR_Y>.
234
+```
235
+
236
+to your summary. \
237
+Each issue that will be closed must stand on a separate line.
238
+
239
+### Milestone
240
+
241
+A PR should only be assigned to a milestone if it will likely be merged into the given version. \
242
+As a rule of thumb, assume that a PR will stay open for an additional month for every 100 added lines. \
243
+PRs without a milestone may not be merged.
244
+
245
+### Labels
246
+
247
+Almost all labels used inside Gitea can be classified as one of the following:
248
+
249
+- `modifies/…`: Determines which parts of the codebase are affected. These labels will be set through the CI.
250
+- `topic/…`:  Determines the conceptual component of Gitea that is affected, i.e. issues, projects, or authentication. At best, PRs should only target one component but there might be overlap. Must be set manually.
251
+- `type/…`: Determines the type of an issue or PR (feature, refactoring, docs, bug, …). If GitHub supported scoped labels, these labels would be exclusive, so you should set **exactly** one, not more or less (every PR should fall into one of the provided categories, and only one).
252
+- `issue/…` / `pr/…`: Labels that are specific to issues or PRs respectively and that are only necessary in a given context, i.e. `issue/not-a-bug` or `pr/need-2-approvals`
253
+
254
+Every PR should be labeled correctly with every label that applies.
255
+
256
+There are also some labels that will be managed automatically.\
257
+In particular, these are
258
+
259
+- the amount of pending required approvals
260
+- has all `backport`s or needs a manual backport
261
+
262
+### Breaking PRs
263
+
264
+#### What is a breaking PR?
265
+
266
+A PR is breaking if it meets one of the following criteria:
267
+
268
+- It changes API output in an incompatible way for existing users
269
+- It removes a setting that an admin could previously set (i.e. via `app.ini`)
270
+- An admin must do something manually to restore the old behavior
271
+
272
+In particular, this means that adding new settings is not breaking.\
273
+Changing the default value of a setting or replacing the setting with another one is breaking, however.
274
+
275
+#### How to handle breaking PRs?
276
+
277
+If your PR has a breaking change, you must add two things to the summary of your PR:
278
+
279
+1. A reasoning why this breaking change is necessary
280
+2. A `BREAKING` section explaining in simple terms (understandable for a typical user) how this PR affects users and how to mitigate these changes. This section can look for example like
281
+
282
+```md
283
+## :warning: BREAKING :warning:
284
+```
285
+
286
+Breaking PRs will not be merged as long as not both of these requirements are met.
287
+
288
+### Maintaining open PRs
289
+
290
+The moment you create a non-draft PR or the moment you convert a draft PR to a non-draft PR is the moment code review starts for it. \
291
+Once that happens, do not rebase or squash your branch anymore as it makes it difficult to review the new changes. \
292
+Merge the base branch into your branch only when you really need to, i.e. because of conflicting changes in the mean time. \
293
+This reduces unnecessary CI runs. \
294
+Don't worry about merge commits messing up your commit history as every PR will be squash merged. \
295
+This means that all changes are joined into a single new commit whose message is as described below.
296
+
297
+### Getting PRs merged
298
+
299
+Changes to Gitea must be reviewed before they are accepted — no matter who
300
+makes the change, even if they are an owner or a maintainer. \
301
+The only exception are critical bugs that prevent Gitea from being compiled or started. \
302
+Specifically, we require two approvals from maintainers for every PR. \
303
+Once this criteria has been met, your PR receives the `lgtm/done` label. \
304
+From this point on, your only responsibility is to fix merge conflicts or respond to/implement requests by maintainers. \
305
+It is the responsibility of the maintainers from this point to get your PR merged.
306
+
307
+If a PR has the `lgtm/done` label and there are no open discussions or merge conflicts anymore, any maintainer can add the `reviewed/wait-merge` label. \
308
+This label means that the PR is part of the merge queue and will be merged as soon as possible. \
309
+The merge queue will be cleared in the order of the list below:
310
+
311
+<https://github.com/go-gitea/gitea/pulls?q=is%3Apr+label%3Areviewed%2Fwait-merge+sort%3Acreated-asc+is%3Aopen>
312
+
313
+Gitea uses it's own tool, the <https://github.com/GiteaBot/gitea-backporter> to automate parts of the review process. \
314
+This tool does the things listed below automatically:
315
+
316
+- create a backport PR if needed once the initial PR was merged
317
+- remove the PR from the merge queue after the PR merged
318
+- keep the oldest branch in the merge queue up to date with merges
319
+
320
+### Final call
321
+
322
+If a PR has been ignored for more than 7 days with no comments or reviews, and the author or any maintainer believes it will not survive a long wait (such as a refactoring PR), they can send "final call" to the TOC by mentioning them in a comment.
323
+
324
+After another 7 days, if there is still zero approval, this is considered a polite refusal, and the PR will be closed to avoid wasting further time. Therefore, the "final call" has a cost, and should be used cautiously.
325
+
326
+However, if there are no objections from maintainers, the PR can be merged with only one approval from the TOC (not the author).
327
+
328
+### Commit messages
329
+
330
+Mergers are able and required to rewrite the PR title and summary (the first comment of a PR) so that it can produce an easily understandable commit message if necessary. \
331
+The final commit message should no longer contain any uncertainty such as `hopefully, <x> won't happen anymore`. Replace uncertainty with certainty.
332
+
333
+#### PR Co-authors
334
+
335
+A person counts as a PR co-author the moment they (co-)authored a commit that is not simply a `Merge base branch into branch` commit. \
336
+Mergers are required to remove such "false-positive" co-authors when writing the commit message. \
337
+The true co-authors must remain in the commit message.
338
+
339
+#### PRs targeting `main`
340
+
341
+The commit message of PRs targeting `main` is always
342
+
343
+```bash
344
+$PR_TITLE ($PR_INDEX)
345
+
346
+$REWRITTEN_PR_SUMMARY
347
+```
348
+
349
+#### Backport PRs
350
+
351
+The commit message of backport PRs is always
352
+
353
+```bash
354
+$PR_TITLE ($INITIAL_PR_INDEX) ($BACKPORT_PR_INDEX)
355
+
356
+$REWRITTEN_PR_SUMMARY
357
+```
358
+
359
+## Documentation
360
+
361
+If you add a new feature or change an existing aspect of Gitea, the documentation for that feature must be created or updated in another PR at [https://gitea.com/gitea/docs](https://gitea.com/gitea/docs).
362
+**The docs directory on main repository will be removed at some time. We will have a yaml file to store configuration file's meta data. After that completed, configuration documentation should be in the main repository.**
363
+
364
+## API v1
365
+
366
+The API is documented by [swagger](https://gitea.com/api/swagger) and is based on [the GitHub API](https://docs.github.com/en/rest).
367
+
368
+### GitHub API compatibility
369
+
370
+Gitea's API should use the same endpoints and fields as the GitHub API as far as possible, unless there are good reasons to deviate. \
371
+If Gitea provides functionality that GitHub does not, a new endpoint can be created. \
372
+If information is provided by Gitea that is not provided by the GitHub API, a new field can be used that doesn't collide with any GitHub fields. \
373
+Updating an existing API should not remove existing fields unless there is a really good reason to do so. \
374
+The same applies to status responses. If you notice a problem, feel free to leave a comment in the code for future refactoring to API v2 (which is currently not planned).
375
+
376
+### Adding/Maintaining API routes
377
+
378
+All expected results (errors, success, fail messages) must be documented ([example](https://github.com/go-gitea/gitea/blob/c620eb5b2d0d874da68ebd734d3864c5224f71f7/routers/api/v1/repo/issue.go#L319-L327)). \
379
+All JSON input types must be defined as a struct in [modules/structs/](modules/structs/) ([example](https://github.com/go-gitea/gitea/blob/c620eb5b2d0d874da68ebd734d3864c5224f71f7/modules/structs/issue.go#L76-L91)) \
380
+and referenced in [routers/api/v1/swagger/options.go](https://github.com/go-gitea/gitea/blob/c620eb5b2d0d874da68ebd734d3864c5224f71f7/routers/api/v1/swagger/options.go). \
381
+They can then be used like [this example](https://github.com/go-gitea/gitea/blob/c620eb5b2d0d874da68ebd734d3864c5224f71f7/routers/api/v1/repo/issue.go#L318). \
382
+All JSON responses must be defined as a struct in [modules/structs/](modules/structs/) ([example](https://github.com/go-gitea/gitea/blob/c620eb5b2d0d874da68ebd734d3864c5224f71f7/modules/structs/issue.go#L36-L68)) \
383
+and referenced in its category in [routers/api/v1/swagger/](routers/api/v1/swagger/) ([example](https://github.com/go-gitea/gitea/blob/c620eb5b2d0d874da68ebd734d3864c5224f71f7/routers/api/v1/swagger/issue.go#L11-L16)) \
384
+They can be used like [this example](https://github.com/go-gitea/gitea/blob/c620eb5b2d0d874da68ebd734d3864c5224f71f7/routers/api/v1/repo/issue.go#L277-L279).
385
+
386
+### When to use what HTTP method
387
+
388
+In general, HTTP methods are chosen as follows:
389
+
390
+- **GET** endpoints return the requested object(s) and status **OK (200)**
391
+- **DELETE** endpoints return the status **No Content (204)** and no content either
392
+- **POST** endpoints are used to **create** new objects (e.g. a User) and return the status **Created (201)** and the created object
393
+- **PUT** endpoints are used to **add/assign** existing Objects (e.g. a user to a team) and return the status **No Content (204)** and no content either
394
+- **PATCH** endpoints are used to **edit/change** an existing object and return the changed object and the status **OK (200)**
395
+
396
+### Requirements for API routes
397
+
398
+All parameters of endpoints changing/editing an object must be optional (except the ones to identify the object, which are required).
399
+
400
+Endpoints returning lists must
401
+
402
+- support pagination (`page` & `limit` options in query)
403
+- set `X-Total-Count` header via **SetTotalCountHeader** ([example](https://github.com/go-gitea/gitea/blob/7aae98cc5d4113f1e9918b7ee7dd09f67c189e3e/routers/api/v1/repo/issue.go#L444))
404
+
405
+## Backports and Frontports
406
+
407
+### What is backported?
408
+
409
+We backport PRs given the following circumstances:
410
+
411
+1. Feature freeze is active, but `<version>-rc0` has not been released yet. Here, we backport as much as possible. <!-- TODO: Is that our definition with the new backport bot? -->
412
+2. `rc0` has been released. Here, we only backport bug- and security-fixes, and small enhancements. Large PRs such as refactors are not backported anymore. <!-- TODO: Is that our definition with the new backport bot? -->
413
+3. We never backport new features.
414
+4. We never backport breaking changes except when
415
+    1. The breaking change has no effect on the vast majority of users
416
+    2. The component triggering the breaking change is marked as experimental
417
+
418
+### How to backport?
419
+
420
+In the past, it was necessary to manually backport your PRs. \
421
+Now, that's not a requirement anymore as our [backport bot](https://github.com/GiteaBot) tries to create backports automatically once the PR is merged when the PR
422
+
423
+- does not have the label `backport/manual`
424
+- has the label `backport/<version>`
425
+
426
+The `backport/manual` label signifies either that you want to backport the change yourself, or that there were conflicts when backporting, thus you **must** do it yourself.
427
+
428
+### Format of backport PRs
429
+
430
+The title of backport PRs should be
431
+
432
+```
433
+<original PR title> (#<original pr number>)
434
+```
435
+
436
+The first two lines of the summary of the backporting PR should be
437
+
438
+```
439
+Backport #<original pr number>
440
+
441
+```
442
+
443
+with the rest of the summary and labels matching the original PR.
444
+
445
+### Frontports
446
+
447
+Frontports behave exactly as described above for backports.
448
+
449
+## Developer Certificate of Origin (DCO)
450
+
451
+We consider the act of contributing to the code by submitting a Pull Request as the "Sign off" or agreement to the certifications and terms of the [DCO](DCO) and [MIT license](LICENSE). \
452
+No further action is required. \
453
+You can also decide to sign off your commits by adding the following line at the end of your commit messages:
454
+
455
+```
456
+Signed-off-by: Joe Smith <joe.smith@email.com>
457
+```
458
+
459
+If you set the `user.name` and `user.email` Git config options, you can add the line to the end of your commits automatically with `git commit -s`.
460
+
461
+We assume in good faith that the information you provide is legally binding.
462
+
463
+## Release Cycle
464
+
465
+We adopted a release schedule to streamline the process of working on, finishing, and issuing releases. \
466
+The overall goal is to make a major release every three or four months, which breaks down into two or three months of general development followed by one month of testing and polishing known as the release freeze. \
467
+All the feature pull requests should be
468
+merged before feature freeze. All feature pull requests haven't been merged before this feature freeze will be moved to next milestone, please notice our feature freeze announcement on discord. And, during the frozen period, a corresponding
469
+release branch is open for fixes backported from main branch. Release candidates
470
+are made during this period for user testing to
471
+obtain a final version that is maintained in this branch.
472
+
473
+During a development cycle, we may also publish any necessary minor releases
474
+for the previous version. For example, if the latest, published release is
475
+v1.2, then minor changes for the previous release—e.g., v1.1.0 -> v1.1.1—are
476
+still possible.
477
+
478
+## Maintainers
479
+
480
+To make sure every PR is checked, we have [maintainers](MAINTAINERS). \
481
+Every PR **must** be reviewed by at least two maintainers (or owners) before it can get merged. \
482
+For refactoring PRs after a week and documentation only PRs, the approval of only one maintainer is enough. \
483
+A maintainer should be a contributor of Gitea and contributed at least
484
+4 accepted PRs. A contributor should apply as a maintainer in the
485
+[Discord](https://discord.gg/Gitea) `#develop` channel. The team maintainers may invite the contributor. A maintainer
486
+should spend some time on code reviews. If a maintainer has no
487
+time to do that, they should apply to leave the maintainers team
488
+and we will give them the honor of being a member of the [advisors
489
+team](https://github.com/orgs/go-gitea/teams/advisors). Of course, if
490
+an advisor has time to code review, we will gladly welcome them back
491
+to the maintainers team. If a maintainer is inactive for more than 3
492
+months and forgets to leave the maintainers team, the owners may move
493
+him or her from the maintainers team to the advisors team.
494
+For security reasons, Maintainers should use 2FA for their accounts and
495
+if possible provide GPG signed commits.
496
+https://help.github.com/articles/securing-your-account-with-two-factor-authentication-2fa/
497
+https://help.github.com/articles/signing-commits-with-gpg/
498
+
499
+Furthermore, any account with write access (like bots and TOC members) **must** use 2FA.
500
+https://help.github.com/articles/securing-your-account-with-two-factor-authentication-2fa/
501
+
502
+## Technical Oversight Committee (TOC)
503
+
504
+At the start of 2023, the `Owners` team was dissolved. Instead, the governance charter proposed a technical oversight committee (TOC) which expands the ownership team of the Gitea project from three elected positions to six positions. Three positions are elected as it has been over the past years, and the other three consist of appointed members from the Gitea company.
505
+https://blog.gitea.com/quarterly-23q1/
506
+
507
+### TOC election process
508
+
509
+Any maintainer is eligible to be part of the community TOC if they are not associated with the Gitea company.
510
+A maintainer can either nominate themselves, or can be nominated by other maintainers to be a candidate for the TOC election.
511
+If you are nominated by someone else, you must first accept your nomination before the vote starts to be a candidate.
512
+
513
+The TOC is elected for one year, the TOC election happens yearly.
514
+After the announcement of the results of the TOC election, elected members have two weeks time to confirm or refuse the seat.
515
+If an elected member does not answer within this timeframe, they are automatically assumed to refuse the seat.
516
+Refusals result in the person with the next highest vote getting the same choice.
517
+As long as seats are empty in the TOC, members of the previous TOC can fill them until an elected member accepts the seat.
518
+
519
+If an elected member that accepts the seat does not have 2FA configured yet, they will be temporarily counted as `answer pending` until they manage to configure 2FA, thus leaving their seat empty for this duration.
520
+
521
+### Current TOC members
522
+
523
+- 2024-01-01 ~ 2024-12-31
524
+  - Company
525
+    - [Jason Song](https://gitea.com/wolfogre) <i@wolfogre.com>
526
+    - [Lunny Xiao](https://gitea.com/lunny) <xiaolunwen@gmail.com>
527
+    - [Matti Ranta](https://gitea.com/techknowlogick) <techknowlogick@gitea.com>
528
+  - Community
529
+    - [6543](https://gitea.com/6543) <6543@obermui.de>
530
+    - [delvh](https://gitea.com/delvh) <dev.lh@web.de>
531
+    - [John Olheiser](https://gitea.com/jolheiser) <john.olheiser@gmail.com>
532
+
533
+### Previous TOC/owners members
534
+
535
+Here's the history of the owners and the time they served:
536
+
537
+- [Lunny Xiao](https://gitea.com/lunny) - 2016, 2017, [2018](https://github.com/go-gitea/gitea/issues/3255), [2019](https://github.com/go-gitea/gitea/issues/5572), [2020](https://github.com/go-gitea/gitea/issues/9230), [2021](https://github.com/go-gitea/gitea/issues/13801), [2022](https://github.com/go-gitea/gitea/issues/17872), 2023
538
+- [Kim Carlbäcker](https://github.com/bkcsoft) - 2016, 2017
539
+- [Thomas Boerger](https://gitea.com/tboerger) - 2016, 2017
540
+- [Lauris Bukšis-Haberkorns](https://gitea.com/lafriks) - [2018](https://github.com/go-gitea/gitea/issues/3255), [2019](https://github.com/go-gitea/gitea/issues/5572), [2020](https://github.com/go-gitea/gitea/issues/9230), [2021](https://github.com/go-gitea/gitea/issues/13801)
541
+- [Matti Ranta](https://gitea.com/techknowlogick) - [2019](https://github.com/go-gitea/gitea/issues/5572), [2020](https://github.com/go-gitea/gitea/issues/9230), [2021](https://github.com/go-gitea/gitea/issues/13801), [2022](https://github.com/go-gitea/gitea/issues/17872), 2023
542
+- [Andrew Thornton](https://gitea.com/zeripath) - [2020](https://github.com/go-gitea/gitea/issues/9230), [2021](https://github.com/go-gitea/gitea/issues/13801), [2022](https://github.com/go-gitea/gitea/issues/17872), 2023
543
+- [6543](https://gitea.com/6543) - 2023
544
+- [John Olheiser](https://gitea.com/jolheiser) - 2023
545
+- [Jason Song](https://gitea.com/wolfogre) - 2023
546
+
547
+## Governance Compensation
548
+
549
+Each member of the community elected TOC will be granted $500 each month as compensation for their work.
550
+
551
+Furthermore, any community release manager for a specific release or LTS will be compensated $500 for the delivery of said release.
552
+
553
+These funds will come from community sources like the OpenCollective rather than directly from the company.
554
+Only non-company members are eligible for this compensation, and if a member of the community TOC takes the responsibility of release manager, they would only be compensated for their TOC duties.
555
+Gitea Ltd employees are not eligible to receive any funds from the OpenCollective unless it is reimbursement for a purchase made for the Gitea project itself.
556
+
557
+## TOC & Working groups
558
+
559
+With Gitea covering many projects outside of the main repository, several groups will be created to help focus on specific areas instead of requiring maintainers to be a jack-of-all-trades. Maintainers are of course more than welcome to be part of multiple groups should they wish to contribute in multiple places.
560
+
561
+The currently proposed groups are:
562
+
563
+- **Core Group**: maintain the primary Gitea repository
564
+- **Integration Group**: maintain the Gitea ecosystem's related tools, including go-sdk/tea/changelog/bots etc.
565
+- **Documentation Group**: maintain related documents and repositories
566
+- **Translation Group**: coordinate with translators and maintain translations
567
+- **Security Group**: managed by TOC directly, members are decided by TOC, maintains security patches/responsible for security items
568
+
569
+## Roadmap
570
+
571
+Each year a roadmap will be discussed with the entire Gitea maintainers team, and feedback will be solicited from various stakeholders.
572
+TOC members need to review the roadmap every year and work together on the direction of the project.
573
+
574
+When a vote is required for a proposal or other change, the vote of community elected TOC members count slightly more than the vote of company elected TOC members. With this approach, we both avoid ties and ensure that changes align with the mission statement and community opinion.
575
+
576
+You can visit our roadmap on the wiki.
577
+
578
+## Versions
579
+
580
+Gitea has the `main` branch as a tip branch and has version branches
581
+such as `release/v1.19`. `release/v1.19` is a release branch and we will
582
+tag `v1.19.0` for binary download. If `v1.19.0` has bugs, we will accept
583
+pull requests on the `release/v1.19` branch and publish a `v1.19.1` tag,
584
+after bringing the bug fix also to the main branch.
585
+
586
+Since the `main` branch is a tip version, if you wish to use Gitea
587
+in production, please download the latest release tag version. All the
588
+branches will be protected via GitHub, all the PRs to every branch must
589
+be reviewed by two maintainers and must pass the automatic tests.
590
+
591
+## Releasing Gitea
592
+
593
+- 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.
594
+- 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.
595
+- 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:
596
+  - 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`.
597
+  - When CI has finished building tag then you have to create a new branch named `release/v$vmaj.$vmin`
598
+- If it is bugfix version create PR for changelog on branch `release/v$vmaj.$vmin` and wait till it is reviewed and merged.
599
+- Add a tag as `git tag -s -F release.notes v$vmaj.$vmin.$`, release.notes file could be a temporary file to only include the changelog this version which you added to `CHANGELOG.md`.
600
+- And then push the tag as `git push origin v$vmaj.$vmin.$`. Drone CI will automatically create a release and upload all the compiled binary. (But currently it doesn't add the release notes automatically. Maybe we should fix that.)
601
+- If needed send a frontport PR for the changelog to branch `main` and update the version in `docs/config.yaml` to refer to the new version.
602
+- Send PR to [blog repository](https://gitea.com/gitea/blog) announcing the release.
603
+- Verify all release assets were correctly published through CI on dl.gitea.com and GitHub releases. Once ACKed:
604
+  - bump the version of https://dl.gitea.com/gitea/version.json
605
+  - merge the blog post PR
606
+  - announce the release in discord `#announcements`

+ 34
- 0
DCO 查看文件

@@ -0,0 +1,34 @@
1
+Developer Certificate of Origin
2
+Version 1.1
3
+
4
+Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
5
+
6
+Everyone is permitted to copy and distribute verbatim copies of this
7
+license document, but changing it is not allowed.
8
+
9
+
10
+Developer's Certificate of Origin 1.1
11
+
12
+By making a contribution to this project, I certify that:
13
+
14
+(a) The contribution was created in whole or in part by me and I
15
+    have the right to submit it under the open source license
16
+    indicated in the file; or
17
+
18
+(b) The contribution is based upon previous work that, to the best
19
+    of my knowledge, is covered under an appropriate open source
20
+    license and I have the right under that license to submit that
21
+    work with modifications, whether created in whole or in part
22
+    by me, under the same open source license (unless I am
23
+    permitted to submit under a different license), as indicated
24
+    in the file; or
25
+
26
+(c) The contribution was provided directly to me by some other
27
+    person who certified (a), (b) or (c) and I have not modified
28
+    it.
29
+
30
+(d) I understand and agree that this project and the contribution
31
+    are public and that a record of the contribution (including all
32
+    personal information I submit with it, including my sign-off) is
33
+    maintained indefinitely and may be redistributed consistent with
34
+    this project or the open source license(s) involved.

+ 85
- 0
Dockerfile 查看文件

@@ -0,0 +1,85 @@
1
+# Build stage
2
+FROM docker.io/library/golang:1.25-alpine3.22 AS build-env
3
+
4
+ARG GOPROXY
5
+ENV GOPROXY=${GOPROXY:-direct}
6
+
7
+ARG GITEA_VERSION
8
+ARG TAGS="sqlite sqlite_unlock_notify"
9
+ENV TAGS="bindata timetzdata $TAGS"
10
+ARG CGO_EXTRA_CFLAGS
11
+
12
+# Build deps
13
+RUN apk --no-cache add \
14
+    build-base \
15
+    git \
16
+    nodejs \
17
+    npm \
18
+    && npm install -g pnpm@10 \
19
+    && rm -rf /var/cache/apk/*
20
+
21
+# Setup repo
22
+COPY . ${GOPATH}/src/code.gitea.io/gitea
23
+WORKDIR ${GOPATH}/src/code.gitea.io/gitea
24
+
25
+# Checkout version if set
26
+RUN if [ -n "${GITEA_VERSION}" ]; then git checkout "${GITEA_VERSION}"; fi \
27
+ && make clean-all build
28
+
29
+# Begin env-to-ini build
30
+RUN go build contrib/environment-to-ini/environment-to-ini.go
31
+
32
+# Copy local files
33
+COPY docker/root /tmp/local
34
+
35
+# Set permissions
36
+RUN chmod 755 /tmp/local/usr/bin/entrypoint \
37
+              /tmp/local/usr/local/bin/gitea \
38
+              /tmp/local/etc/s6/gitea/* \
39
+              /tmp/local/etc/s6/openssh/* \
40
+              /tmp/local/etc/s6/.s6-svscan/* \
41
+              /go/src/code.gitea.io/gitea/gitea \
42
+              /go/src/code.gitea.io/gitea/environment-to-ini
43
+
44
+FROM docker.io/library/alpine:3.22
45
+LABEL maintainer="maintainers@gitea.io"
46
+
47
+EXPOSE 22 3000
48
+
49
+RUN apk --no-cache add \
50
+    bash \
51
+    ca-certificates \
52
+    curl \
53
+    gettext \
54
+    git \
55
+    linux-pam \
56
+    openssh \
57
+    s6 \
58
+    sqlite \
59
+    su-exec \
60
+    gnupg \
61
+    && rm -rf /var/cache/apk/*
62
+
63
+RUN addgroup \
64
+    -S -g 1000 \
65
+    git && \
66
+  adduser \
67
+    -S -H -D \
68
+    -h /data/git \
69
+    -s /bin/bash \
70
+    -u 1000 \
71
+    -G git \
72
+    git && \
73
+  echo "git:*" | chpasswd -e
74
+
75
+ENV USER=git
76
+ENV GITEA_CUSTOM=/data/gitea
77
+
78
+VOLUME ["/data"]
79
+
80
+ENTRYPOINT ["/usr/bin/entrypoint"]
81
+CMD ["/usr/bin/s6-svscan", "/etc/s6"]
82
+
83
+COPY --from=build-env /tmp/local /
84
+COPY --from=build-env /go/src/code.gitea.io/gitea/gitea /app/gitea/gitea
85
+COPY --from=build-env /go/src/code.gitea.io/gitea/environment-to-ini /usr/local/bin/environment-to-ini

+ 90
- 0
Dockerfile.rootless 查看文件

@@ -0,0 +1,90 @@
1
+# Build stage
2
+FROM docker.io/library/golang:1.25-alpine3.22 AS build-env
3
+
4
+ARG GOPROXY
5
+ENV GOPROXY=${GOPROXY:-direct}
6
+
7
+ARG GITEA_VERSION
8
+ARG TAGS="sqlite sqlite_unlock_notify"
9
+ENV TAGS="bindata timetzdata $TAGS"
10
+ARG CGO_EXTRA_CFLAGS
11
+
12
+#Build deps
13
+RUN apk --no-cache add \
14
+    build-base \
15
+    git \
16
+    nodejs \
17
+    npm \
18
+    && npm install -g pnpm@10 \
19
+    && rm -rf /var/cache/apk/*
20
+
21
+# Setup repo
22
+COPY . ${GOPATH}/src/code.gitea.io/gitea
23
+WORKDIR ${GOPATH}/src/code.gitea.io/gitea
24
+
25
+# Checkout version if set
26
+RUN if [ -n "${GITEA_VERSION}" ]; then git checkout "${GITEA_VERSION}"; fi \
27
+ && make clean-all build
28
+
29
+# Begin env-to-ini build
30
+RUN go build contrib/environment-to-ini/environment-to-ini.go
31
+
32
+# Copy local files
33
+COPY docker/rootless /tmp/local
34
+
35
+# Set permissions
36
+RUN chmod 755 /tmp/local/usr/local/bin/docker-entrypoint.sh \
37
+              /tmp/local/usr/local/bin/docker-setup.sh \
38
+              /tmp/local/usr/local/bin/gitea \
39
+              /go/src/code.gitea.io/gitea/gitea \
40
+              /go/src/code.gitea.io/gitea/environment-to-ini
41
+
42
+FROM docker.io/library/alpine:3.22
43
+LABEL maintainer="maintainers@gitea.io"
44
+
45
+EXPOSE 2222 3000
46
+
47
+RUN apk --no-cache add \
48
+    bash \
49
+    ca-certificates \
50
+    dumb-init \
51
+    gettext \
52
+    git \
53
+    curl \
54
+    gnupg \
55
+    openssh-keygen \
56
+    && rm -rf /var/cache/apk/*
57
+
58
+RUN addgroup \
59
+    -S -g 1000 \
60
+    git && \
61
+  adduser \
62
+    -S -H -D \
63
+    -h /var/lib/gitea/git \
64
+    -s /bin/bash \
65
+    -u 1000 \
66
+    -G git \
67
+    git
68
+
69
+RUN mkdir -p /var/lib/gitea /etc/gitea
70
+RUN chown git:git /var/lib/gitea /etc/gitea
71
+
72
+COPY --from=build-env /tmp/local /
73
+COPY --from=build-env --chown=root:root /go/src/code.gitea.io/gitea/gitea /app/gitea/gitea
74
+COPY --from=build-env --chown=root:root /go/src/code.gitea.io/gitea/environment-to-ini /usr/local/bin/environment-to-ini
75
+
76
+# git:git
77
+USER 1000:1000
78
+ENV GITEA_WORK_DIR=/var/lib/gitea
79
+ENV GITEA_CUSTOM=/var/lib/gitea/custom
80
+ENV GITEA_TEMP=/tmp/gitea
81
+ENV TMPDIR=/tmp/gitea
82
+
83
+# TODO add to docs the ability to define the ini to load (useful to test and revert a config)
84
+ENV GITEA_APP_INI=/etc/gitea/app.ini
85
+ENV HOME="/var/lib/gitea/git"
86
+VOLUME ["/var/lib/gitea", "/etc/gitea"]
87
+WORKDIR /var/lib/gitea
88
+
89
+ENTRYPOINT ["/usr/bin/dumb-init", "--", "/usr/local/bin/docker-entrypoint.sh"]
90
+CMD []

+ 20
- 0
LICENSE 查看文件

@@ -0,0 +1,20 @@
1
+Copyright (c) 2016 The Gitea Authors
2
+Copyright (c) 2015 The Gogs Authors
3
+
4
+Permission is hereby granted, free of charge, to any person obtaining a copy
5
+of this software and associated documentation files (the "Software"), to deal
6
+in the Software without restriction, including without limitation the rights
7
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+copies of the Software, and to permit persons to whom the Software is
9
+furnished to do so, subject to the following conditions:
10
+
11
+The above copyright notice and this permission notice shall be included in
12
+all copies or substantial portions of the Software.
13
+
14
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20
+THE SOFTWARE.

+ 66
- 0
MAINTAINERS 查看文件

@@ -0,0 +1,66 @@
1
+Alexey Makhov <amakhov@avito.ru> (@makhov)
2
+Bo-Yi Wu <appleboy.tw@gmail.com> (@appleboy)
3
+Ethan Koenig <ethantkoenig@gmail.com> (@ethantkoenig)
4
+Kees de Vries <bouwko@gmail.com> (@Bwko)
5
+Kim Carlbäcker <kim.carlbacker@gmail.com> (@bkcsoft)
6
+LefsFlare <nobody@nobody.tld> (@LefsFlarey)
7
+Lunny Xiao <xiaolunwen@gmail.com> (@lunny)
8
+Rachid Zarouali <nobody@nobody.tld> (@xinity)
9
+Rémy Boulanouar <admin@dblk.org> (@DblK)
10
+Sandro Santilli <strk@kbt.io> (@strk)
11
+Thibault Meyer <meyer.thibault@gmail.com> (@0xbaadf00d)
12
+Thomas Boerger <thomas@webhippie.de> (@tboerger)
13
+Patrick G <geek1011@outlook.com> (@geek1011)
14
+Antoine Girard <sapk@sapk.fr> (@sapk)
15
+Lauris Bukšis-Haberkorns <lauris@nix.lv> (@lafriks)
16
+Jonas Östanbäck <jonas.ostanback@gmail.com> (@cez81)
17
+David Schneiderbauer <dschneiderbauer@gmail.com> (@daviian)
18
+Peter Žeby <morlinest@gmail.com> (@morlinest)
19
+Matti Ranta <techknowlogick@gitea.io> (@techknowlogick)
20
+Jonas Franz <info@jonasfranz.software> (@jonasfranz)
21
+Alexey Terentyev <axifnx@gmail.com> (@axifive)
22
+Lanre Adelowo <yo@lanre.wtf> (@adelowo)
23
+Konrad Langenberg <k@knt.li> (@kolaente)
24
+He-Long Zhang <outman99@hotmail.com> (@BetaCat0)
25
+Andrew Thornton <art27@cantab.net> (@zeripath)
26
+John Olheiser <john.olheiser@gmail.com> (@jolheiser)
27
+Richard Mahn <rich.mahn@unfoldingword.org> (@richmahn)
28
+Mrsdizzie <info@mrsdizzie.com> (@mrsdizzie)
29
+silverwind <me@silverwind.io> (@silverwind)
30
+Gary Kim <gary@garykim.dev> (@gary-kim)
31
+Guillermo Prandi <gitea.maint@mailfilter.com.ar> (@guillep2k)
32
+Mura Li <typeless@ctli.io> (@typeless)
33
+6543 <6543@obermui.de> (@6543)
34
+David Svantesson <davidsvantesson@gmail.com> (@davidsvantesson)
35
+a1012112796 <1012112796@qq.com> (@a1012112796)
36
+Karl Heinz Marbaise <kama@soebes.de> (@khmarbaise)
37
+Norwin Roosen <git@nroo.de> (@noerw)
38
+Kyle Dumont <kdumontnu@gmail.com> (@kdumontnu)
39
+Janis Estelmann <admin@oldschoolhack.me> (@KN4CK3R)
40
+Jimmy Praet <jimmy.praet@telenet.be> (@jpraet)
41
+Leon Hofmeister <dev.lh@web.de> (@delvh)
42
+Wim <wim@42.be> (@42wim)
43
+Jason Song <i@wolfogre.com> (@wolfogre)
44
+Yarden Shoham <git@yardenshoham.com> (@yardenshoham)
45
+Yu Tian <zettat123@gmail.com> (@Zettat123)
46
+Dong Ge <gedong_1994@163.com> (@sillyguodong)
47
+Xinyi Gong <hestergong@gmail.com> (@HesterG)
48
+wxiaoguang <wxiaoguang@gmail.com> (@wxiaoguang)
49
+Gary Moon <gary@garymoon.net> (@garymoon)
50
+Philip Peterson <philip.c.peterson@gmail.com> (@philip-peterson)
51
+Denys Konovalov <kontakt@denyskon.de> (@denyskon)
52
+Punit Inani <punitinani1@gmail.com> (@puni9869)
53
+CaiCandong  <1290147055@qq.com> (@caicandong)
54
+Rui Chen  <rui@chenrui.dev> (@chenrui333)
55
+Nanguan Lin <nanguanlin6@gmail.com> (@lng2020)
56
+kerwin612 <kerwin612@qq.com> (@kerwin612)
57
+Gary Wang <git@blumia.net> (@BLumia)
58
+Tim-Niclas Oelschläger <zokki.softwareschmiede@gmail.com> (@zokkis)
59
+Yu Liu <1240335630@qq.com> (@HEREYUA)
60
+Kemal Zebari <kemalzebra@gmail.com> (@kemzeb)
61
+Rowan Bohde <rowan.bohde@gmail.com> (@bohde)
62
+hiifong <i@hiif.ong> (@hiifong)
63
+metiftikci <metiftikci@hotmail.com> (@metiftikci)
64
+Christopher Homberger <christopher.homberger@web.de> (@ChristopherHX)
65
+Tobias Balle-Petersen <tobiasbp@gmail.com> (@tobiasbp)
66
+TheFox <thefox0x7@gmail.com> (@TheFox0x7)

+ 954
- 0
Makefile 查看文件

@@ -0,0 +1,954 @@
1
+ifeq ($(USE_REPO_TEST_DIR),1)
2
+
3
+# This rule replaces the whole Makefile when we're trying to use /tmp repository temporary files
4
+location = $(CURDIR)/$(word $(words $(MAKEFILE_LIST)),$(MAKEFILE_LIST))
5
+self := $(location)
6
+
7
+%:
8
+	@tmpdir=`mktemp --tmpdir -d` ; \
9
+	echo Using temporary directory $$tmpdir for test repositories ; \
10
+	USE_REPO_TEST_DIR= $(MAKE) -f $(self) --no-print-directory REPO_TEST_DIR=$$tmpdir/ $@ ; \
11
+	STATUS=$$? ; rm -r "$$tmpdir" ; exit $$STATUS
12
+
13
+else
14
+
15
+# This is the "normal" part of the Makefile
16
+
17
+DIST := dist
18
+DIST_DIRS := $(DIST)/binaries $(DIST)/release
19
+IMPORT := code.gitea.io/gitea
20
+
21
+GO ?= go
22
+SHASUM ?= shasum -a 256
23
+HAS_GO := $(shell hash $(GO) > /dev/null 2>&1 && echo yes)
24
+COMMA := ,
25
+
26
+XGO_VERSION := go-1.25.x
27
+
28
+AIR_PACKAGE ?= github.com/air-verse/air@v1
29
+EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/v3/cmd/editorconfig-checker@v3
30
+GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.9.1
31
+GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.4.0
32
+GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.15
33
+MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.7.0
34
+SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@717e3cb29becaaf00e56953556c6d80f8a01b286
35
+XGO_PACKAGE ?= src.techknowlogick.com/xgo@latest
36
+GO_LICENSES_PACKAGE ?= github.com/google/go-licenses@v1
37
+GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1
38
+ACTIONLINT_PACKAGE ?= github.com/rhysd/actionlint/cmd/actionlint@v1
39
+GOPLS_PACKAGE ?= golang.org/x/tools/gopls@v0.20.0
40
+GOPLS_MODERNIZE_PACKAGE ?= golang.org/x/tools/gopls/internal/analysis/modernize/cmd/modernize@v0.20.0
41
+
42
+DOCKER_IMAGE ?= gitea/gitea
43
+DOCKER_TAG ?= latest
44
+DOCKER_REF := $(DOCKER_IMAGE):$(DOCKER_TAG)
45
+
46
+ifeq ($(HAS_GO), yes)
47
+	CGO_EXTRA_CFLAGS := -DSQLITE_MAX_VARIABLE_NUMBER=32766
48
+	CGO_CFLAGS ?= $(shell $(GO) env CGO_CFLAGS) $(CGO_EXTRA_CFLAGS)
49
+endif
50
+
51
+CGO_ENABLED ?= 0
52
+ifneq (,$(findstring sqlite,$(TAGS))$(findstring pam,$(TAGS)))
53
+	CGO_ENABLED = 1
54
+endif
55
+
56
+STATIC ?=
57
+EXTLDFLAGS ?=
58
+ifneq ($(STATIC),)
59
+	EXTLDFLAGS = -extldflags "-static"
60
+endif
61
+
62
+ifeq ($(GOOS),windows)
63
+	IS_WINDOWS := yes
64
+else ifeq ($(patsubst Windows%,Windows,$(OS)),Windows)
65
+	ifeq ($(GOOS),)
66
+		IS_WINDOWS := yes
67
+	endif
68
+endif
69
+ifeq ($(IS_WINDOWS),yes)
70
+	GOFLAGS := -v -buildmode=exe
71
+	EXECUTABLE ?= gitea.exe
72
+else
73
+	GOFLAGS := -v
74
+	EXECUTABLE ?= gitea
75
+endif
76
+
77
+ifeq ($(shell sed --version 2>/dev/null | grep -q GNU && echo gnu),gnu)
78
+	SED_INPLACE := sed -i
79
+else
80
+	SED_INPLACE := sed -i ''
81
+endif
82
+
83
+EXTRA_GOFLAGS ?=
84
+
85
+MAKE_VERSION := $(shell "$(MAKE)" -v | cat | head -n 1)
86
+MAKE_EVIDENCE_DIR := .make_evidence
87
+
88
+GOTESTFLAGS ?=
89
+ifeq ($(RACE_ENABLED),true)
90
+	GOFLAGS += -race
91
+	GOTESTFLAGS += -race
92
+endif
93
+
94
+STORED_VERSION_FILE := VERSION
95
+
96
+GITHUB_REF_TYPE ?= branch
97
+GITHUB_REF_NAME ?= $(shell git rev-parse --abbrev-ref HEAD)
98
+
99
+# Enable typescript support in Node.js before 22.18
100
+# TODO: Remove this once we can raise the minimum Node.js version to 22.18 (alpine >= 3.23)
101
+NODE_VERSION := $(shell printf "%03d%03d%03d" $(shell node -v 2>/dev/null | cut -c2- | tr '.' ' '))
102
+ifeq ($(shell test "$(NODE_VERSION)" -lt "022018000"; echo $$?),0)
103
+	NODE_VARS := NODE_OPTIONS="--experimental-strip-types"
104
+else
105
+	NODE_VARS :=
106
+endif
107
+
108
+ifneq ($(GITHUB_REF_TYPE),branch)
109
+	VERSION ?= $(subst v,,$(GITHUB_REF_NAME))
110
+	GITEA_VERSION ?= $(VERSION)
111
+else
112
+	ifneq ($(GITHUB_REF_NAME),)
113
+		VERSION ?= $(subst release/v,,$(GITHUB_REF_NAME))-nightly
114
+	else
115
+		VERSION ?= main
116
+	endif
117
+
118
+	STORED_VERSION=$(shell cat $(STORED_VERSION_FILE) 2>/dev/null)
119
+	ifneq ($(STORED_VERSION),)
120
+		GITEA_VERSION ?= $(STORED_VERSION)
121
+	else
122
+		GITEA_VERSION ?= $(shell git describe --tags --always | sed 's/-/+/' | sed 's/^v//')
123
+	endif
124
+endif
125
+
126
+# if version = "main" then update version to "nightly"
127
+ifeq ($(VERSION),main)
128
+	VERSION := main-nightly
129
+endif
130
+
131
+LDFLAGS := $(LDFLAGS) -X "main.MakeVersion=$(MAKE_VERSION)" -X "main.Version=$(GITEA_VERSION)" -X "main.Tags=$(TAGS)"
132
+
133
+LINUX_ARCHS ?= linux/amd64,linux/386,linux/arm-5,linux/arm-6,linux/arm64,linux/riscv64
134
+
135
+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/))
136
+MIGRATE_TEST_PACKAGES ?= $(shell $(GO) list code.gitea.io/gitea/models/migrations/...)
137
+
138
+WEBPACK_SOURCES := $(shell find web_src/js web_src/css -type f)
139
+WEBPACK_CONFIGS := webpack.config.ts tailwind.config.ts
140
+WEBPACK_DEST := public/assets/js/index.js public/assets/css/index.css
141
+WEBPACK_DEST_ENTRIES := public/assets/js public/assets/css public/assets/fonts
142
+
143
+BINDATA_DEST_WILDCARD := modules/migration/bindata.* modules/public/bindata.* modules/options/bindata.* modules/templates/bindata.*
144
+
145
+GENERATED_GO_DEST := modules/charset/invisible_gen.go modules/charset/ambiguous_gen.go
146
+
147
+SVG_DEST_DIR := public/assets/img/svg
148
+
149
+AIR_TMP_DIR := .air
150
+
151
+GO_LICENSE_TMP_DIR := .go-licenses
152
+GO_LICENSE_FILE := assets/go-licenses.json
153
+
154
+TAGS ?=
155
+TAGS_SPLIT := $(subst $(COMMA), ,$(TAGS))
156
+TAGS_EVIDENCE := $(MAKE_EVIDENCE_DIR)/tags
157
+
158
+TEST_TAGS ?= $(TAGS_SPLIT) sqlite sqlite_unlock_notify
159
+
160
+TAR_EXCLUDES := .git data indexers queues log node_modules $(EXECUTABLE) $(DIST) $(MAKE_EVIDENCE_DIR) $(AIR_TMP_DIR) $(GO_LICENSE_TMP_DIR)
161
+
162
+GO_DIRS := build cmd models modules routers services tests
163
+WEB_DIRS := web_src/js web_src/css
164
+
165
+ESLINT_FILES := web_src/js tools *.ts tests/e2e
166
+STYLELINT_FILES := web_src/css web_src/js/components/*.vue
167
+SPELLCHECK_FILES := $(GO_DIRS) $(WEB_DIRS) templates options/locale/locale_en-US.ini .github $(filter-out CHANGELOG.md, $(wildcard *.go *.md *.yml *.yaml *.toml)) $(filter-out tools/misspellings.csv, $(wildcard tools/*))
168
+EDITORCONFIG_FILES := templates .github/workflows options/locale/locale_en-US.ini
169
+
170
+GO_SOURCES := $(wildcard *.go)
171
+GO_SOURCES += $(shell find $(GO_DIRS) -type f -name "*.go")
172
+GO_SOURCES += $(GENERATED_GO_DEST)
173
+
174
+# Force installation of playwright dependencies by setting this flag
175
+ifdef DEPS_PLAYWRIGHT
176
+	PLAYWRIGHT_FLAGS += --with-deps
177
+endif
178
+
179
+SWAGGER_SPEC := templates/swagger/v1_json.tmpl
180
+SWAGGER_SPEC_INPUT := templates/swagger/v1_input.json
181
+SWAGGER_EXCLUDE := code.gitea.io/sdk
182
+
183
+TEST_MYSQL_HOST ?= mysql:3306
184
+TEST_MYSQL_DBNAME ?= testgitea
185
+TEST_MYSQL_USERNAME ?= root
186
+TEST_MYSQL_PASSWORD ?=
187
+TEST_PGSQL_HOST ?= pgsql:5432
188
+TEST_PGSQL_DBNAME ?= testgitea
189
+TEST_PGSQL_USERNAME ?= postgres
190
+TEST_PGSQL_PASSWORD ?= postgres
191
+TEST_PGSQL_SCHEMA ?= gtestschema
192
+TEST_MINIO_ENDPOINT ?= minio:9000
193
+TEST_MSSQL_HOST ?= mssql:1433
194
+TEST_MSSQL_DBNAME ?= gitea
195
+TEST_MSSQL_USERNAME ?= sa
196
+TEST_MSSQL_PASSWORD ?= MwantsaSecurePassword1
197
+
198
+.PHONY: all
199
+all: build
200
+
201
+.PHONY: help
202
+help: Makefile ## print Makefile help information.
203
+	@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)
204
+	@printf "  \033[36m%-46s\033[0m %s\n" "test-e2e[#TestSpecificName]" "test end to end using playwright"
205
+	@printf "  \033[36m%-46s\033[0m %s\n" "test[#TestSpecificName]" "run unit test"
206
+	@printf "  \033[36m%-46s\033[0m %s\n" "test-sqlite[#TestSpecificName]" "run integration test for sqlite"
207
+
208
+.PHONY: go-check
209
+go-check:
210
+	$(eval MIN_GO_VERSION_STR := $(shell grep -Eo '^go\s+[0-9]+\.[0-9]+' go.mod | cut -d' ' -f2))
211
+	$(eval MIN_GO_VERSION := $(shell printf "%03d%03d" $(shell echo '$(MIN_GO_VERSION_STR)' | tr '.' ' ')))
212
+	$(eval GO_VERSION := $(shell printf "%03d%03d" $(shell $(GO) version | grep -Eo '[0-9]+\.[0-9]+' | tr '.' ' ');))
213
+	@if [ "$(GO_VERSION)" -lt "$(MIN_GO_VERSION)" ]; then \
214
+		echo "Gitea requires Go $(MIN_GO_VERSION_STR) or greater to build. You can get it at https://go.dev/dl/"; \
215
+		exit 1; \
216
+	fi
217
+
218
+.PHONY: git-check
219
+git-check:
220
+	@if git lfs >/dev/null 2>&1 ; then : ; else \
221
+		echo "Gitea requires git with lfs support to run tests." ; \
222
+		exit 1; \
223
+	fi
224
+
225
+.PHONY: node-check
226
+node-check:
227
+	$(eval MIN_NODE_VERSION_STR := $(shell grep -Eo '"node":.*[0-9.]+"' package.json | sed -n 's/.*[^0-9.]\([0-9.]*\)"/\1/p'))
228
+	$(eval MIN_NODE_VERSION := $(shell printf "%03d%03d%03d" $(shell echo '$(MIN_NODE_VERSION_STR)' | tr '.' ' ')))
229
+	$(eval PNPM_MISSING := $(shell hash pnpm > /dev/null 2>&1 || echo 1))
230
+	@if [ "$(NODE_VERSION)" -lt "$(MIN_NODE_VERSION)" ]; then \
231
+		echo "Gitea requires Node.js $(MIN_NODE_VERSION_STR) or greater to build. You can get it at https://nodejs.org/en/download/"; \
232
+		exit 1; \
233
+	fi
234
+	@if [ "$(PNPM_MISSING)" = "1" ]; then \
235
+		echo "Gitea requires pnpm to build. You can install it at https://pnpm.io/installation"; \
236
+		exit 1; \
237
+	fi
238
+
239
+.PHONY: clean-all
240
+clean-all: clean ## delete backend, frontend and integration files
241
+	rm -rf $(WEBPACK_DEST_ENTRIES) node_modules
242
+
243
+.PHONY: clean
244
+clean: ## delete backend and integration files
245
+	rm -rf $(EXECUTABLE) $(DIST) $(BINDATA_DEST_WILDCARD) \
246
+		integrations*.test \
247
+		e2e*.test \
248
+		tests/integration/gitea-integration-* \
249
+		tests/integration/indexers-* \
250
+		tests/mysql.ini tests/pgsql.ini tests/mssql.ini man/ \
251
+		tests/e2e/gitea-e2e-*/ \
252
+		tests/e2e/indexers-*/ \
253
+		tests/e2e/reports/ tests/e2e/test-artifacts/ tests/e2e/test-snapshots/
254
+
255
+.PHONY: fmt
256
+fmt: ## format the Go and template code
257
+	@GOFUMPT_PACKAGE=$(GOFUMPT_PACKAGE) $(GO) run build/code-batch-process.go gitea-fmt -w '{file-list}'
258
+	$(eval TEMPLATES := $(shell find templates -type f -name '*.tmpl'))
259
+	@# strip whitespace after '{{' or '(' and before '}}' or ')' unless there is only
260
+	@# whitespace before it
261
+	@$(SED_INPLACE) \
262
+		-e 's/{{[ 	]\{1,\}/{{/g' -e '/^[ 	]\{1,\}}}/! s/[ 	]\{1,\}}}/}}/g' \
263
+	  -e 's/([ 	]\{1,\}/(/g' -e '/^[ 	]\{1,\})/! s/[ 	]\{1,\})/)/g' \
264
+	  $(TEMPLATES)
265
+
266
+.PHONY: fmt-check
267
+fmt-check: fmt
268
+	@diff=$$(git diff --color=always $(GO_SOURCES) templates $(WEB_DIRS)); \
269
+	if [ -n "$$diff" ]; then \
270
+	  echo "Please run 'make fmt' and commit the result:"; \
271
+	  printf "%s" "$${diff}"; \
272
+	  exit 1; \
273
+	fi
274
+
275
+.PHONY: fix
276
+fix: ## apply automated fixes to Go code
277
+	$(GO) run $(GOPLS_MODERNIZE_PACKAGE) -fix ./...
278
+
279
+.PHONY: fix-check
280
+fix-check: fix
281
+	@diff=$$(git diff --color=always $(GO_SOURCES)); \
282
+	if [ -n "$$diff" ]; then \
283
+	  echo "Please run 'make fix' and commit the result:"; \
284
+	  printf "%s" "$${diff}"; \
285
+	  exit 1; \
286
+	fi
287
+
288
+.PHONY: $(TAGS_EVIDENCE)
289
+$(TAGS_EVIDENCE):
290
+	@mkdir -p $(MAKE_EVIDENCE_DIR)
291
+	@echo "$(TAGS)" > $(TAGS_EVIDENCE)
292
+
293
+ifneq "$(TAGS)" "$(shell cat $(TAGS_EVIDENCE) 2>/dev/null)"
294
+TAGS_PREREQ := $(TAGS_EVIDENCE)
295
+endif
296
+
297
+.PHONY: generate-swagger
298
+generate-swagger: $(SWAGGER_SPEC) ## generate the swagger spec from code comments
299
+
300
+$(SWAGGER_SPEC): $(GO_SOURCES) $(SWAGGER_SPEC_INPUT)
301
+	$(GO) run $(SWAGGER_PACKAGE) generate spec --exclude "$(SWAGGER_EXCLUDE)" --input "$(SWAGGER_SPEC_INPUT)" --output './$(SWAGGER_SPEC)'
302
+
303
+.PHONY: swagger-check
304
+swagger-check: generate-swagger
305
+	@diff=$$(git diff --color=always '$(SWAGGER_SPEC)'); \
306
+	if [ -n "$$diff" ]; then \
307
+		echo "Please run 'make generate-swagger' and commit the result:"; \
308
+		printf "%s" "$${diff}"; \
309
+		exit 1; \
310
+	fi
311
+
312
+.PHONY: swagger-validate
313
+swagger-validate: ## check if the swagger spec is valid
314
+	@# swagger "validate" requires that the "basePath" must start with a slash, but we are using Golang template "{{...}}"
315
+	@$(SED_INPLACE) -E -e 's|"basePath":( *)"(.*)"|"basePath":\1"/\2"|g' './$(SWAGGER_SPEC)' # add a prefix slash to basePath
316
+	@# FIXME: there are some warnings
317
+	$(GO) run $(SWAGGER_PACKAGE) validate './$(SWAGGER_SPEC)'
318
+	@$(SED_INPLACE) -E -e 's|"basePath":( *)"/(.*)"|"basePath":\1"\2"|g' './$(SWAGGER_SPEC)' # remove the prefix slash from basePath
319
+
320
+.PHONY: checks
321
+checks: checks-frontend checks-backend ## run various consistency checks
322
+
323
+.PHONY: checks-frontend
324
+checks-frontend: lockfile-check svg-check ## check frontend files
325
+
326
+.PHONY: checks-backend
327
+checks-backend: tidy-check swagger-check fmt-check fix-check swagger-validate security-check ## check backend files
328
+
329
+.PHONY: lint
330
+lint: lint-frontend lint-backend lint-spell ## lint everything
331
+
332
+.PHONY: lint-fix
333
+lint-fix: lint-frontend-fix lint-backend-fix lint-spell-fix ## lint everything and fix issues
334
+
335
+.PHONY: lint-frontend
336
+lint-frontend: lint-js lint-css ## lint frontend files
337
+
338
+.PHONY: lint-frontend-fix
339
+lint-frontend-fix: lint-js-fix lint-css-fix ## lint frontend files and fix issues
340
+
341
+.PHONY: lint-backend
342
+lint-backend: lint-go lint-go-gitea-vet lint-go-gopls lint-editorconfig ## lint backend files
343
+
344
+.PHONY: lint-backend-fix
345
+lint-backend-fix: lint-go-fix lint-go-gitea-vet lint-editorconfig ## lint backend files and fix issues
346
+
347
+.PHONY: lint-js
348
+lint-js: node_modules ## lint js files
349
+	$(NODE_VARS) pnpm exec eslint --color --max-warnings=0 --flag unstable_native_nodejs_ts_config $(ESLINT_FILES)
350
+	$(NODE_VARS) pnpm exec vue-tsc
351
+
352
+.PHONY: lint-js-fix
353
+lint-js-fix: node_modules ## lint js files and fix issues
354
+	$(NODE_VARS) pnpm exec eslint --color --max-warnings=0 --flag unstable_native_nodejs_ts_config $(ESLINT_FILES) --fix
355
+	$(NODE_VARS) pnpm exec vue-tsc
356
+
357
+.PHONY: lint-css
358
+lint-css: node_modules ## lint css files
359
+	$(NODE_VARS) pnpm exec stylelint --color --max-warnings=0 $(STYLELINT_FILES)
360
+
361
+.PHONY: lint-css-fix
362
+lint-css-fix: node_modules ## lint css files and fix issues
363
+	$(NODE_VARS) pnpm exec stylelint --color --max-warnings=0 $(STYLELINT_FILES) --fix
364
+
365
+.PHONY: lint-swagger
366
+lint-swagger: node_modules ## lint swagger files
367
+	$(NODE_VARS) pnpm exec spectral lint -q -F hint $(SWAGGER_SPEC)
368
+
369
+.PHONY: lint-md
370
+lint-md: node_modules ## lint markdown files
371
+	$(NODE_VARS) pnpm exec markdownlint *.md
372
+
373
+.PHONY: lint-spell
374
+lint-spell: ## lint spelling
375
+	@go run $(MISSPELL_PACKAGE) -dict tools/misspellings.csv -error $(SPELLCHECK_FILES)
376
+
377
+.PHONY: lint-spell-fix
378
+lint-spell-fix: ## lint spelling and fix issues
379
+	@go run $(MISSPELL_PACKAGE) -dict tools/misspellings.csv -w $(SPELLCHECK_FILES)
380
+
381
+.PHONY: lint-go
382
+lint-go: ## lint go files
383
+	$(GO) run $(GOLANGCI_LINT_PACKAGE) run
384
+
385
+.PHONY: lint-go-fix
386
+lint-go-fix: ## lint go files and fix issues
387
+	$(GO) run $(GOLANGCI_LINT_PACKAGE) run --fix
388
+
389
+# workaround step for the lint-go-windows CI task because 'go run' can not
390
+# have distinct GOOS/GOARCH for its build and run steps
391
+.PHONY: lint-go-windows
392
+lint-go-windows:
393
+	@GOOS= GOARCH= $(GO) install $(GOLANGCI_LINT_PACKAGE)
394
+	golangci-lint run
395
+
396
+.PHONY: lint-go-gitea-vet
397
+lint-go-gitea-vet: ## lint go files with gitea-vet
398
+	@echo "Running gitea-vet..."
399
+	@GOOS= GOARCH= $(GO) build code.gitea.io/gitea-vet
400
+	@$(GO) vet -vettool=gitea-vet ./...
401
+
402
+.PHONY: lint-go-gopls
403
+lint-go-gopls: ## lint go files with gopls
404
+	@echo "Running gopls check..."
405
+	@GO=$(GO) GOPLS_PACKAGE=$(GOPLS_PACKAGE) tools/lint-go-gopls.sh $(GO_SOURCES)
406
+
407
+.PHONY: lint-editorconfig
408
+lint-editorconfig:
409
+	@echo "Running editorconfig check..."
410
+	@$(GO) run $(EDITORCONFIG_CHECKER_PACKAGE) $(EDITORCONFIG_FILES)
411
+
412
+.PHONY: lint-actions
413
+lint-actions: ## lint action workflow files
414
+	$(GO) run $(ACTIONLINT_PACKAGE)
415
+
416
+.PHONY: lint-templates
417
+lint-templates: .venv node_modules ## lint template files
418
+	@node tools/lint-templates-svg.ts
419
+	@uv run --frozen djlint $(shell find templates -type f -iname '*.tmpl')
420
+
421
+.PHONY: lint-yaml
422
+lint-yaml: .venv ## lint yaml files
423
+	@uv run --frozen yamllint -s .
424
+
425
+.PHONY: watch
426
+watch: ## watch everything and continuously rebuild
427
+	@bash tools/watch.sh
428
+
429
+.PHONY: watch-frontend
430
+watch-frontend: node-check node_modules ## watch frontend files and continuously rebuild
431
+	@rm -rf $(WEBPACK_DEST_ENTRIES)
432
+	NODE_ENV=development $(NODE_VARS) pnpm exec webpack --watch --progress --disable-interpret
433
+
434
+.PHONY: watch-backend
435
+watch-backend: go-check ## watch backend files and continuously rebuild
436
+	GITEA_RUN_MODE=dev $(GO) run $(AIR_PACKAGE) -c .air.toml
437
+
438
+.PHONY: test
439
+test: test-frontend test-backend ## test everything
440
+
441
+.PHONY: test-backend
442
+test-backend: ## test backend files
443
+	@echo "Running go test with $(GOTESTFLAGS) -tags '$(TEST_TAGS)'..."
444
+	@$(GO) test $(GOTESTFLAGS) -tags='$(TEST_TAGS)' $(GO_TEST_PACKAGES)
445
+
446
+.PHONY: test-frontend
447
+test-frontend: node_modules ## test frontend files
448
+	$(NODE_VARS) pnpm exec vitest
449
+
450
+.PHONY: test-check
451
+test-check:
452
+	@echo "Running test-check...";
453
+	@diff=$$(git status -s); \
454
+	if [ -n "$$diff" ]; then \
455
+		echo "make test-backend has changed files in the source tree:"; \
456
+		printf "%s" "$${diff}"; \
457
+		echo "You should change the tests to create these files in a temporary directory."; \
458
+		echo "Do not simply add these files to .gitignore"; \
459
+		exit 1; \
460
+	fi
461
+
462
+.PHONY: test\#%
463
+test\#%:
464
+	@echo "Running go test with -tags '$(TEST_TAGS)'..."
465
+	@$(GO) test $(GOTESTFLAGS) -tags='$(TEST_TAGS)' -run $(subst .,/,$*) $(GO_TEST_PACKAGES)
466
+
467
+.PHONY: coverage
468
+coverage:
469
+	grep '^\(mode: .*\)\|\(.*:[0-9]\+\.[0-9]\+,[0-9]\+\.[0-9]\+ [0-9]\+ [0-9]\+\)$$' coverage.out > coverage-bodged.out
470
+	grep '^\(mode: .*\)\|\(.*:[0-9]\+\.[0-9]\+,[0-9]\+\.[0-9]\+ [0-9]\+ [0-9]\+\)$$' integration.coverage.out > integration.coverage-bodged.out
471
+	$(GO) run build/gocovmerge.go integration.coverage-bodged.out coverage-bodged.out > coverage.all
472
+
473
+.PHONY: unit-test-coverage
474
+unit-test-coverage:
475
+	@echo "Running unit-test-coverage $(GOTESTFLAGS) -tags '$(TEST_TAGS)'..."
476
+	@$(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
477
+
478
+.PHONY: tidy
479
+tidy: ## run go mod tidy
480
+	$(eval MIN_GO_VERSION := $(shell grep -Eo '^go\s+[0-9]+\.[0-9.]+' go.mod | cut -d' ' -f2))
481
+	$(GO) mod tidy -compat=$(MIN_GO_VERSION)
482
+	@$(MAKE) --no-print-directory $(GO_LICENSE_FILE)
483
+
484
+vendor: go.mod go.sum
485
+	$(GO) mod vendor
486
+	@touch vendor
487
+
488
+.PHONY: tidy-check
489
+tidy-check: tidy
490
+	@diff=$$(git diff --color=always go.mod go.sum $(GO_LICENSE_FILE)); \
491
+	if [ -n "$$diff" ]; then \
492
+		echo "Please run 'make tidy' and commit the result:"; \
493
+		printf "%s" "$${diff}"; \
494
+		exit 1; \
495
+	fi
496
+
497
+.PHONY: go-licenses
498
+go-licenses: $(GO_LICENSE_FILE) ## regenerate go licenses
499
+
500
+$(GO_LICENSE_FILE): go.mod go.sum
501
+	@rm -rf $(GO_LICENSE_FILE)
502
+	$(GO) install $(GO_LICENSES_PACKAGE)
503
+	-GOOS=linux CGO_ENABLED=1 go-licenses save . --force --save_path=$(GO_LICENSE_TMP_DIR) 2>/dev/null
504
+	$(GO) run build/generate-go-licenses.go $(GO_LICENSE_TMP_DIR) $(GO_LICENSE_FILE)
505
+	@rm -rf $(GO_LICENSE_TMP_DIR)
506
+
507
+generate-ini-sqlite:
508
+	sed -e 's|{{REPO_TEST_DIR}}|${REPO_TEST_DIR}|g' \
509
+		-e 's|{{TEST_LOGGER}}|$(or $(TEST_LOGGER),test$(COMMA)file)|g' \
510
+		-e 's|{{TEST_TYPE}}|$(or $(TEST_TYPE),integration)|g' \
511
+			tests/sqlite.ini.tmpl > tests/sqlite.ini
512
+
513
+.PHONY: test-sqlite
514
+test-sqlite: integrations.sqlite.test generate-ini-sqlite
515
+	GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/sqlite.ini ./integrations.sqlite.test
516
+
517
+.PHONY: test-sqlite\#%
518
+test-sqlite\#%: integrations.sqlite.test generate-ini-sqlite
519
+	GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/sqlite.ini ./integrations.sqlite.test -test.run $(subst .,/,$*)
520
+
521
+.PHONY: test-sqlite-migration
522
+test-sqlite-migration:  migrations.sqlite.test migrations.individual.sqlite.test
523
+
524
+generate-ini-mysql:
525
+	sed -e 's|{{TEST_MYSQL_HOST}}|${TEST_MYSQL_HOST}|g' \
526
+		-e 's|{{TEST_MYSQL_DBNAME}}|${TEST_MYSQL_DBNAME}|g' \
527
+		-e 's|{{TEST_MYSQL_USERNAME}}|${TEST_MYSQL_USERNAME}|g' \
528
+		-e 's|{{TEST_MYSQL_PASSWORD}}|${TEST_MYSQL_PASSWORD}|g' \
529
+		-e 's|{{REPO_TEST_DIR}}|${REPO_TEST_DIR}|g' \
530
+		-e 's|{{TEST_LOGGER}}|$(or $(TEST_LOGGER),test$(COMMA)file)|g' \
531
+		-e 's|{{TEST_TYPE}}|$(or $(TEST_TYPE),integration)|g' \
532
+			tests/mysql.ini.tmpl > tests/mysql.ini
533
+
534
+.PHONY: test-mysql
535
+test-mysql: integrations.mysql.test generate-ini-mysql
536
+	GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mysql.ini ./integrations.mysql.test
537
+
538
+.PHONY: test-mysql\#%
539
+test-mysql\#%: integrations.mysql.test generate-ini-mysql
540
+	GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mysql.ini ./integrations.mysql.test -test.run $(subst .,/,$*)
541
+
542
+.PHONY: test-mysql-migration
543
+test-mysql-migration: migrations.mysql.test migrations.individual.mysql.test
544
+
545
+generate-ini-pgsql:
546
+	sed -e 's|{{TEST_PGSQL_HOST}}|${TEST_PGSQL_HOST}|g' \
547
+		-e 's|{{TEST_PGSQL_DBNAME}}|${TEST_PGSQL_DBNAME}|g' \
548
+		-e 's|{{TEST_PGSQL_USERNAME}}|${TEST_PGSQL_USERNAME}|g' \
549
+		-e 's|{{TEST_PGSQL_PASSWORD}}|${TEST_PGSQL_PASSWORD}|g' \
550
+		-e 's|{{TEST_PGSQL_SCHEMA}}|${TEST_PGSQL_SCHEMA}|g' \
551
+		-e 's|{{TEST_MINIO_ENDPOINT}}|${TEST_MINIO_ENDPOINT}|g' \
552
+		-e 's|{{REPO_TEST_DIR}}|${REPO_TEST_DIR}|g' \
553
+		-e 's|{{TEST_LOGGER}}|$(or $(TEST_LOGGER),test$(COMMA)file)|g' \
554
+		-e 's|{{TEST_TYPE}}|$(or $(TEST_TYPE),integration)|g' \
555
+			tests/pgsql.ini.tmpl > tests/pgsql.ini
556
+
557
+.PHONY: test-pgsql
558
+test-pgsql: integrations.pgsql.test generate-ini-pgsql
559
+	GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/pgsql.ini ./integrations.pgsql.test
560
+
561
+.PHONY: test-pgsql\#%
562
+test-pgsql\#%: integrations.pgsql.test generate-ini-pgsql
563
+	GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/pgsql.ini ./integrations.pgsql.test -test.run $(subst .,/,$*)
564
+
565
+.PHONY: test-pgsql-migration
566
+test-pgsql-migration: migrations.pgsql.test migrations.individual.pgsql.test
567
+
568
+generate-ini-mssql:
569
+	sed -e 's|{{TEST_MSSQL_HOST}}|${TEST_MSSQL_HOST}|g' \
570
+		-e 's|{{TEST_MSSQL_DBNAME}}|${TEST_MSSQL_DBNAME}|g' \
571
+		-e 's|{{TEST_MSSQL_USERNAME}}|${TEST_MSSQL_USERNAME}|g' \
572
+		-e 's|{{TEST_MSSQL_PASSWORD}}|${TEST_MSSQL_PASSWORD}|g' \
573
+		-e 's|{{REPO_TEST_DIR}}|${REPO_TEST_DIR}|g' \
574
+		-e 's|{{TEST_LOGGER}}|$(or $(TEST_LOGGER),test$(COMMA)file)|g' \
575
+		-e 's|{{TEST_TYPE}}|$(or $(TEST_TYPE),integration)|g' \
576
+			tests/mssql.ini.tmpl > tests/mssql.ini
577
+
578
+.PHONY: test-mssql
579
+test-mssql: integrations.mssql.test generate-ini-mssql
580
+	GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mssql.ini ./integrations.mssql.test
581
+
582
+.PHONY: test-mssql\#%
583
+test-mssql\#%: integrations.mssql.test generate-ini-mssql
584
+	GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mssql.ini ./integrations.mssql.test -test.run $(subst .,/,$*)
585
+
586
+.PHONY: test-mssql-migration
587
+test-mssql-migration: migrations.mssql.test migrations.individual.mssql.test
588
+
589
+.PHONY: playwright
590
+playwright: deps-frontend
591
+	$(NODE_VARS) pnpm exec playwright install $(PLAYWRIGHT_FLAGS)
592
+
593
+.PHONY: test-e2e%
594
+test-e2e%: TEST_TYPE ?= e2e
595
+	# Clear display env variable. Otherwise, chromium tests can fail.
596
+	DISPLAY=
597
+
598
+.PHONY: test-e2e
599
+test-e2e: test-e2e-sqlite
600
+
601
+.PHONY: test-e2e-sqlite
602
+test-e2e-sqlite: playwright e2e.sqlite.test generate-ini-sqlite
603
+	GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/sqlite.ini ./e2e.sqlite.test
604
+
605
+.PHONY: test-e2e-sqlite\#%
606
+test-e2e-sqlite\#%: playwright e2e.sqlite.test generate-ini-sqlite
607
+	GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/sqlite.ini ./e2e.sqlite.test -test.run TestE2e/$*
608
+
609
+.PHONY: test-e2e-mysql
610
+test-e2e-mysql: playwright e2e.mysql.test generate-ini-mysql
611
+	GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mysql.ini ./e2e.mysql.test
612
+
613
+.PHONY: test-e2e-mysql\#%
614
+test-e2e-mysql\#%: playwright e2e.mysql.test generate-ini-mysql
615
+	GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mysql.ini ./e2e.mysql.test -test.run TestE2e/$*
616
+
617
+.PHONY: test-e2e-pgsql
618
+test-e2e-pgsql: playwright e2e.pgsql.test generate-ini-pgsql
619
+	GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/pgsql.ini ./e2e.pgsql.test
620
+
621
+.PHONY: test-e2e-pgsql\#%
622
+test-e2e-pgsql\#%: playwright e2e.pgsql.test generate-ini-pgsql
623
+	GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/pgsql.ini ./e2e.pgsql.test -test.run TestE2e/$*
624
+
625
+.PHONY: test-e2e-mssql
626
+test-e2e-mssql: playwright e2e.mssql.test generate-ini-mssql
627
+	GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mssql.ini ./e2e.mssql.test
628
+
629
+.PHONY: test-e2e-mssql\#%
630
+test-e2e-mssql\#%: playwright e2e.mssql.test generate-ini-mssql
631
+	GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mssql.ini ./e2e.mssql.test -test.run TestE2e/$*
632
+
633
+.PHONY: bench-sqlite
634
+bench-sqlite: integrations.sqlite.test generate-ini-sqlite
635
+	GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/sqlite.ini ./integrations.sqlite.test -test.cpuprofile=cpu.out -test.run DontRunTests -test.bench .
636
+
637
+.PHONY: bench-mysql
638
+bench-mysql: integrations.mysql.test generate-ini-mysql
639
+	GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mysql.ini ./integrations.mysql.test -test.cpuprofile=cpu.out -test.run DontRunTests -test.bench .
640
+
641
+.PHONY: bench-mssql
642
+bench-mssql: integrations.mssql.test generate-ini-mssql
643
+	GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mssql.ini ./integrations.mssql.test -test.cpuprofile=cpu.out -test.run DontRunTests -test.bench .
644
+
645
+.PHONY: bench-pgsql
646
+bench-pgsql: integrations.pgsql.test generate-ini-pgsql
647
+	GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/pgsql.ini ./integrations.pgsql.test -test.cpuprofile=cpu.out -test.run DontRunTests -test.bench .
648
+
649
+.PHONY: integration-test-coverage
650
+integration-test-coverage: integrations.cover.test generate-ini-mysql
651
+	GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mysql.ini ./integrations.cover.test -test.coverprofile=integration.coverage.out
652
+
653
+.PHONY: integration-test-coverage-sqlite
654
+integration-test-coverage-sqlite: integrations.cover.sqlite.test generate-ini-sqlite
655
+	GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/sqlite.ini ./integrations.cover.sqlite.test -test.coverprofile=integration.coverage.out
656
+
657
+integrations.mysql.test: git-check $(GO_SOURCES)
658
+	$(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/integration -o integrations.mysql.test
659
+
660
+integrations.pgsql.test: git-check $(GO_SOURCES)
661
+	$(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/integration -o integrations.pgsql.test
662
+
663
+integrations.mssql.test: git-check $(GO_SOURCES)
664
+	$(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/integration -o integrations.mssql.test
665
+
666
+integrations.sqlite.test: git-check $(GO_SOURCES)
667
+	$(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/integration -o integrations.sqlite.test -tags '$(TEST_TAGS)'
668
+
669
+integrations.cover.test: git-check $(GO_SOURCES)
670
+	$(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/integration -coverpkg $(shell echo $(GO_TEST_PACKAGES) | tr ' ' ',') -o integrations.cover.test
671
+
672
+integrations.cover.sqlite.test: git-check $(GO_SOURCES)
673
+	$(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/integration -coverpkg $(shell echo $(GO_TEST_PACKAGES) | tr ' ' ',') -o integrations.cover.sqlite.test -tags '$(TEST_TAGS)'
674
+
675
+.PHONY: migrations.mysql.test
676
+migrations.mysql.test: $(GO_SOURCES) generate-ini-mysql
677
+	$(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/integration/migration-test -o migrations.mysql.test
678
+	GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mysql.ini ./migrations.mysql.test
679
+
680
+.PHONY: migrations.pgsql.test
681
+migrations.pgsql.test: $(GO_SOURCES) generate-ini-pgsql
682
+	$(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/integration/migration-test -o migrations.pgsql.test
683
+	GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/pgsql.ini ./migrations.pgsql.test
684
+
685
+.PHONY: migrations.mssql.test
686
+migrations.mssql.test: $(GO_SOURCES) generate-ini-mssql
687
+	$(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/integration/migration-test -o migrations.mssql.test
688
+	GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mssql.ini ./migrations.mssql.test
689
+
690
+.PHONY: migrations.sqlite.test
691
+migrations.sqlite.test: $(GO_SOURCES) generate-ini-sqlite
692
+	$(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/integration/migration-test -o migrations.sqlite.test -tags '$(TEST_TAGS)'
693
+	GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/sqlite.ini ./migrations.sqlite.test
694
+
695
+.PHONY: migrations.individual.mysql.test
696
+migrations.individual.mysql.test: $(GO_SOURCES)
697
+	GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mysql.ini $(GO) test $(GOTESTFLAGS) -tags='$(TEST_TAGS)' -p 1 $(MIGRATE_TEST_PACKAGES)
698
+
699
+.PHONY: migrations.individual.sqlite.test\#%
700
+migrations.individual.sqlite.test\#%: $(GO_SOURCES) generate-ini-sqlite
701
+	GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/sqlite.ini $(GO) test $(GOTESTFLAGS) -tags '$(TEST_TAGS)' code.gitea.io/gitea/models/migrations/$*
702
+
703
+.PHONY: migrations.individual.pgsql.test
704
+migrations.individual.pgsql.test: $(GO_SOURCES)
705
+	GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/pgsql.ini $(GO) test $(GOTESTFLAGS) -tags='$(TEST_TAGS)' -p 1 $(MIGRATE_TEST_PACKAGES)
706
+
707
+.PHONY: migrations.individual.pgsql.test\#%
708
+migrations.individual.pgsql.test\#%: $(GO_SOURCES) generate-ini-pgsql
709
+	GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/pgsql.ini $(GO) test $(GOTESTFLAGS) -tags '$(TEST_TAGS)' code.gitea.io/gitea/models/migrations/$*
710
+
711
+.PHONY: migrations.individual.mssql.test
712
+migrations.individual.mssql.test: $(GO_SOURCES) generate-ini-mssql
713
+	GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mssql.ini $(GO) test $(GOTESTFLAGS) -tags='$(TEST_TAGS)' -p 1 $(MIGRATE_TEST_PACKAGES)
714
+
715
+.PHONY: migrations.individual.mssql.test\#%
716
+migrations.individual.mssql.test\#%: $(GO_SOURCES) generate-ini-mssql
717
+	GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mssql.ini $(GO) test $(GOTESTFLAGS) -tags '$(TEST_TAGS)' code.gitea.io/gitea/models/migrations/$*
718
+
719
+.PHONY: migrations.individual.sqlite.test
720
+migrations.individual.sqlite.test: $(GO_SOURCES) generate-ini-sqlite
721
+	GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/sqlite.ini $(GO) test $(GOTESTFLAGS) -tags='$(TEST_TAGS)' -p 1 $(MIGRATE_TEST_PACKAGES)
722
+
723
+.PHONY: migrations.individual.sqlite.test\#%
724
+migrations.individual.sqlite.test\#%: $(GO_SOURCES) generate-ini-sqlite
725
+	GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/sqlite.ini $(GO) test $(GOTESTFLAGS) -tags '$(TEST_TAGS)' code.gitea.io/gitea/models/migrations/$*
726
+
727
+e2e.mysql.test: $(GO_SOURCES)
728
+	$(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/e2e -o e2e.mysql.test
729
+
730
+e2e.pgsql.test: $(GO_SOURCES)
731
+	$(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/e2e -o e2e.pgsql.test
732
+
733
+e2e.mssql.test: $(GO_SOURCES)
734
+	$(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/e2e -o e2e.mssql.test
735
+
736
+e2e.sqlite.test: $(GO_SOURCES)
737
+	$(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/e2e -o e2e.sqlite.test -tags '$(TEST_TAGS)'
738
+
739
+.PHONY: check
740
+check: test
741
+
742
+.PHONY: install $(TAGS_PREREQ)
743
+install: $(wildcard *.go)
744
+	CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) install -v -tags '$(TAGS)' -ldflags '-s -w $(LDFLAGS)'
745
+
746
+.PHONY: build
747
+build: frontend backend ## build everything
748
+
749
+.PHONY: frontend
750
+frontend: $(WEBPACK_DEST) ## build frontend files
751
+
752
+.PHONY: backend
753
+backend: go-check generate-backend $(EXECUTABLE) ## build backend files
754
+
755
+# 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
756
+.PHONY: generate
757
+generate: generate-backend ## run "go generate"
758
+
759
+.PHONY: generate-backend
760
+generate-backend: $(TAGS_PREREQ) generate-go
761
+
762
+.PHONY: generate-go
763
+generate-go: $(TAGS_PREREQ)
764
+	@echo "Running go generate..."
765
+	@CC= GOOS= GOARCH= CGO_ENABLED=0 $(GO) generate -tags '$(TAGS)' ./...
766
+
767
+.PHONY: security-check
768
+security-check:
769
+	go run $(GOVULNCHECK_PACKAGE) -show color ./...
770
+
771
+$(EXECUTABLE): $(GO_SOURCES) $(TAGS_PREREQ)
772
+ifneq ($(and $(STATIC),$(findstring pam,$(TAGS))),)
773
+  $(error pam support set via TAGS doesn't support static builds)
774
+endif
775
+	CGO_ENABLED="$(CGO_ENABLED)" CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) build $(GOFLAGS) $(EXTRA_GOFLAGS) -tags '$(TAGS)' -ldflags '-s -w $(EXTLDFLAGS) $(LDFLAGS)' -o $@
776
+
777
+.PHONY: release
778
+release: frontend generate release-windows release-linux release-darwin release-freebsd release-copy release-compress vendor release-sources release-check
779
+
780
+$(DIST_DIRS):
781
+	mkdir -p $(DIST_DIRS)
782
+
783
+.PHONY: release-windows
784
+release-windows: | $(DIST_DIRS)
785
+	CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -buildmode exe -dest $(DIST)/binaries -tags 'osusergo $(TAGS)' -ldflags '-s -w -linkmode external -extldflags "-static" $(LDFLAGS)' -targets 'windows/*' -out gitea-$(VERSION) .
786
+ifeq (,$(findstring gogit,$(TAGS)))
787
+	CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -buildmode exe -dest $(DIST)/binaries -tags 'osusergo gogit $(TAGS)' -ldflags '-s -w -linkmode external -extldflags "-static" $(LDFLAGS)' -targets 'windows/*' -out gitea-$(VERSION)-gogit .
788
+endif
789
+
790
+.PHONY: release-linux
791
+release-linux: | $(DIST_DIRS)
792
+	CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '-s -w -linkmode external -extldflags "-static" $(LDFLAGS)' -targets '$(LINUX_ARCHS)' -out gitea-$(VERSION) .
793
+
794
+.PHONY: release-darwin
795
+release-darwin: | $(DIST_DIRS)
796
+	CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '-s -w $(LDFLAGS)' -targets 'darwin-10.12/amd64,darwin-10.12/arm64' -out gitea-$(VERSION) .
797
+
798
+.PHONY: release-freebsd
799
+release-freebsd: | $(DIST_DIRS)
800
+	CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '-s -w $(LDFLAGS)' -targets 'freebsd/amd64' -out gitea-$(VERSION) .
801
+
802
+.PHONY: release-copy
803
+release-copy: | $(DIST_DIRS)
804
+	cd $(DIST); for file in `find . -type f -name "*"`; do cp $${file} ./release/; done;
805
+
806
+.PHONY: release-check
807
+release-check: | $(DIST_DIRS)
808
+	cd $(DIST)/release/; for file in `find . -type f -name "*"`; do echo "checksumming $${file}" && $(SHASUM) `echo $${file} | sed 's/^..//'` > $${file}.sha256; done;
809
+
810
+.PHONY: release-compress
811
+release-compress: | $(DIST_DIRS)
812
+	cd $(DIST)/release/; for file in `find . -type f -name "*"`; do echo "compressing $${file}" && $(GO) run $(GXZ_PACKAGE) -k -9 $${file}; done;
813
+
814
+.PHONY: release-sources
815
+release-sources: | $(DIST_DIRS)
816
+	echo $(VERSION) > $(STORED_VERSION_FILE)
817
+# bsdtar needs a ^ to prevent matching subdirectories
818
+	$(eval EXCL := --exclude=$(shell tar --help | grep -q bsdtar && echo "^")./)
819
+# use transform to a add a release-folder prefix; in bsdtar the transform parameter equivalent is -s
820
+	$(eval TRANSFORM := $(shell tar --help | grep -q bsdtar && echo "-s '/^./gitea-src-$(VERSION)/'" || echo "--transform 's|^./|gitea-src-$(VERSION)/|'"))
821
+	tar $(addprefix $(EXCL),$(TAR_EXCLUDES)) $(TRANSFORM) -czf $(DIST)/release/gitea-src-$(VERSION).tar.gz .
822
+	rm -f $(STORED_VERSION_FILE)
823
+
824
+.PHONY: deps
825
+deps: deps-frontend deps-backend deps-tools deps-py ## install dependencies
826
+
827
+.PHONY: deps-py
828
+deps-py: .venv ## install python dependencies
829
+
830
+.PHONY: deps-frontend
831
+deps-frontend: node_modules ## install frontend dependencies
832
+
833
+.PHONY: deps-backend
834
+deps-backend: ## install backend dependencies
835
+	$(GO) mod download
836
+
837
+.PHONY: deps-tools
838
+deps-tools: ## install tool dependencies
839
+	$(GO) install $(AIR_PACKAGE) & \
840
+	$(GO) install $(EDITORCONFIG_CHECKER_PACKAGE) & \
841
+	$(GO) install $(GOFUMPT_PACKAGE) & \
842
+	$(GO) install $(GOLANGCI_LINT_PACKAGE) & \
843
+	$(GO) install $(GXZ_PACKAGE) & \
844
+	$(GO) install $(MISSPELL_PACKAGE) & \
845
+	$(GO) install $(SWAGGER_PACKAGE) & \
846
+	$(GO) install $(XGO_PACKAGE) & \
847
+	$(GO) install $(GO_LICENSES_PACKAGE) & \
848
+	$(GO) install $(GOVULNCHECK_PACKAGE) & \
849
+	$(GO) install $(ACTIONLINT_PACKAGE) & \
850
+	$(GO) install $(GOPLS_PACKAGE) & \
851
+	$(GO) install $(GOPLS_MODERNIZE_PACKAGE) & \
852
+	wait
853
+
854
+node_modules: pnpm-lock.yaml
855
+	$(NODE_VARS) pnpm install --frozen-lockfile
856
+	@touch node_modules
857
+
858
+.venv: uv.lock
859
+	uv sync
860
+	@touch .venv
861
+
862
+.PHONY: update
863
+update: update-js update-py ## update js and py dependencies
864
+
865
+.PHONY: update-js
866
+update-js: node-check | node_modules ## update js dependencies
867
+	$(NODE_VARS) pnpm exec updates -u -f package.json
868
+	rm -rf node_modules pnpm-lock.yaml
869
+	$(NODE_VARS) pnpm install
870
+	$(NODE_VARS) pnpm exec nolyfill install
871
+	$(NODE_VARS) pnpm install
872
+	@touch node_modules
873
+
874
+.PHONY: update-py
875
+update-py: node-check | node_modules ## update py dependencies
876
+	$(NODE_VARS) pnpm exec updates -u -f pyproject.toml
877
+	rm -rf .venv uv.lock
878
+	uv sync
879
+	@touch .venv
880
+
881
+.PHONY: webpack
882
+webpack: $(WEBPACK_DEST) ## build webpack files
883
+
884
+$(WEBPACK_DEST): $(WEBPACK_SOURCES) $(WEBPACK_CONFIGS) pnpm-lock.yaml
885
+	@$(MAKE) -s node-check node_modules
886
+	@rm -rf $(WEBPACK_DEST_ENTRIES)
887
+	@echo "Running webpack..."
888
+	@BROWSERSLIST_IGNORE_OLD_DATA=true $(NODE_VARS) pnpm exec webpack --disable-interpret
889
+	@touch $(WEBPACK_DEST)
890
+
891
+.PHONY: svg
892
+svg: node-check | node_modules ## build svg files
893
+	rm -rf $(SVG_DEST_DIR)
894
+	node tools/generate-svg.ts
895
+
896
+.PHONY: svg-check
897
+svg-check: svg
898
+	@git add $(SVG_DEST_DIR)
899
+	@diff=$$(git diff --color=always --cached $(SVG_DEST_DIR)); \
900
+	if [ -n "$$diff" ]; then \
901
+		echo "Please run 'make svg' and 'git add $(SVG_DEST_DIR)' and commit the result:"; \
902
+		printf "%s" "$${diff}"; \
903
+		exit 1; \
904
+	fi
905
+
906
+.PHONY: lockfile-check
907
+lockfile-check:
908
+	$(NODE_VARS) pnpm install --frozen-lockfile
909
+	@diff=$$(git diff --color=always pnpm-lock.yaml); \
910
+	if [ -n "$$diff" ]; then \
911
+		echo "pnpm-lock.yaml is inconsistent with package.json"; \
912
+		echo "Please run 'pnpm install --frozen-lockfile' and commit the result:"; \
913
+		printf "%s" "$${diff}"; \
914
+		exit 1; \
915
+	fi
916
+
917
+.PHONY: update-translations
918
+update-translations:
919
+	mkdir -p ./translations
920
+	cd ./translations && curl -L https://crowdin.com/download/project/gitea.zip > gitea.zip && unzip gitea.zip
921
+	rm ./translations/gitea.zip
922
+	$(SED_INPLACE) -e 's/="/=/g' -e 's/"$$//g' ./translations/*.ini
923
+	$(SED_INPLACE) -e 's/\\"/"/g' ./translations/*.ini
924
+	mv ./translations/*.ini ./options/locale/
925
+	rmdir ./translations
926
+
927
+.PHONY: generate-gitignore
928
+generate-gitignore: ## update gitignore files
929
+	$(GO) run build/generate-gitignores.go
930
+
931
+.PHONY: generate-images
932
+generate-images: | node_modules ## generate images
933
+	cd tools && node generate-images.ts $(TAGS)
934
+
935
+.PHONY: generate-manpage
936
+generate-manpage: ## generate manpage
937
+	@[ -f gitea ] || make backend
938
+	@mkdir -p man/man1/ man/man5
939
+	@./gitea docs --man > man/man1/gitea.1
940
+	@gzip -9 man/man1/gitea.1 && echo man/man1/gitea.1.gz created
941
+	@#TODO A small script that formats config-cheat-sheet.en-us.md nicely for use as a config man page
942
+
943
+.PHONY: docker
944
+docker:
945
+	docker build --disable-content-trust=false -t $(DOCKER_REF) .
946
+# support also build args docker build --build-arg GITEA_VERSION=v1.2.3 --build-arg TAGS="bindata sqlite sqlite_unlock_notify"  .
947
+
948
+# This endif closes the if at the top of the file
949
+endif
950
+
951
+# Disable parallel execution because it would break some targets that don't
952
+# specify exact dependencies like 'backend' which does currently not depend
953
+# on 'frontend' to enable Node.js-less builds from source tarballs.
954
+.NOTPARALLEL:

+ 213
- 0
README.md 查看文件

@@ -0,0 +1,213 @@
1
+# Gitea
2
+
3
+[![](https://github.com/go-gitea/gitea/actions/workflows/release-nightly.yml/badge.svg?branch=main)](https://github.com/go-gitea/gitea/actions/workflows/release-nightly.yml?query=branch%3Amain "Release Nightly")
4
+[![](https://img.shields.io/discord/322538954119184384.svg?logo=discord&logoColor=white&label=Discord&color=5865F2)](https://discord.gg/Gitea "Join the Discord chat at https://discord.gg/Gitea")
5
+[![](https://goreportcard.com/badge/code.gitea.io/gitea)](https://goreportcard.com/report/code.gitea.io/gitea "Go Report Card")
6
+[![](https://pkg.go.dev/badge/code.gitea.io/gitea?status.svg)](https://pkg.go.dev/code.gitea.io/gitea "GoDoc")
7
+[![](https://img.shields.io/github/release/go-gitea/gitea.svg)](https://github.com/go-gitea/gitea/releases/latest "GitHub release")
8
+[![](https://www.codetriage.com/go-gitea/gitea/badges/users.svg)](https://www.codetriage.com/go-gitea/gitea "Help Contribute to Open Source")
9
+[![](https://opencollective.com/gitea/tiers/backers/badge.svg?label=backers&color=brightgreen)](https://opencollective.com/gitea "Become a backer/sponsor of gitea")
10
+[![](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT "License: MIT")
11
+[![Contribute with Gitpod](https://img.shields.io/badge/Contribute%20with-Gitpod-908a85?logo=gitpod&color=green)](https://gitpod.io/#https://github.com/go-gitea/gitea)
12
+[![](https://badges.crowdin.net/gitea/localized.svg)](https://translate.gitea.com "Crowdin")
13
+
14
+[繁體中文](./README.zh-tw.md) | [简体中文](./README.zh-cn.md)
15
+
16
+## Purpose
17
+
18
+The goal of this project is to make the easiest, fastest, and most
19
+painless way of setting up a self-hosted Git service.
20
+
21
+As Gitea is written in Go, it works across **all** the platforms and
22
+architectures that are supported by Go, including Linux, macOS, and
23
+Windows on x86, amd64, ARM and PowerPC architectures.
24
+This project has been
25
+[forked](https://blog.gitea.com/welcome-to-gitea/) from
26
+[Gogs](https://gogs.io) since November of 2016, but a lot has changed.
27
+
28
+For online demonstrations, you can visit [demo.gitea.com](https://demo.gitea.com).
29
+
30
+For accessing free Gitea service (with a limited number of repositories), you can visit [gitea.com](https://gitea.com/user/login).
31
+
32
+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).
33
+
34
+## Documentation
35
+
36
+You can find comprehensive documentation on our official [documentation website](https://docs.gitea.com/).
37
+
38
+It includes installation, administration, usage, development, contributing guides, and more to help you get started and explore all features effectively.
39
+
40
+If you have any suggestions or would like to contribute to it, you can visit the [documentation repository](https://gitea.com/gitea/docs)
41
+
42
+## Building
43
+
44
+From the root of the source tree, run:
45
+
46
+    TAGS="bindata" make build
47
+
48
+or if SQLite support is required:
49
+
50
+    TAGS="bindata sqlite sqlite_unlock_notify" make build
51
+
52
+The `build` target is split into two sub-targets:
53
+
54
+- `make backend` which requires [Go Stable](https://go.dev/dl/), the required version is defined in [go.mod](/go.mod).
55
+- `make frontend` which requires [Node.js LTS](https://nodejs.org/en/download/) or greater and [pnpm](https://pnpm.io/installation).
56
+
57
+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.
58
+
59
+More info: https://docs.gitea.com/installation/install-from-source
60
+
61
+## Using
62
+
63
+After building, a binary file named `gitea` will be generated in the root of the source tree by default. To run it, use:
64
+
65
+    ./gitea web
66
+
67
+> [!NOTE]
68
+> If you're interested in using our APIs, we have experimental support with [documentation](https://docs.gitea.com/api).
69
+
70
+## Contributing
71
+
72
+Expected workflow is: Fork -> Patch -> Push -> Pull Request
73
+
74
+> [!NOTE]
75
+>
76
+> 1. **YOU MUST READ THE [CONTRIBUTORS GUIDE](CONTRIBUTING.md) BEFORE STARTING TO WORK ON A PULL REQUEST.**
77
+> 2. If you have found a vulnerability in the project, please write privately to **security@gitea.io**. Thanks!
78
+
79
+## Translating
80
+
81
+[![Crowdin](https://badges.crowdin.net/gitea/localized.svg)](https://translate.gitea.com)
82
+
83
+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.
84
+
85
+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.
86
+
87
+Get more information from [documentation](https://docs.gitea.com/contributing/localization).
88
+
89
+## Official and Third-Party Projects
90
+
91
+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.
92
+
93
+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.
94
+
95
+## Communication
96
+
97
+[![](https://img.shields.io/discord/322538954119184384.svg?logo=discord&logoColor=white&label=Discord&color=5865F2)](https://discord.gg/Gitea "Join the Discord chat at https://discord.gg/Gitea")
98
+
99
+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/).
100
+
101
+## Authors
102
+
103
+- [Maintainers](https://github.com/orgs/go-gitea/people)
104
+- [Contributors](https://github.com/go-gitea/gitea/graphs/contributors)
105
+- [Translators](options/locale/TRANSLATORS)
106
+
107
+## Backers
108
+
109
+Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/gitea#backer)]
110
+
111
+<a href="https://opencollective.com/gitea#backers" target="_blank"><img src="https://opencollective.com/gitea/backers.svg?width=890"></a>
112
+
113
+## Sponsors
114
+
115
+Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [[Become a sponsor](https://opencollective.com/gitea#sponsor)]
116
+
117
+<a href="https://opencollective.com/gitea/sponsor/0/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/0/avatar.svg"></a>
118
+<a href="https://opencollective.com/gitea/sponsor/1/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/1/avatar.svg"></a>
119
+<a href="https://opencollective.com/gitea/sponsor/2/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/2/avatar.svg"></a>
120
+<a href="https://opencollective.com/gitea/sponsor/3/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/3/avatar.svg"></a>
121
+<a href="https://opencollective.com/gitea/sponsor/4/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/4/avatar.svg"></a>
122
+<a href="https://opencollective.com/gitea/sponsor/5/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/5/avatar.svg"></a>
123
+<a href="https://opencollective.com/gitea/sponsor/6/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/6/avatar.svg"></a>
124
+<a href="https://opencollective.com/gitea/sponsor/7/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/7/avatar.svg"></a>
125
+<a href="https://opencollective.com/gitea/sponsor/8/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/8/avatar.svg"></a>
126
+<a href="https://opencollective.com/gitea/sponsor/9/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/9/avatar.svg"></a>
127
+
128
+## FAQ
129
+
130
+**How do you pronounce Gitea?**
131
+
132
+Gitea is pronounced [/ɡɪ’ti:/](https://youtu.be/EM71-2uDAoY) as in "gi-tea" with a hard g.
133
+
134
+**Why is this not hosted on a Gitea instance?**
135
+
136
+We're [working on it](https://github.com/go-gitea/gitea/issues/1029).
137
+
138
+**Where can I find the security patches?**
139
+
140
+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.
141
+
142
+## License
143
+
144
+This project is licensed under the MIT License.
145
+See the [LICENSE](https://github.com/go-gitea/gitea/blob/main/LICENSE) file
146
+for the full license text.
147
+
148
+## Further information
149
+
150
+<details>
151
+<summary>Looking for an overview of the interface? Check it out!</summary>
152
+
153
+### Login/Register Page
154
+
155
+![Login](https://dl.gitea.com/screenshots/login.png)
156
+![Register](https://dl.gitea.com/screenshots/register.png)
157
+
158
+### User Dashboard
159
+
160
+![Home](https://dl.gitea.com/screenshots/home.png)
161
+![Issues](https://dl.gitea.com/screenshots/issues.png)
162
+![Pull Requests](https://dl.gitea.com/screenshots/pull_requests.png)
163
+![Milestones](https://dl.gitea.com/screenshots/milestones.png)
164
+
165
+### User Profile
166
+
167
+![Profile](https://dl.gitea.com/screenshots/user_profile.png)
168
+
169
+### Explore
170
+
171
+![Repos](https://dl.gitea.com/screenshots/explore_repos.png)
172
+![Users](https://dl.gitea.com/screenshots/explore_users.png)
173
+![Orgs](https://dl.gitea.com/screenshots/explore_orgs.png)
174
+
175
+### Repository
176
+
177
+![Home](https://dl.gitea.com/screenshots/repo_home.png)
178
+![Commits](https://dl.gitea.com/screenshots/repo_commits.png)
179
+![Branches](https://dl.gitea.com/screenshots/repo_branches.png)
180
+![Labels](https://dl.gitea.com/screenshots/repo_labels.png)
181
+![Milestones](https://dl.gitea.com/screenshots/repo_milestones.png)
182
+![Releases](https://dl.gitea.com/screenshots/repo_releases.png)
183
+![Tags](https://dl.gitea.com/screenshots/repo_tags.png)
184
+
185
+#### Repository Issue
186
+
187
+![List](https://dl.gitea.com/screenshots/repo_issues.png)
188
+![Issue](https://dl.gitea.com/screenshots/repo_issue.png)
189
+
190
+#### Repository Pull Requests
191
+
192
+![List](https://dl.gitea.com/screenshots/repo_pull_requests.png)
193
+![Pull Request](https://dl.gitea.com/screenshots/repo_pull_request.png)
194
+![File](https://dl.gitea.com/screenshots/repo_pull_request_file.png)
195
+![Commits](https://dl.gitea.com/screenshots/repo_pull_request_commits.png)
196
+
197
+#### Repository Actions
198
+
199
+![List](https://dl.gitea.com/screenshots/repo_actions.png)
200
+![Details](https://dl.gitea.com/screenshots/repo_actions_run.png)
201
+
202
+#### Repository Activity
203
+
204
+![Activity](https://dl.gitea.com/screenshots/repo_activity.png)
205
+![Contributors](https://dl.gitea.com/screenshots/repo_contributors.png)
206
+![Code Frequency](https://dl.gitea.com/screenshots/repo_code_frequency.png)
207
+![Recent Commits](https://dl.gitea.com/screenshots/repo_recent_commits.png)
208
+
209
+### Organization
210
+
211
+![Home](https://dl.gitea.com/screenshots/org_home.png)
212
+
213
+</details>

+ 206
- 0
README.zh-cn.md 查看文件

@@ -0,0 +1,206 @@
1
+# Gitea
2
+
3
+[![](https://github.com/go-gitea/gitea/actions/workflows/release-nightly.yml/badge.svg?branch=main)](https://github.com/go-gitea/gitea/actions/workflows/release-nightly.yml?query=branch%3Amain "Release Nightly")
4
+[![](https://img.shields.io/discord/322538954119184384.svg?logo=discord&logoColor=white&label=Discord&color=5865F2)](https://discord.gg/Gitea "Join the Discord chat at https://discord.gg/Gitea")
5
+[![](https://goreportcard.com/badge/code.gitea.io/gitea)](https://goreportcard.com/report/code.gitea.io/gitea "Go Report Card")
6
+[![](https://pkg.go.dev/badge/code.gitea.io/gitea?status.svg)](https://pkg.go.dev/code.gitea.io/gitea "GoDoc")
7
+[![](https://img.shields.io/github/release/go-gitea/gitea.svg)](https://github.com/go-gitea/gitea/releases/latest "GitHub release")
8
+[![](https://www.codetriage.com/go-gitea/gitea/badges/users.svg)](https://www.codetriage.com/go-gitea/gitea "Help Contribute to Open Source")
9
+[![](https://opencollective.com/gitea/tiers/backers/badge.svg?label=backers&color=brightgreen)](https://opencollective.com/gitea "Become a backer/sponsor of gitea")
10
+[![](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT "License: MIT")
11
+[![Contribute with Gitpod](https://img.shields.io/badge/Contribute%20with-Gitpod-908a85?logo=gitpod&color=green)](https://gitpod.io/#https://github.com/go-gitea/gitea)
12
+[![](https://badges.crowdin.net/gitea/localized.svg)](https://translate.gitea.com "Crowdin")
13
+
14
+[English](./README.md) | [繁體中文](./README.zh-tw.md)
15
+
16
+## 目的
17
+
18
+这个项目的目标是提供最简单、最快速、最无痛的方式来设置自托管的 Git 服务。
19
+
20
+由于 Gitea 是用 Go 语言编写的,它可以在 Go 支持的所有平台和架构上运行,包括 Linux、macOS 和 Windows 的 x86、amd64、ARM 和 PowerPC 架构。这个项目自 2016 年 11 月从 [Gogs](https://gogs.io) [分叉](https://blog.gitea.com/welcome-to-gitea/) 而来,但已经有了很多变化。
21
+
22
+在线演示可以访问 [demo.gitea.com](https://demo.gitea.com)。
23
+
24
+要访问免费的 Gitea 服务(有一定数量的仓库限制),可以访问 [gitea.com](https://gitea.com/user/login)。
25
+
26
+要快速部署您自己的专用 Gitea 实例,可以在 [cloud.gitea.com](https://cloud.gitea.com) 开始免费试用。
27
+
28
+## 文件
29
+
30
+您可以在我们的官方 [文件网站](https://docs.gitea.com/) 上找到全面的文件。
31
+
32
+它包括安装、管理、使用、开发、贡献指南等,帮助您快速入门并有效地探索所有功能。
33
+
34
+如果您有任何建议或想要贡献,可以访问 [文件仓库](https://gitea.com/gitea/docs)
35
+
36
+## 构建
37
+
38
+从源代码树的根目录运行:
39
+
40
+    TAGS="bindata" make build
41
+
42
+如果需要 SQLite 支持:
43
+
44
+    TAGS="bindata sqlite sqlite_unlock_notify" make build
45
+
46
+`build` 目标分为两个子目标:
47
+
48
+- `make backend` 需要 [Go Stable](https://go.dev/dl/),所需版本在 [go.mod](/go.mod) 中定义。
49
+- `make frontend` 需要 [Node.js LTS](https://nodejs.org/en/download/) 或更高版本。
50
+
51
+需要互联网连接来下载 go 和 npm 模块。从包含预构建前端文件的官方源代码压缩包构建时,不会触发 `frontend` 目标,因此可以在没有 Node.js 的情况下构建。
52
+
53
+更多信息:https://docs.gitea.com/installation/install-from-source
54
+
55
+## 使用
56
+
57
+构建后,默认情况下会在源代码树的根目录生成一个名为 `gitea` 的二进制文件。要运行它,请使用:
58
+
59
+    ./gitea web
60
+
61
+> [!注意]
62
+> 如果您对使用我们的 API 感兴趣,我们提供了实验性支持,并附有 [文件](https://docs.gitea.com/api)。
63
+
64
+## 贡献
65
+
66
+预期的工作流程是:Fork -> Patch -> Push -> Pull Request
67
+
68
+> [!注意]
69
+>
70
+> 1. **在开始进行 Pull Request 之前,您必须阅读 [贡献者指南](CONTRIBUTING.md)。**
71
+> 2. 如果您在项目中发现了漏洞,请私下写信给 **security@gitea.io**。谢谢!
72
+
73
+## 翻译
74
+
75
+[![Crowdin](https://badges.crowdin.net/gitea/localized.svg)](https://translate.gitea.com)
76
+
77
+翻译通过 [Crowdin](https://translate.gitea.com) 进行。如果您想翻译成新的语言,请在 Crowdin 项目中请求管理员添加新语言。
78
+
79
+您也可以创建一个 issue 来添加语言,或者在 discord 的 #translation 频道上询问。如果您需要上下文或发现一些翻译问题,可以在字符串上留言或在 Discord 上询问。对于一般的翻译问题,文档中有一个部分。目前有点空,但我们希望随着问题的出现而填充它。
80
+
81
+更多信息请参阅 [文件](https://docs.gitea.com/contributing/localization)。
82
+
83
+## 官方和第三方项目
84
+
85
+我们提供了一个官方的 [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)。
86
+
87
+我们在 [gitea/awesome-gitea](https://gitea.com/gitea/awesome-gitea) 维护了一个 Gitea 相关项目的列表,您可以在那里发现更多的第三方项目,包括 SDK、插件、主题等。
88
+
89
+## 通讯
90
+
91
+[![](https://img.shields.io/discord/322538954119184384.svg?logo=discord&logoColor=white&label=Discord&color=5865F2)](https://discord.gg/Gitea "Join the Discord chat at https://discord.gg/Gitea")
92
+
93
+如果您有任何文件未涵盖的问题,可以在我们的 [Discord 服务器](https://discord.gg/Gitea) 上与我们联系,或者在 [discourse 论坛](https://forum.gitea.com/) 上创建帖子。
94
+
95
+## 作者
96
+
97
+- [维护者](https://github.com/orgs/go-gitea/people)
98
+- [贡献者](https://github.com/go-gitea/gitea/graphs/contributors)
99
+- [翻译者](options/locale/TRANSLATORS)
100
+
101
+## 支持者
102
+
103
+感谢所有支持者! 🙏 [[成为支持者](https://opencollective.com/gitea#backer)]
104
+
105
+<a href="https://opencollective.com/gitea#backers" target="_blank"><img src="https://opencollective.com/gitea/backers.svg?width=890"></a>
106
+
107
+## 赞助商
108
+
109
+通过成为赞助商来支持这个项目。您的标志将显示在这里,并带有链接到您的网站。 [[成为赞助商](https://opencollective.com/gitea#sponsor)]
110
+
111
+<a href="https://opencollective.com/gitea/sponsor/0/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/0/avatar.svg"></a>
112
+<a href="https://opencollective.com/gitea/sponsor/1/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/1/avatar.svg"></a>
113
+<a href="https://opencollective.com/gitea/sponsor/2/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/2/avatar.svg"></a>
114
+<a href="https://opencollective.com/gitea/sponsor/3/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/3/avatar.svg"></a>
115
+<a href="https://opencollective.com/gitea/sponsor/4/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/4/avatar.svg"></a>
116
+<a href="https://opencollective.com/gitea/sponsor/5/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/5/avatar.svg"></a>
117
+<a href="https://opencollective.com/gitea/sponsor/6/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/6/avatar.svg"></a>
118
+<a href="https://opencollective.com/gitea/sponsor/7/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/7/avatar.svg"></a>
119
+<a href="https://opencollective.com/gitea/sponsor/8/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/8/avatar.svg"></a>
120
+<a href="https://opencollective.com/gitea/sponsor/9/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/9/avatar.svg"></a>
121
+
122
+## 常见问题
123
+
124
+**Gitea 怎么发音?**
125
+
126
+Gitea 的发音是 [/ɡɪ’ti:/](https://youtu.be/EM71-2uDAoY),就像 "gi-tea" 一样,g 是硬音。
127
+
128
+**为什么这个项目没有托管在 Gitea 实例上?**
129
+
130
+我们正在 [努力](https://github.com/go-gitea/gitea/issues/1029)。
131
+
132
+**在哪里可以找到安全补丁?**
133
+
134
+在 [发布日志](https://github.com/go-gitea/gitea/releases) 或 [变更日志](https://github.com/go-gitea/gitea/blob/main/CHANGELOG.md) 中,搜索关键词 `SECURITY` 以找到安全补丁。
135
+
136
+## 许可证
137
+
138
+这个项目是根据 MIT 许可证授权的。
139
+请参阅 [LICENSE](https://github.com/go-gitea/gitea/blob/main/LICENSE) 文件以获取完整的许可证文本。
140
+
141
+## 进一步信息
142
+
143
+<details>
144
+<summary>寻找界面概述?查看这里!</summary>
145
+
146
+### 登录/注册页面
147
+
148
+![Login](https://dl.gitea.com/screenshots/login.png)
149
+![Register](https://dl.gitea.com/screenshots/register.png)
150
+
151
+### 用户仪表板
152
+
153
+![Home](https://dl.gitea.com/screenshots/home.png)
154
+![Issues](https://dl.gitea.com/screenshots/issues.png)
155
+![Pull Requests](https://dl.gitea.com/screenshots/pull_requests.png)
156
+![Milestones](https://dl.gitea.com/screenshots/milestones.png)
157
+
158
+### 用户资料
159
+
160
+![Profile](https://dl.gitea.com/screenshots/user_profile.png)
161
+
162
+### 探索
163
+
164
+![Repos](https://dl.gitea.com/screenshots/explore_repos.png)
165
+![Users](https://dl.gitea.com/screenshots/explore_users.png)
166
+![Orgs](https://dl.gitea.com/screenshots/explore_orgs.png)
167
+
168
+### 仓库
169
+
170
+![Home](https://dl.gitea.com/screenshots/repo_home.png)
171
+![Commits](https://dl.gitea.com/screenshots/repo_commits.png)
172
+![Branches](https://dl.gitea.com/screenshots/repo_branches.png)
173
+![Labels](https://dl.gitea.com/screenshots/repo_labels.png)
174
+![Milestones](https://dl.gitea.com/screenshots/repo_milestones.png)
175
+![Releases](https://dl.gitea.com/screenshots/repo_releases.png)
176
+![Tags](https://dl.gitea.com/screenshots/repo_tags.png)
177
+
178
+#### 仓库问题
179
+
180
+![List](https://dl.gitea.com/screenshots/repo_issues.png)
181
+![Issue](https://dl.gitea.com/screenshots/repo_issue.png)
182
+
183
+#### 仓库拉取请求
184
+
185
+![List](https://dl.gitea.com/screenshots/repo_pull_requests.png)
186
+![Pull Request](https://dl.gitea.com/screenshots/repo_pull_request.png)
187
+![File](https://dl.gitea.com/screenshots/repo_pull_request_file.png)
188
+![Commits](https://dl.gitea.com/screenshots/repo_pull_request_commits.png)
189
+
190
+#### 仓库操作
191
+
192
+![List](https://dl.gitea.com/screenshots/repo_actions.png)
193
+![Details](https://dl.gitea.com/screenshots/repo_actions_run.png)
194
+
195
+#### 仓库活动
196
+
197
+![Activity](https://dl.gitea.com/screenshots/repo_activity.png)
198
+![Contributors](https://dl.gitea.com/screenshots/repo_contributors.png)
199
+![Code Frequency](https://dl.gitea.com/screenshots/repo_code_frequency.png)
200
+![Recent Commits](https://dl.gitea.com/screenshots/repo_recent_commits.png)
201
+
202
+### 组织
203
+
204
+![Home](https://dl.gitea.com/screenshots/org_home.png)
205
+
206
+</details>

+ 206
- 0
README.zh-tw.md 查看文件

@@ -0,0 +1,206 @@
1
+# Gitea
2
+
3
+[![](https://github.com/go-gitea/gitea/actions/workflows/release-nightly.yml/badge.svg?branch=main)](https://github.com/go-gitea/gitea/actions/workflows/release-nightly.yml?query=branch%3Amain "Release Nightly")
4
+[![](https://img.shields.io/discord/322538954119184384.svg?logo=discord&logoColor=white&label=Discord&color=5865F2)](https://discord.gg/Gitea "Join the Discord chat at https://discord.gg/Gitea")
5
+[![](https://goreportcard.com/badge/code.gitea.io/gitea)](https://goreportcard.com/report/code.gitea.io/gitea "Go Report Card")
6
+[![](https://pkg.go.dev/badge/code.gitea.io/gitea?status.svg)](https://pkg.go.dev/code.gitea.io/gitea "GoDoc")
7
+[![](https://img.shields.io/github/release/go-gitea/gitea.svg)](https://github.com/go-gitea/gitea/releases/latest "GitHub release")
8
+[![](https://www.codetriage.com/go-gitea/gitea/badges/users.svg)](https://www.codetriage.com/go-gitea/gitea "Help Contribute to Open Source")
9
+[![](https://opencollective.com/gitea/tiers/backers/badge.svg?label=backers&color=brightgreen)](https://opencollective.com/gitea "Become a backer/sponsor of gitea")
10
+[![](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT "License: MIT")
11
+[![Contribute with Gitpod](https://img.shields.io/badge/Contribute%20with-Gitpod-908a85?logo=gitpod&color=green)](https://gitpod.io/#https://github.com/go-gitea/gitea)
12
+[![](https://badges.crowdin.net/gitea/localized.svg)](https://translate.gitea.com "Crowdin")
13
+
14
+[English](./README.md) | [简体中文](./README.zh-cn.md)
15
+
16
+## 目的
17
+
18
+這個項目的目標是提供最簡單、最快速、最無痛的方式來設置自託管的 Git 服務。
19
+
20
+由於 Gitea 是用 Go 語言編寫的,它可以在 Go 支援的所有平台和架構上運行,包括 Linux、macOS 和 Windows 的 x86、amd64、ARM 和 PowerPC 架構。這個項目自 2016 年 11 月從 [Gogs](https://gogs.io) [分叉](https://blog.gitea.com/welcome-to-gitea/) 而來,但已經有了很多變化。
21
+
22
+在線演示可以訪問 [demo.gitea.com](https://demo.gitea.com)。
23
+
24
+要訪問免費的 Gitea 服務(有一定數量的倉庫限制),可以訪問 [gitea.com](https://gitea.com/user/login)。
25
+
26
+要快速部署您自己的專用 Gitea 實例,可以在 [cloud.gitea.com](https://cloud.gitea.com) 開始免費試用。
27
+
28
+## 文件
29
+
30
+您可以在我們的官方 [文件網站](https://docs.gitea.com/) 上找到全面的文件。
31
+
32
+它包括安裝、管理、使用、開發、貢獻指南等,幫助您快速入門並有效地探索所有功能。
33
+
34
+如果您有任何建議或想要貢獻,可以訪問 [文件倉庫](https://gitea.com/gitea/docs)
35
+
36
+## 構建
37
+
38
+從源代碼樹的根目錄運行:
39
+
40
+    TAGS="bindata" make build
41
+
42
+如果需要 SQLite 支援:
43
+
44
+    TAGS="bindata sqlite sqlite_unlock_notify" make build
45
+
46
+`build` 目標分為兩個子目標:
47
+
48
+- `make backend` 需要 [Go Stable](https://go.dev/dl/),所需版本在 [go.mod](/go.mod) 中定義。
49
+- `make frontend` 需要 [Node.js LTS](https://nodejs.org/en/download/) 或更高版本。
50
+
51
+需要互聯網連接來下載 go 和 npm 模塊。從包含預構建前端文件的官方源代碼壓縮包構建時,不會觸發 `frontend` 目標,因此可以在沒有 Node.js 的情況下構建。
52
+
53
+更多信息:https://docs.gitea.com/installation/install-from-source
54
+
55
+## 使用
56
+
57
+構建後,默認情況下會在源代碼樹的根目錄生成一個名為 `gitea` 的二進制文件。要運行它,請使用:
58
+
59
+    ./gitea web
60
+
61
+> [!注意]
62
+> 如果您對使用我們的 API 感興趣,我們提供了實驗性支援,並附有 [文件](https://docs.gitea.com/api)。
63
+
64
+## 貢獻
65
+
66
+預期的工作流程是:Fork -> Patch -> Push -> Pull Request
67
+
68
+> [!注意]
69
+>
70
+> 1. **在開始進行 Pull Request 之前,您必須閱讀 [貢獻者指南](CONTRIBUTING.md)。**
71
+> 2. 如果您在項目中發現了漏洞,請私下寫信給 **security@gitea.io**。謝謝!
72
+
73
+## 翻譯
74
+
75
+[![Crowdin](https://badges.crowdin.net/gitea/localized.svg)](https://translate.gitea.com)
76
+
77
+翻譯通過 [Crowdin](https://translate.gitea.com) 進行。如果您想翻譯成新的語言,請在 Crowdin 項目中請求管理員添加新語言。
78
+
79
+您也可以創建一個 issue 來添加語言,或者在 discord 的 #translation 頻道上詢問。如果您需要上下文或發現一些翻譯問題,可以在字符串上留言或在 Discord 上詢問。對於一般的翻譯問題,文檔中有一個部分。目前有點空,但我們希望隨著問題的出現而填充它。
80
+
81
+更多信息請參閱 [文件](https://docs.gitea.com/contributing/localization)。
82
+
83
+## 官方和第三方項目
84
+
85
+我們提供了一個官方的 [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)。
86
+
87
+我們在 [gitea/awesome-gitea](https://gitea.com/gitea/awesome-gitea) 維護了一個 Gitea 相關項目的列表,您可以在那裡發現更多的第三方項目,包括 SDK、插件、主題等。
88
+
89
+## 通訊
90
+
91
+[![](https://img.shields.io/discord/322538954119184384.svg?logo=discord&logoColor=white&label=Discord&color=5865F2)](https://discord.gg/Gitea "Join the Discord chat at https://discord.gg/Gitea")
92
+
93
+如果您有任何文件未涵蓋的問題,可以在我們的 [Discord 服務器](https://discord.gg/Gitea) 上與我們聯繫,或者在 [discourse 論壇](https://forum.gitea.com/) 上創建帖子。
94
+
95
+## 作者
96
+
97
+- [維護者](https://github.com/orgs/go-gitea/people)
98
+- [貢獻者](https://github.com/go-gitea/gitea/graphs/contributors)
99
+- [翻譯者](options/locale/TRANSLATORS)
100
+
101
+## 支持者
102
+
103
+感謝所有支持者! 🙏 [[成為支持者](https://opencollective.com/gitea#backer)]
104
+
105
+<a href="https://opencollective.com/gitea#backers" target="_blank"><img src="https://opencollective.com/gitea/backers.svg?width=890"></a>
106
+
107
+## 贊助商
108
+
109
+通過成為贊助商來支持這個項目。您的標誌將顯示在這裡,並帶有鏈接到您的網站。 [[成為贊助商](https://opencollective.com/gitea#sponsor)]
110
+
111
+<a href="https://opencollective.com/gitea/sponsor/0/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/0/avatar.svg"></a>
112
+<a href="https://opencollective.com/gitea/sponsor/1/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/1/avatar.svg"></a>
113
+<a href="https://opencollective.com/gitea/sponsor/2/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/2/avatar.svg"></a>
114
+<a href="https://opencollective.com/gitea/sponsor/3/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/3/avatar.svg"></a>
115
+<a href="https://opencollective.com/gitea/sponsor/4/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/4/avatar.svg"></a>
116
+<a href="https://opencollective.com/gitea/sponsor/5/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/5/avatar.svg"></a>
117
+<a href="https://opencollective.com/gitea/sponsor/6/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/6/avatar.svg"></a>
118
+<a href="https://opencollective.com/gitea/sponsor/7/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/7/avatar.svg"></a>
119
+<a href="https://opencollective.com/gitea/sponsor/8/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/8/avatar.svg"></a>
120
+<a href="https://opencollective.com/gitea/sponsor/9/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/9/avatar.svg"></a>
121
+
122
+## 常見問題
123
+
124
+**Gitea 怎麼發音?**
125
+
126
+Gitea 的發音是 [/ɡɪ’ti:/](https://youtu.be/EM71-2uDAoY),就像 "gi-tea" 一樣,g 是硬音。
127
+
128
+**為什麼這個項目沒有託管在 Gitea 實例上?**
129
+
130
+我們正在 [努力](https://github.com/go-gitea/gitea/issues/1029)。
131
+
132
+**在哪裡可以找到安全補丁?**
133
+
134
+在 [發佈日誌](https://github.com/go-gitea/gitea/releases) 或 [變更日誌](https://github.com/go-gitea/gitea/blob/main/CHANGELOG.md) 中,搜索關鍵詞 `SECURITY` 以找到安全補丁。
135
+
136
+## 許可證
137
+
138
+這個項目是根據 MIT 許可證授權的。
139
+請參閱 [LICENSE](https://github.com/go-gitea/gitea/blob/main/LICENSE) 文件以獲取完整的許可證文本。
140
+
141
+## 進一步信息
142
+
143
+<details>
144
+<summary>尋找界面概述?查看這裡!</summary>
145
+
146
+### 登錄/註冊頁面
147
+
148
+![Login](https://dl.gitea.com/screenshots/login.png)
149
+![Register](https://dl.gitea.com/screenshots/register.png)
150
+
151
+### 用戶儀表板
152
+
153
+![Home](https://dl.gitea.com/screenshots/home.png)
154
+![Issues](https://dl.gitea.com/screenshots/issues.png)
155
+![Pull Requests](https://dl.gitea.com/screenshots/pull_requests.png)
156
+![Milestones](https://dl.gitea.com/screenshots/milestones.png)
157
+
158
+### 用戶資料
159
+
160
+![Profile](https://dl.gitea.com/screenshots/user_profile.png)
161
+
162
+### 探索
163
+
164
+![Repos](https://dl.gitea.com/screenshots/explore_repos.png)
165
+![Users](https://dl.gitea.com/screenshots/explore_users.png)
166
+![Orgs](https://dl.gitea.com/screenshots/explore_orgs.png)
167
+
168
+### 倉庫
169
+
170
+![Home](https://dl.gitea.com/screenshots/repo_home.png)
171
+![Commits](https://dl.gitea.com/screenshots/repo_commits.png)
172
+![Branches](https://dl.gitea.com/screenshots/repo_branches.png)
173
+![Labels](https://dl.gitea.com/screenshots/repo_labels.png)
174
+![Milestones](https://dl.gitea.com/screenshots/repo_milestones.png)
175
+![Releases](https://dl.gitea.com/screenshots/repo_releases.png)
176
+![Tags](https://dl.gitea.com/screenshots/repo_tags.png)
177
+
178
+#### 倉庫問題
179
+
180
+![List](https://dl.gitea.com/screenshots/repo_issues.png)
181
+![Issue](https://dl.gitea.com/screenshots/repo_issue.png)
182
+
183
+#### 倉庫拉取請求
184
+
185
+![List](https://dl.gitea.com/screenshots/repo_pull_requests.png)
186
+![Pull Request](https://dl.gitea.com/screenshots/repo_pull_request.png)
187
+![File](https://dl.gitea.com/screenshots/repo_pull_request_file.png)
188
+![Commits](https://dl.gitea.com/screenshots/repo_pull_request_commits.png)
189
+
190
+#### 倉庫操作
191
+
192
+![List](https://dl.gitea.com/screenshots/repo_actions.png)
193
+![Details](https://dl.gitea.com/screenshots/repo_actions_run.png)
194
+
195
+#### 倉庫活動
196
+
197
+![Activity](https://dl.gitea.com/screenshots/repo_activity.png)
198
+![Contributors](https://dl.gitea.com/screenshots/repo_contributors.png)
199
+![Code Frequency](https://dl.gitea.com/screenshots/repo_code_frequency.png)
200
+![Recent Commits](https://dl.gitea.com/screenshots/repo_recent_commits.png)
201
+
202
+### 組織
203
+
204
+![Home](https://dl.gitea.com/screenshots/org_home.png)
205
+
206
+</details>

+ 85
- 0
SECURITY.md 查看文件

@@ -0,0 +1,85 @@
1
+# Reporting security issues
2
+
3
+The Gitea maintainers take security seriously.
4
+
5
+If you discover a security issue, please bring it to their attention right away!
6
+
7
+Previous vulnerabilities are listed at https://about.gitea.com/security.
8
+
9
+## Reporting a Vulnerability
10
+
11
+Please **DO NOT** file a public issue, instead send your report privately to `security@gitea.io`.
12
+
13
+## Protecting Security Information
14
+
15
+Due to the sensitive nature of security information, you can use the below GPG public key to encrypt your mail body.
16
+
17
+The PGP key is valid until July 4, 2026.
18
+
19
+```
20
+Key ID: 6FCD2D5B
21
+Key Type: RSA
22
+Expires: 7/4/2026
23
+Key Size: 4096/4096
24
+Fingerprint: 3DE0 3D1E 144A 7F06 9359 99DC AAFD 2381 6FCD 2D5B
25
+```
26
+
27
+UserID: Gitea Security <security@gitea.io>
28
+
29
+```
30
+-----BEGIN PGP PUBLIC KEY BLOCK-----
31
+
32
+mQINBGK1Z/4BEADFMqXA9DeeChmSxUjF0Be5sq99ZUhgrZjcN/wOzz0wuCJZC0l8
33
+4uC+d6mfv7JpJYlzYzOK97/x5UguKHkYNZ6mm1G9KHaXmoIBDLKDzfPdJopVNv2r
34
+OajijaE0uMCnMjadlg5pbhMLRQG8a9J32yyaz7ZEAw72Ab31fvvcA53NkuqO4j2w
35
+k7dtFQzhbNOYV0VffQT90WDZdalYHB1JHyEQ+70U9OjVD5ggNYSzX98Eu3Hjn7V7
36
+kqFrcAxr5TE1elf0IXJcuBJtFzQSTUGlQldKOHtGTGgGjj9r/FFAE5ioBgVD05bV
37
+rEEgIMM/GqYaG/nbNpWE6P3mEc2Mnn3pZaRJL0LuF26TLjnqEcMMDp5iIhLdFzXR
38
+3tMdtKgQFu+Mtzs3ipwWARYgHyU09RJsI2HeBx7RmZO/Xqrec763Z7zdJ7SpCn0Z
39
+q+pHZl24JYR0Kf3T/ZiOC0cGd2QJqpJtg5J6S/OqfX9NH6MsCczO8pUC1N/aHH2X
40
+CTme2nF56izORqDWKoiICteL3GpYsCV9nyCidcCmoQsS+DKvE86YhIhVIVWGRY2F
41
+lzpAjnN9/KLtQroutrm+Ft0mdjDiJUeFVl1cOHDhoyfCsQh62HumoyZoZvqzQd6e
42
+AbN11nq6aViMe2Q3je1AbiBnRnQSHxt1Tc8X4IshO3MQK1Sk7oPI6LA5oQARAQAB
43
+tCJHaXRlYSBTZWN1cml0eSA8c2VjdXJpdHlAZ2l0ZWEuaW8+iQJXBBMBCABBAhsD
44
+BQsJCAcCAiICBhUKCQgLAgQWAgMBAh4HAheAFiEEPeA9HhRKfwaTWZncqv0jgW/N
45
+LVsFAmhoHmkFCQeT6esACgkQqv0jgW/NLVuFLRAAmjBQSKRAgs2bFIEj7HLAbDp4
46
+f+XkdH+GsT3jRPOZ9QZgmtM+TfoE4yNgIVfOl+s4RdjM/W4QzqZuPQ55hbEHd056
47
+cJmm7B+6GsHFcdrPmh65sOCEIyh4+t45dUfeWpFsDPqm9j1UHXAJQIpB8vDEVAPH
48
+t+3wLCk8GMPJs1o5tIyMmaO23ngvkwn8eG7KgY+rp2PzObrb5g7ppci0ILzILkrp
49
+HVjZsEfUWRgSVF7LuU5ppqDKrlcqwUpQq6n3kGMZcLrCp6ACKP04TBmTfUxNwdL7
50
+I0N7apI2Pbct9T1Gv/lYAUFWyU2c3gh/EBLbO6BukaLOFRQHrtNfdJV/YnMPlcXr
51
+LUJjK9K4eAH9DsrZqrisz/LthsC2BaNIN3KRMTk5YTYgmIh8GXzSgihORmtDFELC
52
+RroID3pTuS0zjXh+wpY9GuPTh7UW23p42Daxca4fAT4k5EclvDRUrL21xMopPMiL
53
+HuNdELz4FVchRTy05PjzKVyjVInDNojE2KUxnjxZDzYJ6aT/g+coD5yfntYm8BEj
54
++ZzL0ndZES54hzKLpv7zwBQwFzam68clZYmDPILOPTflQDfpGEWmJK4undFU5obz
55
+ZsQRz0R3ulspChATbZxO0d5LX2obLpKO9X3b5VoO1KF+R8Vjw1Y0KxrNZ6rIcfqH
56
+Z50QVQKSe9dm08K0ON+5Ag0EYrVn/gEQALrFLQjCR3GjuHSindz0rd3Fnx/t7Sen
57
+T+p07yCSSoSlmnJHCQmwh4vfg1blyz0zZ4vkIhtpHsEgc+ZAG+WQXSsJ2iRz+eSN
58
+GwoOQl4XC3n+QWkc1ws+btr48+6UqXIQU+F8TPQyx/PIgi2nZXJB7f5+mjCqsk46
59
+XvH4nTr4kJjuqMSR/++wvre2qNQRa/q/dTsK0OaN/mJsdX6Oi+aGNaQJUhIG7F+E
60
+ZDMkn/O6xnwWNzy/+bpg43qH/Gk0eakOmz5NmQLRkV58SZLiJvuCUtkttf6CyhnX
61
+03OcWaajv5W8qA39dBYQgDrrPbBWUnwfO3yMveqhwV4JjDoe8sPAyn1NwzakNYqP
62
+RzsWyLrLS7R7J9s3FkZXhQw/QQcsaSMcGNQO047dm1P83N8JY5aEpiRo9zSWjoiw
63
+qoExANj5lUTZPe8M50lI182FrcjAN7dClO3QI6pg7wy0erMxfFly3j8UQ91ysS9T
64
+s+GsP9I3cmWWQcKYxWHtE8xTXnNCVPFZQj2nwhJzae8ypfOtulBRA3dUKWGKuDH/
65
+axFENhUsT397aOU3qkP/od4a64JyNIEo4CTTSPVeWd7njsGqli2U3A4xL2CcyYvt
66
+D/MWcMBGEoLSNTswwKdom4FaJpn5KThnK/T0bQcmJblJhoCtppXisbexZnCpuS0x
67
+Zdlm2T14KJ3LABEBAAGJAjwEGAEIACYCGwwWIQQ94D0eFEp/BpNZmdyq/SOBb80t
68
+WwUCaGgeJAUJB5PppgAKCRCq/SOBb80tW/NWEACB6Jrf0gWlk7e+hNCdnbM0ZVWU
69
+f2sHNFfXxxsdhpcDgKbNHtkZb8nZgv8AX+5fTtUwMVa3vKcdw30xFiIM5N7cCIPV
70
+vg/5z5BtfEaitnabEUG2iiVDIy8IHXIcK10rX+7BosA3QDl2PsiBHwyi5G13lRk8
71
+zGTSNDuOalug33h5/lr2dPigamkq74Aoy29q8Rjad6GfWHipL2bFimgtY+Zdi0BH
72
+NLk4EJXxj1SgVx5dtkQzWJReBA5M+FQ4QYQZBO+f4TDoOLmjui152uhkoLBQbGAa
73
+WWJFTVxm0bG5MXloEL3gA8DfU7XDwuW/sHJC5pBko8RpQViooOhckMepZV3Y83DK
74
+bwLYa3JmPgj2rEv4993dvrJbQhpGd082HOxOsllCs8pgNq1SnXpWYfcGTgGKC3ts
75
+U8YZUUJUQ7mi2L8Tv3ix20c9EiGmA30JAmA8eZTC3cWup91ZkkVBFRml2czTXajd
76
+RWZ6GbHV5503ueDQcB8yBVgF3CSixs67+dGSbD3p86OqGrjAcJzM5TFbNKcnGLdE
77
+kGbZpNwAISy750lXzXKmyrh5RTCeTOQerbwCMBvHZO+HAevA/LXDTw2OAiSIQlP5
78
+sYA4sFYLQ30OAkgJcmdp/pSgVj/erNtSN07ClrOpDb/uFpQymO6K2h0Pst3feNVK
79
+9M2VbqL9C51z/wyHLg==
80
+=SfZA
81
+-----END PGP PUBLIC KEY BLOCK-----
82
+
83
+```
84
+
85
+Security reports are greatly appreciated and we will publicly thank you for it, although we keep your name confidential if you request it.

+ 1
- 0
assets/emoji.json
文件差異過大導致無法顯示
查看文件


+ 31
- 0
assets/favicon.svg 查看文件

@@ -0,0 +1,31 @@
1
+<?xml version="1.0" encoding="utf-8"?>
2
+<svg version="1.1" id="main_outline" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
3
+	 y="0px" viewBox="0 0 640 640" style="enable-background:new 0 0 640 640;" xml:space="preserve">
4
+<g>
5
+	<path id="teabag" style="fill:#FFFFFF" d="M395.9,484.2l-126.9-61c-12.5-6-17.9-21.2-11.8-33.8l61-126.9c6-12.5,21.2-17.9,33.8-11.8
6
+		c17.2,8.3,27.1,13,27.1,13l-0.1-109.2l16.7-0.1l0.1,117.1c0,0,57.4,24.2,83.1,40.1c3.7,2.3,10.2,6.8,12.9,14.4
7
+		c2.1,6.1,2,13.1-1,19.3l-61,126.9C423.6,484.9,408.4,490.3,395.9,484.2z"/>
8
+	<g>
9
+		<g>
10
+			<path style="fill:#609926" d="M622.7,149.8c-4.1-4.1-9.6-4-9.6-4s-117.2,6.6-177.9,8c-13.3,0.3-26.5,0.6-39.6,0.7c0,39.1,0,78.2,0,117.2
11
+				c-5.5-2.6-11.1-5.3-16.6-7.9c0-36.4-0.1-109.2-0.1-109.2c-29,0.4-89.2-2.2-89.2-2.2s-141.4-7.1-156.8-8.5
12
+				c-9.8-0.6-22.5-2.1-39,1.5c-8.7,1.8-33.5,7.4-53.8,26.9C-4.9,212.4,6.6,276.2,8,285.8c1.7,11.7,6.9,44.2,31.7,72.5
13
+				c45.8,56.1,144.4,54.8,144.4,54.8s12.1,28.9,30.6,55.5c25,33.1,50.7,58.9,75.7,62c63,0,188.9-0.1,188.9-0.1s12,0.1,28.3-10.3
14
+				c14-8.5,26.5-23.4,26.5-23.4s12.9-13.8,30.9-45.3c5.5-9.7,10.1-19.1,14.1-28c0,0,55.2-117.1,55.2-231.1
15
+				C633.2,157.9,624.7,151.8,622.7,149.8z M125.6,353.9c-25.9-8.5-36.9-18.7-36.9-18.7S69.6,321.8,60,295.4
16
+				c-16.5-44.2-1.4-71.2-1.4-71.2s8.4-22.5,38.5-30c13.8-3.7,31-3.1,31-3.1s7.1,59.4,15.7,94.2c7.2,29.2,24.8,77.7,24.8,77.7
17
+				S142.5,359.9,125.6,353.9z M425.9,461.5c0,0-6.1,14.5-19.6,15.4c-5.8,0.4-10.3-1.2-10.3-1.2s-0.3-0.1-5.3-2.1l-112.9-55
18
+				c0,0-10.9-5.7-12.8-15.6c-2.2-8.1,2.7-18.1,2.7-18.1L322,273c0,0,4.8-9.7,12.2-13c0.6-0.3,2.3-1,4.5-1.5c8.1-2.1,18,2.8,18,2.8
19
+				l110.7,53.7c0,0,12.6,5.7,15.3,16.2c1.9,7.4-0.5,14-1.8,17.2C474.6,363.8,425.9,461.5,425.9,461.5z"/>
20
+			<path style="fill:#609926" d="M326.8,380.1c-8.2,0.1-15.4,5.8-17.3,13.8c-1.9,8,2,16.3,9.1,20c7.7,4,17.5,1.8,22.7-5.4
21
+				c5.1-7.1,4.3-16.9-1.8-23.1l24-49.1c1.5,0.1,3.7,0.2,6.2-0.5c4.1-0.9,7.1-3.6,7.1-3.6c4.2,1.8,8.6,3.8,13.2,6.1
22
+				c4.8,2.4,9.3,4.9,13.4,7.3c0.9,0.5,1.8,1.1,2.8,1.9c1.6,1.3,3.4,3.1,4.7,5.5c1.9,5.5-1.9,14.9-1.9,14.9
23
+				c-2.3,7.6-18.4,40.6-18.4,40.6c-8.1-0.2-15.3,5-17.7,12.5c-2.6,8.1,1.1,17.3,8.9,21.3c7.8,4,17.4,1.7,22.5-5.3
24
+				c5-6.8,4.6-16.3-1.1-22.6c1.9-3.7,3.7-7.4,5.6-11.3c5-10.4,13.5-30.4,13.5-30.4c0.9-1.7,5.7-10.3,2.7-21.3
25
+				c-2.5-11.4-12.6-16.7-12.6-16.7c-12.2-7.9-29.2-15.2-29.2-15.2s0-4.1-1.1-7.1c-1.1-3.1-2.8-5.1-3.9-6.3c4.7-9.7,9.4-19.3,14.1-29
26
+				c-4.1-2-8.1-4-12.2-6.1c-4.8,9.8-9.7,19.7-14.5,29.5c-6.7-0.1-12.9,3.5-16.1,9.4c-3.4,6.3-2.7,14.1,1.9,19.8
27
+				C343.2,346.5,335,363.3,326.8,380.1z"/>
28
+		</g>
29
+	</g>
30
+</g>
31
+</svg>

+ 1332
- 0
assets/go-licenses.json
文件差異過大導致無法顯示
查看文件


+ 31
- 0
assets/logo.svg 查看文件

@@ -0,0 +1,31 @@
1
+<?xml version="1.0" encoding="utf-8"?>
2
+<svg version="1.1" id="main_outline" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
3
+	 y="0px" viewBox="0 0 640 640" style="enable-background:new 0 0 640 640;" xml:space="preserve">
4
+<g>
5
+	<path id="teabag" style="fill:#FFFFFF" d="M395.9,484.2l-126.9-61c-12.5-6-17.9-21.2-11.8-33.8l61-126.9c6-12.5,21.2-17.9,33.8-11.8
6
+		c17.2,8.3,27.1,13,27.1,13l-0.1-109.2l16.7-0.1l0.1,117.1c0,0,57.4,24.2,83.1,40.1c3.7,2.3,10.2,6.8,12.9,14.4
7
+		c2.1,6.1,2,13.1-1,19.3l-61,126.9C423.6,484.9,408.4,490.3,395.9,484.2z"/>
8
+	<g>
9
+		<g>
10
+			<path style="fill:#609926" d="M622.7,149.8c-4.1-4.1-9.6-4-9.6-4s-117.2,6.6-177.9,8c-13.3,0.3-26.5,0.6-39.6,0.7c0,39.1,0,78.2,0,117.2
11
+				c-5.5-2.6-11.1-5.3-16.6-7.9c0-36.4-0.1-109.2-0.1-109.2c-29,0.4-89.2-2.2-89.2-2.2s-141.4-7.1-156.8-8.5
12
+				c-9.8-0.6-22.5-2.1-39,1.5c-8.7,1.8-33.5,7.4-53.8,26.9C-4.9,212.4,6.6,276.2,8,285.8c1.7,11.7,6.9,44.2,31.7,72.5
13
+				c45.8,56.1,144.4,54.8,144.4,54.8s12.1,28.9,30.6,55.5c25,33.1,50.7,58.9,75.7,62c63,0,188.9-0.1,188.9-0.1s12,0.1,28.3-10.3
14
+				c14-8.5,26.5-23.4,26.5-23.4s12.9-13.8,30.9-45.3c5.5-9.7,10.1-19.1,14.1-28c0,0,55.2-117.1,55.2-231.1
15
+				C633.2,157.9,624.7,151.8,622.7,149.8z M125.6,353.9c-25.9-8.5-36.9-18.7-36.9-18.7S69.6,321.8,60,295.4
16
+				c-16.5-44.2-1.4-71.2-1.4-71.2s8.4-22.5,38.5-30c13.8-3.7,31-3.1,31-3.1s7.1,59.4,15.7,94.2c7.2,29.2,24.8,77.7,24.8,77.7
17
+				S142.5,359.9,125.6,353.9z M425.9,461.5c0,0-6.1,14.5-19.6,15.4c-5.8,0.4-10.3-1.2-10.3-1.2s-0.3-0.1-5.3-2.1l-112.9-55
18
+				c0,0-10.9-5.7-12.8-15.6c-2.2-8.1,2.7-18.1,2.7-18.1L322,273c0,0,4.8-9.7,12.2-13c0.6-0.3,2.3-1,4.5-1.5c8.1-2.1,18,2.8,18,2.8
19
+				l110.7,53.7c0,0,12.6,5.7,15.3,16.2c1.9,7.4-0.5,14-1.8,17.2C474.6,363.8,425.9,461.5,425.9,461.5z"/>
20
+			<path style="fill:#609926" d="M326.8,380.1c-8.2,0.1-15.4,5.8-17.3,13.8c-1.9,8,2,16.3,9.1,20c7.7,4,17.5,1.8,22.7-5.4
21
+				c5.1-7.1,4.3-16.9-1.8-23.1l24-49.1c1.5,0.1,3.7,0.2,6.2-0.5c4.1-0.9,7.1-3.6,7.1-3.6c4.2,1.8,8.6,3.8,13.2,6.1
22
+				c4.8,2.4,9.3,4.9,13.4,7.3c0.9,0.5,1.8,1.1,2.8,1.9c1.6,1.3,3.4,3.1,4.7,5.5c1.9,5.5-1.9,14.9-1.9,14.9
23
+				c-2.3,7.6-18.4,40.6-18.4,40.6c-8.1-0.2-15.3,5-17.7,12.5c-2.6,8.1,1.1,17.3,8.9,21.3c7.8,4,17.4,1.7,22.5-5.3
24
+				c5-6.8,4.6-16.3-1.1-22.6c1.9-3.7,3.7-7.4,5.6-11.3c5-10.4,13.5-30.4,13.5-30.4c0.9-1.7,5.7-10.3,2.7-21.3
25
+				c-2.5-11.4-12.6-16.7-12.6-16.7c-12.2-7.9-29.2-15.2-29.2-15.2s0-4.1-1.1-7.1c-1.1-3.1-2.8-5.1-3.9-6.3c4.7-9.7,9.4-19.3,14.1-29
26
+				c-4.1-2-8.1-4-12.2-6.1c-4.8,9.8-9.7,19.7-14.5,29.5c-6.7-0.1-12.9,3.5-16.1,9.4c-3.4,6.3-2.7,14.1,1.9,19.8
27
+				C343.2,346.5,335,363.3,326.8,380.1z"/>
28
+		</g>
29
+	</g>
30
+</g>
31
+</svg>

+ 14
- 0
build.go 查看文件

@@ -0,0 +1,14 @@
1
+// Copyright 2020 The Gitea Authors. All rights reserved.
2
+// SPDX-License-Identifier: MIT
3
+
4
+//go:build vendor
5
+
6
+package main
7
+
8
+// Libraries that are included to vendor utilities used during Makefile build.
9
+// These libraries will not be included in a normal compilation.
10
+
11
+import (
12
+	// for vet
13
+	_ "code.gitea.io/gitea-vet"
14
+)

+ 115
- 0
build/backport-locales.go 查看文件

@@ -0,0 +1,115 @@
1
+// Copyright 2023 The Gitea Authors. All rights reserved.
2
+// SPDX-License-Identifier: MIT
3
+
4
+//go:build ignore
5
+
6
+package main
7
+
8
+import (
9
+	"fmt"
10
+	"os"
11
+	"os/exec"
12
+	"path/filepath"
13
+	"strings"
14
+
15
+	"code.gitea.io/gitea/modules/container"
16
+	"code.gitea.io/gitea/modules/setting"
17
+)
18
+
19
+func main() {
20
+	if len(os.Args) != 2 {
21
+		println("usage: backport-locales <to-ref>")
22
+		println("eg: backport-locales release/v1.19")
23
+		os.Exit(1)
24
+	}
25
+
26
+	mustNoErr := func(err error) {
27
+		if err != nil {
28
+			panic(err)
29
+		}
30
+	}
31
+	collectInis := func(ref string) map[string]setting.ConfigProvider {
32
+		inis := map[string]setting.ConfigProvider{}
33
+		err := filepath.WalkDir("options/locale", func(path string, d os.DirEntry, err error) error {
34
+			if err != nil {
35
+				return err
36
+			}
37
+			if d.IsDir() || !strings.HasSuffix(d.Name(), ".ini") {
38
+				return nil
39
+			}
40
+			cfg, err := setting.NewConfigProviderForLocale(path)
41
+			mustNoErr(err)
42
+			inis[path] = cfg
43
+			fmt.Printf("collecting: %s @ %s\n", path, ref)
44
+			return nil
45
+		})
46
+		mustNoErr(err)
47
+		return inis
48
+	}
49
+
50
+	// collect new locales from current working directory
51
+	inisNew := collectInis("HEAD")
52
+
53
+	// switch to the target ref, and collect the old locales
54
+	cmd := exec.Command("git", "checkout", os.Args[1])
55
+	cmd.Stdout = os.Stdout
56
+	cmd.Stderr = os.Stderr
57
+	mustNoErr(cmd.Run())
58
+	inisOld := collectInis(os.Args[1])
59
+
60
+	// use old en-US as the base, and copy the new translations to the old locales
61
+	enUsOld := inisOld["options/locale/locale_en-US.ini"]
62
+	brokenWarned := make(container.Set[string])
63
+	for path, iniOld := range inisOld {
64
+		if iniOld == enUsOld {
65
+			continue
66
+		}
67
+		iniNew := inisNew[path]
68
+		if iniNew == nil {
69
+			continue
70
+		}
71
+		for _, secEnUS := range enUsOld.Sections() {
72
+			secOld := iniOld.Section(secEnUS.Name())
73
+			secNew := iniNew.Section(secEnUS.Name())
74
+			for _, keyEnUs := range secEnUS.Keys() {
75
+				if secNew.HasKey(keyEnUs.Name()) {
76
+					oldStr := secOld.Key(keyEnUs.Name()).String()
77
+					newStr := secNew.Key(keyEnUs.Name()).String()
78
+					broken := oldStr != "" && strings.Count(oldStr, "%") != strings.Count(newStr, "%")
79
+					broken = broken || strings.Contains(oldStr, "\n") || strings.Contains(oldStr, "\n")
80
+					if broken {
81
+						brokenWarned.Add(secOld.Name() + "." + keyEnUs.Name())
82
+						fmt.Println("----")
83
+						fmt.Printf("WARNING: skip broken locale: %s , [%s] %s\n", path, secEnUS.Name(), keyEnUs.Name())
84
+						fmt.Printf("\told: %s\n", strings.ReplaceAll(oldStr, "\n", "\\n"))
85
+						fmt.Printf("\tnew: %s\n", strings.ReplaceAll(newStr, "\n", "\\n"))
86
+						continue
87
+					}
88
+					secOld.Key(keyEnUs.Name()).SetValue(newStr)
89
+				}
90
+			}
91
+		}
92
+		mustNoErr(iniOld.SaveTo(path))
93
+	}
94
+
95
+	fmt.Println("========")
96
+
97
+	for path, iniNew := range inisNew {
98
+		for _, sec := range iniNew.Sections() {
99
+			for _, key := range sec.Keys() {
100
+				str := sec.Key(key.Name()).String()
101
+				broken := strings.Contains(str, "\n")
102
+				broken = broken || strings.HasPrefix(str, "`") != strings.HasSuffix(str, "`")
103
+				broken = broken || strings.HasPrefix(str, "\"`")
104
+				broken = broken || strings.HasPrefix(str, "`\"")
105
+				broken = broken || strings.Count(str, `"`)%2 == 1
106
+				broken = broken || strings.Count(str, "`")%2 == 1
107
+				if broken && !brokenWarned.Contains(sec.Name()+"."+key.Name()) {
108
+					fmt.Printf("WARNING: found broken locale: %s , [%s] %s\n", path, sec.Name(), key.Name())
109
+					fmt.Printf("\tstr: %s\n", strings.ReplaceAll(str, "\n", "\\n"))
110
+					fmt.Println("----")
111
+				}
112
+			}
113
+		}
114
+	}
115
+}

+ 281
- 0
build/code-batch-process.go 查看文件

@@ -0,0 +1,281 @@
1
+// Copyright 2021 The Gitea Authors. All rights reserved.
2
+// SPDX-License-Identifier: MIT
3
+
4
+//go:build ignore
5
+
6
+package main
7
+
8
+import (
9
+	"fmt"
10
+	"log"
11
+	"os"
12
+	"os/exec"
13
+	"path/filepath"
14
+	"regexp"
15
+	"strconv"
16
+	"strings"
17
+
18
+	"code.gitea.io/gitea/build/codeformat"
19
+)
20
+
21
+// Windows has a limitation for command line arguments, the size can not exceed 32KB.
22
+// So we have to feed the files to some tools (like gofmt) batch by batch
23
+
24
+// We also introduce a `gitea-fmt` command, it does better import formatting than gofmt/goimports. `gitea-fmt` calls `gofmt` internally.
25
+
26
+var optionLogVerbose bool
27
+
28
+func logVerbose(msg string, args ...any) {
29
+	if optionLogVerbose {
30
+		log.Printf(msg, args...)
31
+	}
32
+}
33
+
34
+func passThroughCmd(cmd string, args []string) error {
35
+	foundCmd, err := exec.LookPath(cmd)
36
+	if err != nil {
37
+		log.Fatalf("can not find cmd: %s", cmd)
38
+	}
39
+	c := exec.Cmd{
40
+		Path:   foundCmd,
41
+		Args:   append([]string{cmd}, args...),
42
+		Stdin:  os.Stdin,
43
+		Stdout: os.Stdout,
44
+		Stderr: os.Stderr,
45
+	}
46
+	return c.Run()
47
+}
48
+
49
+type fileCollector struct {
50
+	dirs            []string
51
+	includePatterns []*regexp.Regexp
52
+	excludePatterns []*regexp.Regexp
53
+	batchSize       int
54
+}
55
+
56
+func newFileCollector(fileFilter string, batchSize int) (*fileCollector, error) {
57
+	co := &fileCollector{batchSize: batchSize}
58
+	if fileFilter == "go-own" {
59
+		co.dirs = []string{
60
+			"build",
61
+			"cmd",
62
+			"contrib",
63
+			"tests",
64
+			"models",
65
+			"modules",
66
+			"routers",
67
+			"services",
68
+		}
69
+		co.includePatterns = append(co.includePatterns, regexp.MustCompile(`.*\.go$`))
70
+
71
+		co.excludePatterns = append(co.excludePatterns, regexp.MustCompile(`.*\bbindata\.go$`))
72
+		co.excludePatterns = append(co.excludePatterns, regexp.MustCompile(`\.pb\.go$`))
73
+		co.excludePatterns = append(co.excludePatterns, regexp.MustCompile(`tests/gitea-repositories-meta`))
74
+		co.excludePatterns = append(co.excludePatterns, regexp.MustCompile(`tests/integration/migration-test`))
75
+		co.excludePatterns = append(co.excludePatterns, regexp.MustCompile(`modules/git/tests`))
76
+		co.excludePatterns = append(co.excludePatterns, regexp.MustCompile(`models/fixtures`))
77
+		co.excludePatterns = append(co.excludePatterns, regexp.MustCompile(`models/migrations/fixtures`))
78
+		co.excludePatterns = append(co.excludePatterns, regexp.MustCompile(`services/gitdiff/testdata`))
79
+	}
80
+
81
+	if co.dirs == nil {
82
+		return nil, fmt.Errorf("unknown file-filter: %s", fileFilter)
83
+	}
84
+	return co, nil
85
+}
86
+
87
+func (fc *fileCollector) matchPatterns(path string, regexps []*regexp.Regexp) bool {
88
+	path = strings.ReplaceAll(path, "\\", "/")
89
+	for _, re := range regexps {
90
+		if re.MatchString(path) {
91
+			return true
92
+		}
93
+	}
94
+	return false
95
+}
96
+
97
+func (fc *fileCollector) collectFiles() (res [][]string, err error) {
98
+	var batch []string
99
+	for _, dir := range fc.dirs {
100
+		err = filepath.WalkDir(dir, func(path string, d os.DirEntry, err error) error {
101
+			include := len(fc.includePatterns) == 0 || fc.matchPatterns(path, fc.includePatterns)
102
+			exclude := fc.matchPatterns(path, fc.excludePatterns)
103
+			process := include && !exclude
104
+			if !process {
105
+				if d.IsDir() {
106
+					if exclude {
107
+						logVerbose("exclude dir %s", path)
108
+						return filepath.SkipDir
109
+					}
110
+					// for a directory, if it is not excluded explicitly, we should walk into
111
+					return nil
112
+				}
113
+				// for a file, we skip it if it shouldn't be processed
114
+				logVerbose("skip process %s", path)
115
+				return nil
116
+			}
117
+			if d.IsDir() {
118
+				// skip dir, we don't add dirs to the file list now
119
+				return nil
120
+			}
121
+			if len(batch) >= fc.batchSize {
122
+				res = append(res, batch)
123
+				batch = nil
124
+			}
125
+			batch = append(batch, path)
126
+			return nil
127
+		})
128
+		if err != nil {
129
+			return nil, err
130
+		}
131
+	}
132
+	res = append(res, batch)
133
+	return res, nil
134
+}
135
+
136
+// substArgFiles expands the {file-list} to a real file list for commands
137
+func substArgFiles(args, files []string) []string {
138
+	for i, s := range args {
139
+		if s == "{file-list}" {
140
+			newArgs := append(args[:i], files...)
141
+			newArgs = append(newArgs, args[i+1:]...)
142
+			return newArgs
143
+		}
144
+	}
145
+	return args
146
+}
147
+
148
+func exitWithCmdErrors(subCmd string, subArgs []string, cmdErrors []error) {
149
+	for _, err := range cmdErrors {
150
+		if err != nil {
151
+			if exitError, ok := err.(*exec.ExitError); ok {
152
+				exitCode := exitError.ExitCode()
153
+				log.Printf("run command failed (code=%d): %s %v", exitCode, subCmd, subArgs)
154
+				os.Exit(exitCode)
155
+			} else {
156
+				log.Fatalf("run command failed (err=%s) %s %v", err, subCmd, subArgs)
157
+			}
158
+		}
159
+	}
160
+}
161
+
162
+func parseArgs() (mainOptions map[string]string, subCmd string, subArgs []string) {
163
+	mainOptions = map[string]string{}
164
+	for i := 1; i < len(os.Args); i++ {
165
+		arg := os.Args[i]
166
+		if arg == "" {
167
+			break
168
+		}
169
+		if arg[0] == '-' {
170
+			arg = strings.TrimPrefix(arg, "-")
171
+			arg = strings.TrimPrefix(arg, "-")
172
+			fields := strings.SplitN(arg, "=", 2)
173
+			if len(fields) == 1 {
174
+				mainOptions[fields[0]] = "1"
175
+			} else {
176
+				mainOptions[fields[0]] = fields[1]
177
+			}
178
+		} else {
179
+			subCmd = arg
180
+			subArgs = os.Args[i+1:]
181
+			break
182
+		}
183
+	}
184
+	return mainOptions, subCmd, subArgs
185
+}
186
+
187
+func showUsage() {
188
+	fmt.Printf(`Usage: %[1]s [options] {command} [arguments]
189
+
190
+Options:
191
+  --verbose
192
+  --file-filter=go-own
193
+  --batch-size=100
194
+
195
+Commands:
196
+  %[1]s gofmt ...
197
+
198
+Arguments:
199
+  {file-list}     the file list
200
+
201
+Example:
202
+  %[1]s gofmt -s -d {file-list}
203
+
204
+`, "file-batch-exec")
205
+}
206
+
207
+func newFileCollectorFromMainOptions(mainOptions map[string]string) (fc *fileCollector, err error) {
208
+	fileFilter := mainOptions["file-filter"]
209
+	if fileFilter == "" {
210
+		fileFilter = "go-own"
211
+	}
212
+	batchSize, _ := strconv.Atoi(mainOptions["batch-size"])
213
+	if batchSize == 0 {
214
+		batchSize = 100
215
+	}
216
+
217
+	return newFileCollector(fileFilter, batchSize)
218
+}
219
+
220
+func containsString(a []string, s string) bool {
221
+	for _, v := range a {
222
+		if v == s {
223
+			return true
224
+		}
225
+	}
226
+	return false
227
+}
228
+
229
+func giteaFormatGoImports(files []string, doWriteFile bool) error {
230
+	for _, file := range files {
231
+		if err := codeformat.FormatGoImports(file, doWriteFile); err != nil {
232
+			log.Printf("failed to format go imports: %s, err=%v", file, err)
233
+			return err
234
+		}
235
+	}
236
+	return nil
237
+}
238
+
239
+func main() {
240
+	mainOptions, subCmd, subArgs := parseArgs()
241
+	if subCmd == "" {
242
+		showUsage()
243
+		os.Exit(1)
244
+	}
245
+	optionLogVerbose = mainOptions["verbose"] != ""
246
+
247
+	fc, err := newFileCollectorFromMainOptions(mainOptions)
248
+	if err != nil {
249
+		log.Fatalf("can not create file collector: %s", err.Error())
250
+	}
251
+
252
+	fileBatches, err := fc.collectFiles()
253
+	if err != nil {
254
+		log.Fatalf("can not collect files: %s", err.Error())
255
+	}
256
+
257
+	processed := 0
258
+	var cmdErrors []error
259
+	for _, files := range fileBatches {
260
+		if len(files) == 0 {
261
+			break
262
+		}
263
+		substArgs := substArgFiles(subArgs, files)
264
+		logVerbose("batch cmd: %s %v", subCmd, substArgs)
265
+		switch subCmd {
266
+		case "gitea-fmt":
267
+			if containsString(subArgs, "-d") {
268
+				log.Print("the -d option is not supported by gitea-fmt")
269
+			}
270
+			cmdErrors = append(cmdErrors, giteaFormatGoImports(files, containsString(subArgs, "-w")))
271
+			cmdErrors = append(cmdErrors, passThroughCmd("gofmt", append([]string{"-w", "-r", "interface{} -> any"}, substArgs...)))
272
+			cmdErrors = append(cmdErrors, passThroughCmd("go", append([]string{"run", os.Getenv("GOFUMPT_PACKAGE"), "-extra"}, substArgs...)))
273
+		default:
274
+			log.Fatalf("unknown cmd: %s %v", subCmd, subArgs)
275
+		}
276
+		processed += len(files)
277
+	}
278
+
279
+	logVerbose("processed %d files", processed)
280
+	exitWithCmdErrors(subCmd, subArgs, cmdErrors)
281
+}

+ 195
- 0
build/codeformat/formatimports.go 查看文件

@@ -0,0 +1,195 @@
1
+// Copyright 2021 The Gitea Authors. All rights reserved.
2
+// SPDX-License-Identifier: MIT
3
+
4
+package codeformat
5
+
6
+import (
7
+	"bytes"
8
+	"errors"
9
+	"io"
10
+	"os"
11
+	"sort"
12
+	"strings"
13
+)
14
+
15
+var importPackageGroupOrders = map[string]int{
16
+	"":                     1, // internal
17
+	"code.gitea.io/gitea/": 2,
18
+}
19
+
20
+var errInvalidCommentBetweenImports = errors.New("comments between imported packages are invalid, please move comments to the end of the package line")
21
+
22
+var (
23
+	importBlockBegin = []byte("\nimport (\n")
24
+	importBlockEnd   = []byte("\n)")
25
+)
26
+
27
+type importLineParsed struct {
28
+	group   string
29
+	pkg     string
30
+	content string
31
+}
32
+
33
+func parseImportLine(line string) (*importLineParsed, error) {
34
+	il := &importLineParsed{content: line}
35
+	p1 := strings.IndexRune(line, '"')
36
+	if p1 == -1 {
37
+		return nil, errors.New("invalid import line: " + line)
38
+	}
39
+	p1++
40
+	p := strings.IndexRune(line[p1:], '"')
41
+	if p == -1 {
42
+		return nil, errors.New("invalid import line: " + line)
43
+	}
44
+	p2 := p1 + p
45
+	il.pkg = line[p1:p2]
46
+
47
+	pDot := strings.IndexRune(il.pkg, '.')
48
+	pSlash := strings.IndexRune(il.pkg, '/')
49
+	if pDot != -1 && pDot < pSlash {
50
+		il.group = "domain-package"
51
+	}
52
+	for groupName := range importPackageGroupOrders {
53
+		if groupName == "" {
54
+			continue // skip internal
55
+		}
56
+		if strings.HasPrefix(il.pkg, groupName) {
57
+			il.group = groupName
58
+		}
59
+	}
60
+	return il, nil
61
+}
62
+
63
+type (
64
+	importLineGroup    []*importLineParsed
65
+	importLineGroupMap map[string]importLineGroup
66
+)
67
+
68
+func formatGoImports(contentBytes []byte) ([]byte, error) {
69
+	p1 := bytes.Index(contentBytes, importBlockBegin)
70
+	if p1 == -1 {
71
+		return nil, nil
72
+	}
73
+	p1 += len(importBlockBegin)
74
+	p := bytes.Index(contentBytes[p1:], importBlockEnd)
75
+	if p == -1 {
76
+		return nil, nil
77
+	}
78
+	p2 := p1 + p
79
+
80
+	importGroups := importLineGroupMap{}
81
+	r := bytes.NewBuffer(contentBytes[p1:p2])
82
+	eof := false
83
+	for !eof {
84
+		line, err := r.ReadString('\n')
85
+		eof = err == io.EOF
86
+		if err != nil && !eof {
87
+			return nil, err
88
+		}
89
+		line = strings.TrimSpace(line)
90
+		if line != "" {
91
+			if strings.HasPrefix(line, "//") || strings.HasPrefix(line, "/*") {
92
+				return nil, errInvalidCommentBetweenImports
93
+			}
94
+			importLine, err := parseImportLine(line)
95
+			if err != nil {
96
+				return nil, err
97
+			}
98
+			importGroups[importLine.group] = append(importGroups[importLine.group], importLine)
99
+		}
100
+	}
101
+
102
+	var groupNames []string
103
+	for groupName, importLines := range importGroups {
104
+		groupNames = append(groupNames, groupName)
105
+		sort.Slice(importLines, func(i, j int) bool {
106
+			return strings.Compare(importLines[i].pkg, importLines[j].pkg) < 0
107
+		})
108
+	}
109
+
110
+	sort.Slice(groupNames, func(i, j int) bool {
111
+		n1 := groupNames[i]
112
+		n2 := groupNames[j]
113
+		o1 := importPackageGroupOrders[n1]
114
+		o2 := importPackageGroupOrders[n2]
115
+		if o1 != 0 && o2 != 0 {
116
+			return o1 < o2
117
+		}
118
+		if o1 == 0 && o2 == 0 {
119
+			return strings.Compare(n1, n2) < 0
120
+		}
121
+		return o1 != 0
122
+	})
123
+
124
+	formattedBlock := bytes.Buffer{}
125
+	for _, groupName := range groupNames {
126
+		hasNormalImports := false
127
+		hasDummyImports := false
128
+		// non-dummy import comes first
129
+		for _, importLine := range importGroups[groupName] {
130
+			if strings.HasPrefix(importLine.content, "_") {
131
+				hasDummyImports = true
132
+			} else {
133
+				formattedBlock.WriteString("\t" + importLine.content + "\n")
134
+				hasNormalImports = true
135
+			}
136
+		}
137
+		// dummy (_ "pkg") comes later
138
+		if hasDummyImports {
139
+			if hasNormalImports {
140
+				formattedBlock.WriteString("\n")
141
+			}
142
+			for _, importLine := range importGroups[groupName] {
143
+				if strings.HasPrefix(importLine.content, "_") {
144
+					formattedBlock.WriteString("\t" + importLine.content + "\n")
145
+				}
146
+			}
147
+		}
148
+		formattedBlock.WriteString("\n")
149
+	}
150
+	formattedBlockBytes := bytes.TrimRight(formattedBlock.Bytes(), "\n")
151
+
152
+	var formattedBytes []byte
153
+	formattedBytes = append(formattedBytes, contentBytes[:p1]...)
154
+	formattedBytes = append(formattedBytes, formattedBlockBytes...)
155
+	formattedBytes = append(formattedBytes, contentBytes[p2:]...)
156
+	return formattedBytes, nil
157
+}
158
+
159
+// FormatGoImports format the imports by our rules (see unit tests)
160
+func FormatGoImports(file string, doWriteFile bool) error {
161
+	f, err := os.Open(file)
162
+	if err != nil {
163
+		return err
164
+	}
165
+	var contentBytes []byte
166
+	{
167
+		defer f.Close()
168
+		contentBytes, err = io.ReadAll(f)
169
+		if err != nil {
170
+			return err
171
+		}
172
+	}
173
+	formattedBytes, err := formatGoImports(contentBytes)
174
+	if err != nil {
175
+		return err
176
+	}
177
+	if formattedBytes == nil {
178
+		return nil
179
+	}
180
+	if bytes.Equal(contentBytes, formattedBytes) {
181
+		return nil
182
+	}
183
+
184
+	if doWriteFile {
185
+		f, err = os.OpenFile(file, os.O_TRUNC|os.O_WRONLY, 0o644)
186
+		if err != nil {
187
+			return err
188
+		}
189
+		defer f.Close()
190
+		_, err = f.Write(formattedBytes)
191
+		return err
192
+	}
193
+
194
+	return err
195
+}

+ 124
- 0
build/codeformat/formatimports_test.go 查看文件

@@ -0,0 +1,124 @@
1
+// Copyright 2021 The Gitea Authors. All rights reserved.
2
+// SPDX-License-Identifier: MIT
3
+
4
+package codeformat
5
+
6
+import (
7
+	"testing"
8
+
9
+	"github.com/stretchr/testify/assert"
10
+)
11
+
12
+func TestFormatImportsSimple(t *testing.T) {
13
+	formatted, err := formatGoImports([]byte(`
14
+package codeformat
15
+
16
+import (
17
+	"github.com/stretchr/testify/assert"
18
+	"testing"
19
+)
20
+`))
21
+
22
+	expected := `
23
+package codeformat
24
+
25
+import (
26
+	"testing"
27
+
28
+	"github.com/stretchr/testify/assert"
29
+)
30
+`
31
+
32
+	assert.NoError(t, err)
33
+	assert.Equal(t, expected, string(formatted))
34
+}
35
+
36
+func TestFormatImportsGroup(t *testing.T) {
37
+	// gofmt/goimports won't group the packages, for example, they produce such code:
38
+	//     "bytes"
39
+	//     "image"
40
+	//        (a blank line)
41
+	//     "fmt"
42
+	//     "image/color/palette"
43
+	// our formatter does better, and these packages are grouped into one.
44
+
45
+	formatted, err := formatGoImports([]byte(`
46
+package test
47
+
48
+import (
49
+	"bytes"
50
+	"fmt"
51
+	"image"
52
+	"image/color"
53
+
54
+	_ "image/gif"  // for processing gif images
55
+	_ "image/jpeg" // for processing jpeg images
56
+	_ "image/png"  // for processing png images
57
+
58
+	"code.gitea.io/other/package"
59
+
60
+	"code.gitea.io/gitea/modules/setting"
61
+	"code.gitea.io/gitea/modules/util"
62
+
63
+  "xorm.io/the/package"
64
+
65
+	"github.com/issue9/identicon"
66
+	"github.com/nfnt/resize"
67
+	"github.com/oliamb/cutter"
68
+)
69
+`))
70
+
71
+	expected := `
72
+package test
73
+
74
+import (
75
+	"bytes"
76
+	"fmt"
77
+	"image"
78
+	"image/color"
79
+
80
+	_ "image/gif"  // for processing gif images
81
+	_ "image/jpeg" // for processing jpeg images
82
+	_ "image/png"  // for processing png images
83
+
84
+	"code.gitea.io/gitea/modules/setting"
85
+	"code.gitea.io/gitea/modules/util"
86
+
87
+	"code.gitea.io/other/package"
88
+	"github.com/issue9/identicon"
89
+	"github.com/nfnt/resize"
90
+	"github.com/oliamb/cutter"
91
+	"xorm.io/the/package"
92
+)
93
+`
94
+
95
+	assert.NoError(t, err)
96
+	assert.Equal(t, expected, string(formatted))
97
+}
98
+
99
+func TestFormatImportsInvalidComment(t *testing.T) {
100
+	// why we shouldn't write comments between imports: it breaks the grouping of imports
101
+	// for example:
102
+	//    "pkg1"
103
+	//    "pkg2"
104
+	//    // a comment
105
+	//    "pkgA"
106
+	//    "pkgB"
107
+	// the comment splits the packages into two groups, pkg1/2 are sorted separately, pkgA/B are sorted separately
108
+	// we don't want such code, so the code should be:
109
+	//    "pkg1"
110
+	//    "pkg2"
111
+	//    "pkgA" // a comment
112
+	//    "pkgB"
113
+
114
+	_, err := formatGoImports([]byte(`
115
+package test
116
+
117
+import (
118
+  "image/jpeg"
119
+	// for processing gif images
120
+	"image/gif"
121
+)
122
+`))
123
+	assert.ErrorIs(t, err, errInvalidCommentBetweenImports)
124
+}

+ 27
- 0
build/generate-bindata.go 查看文件

@@ -0,0 +1,27 @@
1
+// Copyright 2020 The Gitea Authors. All rights reserved.
2
+// SPDX-License-Identifier: MIT
3
+
4
+//go:build ignore
5
+
6
+package main
7
+
8
+import (
9
+	"fmt"
10
+	"os"
11
+
12
+	"code.gitea.io/gitea/modules/assetfs"
13
+)
14
+
15
+func main() {
16
+	if len(os.Args) != 3 {
17
+		fmt.Println("usage: ./generate-bindata {local-directory} {bindata-filename}")
18
+		os.Exit(1)
19
+	}
20
+
21
+	dir, filename := os.Args[1], os.Args[2]
22
+	fmt.Printf("generating bindata for %s to %s\n", dir, filename)
23
+	if err := assetfs.GenerateEmbedBindata(dir, filename); err != nil {
24
+		fmt.Printf("failed: %s\n", err.Error())
25
+		os.Exit(1)
26
+	}
27
+}

+ 219
- 0
build/generate-emoji.go 查看文件

@@ -0,0 +1,219 @@
1
+// Copyright 2020 The Gitea Authors. All rights reserved.
2
+// Copyright 2015 Kenneth Shaw
3
+// SPDX-License-Identifier: MIT
4
+
5
+//go:build ignore
6
+
7
+package main
8
+
9
+import (
10
+	"flag"
11
+	"fmt"
12
+	"go/format"
13
+	"io"
14
+	"log"
15
+	"net/http"
16
+	"os"
17
+	"regexp"
18
+	"sort"
19
+	"strconv"
20
+	"strings"
21
+	"unicode/utf8"
22
+
23
+	"code.gitea.io/gitea/modules/json"
24
+)
25
+
26
+const (
27
+	gemojiURL         = "https://raw.githubusercontent.com/github/gemoji/master/db/emoji.json"
28
+	maxUnicodeVersion = 15
29
+)
30
+
31
+var flagOut = flag.String("o", "modules/emoji/emoji_data.go", "out")
32
+
33
+// Gemoji is a set of emoji data.
34
+type Gemoji []Emoji
35
+
36
+// Emoji represents a single emoji and associated data.
37
+type Emoji struct {
38
+	Emoji          string   `json:"emoji"`
39
+	Description    string   `json:"description,omitempty"`
40
+	Aliases        []string `json:"aliases"`
41
+	UnicodeVersion string   `json:"unicode_version,omitempty"`
42
+	SkinTones      bool     `json:"skin_tones,omitempty"`
43
+}
44
+
45
+// Don't include some fields in JSON
46
+func (e Emoji) MarshalJSON() ([]byte, error) {
47
+	type emoji Emoji
48
+	x := emoji(e)
49
+	x.UnicodeVersion = ""
50
+	x.Description = ""
51
+	x.SkinTones = false
52
+	return json.Marshal(x)
53
+}
54
+
55
+func main() {
56
+	flag.Parse()
57
+
58
+	// generate data
59
+	buf, err := generate()
60
+	if err != nil {
61
+		log.Fatalf("generate err: %v", err)
62
+	}
63
+
64
+	// write
65
+	err = os.WriteFile(*flagOut, buf, 0o644)
66
+	if err != nil {
67
+		log.Fatalf("WriteFile err: %v", err)
68
+	}
69
+}
70
+
71
+var replacer = strings.NewReplacer(
72
+	"main.Gemoji", "Gemoji",
73
+	"main.Emoji", "\n",
74
+	"}}", "},\n}",
75
+	", Description:", ", ",
76
+	", Aliases:", ", ",
77
+	", UnicodeVersion:", ", ",
78
+	", SkinTones:", ", ",
79
+)
80
+
81
+var emojiRE = regexp.MustCompile(`\{Emoji:"([^"]*)"`)
82
+
83
+func generate() ([]byte, error) {
84
+	// load gemoji data
85
+	res, err := http.Get(gemojiURL)
86
+	if err != nil {
87
+		return nil, err
88
+	}
89
+	defer res.Body.Close()
90
+
91
+	// read all
92
+	body, err := io.ReadAll(res.Body)
93
+	if err != nil {
94
+		return nil, err
95
+	}
96
+
97
+	// unmarshal
98
+	var data Gemoji
99
+	err = json.Unmarshal(body, &data)
100
+	if err != nil {
101
+		return nil, err
102
+	}
103
+
104
+	skinTones := make(map[string]string)
105
+
106
+	skinTones["\U0001f3fb"] = "Light Skin Tone"
107
+	skinTones["\U0001f3fc"] = "Medium-Light Skin Tone"
108
+	skinTones["\U0001f3fd"] = "Medium Skin Tone"
109
+	skinTones["\U0001f3fe"] = "Medium-Dark Skin Tone"
110
+	skinTones["\U0001f3ff"] = "Dark Skin Tone"
111
+
112
+	var tmp Gemoji
113
+
114
+	// filter out emoji that require greater than max unicode version
115
+	for i := range data {
116
+		val, _ := strconv.ParseFloat(data[i].UnicodeVersion, 64)
117
+		if int(val) <= maxUnicodeVersion {
118
+			tmp = append(tmp, data[i])
119
+		}
120
+	}
121
+	data = tmp
122
+
123
+	sort.Slice(data, func(i, j int) bool {
124
+		return data[i].Aliases[0] < data[j].Aliases[0]
125
+	})
126
+
127
+	aliasMap := make(map[string]int, len(data))
128
+
129
+	for i, e := range data {
130
+		if e.Emoji == "" || len(e.Aliases) == 0 {
131
+			continue
132
+		}
133
+		for _, a := range e.Aliases {
134
+			if a == "" {
135
+				continue
136
+			}
137
+			aliasMap[a] = i
138
+		}
139
+	}
140
+
141
+	// gitea customizations
142
+	i, ok := aliasMap["tada"]
143
+	if ok {
144
+		data[i].Aliases = append(data[i].Aliases, "hooray")
145
+	}
146
+	i, ok = aliasMap["laughing"]
147
+	if ok {
148
+		data[i].Aliases = append(data[i].Aliases, "laugh")
149
+	}
150
+
151
+	// write a JSON file to use with tribute (write before adding skin tones since we can't support them there yet)
152
+	file, _ := json.Marshal(data)
153
+	_ = os.WriteFile("assets/emoji.json", file, 0o644)
154
+
155
+	// Add skin tones to emoji that support it
156
+	var (
157
+		s              []string
158
+		newEmoji       string
159
+		newDescription string
160
+		newData        Emoji
161
+	)
162
+
163
+	for i := range data {
164
+		if data[i].SkinTones {
165
+			for k, v := range skinTones {
166
+				s = strings.Split(data[i].Emoji, "")
167
+
168
+				if utf8.RuneCountInString(data[i].Emoji) == 1 {
169
+					s = append(s, k)
170
+				} else {
171
+					// insert into slice after first element because all emoji that support skin tones
172
+					// have that modifier placed at this spot
173
+					s = append(s, "")
174
+					copy(s[2:], s[1:])
175
+					s[1] = k
176
+				}
177
+
178
+				newEmoji = strings.Join(s, "")
179
+				newDescription = data[i].Description + ": " + v
180
+				newAlias := data[i].Aliases[0] + "_" + strings.ReplaceAll(v, " ", "_")
181
+
182
+				newData = Emoji{newEmoji, newDescription, []string{newAlias}, "12.0", false}
183
+				data = append(data, newData)
184
+			}
185
+		}
186
+	}
187
+
188
+	sort.Slice(data, func(i, j int) bool {
189
+		return data[i].Aliases[0] < data[j].Aliases[0]
190
+	})
191
+
192
+	// add header
193
+	str := replacer.Replace(fmt.Sprintf(hdr, gemojiURL, data))
194
+
195
+	// change the format of the unicode string
196
+	str = emojiRE.ReplaceAllStringFunc(str, func(s string) string {
197
+		var err error
198
+		s, err = strconv.Unquote(s[len("{Emoji:"):])
199
+		if err != nil {
200
+			panic(err)
201
+		}
202
+		return "{" + strconv.QuoteToASCII(s)
203
+	})
204
+
205
+	// format
206
+	return format.Source([]byte(str))
207
+}
208
+
209
+const hdr = `
210
+// Copyright 2020 The Gitea Authors. All rights reserved.
211
+// SPDX-License-Identifier: MIT
212
+
213
+
214
+package emoji
215
+
216
+// Code generated by build/generate-emoji.go. DO NOT EDIT.
217
+// Sourced from %s
218
+var GemojiData = %#v
219
+`

+ 126
- 0
build/generate-gitignores.go 查看文件

@@ -0,0 +1,126 @@
1
+//go:build ignore
2
+
3
+package main
4
+
5
+import (
6
+	"archive/tar"
7
+	"compress/gzip"
8
+	"flag"
9
+	"fmt"
10
+	"io"
11
+	"log"
12
+	"net/http"
13
+	"os"
14
+	"path"
15
+	"path/filepath"
16
+	"strings"
17
+
18
+	"code.gitea.io/gitea/modules/util"
19
+)
20
+
21
+func main() {
22
+	var (
23
+		prefix         = "gitea-gitignore"
24
+		url            = "https://api.github.com/repos/github/gitignore/tarball"
25
+		githubApiToken = ""
26
+		githubUsername = ""
27
+		destination    = ""
28
+	)
29
+
30
+	flag.StringVar(&destination, "dest", "options/gitignore/", "destination for the gitignores")
31
+	flag.StringVar(&githubUsername, "username", "", "github username")
32
+	flag.StringVar(&githubApiToken, "token", "", "github api token")
33
+	flag.Parse()
34
+
35
+	file, err := os.CreateTemp(os.TempDir(), prefix)
36
+	if err != nil {
37
+		log.Fatalf("Failed to create temp file. %s", err)
38
+	}
39
+
40
+	defer util.Remove(file.Name())
41
+
42
+	req, err := http.NewRequest("GET", url, nil)
43
+	if err != nil {
44
+		log.Fatalf("Failed to download archive. %s", err)
45
+	}
46
+
47
+	if len(githubApiToken) > 0 && len(githubUsername) > 0 {
48
+		req.SetBasicAuth(githubUsername, githubApiToken)
49
+	}
50
+
51
+	resp, err := http.DefaultClient.Do(req)
52
+	if err != nil {
53
+		log.Fatalf("Failed to download archive. %s", err)
54
+	}
55
+	defer resp.Body.Close()
56
+
57
+	if _, err := io.Copy(file, resp.Body); err != nil {
58
+		log.Fatalf("Failed to copy archive to file. %s", err)
59
+	}
60
+
61
+	if _, err := file.Seek(0, 0); err != nil {
62
+		log.Fatalf("Failed to reset seek on archive. %s", err)
63
+	}
64
+
65
+	gz, err := gzip.NewReader(file)
66
+	if err != nil {
67
+		log.Fatalf("Failed to gunzip the archive. %s", err)
68
+	}
69
+
70
+	tr := tar.NewReader(gz)
71
+
72
+	filesToCopy := make(map[string]string, 0)
73
+
74
+	for {
75
+		hdr, err := tr.Next()
76
+
77
+		if err == io.EOF {
78
+			break
79
+		}
80
+
81
+		if err != nil {
82
+			log.Fatalf("Failed to iterate archive. %s", err)
83
+		}
84
+
85
+		if filepath.Ext(hdr.Name) != ".gitignore" {
86
+			continue
87
+		}
88
+
89
+		if hdr.Typeflag == tar.TypeSymlink {
90
+			fmt.Printf("Found symlink %s -> %s\n", hdr.Name, hdr.Linkname)
91
+			filesToCopy[strings.TrimSuffix(filepath.Base(hdr.Name), ".gitignore")] = strings.TrimSuffix(filepath.Base(hdr.Linkname), ".gitignore")
92
+			continue
93
+		}
94
+
95
+		out, err := os.Create(path.Join(destination, strings.TrimSuffix(filepath.Base(hdr.Name), ".gitignore")))
96
+		if err != nil {
97
+			log.Fatalf("Failed to create new file. %s", err)
98
+		}
99
+
100
+		defer out.Close()
101
+
102
+		if _, err := io.Copy(out, tr); err != nil {
103
+			log.Fatalf("Failed to write new file. %s", err)
104
+		} else {
105
+			fmt.Printf("Written %s\n", out.Name())
106
+		}
107
+	}
108
+
109
+	for dst, src := range filesToCopy {
110
+		// Read all content of src to data
111
+		src = path.Join(destination, src)
112
+		data, err := os.ReadFile(src)
113
+		if err != nil {
114
+			log.Fatalf("Failed to read src file. %s", err)
115
+		}
116
+		// Write data to dst
117
+		dst = path.Join(destination, dst)
118
+		err = os.WriteFile(dst, data, 0o644)
119
+		if err != nil {
120
+			log.Fatalf("Failed to write new file. %s", err)
121
+		}
122
+		fmt.Printf("Written (copy of %s) %s\n", src, dst)
123
+	}
124
+
125
+	fmt.Println("Done")
126
+}

+ 118
- 0
build/generate-go-licenses.go 查看文件

@@ -0,0 +1,118 @@
1
+// Copyright 2022 The Gitea Authors. All rights reserved.
2
+// SPDX-License-Identifier: MIT
3
+
4
+//go:build ignore
5
+
6
+package main
7
+
8
+import (
9
+	"encoding/json"
10
+	"fmt"
11
+	"io/fs"
12
+	"os"
13
+	"path"
14
+	"path/filepath"
15
+	"regexp"
16
+	"sort"
17
+	"strings"
18
+
19
+	"code.gitea.io/gitea/modules/container"
20
+)
21
+
22
+// regexp is based on go-license, excluding README and NOTICE
23
+// https://github.com/google/go-licenses/blob/master/licenses/find.go
24
+var licenseRe = regexp.MustCompile(`^(?i)((UN)?LICEN(S|C)E|COPYING).*$`)
25
+
26
+type LicenseEntry struct {
27
+	Name        string `json:"name"`
28
+	Path        string `json:"path"`
29
+	LicenseText string `json:"licenseText"`
30
+}
31
+
32
+func main() {
33
+	if len(os.Args) != 3 {
34
+		fmt.Println("usage: go run generate-go-licenses.go <base-dir> <out-json-file>")
35
+		os.Exit(1)
36
+	}
37
+
38
+	base, out := os.Args[1], os.Args[2]
39
+
40
+	// Add ext for excluded files because license_test.go will be included for some reason.
41
+	// And there are more files that should be excluded, check with:
42
+	//
43
+	// go run github.com/google/go-licenses@v1.6.0 save . --force --save_path=.go-licenses 2>/dev/null
44
+	// find .go-licenses -type f | while read FILE; do echo "${$(basename $FILE)##*.}"; done | sort -u
45
+	//    AUTHORS
46
+	//    COPYING
47
+	//    LICENSE
48
+	//    Makefile
49
+	//    NOTICE
50
+	//    gitignore
51
+	//    go
52
+	//    md
53
+	//    mod
54
+	//    sum
55
+	//    toml
56
+	//    txt
57
+	//    yml
58
+	//
59
+	// It could be removed once we have a better regex.
60
+	excludedExt := container.SetOf(".gitignore", ".go", ".mod", ".sum", ".toml", ".yml")
61
+
62
+	var paths []string
63
+	err := filepath.WalkDir(base, func(path string, entry fs.DirEntry, err error) error {
64
+		if err != nil {
65
+			return err
66
+		}
67
+		if entry.IsDir() || !licenseRe.MatchString(entry.Name()) || excludedExt.Contains(filepath.Ext(entry.Name())) {
68
+			return nil
69
+		}
70
+		paths = append(paths, path)
71
+		return nil
72
+	})
73
+	if err != nil {
74
+		panic(err)
75
+	}
76
+
77
+	sort.Strings(paths)
78
+
79
+	var entries []LicenseEntry
80
+	for _, filePath := range paths {
81
+		licenseText, err := os.ReadFile(filePath)
82
+		if err != nil {
83
+			panic(err)
84
+		}
85
+
86
+		pkgPath := filepath.ToSlash(filePath)
87
+		pkgPath = strings.TrimPrefix(pkgPath, base+"/")
88
+		pkgName := path.Dir(pkgPath)
89
+
90
+		// There might be a bug somewhere in go-licenses that sometimes interprets the
91
+		// root package as "." and sometimes as "code.gitea.io/gitea". Workaround by
92
+		// removing both of them for the sake of stable output.
93
+		if pkgName == "." || pkgName == "code.gitea.io/gitea" {
94
+			continue
95
+		}
96
+
97
+		entries = append(entries, LicenseEntry{
98
+			Name:        pkgName,
99
+			Path:        pkgPath,
100
+			LicenseText: string(licenseText),
101
+		})
102
+	}
103
+
104
+	jsonBytes, err := json.MarshalIndent(entries, "", "  ")
105
+	if err != nil {
106
+		panic(err)
107
+	}
108
+
109
+	// Ensure file has a final newline
110
+	if jsonBytes[len(jsonBytes)-1] != '\n' {
111
+		jsonBytes = append(jsonBytes, '\n')
112
+	}
113
+
114
+	err = os.WriteFile(out, jsonBytes, 0o644)
115
+	if err != nil {
116
+		panic(err)
117
+	}
118
+}

+ 118
- 0
build/gocovmerge.go 查看文件

@@ -0,0 +1,118 @@
1
+// Copyright 2020 The Gitea Authors. All rights reserved.
2
+// Copyright (c) 2015, Wade Simmons
3
+// SPDX-License-Identifier: MIT
4
+
5
+// gocovmerge takes the results from multiple `go test -coverprofile` runs and
6
+// merges them into one profile
7
+
8
+//go:build ignore
9
+
10
+package main
11
+
12
+import (
13
+	"flag"
14
+	"fmt"
15
+	"io"
16
+	"log"
17
+	"os"
18
+	"sort"
19
+
20
+	"golang.org/x/tools/cover"
21
+)
22
+
23
+func mergeProfiles(p, merge *cover.Profile) {
24
+	if p.Mode != merge.Mode {
25
+		log.Fatalf("cannot merge profiles with different modes")
26
+	}
27
+	// Since the blocks are sorted, we can keep track of where the last block
28
+	// was inserted and only look at the blocks after that as targets for merge
29
+	startIndex := 0
30
+	for _, b := range merge.Blocks {
31
+		startIndex = mergeProfileBlock(p, b, startIndex)
32
+	}
33
+}
34
+
35
+func mergeProfileBlock(p *cover.Profile, pb cover.ProfileBlock, startIndex int) int {
36
+	sortFunc := func(i int) bool {
37
+		pi := p.Blocks[i+startIndex]
38
+		return pi.StartLine >= pb.StartLine && (pi.StartLine != pb.StartLine || pi.StartCol >= pb.StartCol)
39
+	}
40
+
41
+	i := 0
42
+	if sortFunc(i) != true {
43
+		i = sort.Search(len(p.Blocks)-startIndex, sortFunc)
44
+	}
45
+	i += startIndex
46
+	if i < len(p.Blocks) && p.Blocks[i].StartLine == pb.StartLine && p.Blocks[i].StartCol == pb.StartCol {
47
+		if p.Blocks[i].EndLine != pb.EndLine || p.Blocks[i].EndCol != pb.EndCol {
48
+			log.Fatalf("OVERLAP MERGE: %v %v %v", p.FileName, p.Blocks[i], pb)
49
+		}
50
+		switch p.Mode {
51
+		case "set":
52
+			p.Blocks[i].Count |= pb.Count
53
+		case "count", "atomic":
54
+			p.Blocks[i].Count += pb.Count
55
+		default:
56
+			log.Fatalf("unsupported covermode: '%s'", p.Mode)
57
+		}
58
+	} else {
59
+		if i > 0 {
60
+			pa := p.Blocks[i-1]
61
+			if pa.EndLine >= pb.EndLine && (pa.EndLine != pb.EndLine || pa.EndCol > pb.EndCol) {
62
+				log.Fatalf("OVERLAP BEFORE: %v %v %v", p.FileName, pa, pb)
63
+			}
64
+		}
65
+		if i < len(p.Blocks)-1 {
66
+			pa := p.Blocks[i+1]
67
+			if pa.StartLine <= pb.StartLine && (pa.StartLine != pb.StartLine || pa.StartCol < pb.StartCol) {
68
+				log.Fatalf("OVERLAP AFTER: %v %v %v", p.FileName, pa, pb)
69
+			}
70
+		}
71
+		p.Blocks = append(p.Blocks, cover.ProfileBlock{})
72
+		copy(p.Blocks[i+1:], p.Blocks[i:])
73
+		p.Blocks[i] = pb
74
+	}
75
+	return i + 1
76
+}
77
+
78
+func addProfile(profiles []*cover.Profile, p *cover.Profile) []*cover.Profile {
79
+	i := sort.Search(len(profiles), func(i int) bool { return profiles[i].FileName >= p.FileName })
80
+	if i < len(profiles) && profiles[i].FileName == p.FileName {
81
+		mergeProfiles(profiles[i], p)
82
+	} else {
83
+		profiles = append(profiles, nil)
84
+		copy(profiles[i+1:], profiles[i:])
85
+		profiles[i] = p
86
+	}
87
+	return profiles
88
+}
89
+
90
+func dumpProfiles(profiles []*cover.Profile, out io.Writer) {
91
+	if len(profiles) == 0 {
92
+		return
93
+	}
94
+	fmt.Fprintf(out, "mode: %s\n", profiles[0].Mode)
95
+	for _, p := range profiles {
96
+		for _, b := range p.Blocks {
97
+			fmt.Fprintf(out, "%s:%d.%d,%d.%d %d %d\n", p.FileName, b.StartLine, b.StartCol, b.EndLine, b.EndCol, b.NumStmt, b.Count)
98
+		}
99
+	}
100
+}
101
+
102
+func main() {
103
+	flag.Parse()
104
+
105
+	var merged []*cover.Profile
106
+
107
+	for _, file := range flag.Args() {
108
+		profiles, err := cover.ParseProfiles(file)
109
+		if err != nil {
110
+			log.Fatalf("failed to parse profile '%s': %v", file, err)
111
+		}
112
+		for _, p := range profiles {
113
+			merged = addProfile(merged, p)
114
+		}
115
+	}
116
+
117
+	dumpProfiles(merged, os.Stdout)
118
+}

+ 20
- 0
build/test-echo.go 查看文件

@@ -0,0 +1,20 @@
1
+// Copyright 2023 The Gitea Authors. All rights reserved.
2
+// SPDX-License-Identifier: MIT
3
+
4
+//go:build ignore
5
+
6
+package main
7
+
8
+import (
9
+	"fmt"
10
+	"io"
11
+	"os"
12
+)
13
+
14
+func main() {
15
+	_, err := io.Copy(os.Stdout, os.Stdin)
16
+	if err != nil {
17
+		fmt.Fprintf(os.Stderr, "Error: %v", err)
18
+		os.Exit(1)
19
+	}
20
+}

+ 24
- 0
build/test-env-check.sh 查看文件

@@ -0,0 +1,24 @@
1
+#!/bin/sh
2
+
3
+set -e
4
+
5
+if [ ! -f ./build/test-env-check.sh ]; then
6
+  echo "${0} can only be executed in gitea source root directory"
7
+  exit 1
8
+fi
9
+
10
+
11
+echo "check uid ..."
12
+
13
+# the uid of gitea defined in "https://gitea.com/gitea/test-env" is 1000
14
+gitea_uid=$(id -u gitea)
15
+if [ "$gitea_uid" != "1000" ]; then
16
+  echo "The uid of linux user 'gitea' is expected to be 1000, but it is $gitea_uid"
17
+  exit 1
18
+fi
19
+
20
+cur_uid=$(id -u)
21
+if [ "$cur_uid" != "0" -a "$cur_uid" != "$gitea_uid" ]; then
22
+  echo "The uid of current linux user is expected to be 0 or $gitea_uid, but it is $cur_uid"
23
+  exit 1
24
+fi

+ 11
- 0
build/test-env-prepare.sh 查看文件

@@ -0,0 +1,11 @@
1
+#!/bin/sh
2
+
3
+set -e
4
+
5
+if [ ! -f ./build/test-env-prepare.sh ]; then
6
+  echo "${0} can only be executed in gitea source root directory"
7
+  exit 1
8
+fi
9
+
10
+echo "change the owner of files to gitea ..."
11
+chown -R gitea:gitea .

+ 52
- 0
build/update-locales.sh 查看文件

@@ -0,0 +1,52 @@
1
+#!/bin/sh
2
+
3
+# this script runs in alpine image which only has `sh` shell
4
+
5
+set +e
6
+if sed --version 2>/dev/null | grep -q GNU; then
7
+  SED_INPLACE="sed -i"
8
+else
9
+  SED_INPLACE="sed -i ''"
10
+fi
11
+set -e
12
+
13
+if [ ! -f ./options/locale/locale_en-US.ini ]; then
14
+  echo "please run this script in the root directory of the project"
15
+  exit 1
16
+fi
17
+
18
+mv ./options/locale/locale_en-US.ini ./options/
19
+
20
+# the "ini" library for locale has many quirks, its behavior is different from Crowdin.
21
+# see i18n_test.go for more details
22
+
23
+# this script helps to unquote the Crowdin outputs for the quirky ini library
24
+# * find all `key="...\"..."` lines
25
+# * remove the leading quote
26
+# * remove the trailing quote
27
+# * unescape the quotes
28
+# * eg: key="...\"..." => key=..."...
29
+$SED_INPLACE -r -e '/^[-.A-Za-z0-9_]+[ ]*=[ ]*".*"$/ {
30
+	s/^([-.A-Za-z0-9_]+)[ ]*=[ ]*"/\1=/
31
+	s/"$//
32
+	s/\\"/"/g
33
+	}' ./options/locale/*.ini
34
+
35
+# * if the escaped line is incomplete like `key="...` or `key=..."`, quote it with backticks
36
+# * eg: key="... => key=`"...`
37
+# * eg: key=..." => key=`..."`
38
+$SED_INPLACE -r -e 's/^([-.A-Za-z0-9_]+)[ ]*=[ ]*(".*[^"])$/\1=`\2`/' ./options/locale/*.ini
39
+$SED_INPLACE -r -e 's/^([-.A-Za-z0-9_]+)[ ]*=[ ]*([^"].*")$/\1=`\2`/' ./options/locale/*.ini
40
+
41
+# Remove translation under 25% of en_us
42
+baselines=$(wc -l "./options/locale_en-US.ini" | cut -d" " -f1)
43
+baselines=$((baselines / 4))
44
+for filename in ./options/locale/*.ini; do
45
+  lines=$(wc -l "$filename" | cut -d" " -f1)
46
+  if [ $lines -lt $baselines ]; then
47
+    echo "Removing $filename: $lines/$baselines"
48
+    rm "$filename"
49
+  fi
50
+done
51
+
52
+mv ./options/locale_en-US.ini ./options/locale/

+ 53
- 0
cmd/actions.go 查看文件

@@ -0,0 +1,53 @@
1
+// Copyright 2023 The Gitea Authors. All rights reserved.
2
+// SPDX-License-Identifier: MIT
3
+
4
+package cmd
5
+
6
+import (
7
+	"context"
8
+	"fmt"
9
+
10
+	"code.gitea.io/gitea/modules/private"
11
+	"code.gitea.io/gitea/modules/setting"
12
+
13
+	"github.com/urfave/cli/v3"
14
+)
15
+
16
+var (
17
+	// CmdActions represents the available actions sub-commands.
18
+	CmdActions = &cli.Command{
19
+		Name:  "actions",
20
+		Usage: "Manage Gitea Actions",
21
+		Commands: []*cli.Command{
22
+			subcmdActionsGenRunnerToken,
23
+		},
24
+	}
25
+
26
+	subcmdActionsGenRunnerToken = &cli.Command{
27
+		Name:    "generate-runner-token",
28
+		Usage:   "Generate a new token for a runner to use to register with the server",
29
+		Action:  runGenerateActionsRunnerToken,
30
+		Aliases: []string{"grt"},
31
+		Flags: []cli.Flag{
32
+			&cli.StringFlag{
33
+				Name:    "scope",
34
+				Aliases: []string{"s"},
35
+				Value:   "",
36
+				Usage:   "{owner}[/{repo}] - leave empty for a global runner",
37
+			},
38
+		},
39
+	}
40
+)
41
+
42
+func runGenerateActionsRunnerToken(ctx context.Context, c *cli.Command) error {
43
+	setting.MustInstalled()
44
+
45
+	scope := c.String("scope")
46
+
47
+	respText, extra := private.GenerateActionsRunnerToken(ctx, scope)
48
+	if extra.HasError() {
49
+		return handleCliResponseExtra(extra)
50
+	}
51
+	_, _ = fmt.Printf("%s\n", respText.Text)
52
+	return nil
53
+}

+ 167
- 0
cmd/admin.go 查看文件

@@ -0,0 +1,167 @@
1
+// Copyright 2016 The Gogs Authors. All rights reserved.
2
+// Copyright 2016 The Gitea Authors. All rights reserved.
3
+// SPDX-License-Identifier: MIT
4
+
5
+package cmd
6
+
7
+import (
8
+	"context"
9
+	"fmt"
10
+
11
+	"code.gitea.io/gitea/models/db"
12
+	repo_model "code.gitea.io/gitea/models/repo"
13
+	"code.gitea.io/gitea/modules/git"
14
+	"code.gitea.io/gitea/modules/gitrepo"
15
+	"code.gitea.io/gitea/modules/log"
16
+	repo_module "code.gitea.io/gitea/modules/repository"
17
+
18
+	"github.com/urfave/cli/v3"
19
+)
20
+
21
+var (
22
+	// CmdAdmin represents the available admin sub-command.
23
+	CmdAdmin = &cli.Command{
24
+		Name:  "admin",
25
+		Usage: "Perform common administrative operations",
26
+		Commands: []*cli.Command{
27
+			subcmdUser,
28
+			subcmdRepoSyncReleases,
29
+			subcmdRegenerate,
30
+			subcmdAuth,
31
+			subcmdSendMail,
32
+		},
33
+	}
34
+
35
+	subcmdRepoSyncReleases = &cli.Command{
36
+		Name:   "repo-sync-releases",
37
+		Usage:  "Synchronize repository releases with tags",
38
+		Action: runRepoSyncReleases,
39
+	}
40
+
41
+	subcmdRegenerate = &cli.Command{
42
+		Name:  "regenerate",
43
+		Usage: "Regenerate specific files",
44
+		Commands: []*cli.Command{
45
+			microcmdRegenHooks,
46
+			microcmdRegenKeys,
47
+		},
48
+	}
49
+
50
+	subcmdAuth = &cli.Command{
51
+		Name:  "auth",
52
+		Usage: "Modify external auth providers",
53
+		Commands: []*cli.Command{
54
+			microcmdAuthAddOauth(),
55
+			microcmdAuthUpdateOauth(),
56
+			microcmdAuthAddLdapBindDn(),
57
+			microcmdAuthUpdateLdapBindDn(),
58
+			microcmdAuthAddLdapSimpleAuth(),
59
+			microcmdAuthUpdateLdapSimpleAuth(),
60
+			microcmdAuthAddSMTP(),
61
+			microcmdAuthUpdateSMTP(),
62
+			microcmdAuthList,
63
+			microcmdAuthDelete,
64
+		},
65
+	}
66
+
67
+	subcmdSendMail = &cli.Command{
68
+		Name:   "sendmail",
69
+		Usage:  "Send a message to all users",
70
+		Action: runSendMail,
71
+		Flags: []cli.Flag{
72
+			&cli.StringFlag{
73
+				Name:     "title",
74
+				Usage:    "a title of a message",
75
+				Required: true,
76
+			},
77
+			&cli.StringFlag{
78
+				Name:  "content",
79
+				Usage: "a content of a message",
80
+				Value: "",
81
+			},
82
+			&cli.BoolFlag{
83
+				Name:    "force",
84
+				Aliases: []string{"f"},
85
+				Usage:   "A flag to bypass a confirmation step",
86
+			},
87
+		},
88
+	}
89
+)
90
+
91
+func idFlag() *cli.Int64Flag {
92
+	return &cli.Int64Flag{
93
+		Name:  "id",
94
+		Usage: "ID of authentication source",
95
+	}
96
+}
97
+
98
+func runRepoSyncReleases(ctx context.Context, _ *cli.Command) error {
99
+	if err := initDB(ctx); err != nil {
100
+		return err
101
+	}
102
+
103
+	if err := git.InitSimple(); err != nil {
104
+		return err
105
+	}
106
+
107
+	log.Trace("Synchronizing repository releases (this may take a while)")
108
+	for page := 1; ; page++ {
109
+		repos, count, err := repo_model.SearchRepositoryByName(ctx, repo_model.SearchRepoOptions{
110
+			ListOptions: db.ListOptions{
111
+				PageSize: repo_model.RepositoryListDefaultPageSize,
112
+				Page:     page,
113
+			},
114
+			Private: true,
115
+		})
116
+		if err != nil {
117
+			return fmt.Errorf("SearchRepositoryByName: %w", err)
118
+		}
119
+		if len(repos) == 0 {
120
+			break
121
+		}
122
+		log.Trace("Processing next %d repos of %d", len(repos), count)
123
+		for _, repo := range repos {
124
+			log.Trace("Synchronizing repo %s with path %s", repo.FullName(), repo.RepoPath())
125
+			gitRepo, err := gitrepo.OpenRepository(ctx, repo)
126
+			if err != nil {
127
+				log.Warn("OpenRepository: %v", err)
128
+				continue
129
+			}
130
+
131
+			oldnum, err := getReleaseCount(ctx, repo.ID)
132
+			if err != nil {
133
+				log.Warn(" GetReleaseCountByRepoID: %v", err)
134
+			}
135
+			log.Trace(" currentNumReleases is %d, running SyncReleasesWithTags", oldnum)
136
+
137
+			if err = repo_module.SyncReleasesWithTags(ctx, repo, gitRepo); err != nil {
138
+				log.Warn(" SyncReleasesWithTags: %v", err)
139
+				gitRepo.Close()
140
+				continue
141
+			}
142
+
143
+			count, err = getReleaseCount(ctx, repo.ID)
144
+			if err != nil {
145
+				log.Warn(" GetReleaseCountByRepoID: %v", err)
146
+				gitRepo.Close()
147
+				continue
148
+			}
149
+
150
+			log.Trace(" repo %s releases synchronized to tags: from %d to %d",
151
+				repo.FullName(), oldnum, count)
152
+			gitRepo.Close()
153
+		}
154
+	}
155
+
156
+	return nil
157
+}
158
+
159
+func getReleaseCount(ctx context.Context, id int64) (int64, error) {
160
+	return db.Count[repo_model.Release](
161
+		ctx,
162
+		repo_model.FindReleasesOptions{
163
+			RepoID:      id,
164
+			IncludeTags: true,
165
+		},
166
+	)
167
+}

+ 106
- 0
cmd/admin_auth.go 查看文件

@@ -0,0 +1,106 @@
1
+// Copyright 2023 The Gitea Authors. All rights reserved.
2
+// SPDX-License-Identifier: MIT
3
+
4
+package cmd
5
+
6
+import (
7
+	"context"
8
+	"errors"
9
+	"fmt"
10
+	"os"
11
+	"text/tabwriter"
12
+
13
+	auth_model "code.gitea.io/gitea/models/auth"
14
+	"code.gitea.io/gitea/models/db"
15
+	auth_service "code.gitea.io/gitea/services/auth"
16
+
17
+	"github.com/urfave/cli/v3"
18
+)
19
+
20
+var (
21
+	microcmdAuthDelete = &cli.Command{
22
+		Name:   "delete",
23
+		Usage:  "Delete specific auth source",
24
+		Flags:  []cli.Flag{idFlag()},
25
+		Action: runDeleteAuth,
26
+	}
27
+	microcmdAuthList = &cli.Command{
28
+		Name:   "list",
29
+		Usage:  "List auth sources",
30
+		Action: runListAuth,
31
+		Flags: []cli.Flag{
32
+			&cli.IntFlag{
33
+				Name:  "min-width",
34
+				Usage: "Minimal cell width including any padding for the formatted table",
35
+				Value: 0,
36
+			},
37
+			&cli.IntFlag{
38
+				Name:  "tab-width",
39
+				Usage: "width of tab characters in formatted table (equivalent number of spaces)",
40
+				Value: 8,
41
+			},
42
+			&cli.IntFlag{
43
+				Name:  "padding",
44
+				Usage: "padding added to a cell before computing its width",
45
+				Value: 1,
46
+			},
47
+			&cli.StringFlag{
48
+				Name:  "pad-char",
49
+				Usage: `ASCII char used for padding if padchar == '\\t', the Writer will assume that the width of a '\\t' in the formatted output is tabwidth, and cells are left-aligned independent of align_left (for correct-looking results, tabwidth must correspond to the tab width in the viewer displaying the result)`,
50
+				Value: "\t",
51
+			},
52
+			&cli.BoolFlag{
53
+				Name:  "vertical-bars",
54
+				Usage: "Set to true to print vertical bars between columns",
55
+			},
56
+		},
57
+	}
58
+)
59
+
60
+func runListAuth(ctx context.Context, c *cli.Command) error {
61
+	if err := initDB(ctx); err != nil {
62
+		return err
63
+	}
64
+
65
+	authSources, err := db.Find[auth_model.Source](ctx, auth_model.FindSourcesOptions{})
66
+	if err != nil {
67
+		return err
68
+	}
69
+
70
+	flags := tabwriter.AlignRight
71
+	if c.Bool("vertical-bars") {
72
+		flags |= tabwriter.Debug
73
+	}
74
+
75
+	padChar := byte('\t')
76
+	if len(c.String("pad-char")) > 0 {
77
+		padChar = c.String("pad-char")[0]
78
+	}
79
+
80
+	// loop through each source and print
81
+	w := tabwriter.NewWriter(os.Stdout, c.Int("min-width"), c.Int("tab-width"), c.Int("padding"), padChar, flags)
82
+	fmt.Fprintf(w, "ID\tName\tType\tEnabled\n")
83
+	for _, source := range authSources {
84
+		fmt.Fprintf(w, "%d\t%s\t%s\t%t\n", source.ID, source.Name, source.Type.String(), source.IsActive)
85
+	}
86
+	w.Flush()
87
+
88
+	return nil
89
+}
90
+
91
+func runDeleteAuth(ctx context.Context, c *cli.Command) error {
92
+	if !c.IsSet("id") {
93
+		return errors.New("--id flag is missing")
94
+	}
95
+
96
+	if err := initDB(ctx); err != nil {
97
+		return err
98
+	}
99
+
100
+	source, err := auth_model.GetSourceByID(ctx, c.Int64("id"))
101
+	if err != nil {
102
+		return err
103
+	}
104
+
105
+	return auth_service.DeleteSource(ctx, source)
106
+}

+ 455
- 0
cmd/admin_auth_ldap.go 查看文件

@@ -0,0 +1,455 @@
1
+// Copyright 2019 The Gitea Authors. All rights reserved.
2
+// SPDX-License-Identifier: MIT
3
+
4
+package cmd
5
+
6
+import (
7
+	"context"
8
+	"fmt"
9
+	"strings"
10
+
11
+	"code.gitea.io/gitea/models/auth"
12
+	"code.gitea.io/gitea/modules/util"
13
+	"code.gitea.io/gitea/services/auth/source/ldap"
14
+
15
+	"github.com/urfave/cli/v3"
16
+)
17
+
18
+type (
19
+	authService struct {
20
+		initDB            func(ctx context.Context) error
21
+		createAuthSource  func(context.Context, *auth.Source) error
22
+		updateAuthSource  func(context.Context, *auth.Source) error
23
+		getAuthSourceByID func(ctx context.Context, id int64) (*auth.Source, error)
24
+	}
25
+)
26
+
27
+func commonLdapCLIFlags() []cli.Flag {
28
+	return []cli.Flag{
29
+		&cli.StringFlag{
30
+			Name:  "name",
31
+			Usage: "Authentication name.",
32
+		},
33
+		&cli.BoolFlag{
34
+			Name:  "not-active",
35
+			Usage: "Deactivate the authentication source.",
36
+		},
37
+		&cli.BoolFlag{
38
+			Name:  "active",
39
+			Usage: "Activate the authentication source.",
40
+		},
41
+		&cli.StringFlag{
42
+			Name:  "security-protocol",
43
+			Usage: "Security protocol name.",
44
+		},
45
+		&cli.BoolFlag{
46
+			Name:  "skip-tls-verify",
47
+			Usage: "Disable TLS verification.",
48
+		},
49
+		&cli.StringFlag{
50
+			Name:  "host",
51
+			Usage: "The address where the LDAP server can be reached.",
52
+		},
53
+		&cli.IntFlag{
54
+			Name:  "port",
55
+			Usage: "The port to use when connecting to the LDAP server.",
56
+		},
57
+		&cli.StringFlag{
58
+			Name:  "user-search-base",
59
+			Usage: "The LDAP base at which user accounts will be searched for.",
60
+		},
61
+		&cli.StringFlag{
62
+			Name:  "user-filter",
63
+			Usage: "An LDAP filter declaring how to find the user record that is attempting to authenticate.",
64
+		},
65
+		&cli.StringFlag{
66
+			Name:  "admin-filter",
67
+			Usage: "An LDAP filter specifying if a user should be given administrator privileges.",
68
+		},
69
+		&cli.StringFlag{
70
+			Name:  "restricted-filter",
71
+			Usage: "An LDAP filter specifying if a user should be given restricted status.",
72
+		},
73
+		&cli.BoolFlag{
74
+			Name:  "allow-deactivate-all",
75
+			Usage: "Allow empty search results to deactivate all users.",
76
+		},
77
+		&cli.StringFlag{
78
+			Name:  "username-attribute",
79
+			Usage: "The attribute of the user’s LDAP record containing the user name.",
80
+		},
81
+		&cli.StringFlag{
82
+			Name:  "firstname-attribute",
83
+			Usage: "The attribute of the user’s LDAP record containing the user’s first name.",
84
+		},
85
+		&cli.StringFlag{
86
+			Name:  "surname-attribute",
87
+			Usage: "The attribute of the user’s LDAP record containing the user’s surname.",
88
+		},
89
+		&cli.StringFlag{
90
+			Name:  "email-attribute",
91
+			Usage: "The attribute of the user’s LDAP record containing the user’s email address.",
92
+		},
93
+		&cli.StringFlag{
94
+			Name:  "public-ssh-key-attribute",
95
+			Usage: "The attribute of the user’s LDAP record containing the user’s public ssh key.",
96
+		},
97
+		&cli.BoolFlag{
98
+			Name:  "skip-local-2fa",
99
+			Usage: "Set to true to skip local 2fa for users authenticated by this source",
100
+		},
101
+		&cli.StringFlag{
102
+			Name:  "avatar-attribute",
103
+			Usage: "The attribute of the user’s LDAP record containing the user’s avatar.",
104
+		},
105
+	}
106
+}
107
+
108
+func ldapBindDnCLIFlags() []cli.Flag {
109
+	return append(commonLdapCLIFlags(),
110
+		&cli.StringFlag{
111
+			Name:  "bind-dn",
112
+			Usage: "The DN to bind to the LDAP server with when searching for the user.",
113
+		},
114
+		&cli.StringFlag{
115
+			Name:  "bind-password",
116
+			Usage: "The password for the Bind DN, if any.",
117
+		},
118
+		&cli.BoolFlag{
119
+			Name:  "attributes-in-bind",
120
+			Usage: "Fetch attributes in bind DN context.",
121
+		},
122
+		&cli.BoolFlag{
123
+			Name:  "synchronize-users",
124
+			Usage: "Enable user synchronization.",
125
+		},
126
+		&cli.BoolFlag{
127
+			Name:  "disable-synchronize-users",
128
+			Usage: "Disable user synchronization.",
129
+		},
130
+		&cli.UintFlag{
131
+			Name:  "page-size",
132
+			Usage: "Search page size.",
133
+		},
134
+		&cli.BoolFlag{
135
+			Name:  "enable-groups",
136
+			Usage: "Enable LDAP groups",
137
+		},
138
+		&cli.StringFlag{
139
+			Name:  "group-search-base-dn",
140
+			Usage: "The LDAP base DN at which group accounts will be searched for",
141
+		},
142
+		&cli.StringFlag{
143
+			Name:  "group-member-attribute",
144
+			Usage: "Group attribute containing list of users",
145
+		},
146
+		&cli.StringFlag{
147
+			Name:  "group-user-attribute",
148
+			Usage: "User attribute listed in group",
149
+		},
150
+		&cli.StringFlag{
151
+			Name:  "group-filter",
152
+			Usage: "Verify group membership in LDAP",
153
+		},
154
+		&cli.StringFlag{
155
+			Name:  "group-team-map",
156
+			Usage: "Map LDAP groups to Organization teams",
157
+		},
158
+		&cli.BoolFlag{
159
+			Name:  "group-team-map-removal",
160
+			Usage: "Remove users from synchronized teams if user does not belong to corresponding LDAP group",
161
+		})
162
+}
163
+
164
+func ldapSimpleAuthCLIFlags() []cli.Flag {
165
+	return append(commonLdapCLIFlags(),
166
+		&cli.StringFlag{
167
+			Name:  "user-dn",
168
+			Usage: "The user's DN.",
169
+		})
170
+}
171
+
172
+func microcmdAuthAddLdapBindDn() *cli.Command {
173
+	return &cli.Command{
174
+		Name:  "add-ldap",
175
+		Usage: "Add new LDAP (via Bind DN) authentication source",
176
+		Action: func(ctx context.Context, cmd *cli.Command) error {
177
+			return newAuthService().addLdapBindDn(ctx, cmd)
178
+		},
179
+		Flags: ldapBindDnCLIFlags(),
180
+	}
181
+}
182
+
183
+func microcmdAuthUpdateLdapBindDn() *cli.Command {
184
+	return &cli.Command{
185
+		Name:  "update-ldap",
186
+		Usage: "Update existing LDAP (via Bind DN) authentication source",
187
+		Action: func(ctx context.Context, cmd *cli.Command) error {
188
+			return newAuthService().updateLdapBindDn(ctx, cmd)
189
+		},
190
+		Flags: append([]cli.Flag{idFlag()}, ldapBindDnCLIFlags()...),
191
+	}
192
+}
193
+
194
+func microcmdAuthAddLdapSimpleAuth() *cli.Command {
195
+	return &cli.Command{
196
+		Name:  "add-ldap-simple",
197
+		Usage: "Add new LDAP (simple auth) authentication source",
198
+		Action: func(ctx context.Context, cmd *cli.Command) error {
199
+			return newAuthService().addLdapSimpleAuth(ctx, cmd)
200
+		},
201
+		Flags: ldapSimpleAuthCLIFlags(),
202
+	}
203
+}
204
+
205
+func microcmdAuthUpdateLdapSimpleAuth() *cli.Command {
206
+	return &cli.Command{
207
+		Name:  "update-ldap-simple",
208
+		Usage: "Update existing LDAP (simple auth) authentication source",
209
+		Action: func(ctx context.Context, cmd *cli.Command) error {
210
+			return newAuthService().updateLdapSimpleAuth(ctx, cmd)
211
+		},
212
+		Flags: append([]cli.Flag{idFlag()}, ldapSimpleAuthCLIFlags()...),
213
+	}
214
+}
215
+
216
+// newAuthService creates a service with default functions.
217
+func newAuthService() *authService {
218
+	return &authService{
219
+		initDB:            initDB,
220
+		createAuthSource:  auth.CreateSource,
221
+		updateAuthSource:  auth.UpdateSource,
222
+		getAuthSourceByID: auth.GetSourceByID,
223
+	}
224
+}
225
+
226
+// parseAuthSourceLdap assigns values on authSource according to command line flags.
227
+func parseAuthSourceLdap(c *cli.Command, authSource *auth.Source) {
228
+	if c.IsSet("name") {
229
+		authSource.Name = c.String("name")
230
+	}
231
+	if c.IsSet("not-active") {
232
+		authSource.IsActive = !c.Bool("not-active")
233
+	}
234
+	if c.IsSet("active") {
235
+		authSource.IsActive = c.Bool("active")
236
+	}
237
+	if c.IsSet("synchronize-users") {
238
+		authSource.IsSyncEnabled = c.Bool("synchronize-users")
239
+	}
240
+	if c.IsSet("disable-synchronize-users") {
241
+		authSource.IsSyncEnabled = !c.Bool("disable-synchronize-users")
242
+	}
243
+	authSource.TwoFactorPolicy = util.Iif(c.Bool("skip-local-2fa"), "skip", "")
244
+}
245
+
246
+// parseLdapConfig assigns values on config according to command line flags.
247
+func parseLdapConfig(c *cli.Command, config *ldap.Source) error {
248
+	if c.IsSet("name") {
249
+		config.Name = c.String("name")
250
+	}
251
+	if c.IsSet("host") {
252
+		config.Host = c.String("host")
253
+	}
254
+	if c.IsSet("port") {
255
+		config.Port = c.Int("port")
256
+	}
257
+	if c.IsSet("security-protocol") {
258
+		p, ok := findLdapSecurityProtocolByName(c.String("security-protocol"))
259
+		if !ok {
260
+			return fmt.Errorf("unknown security protocol name: %s", c.String("security-protocol"))
261
+		}
262
+		config.SecurityProtocol = p
263
+	}
264
+	if c.IsSet("skip-tls-verify") {
265
+		config.SkipVerify = c.Bool("skip-tls-verify")
266
+	}
267
+	if c.IsSet("bind-dn") {
268
+		config.BindDN = c.String("bind-dn")
269
+	}
270
+	if c.IsSet("user-dn") {
271
+		config.UserDN = c.String("user-dn")
272
+	}
273
+	if c.IsSet("bind-password") {
274
+		config.BindPassword = c.String("bind-password")
275
+	}
276
+	if c.IsSet("user-search-base") {
277
+		config.UserBase = c.String("user-search-base")
278
+	}
279
+	if c.IsSet("username-attribute") {
280
+		config.AttributeUsername = c.String("username-attribute")
281
+	}
282
+	if c.IsSet("firstname-attribute") {
283
+		config.AttributeName = c.String("firstname-attribute")
284
+	}
285
+	if c.IsSet("surname-attribute") {
286
+		config.AttributeSurname = c.String("surname-attribute")
287
+	}
288
+	if c.IsSet("email-attribute") {
289
+		config.AttributeMail = c.String("email-attribute")
290
+	}
291
+	if c.IsSet("attributes-in-bind") {
292
+		config.AttributesInBind = c.Bool("attributes-in-bind")
293
+	}
294
+	if c.IsSet("public-ssh-key-attribute") {
295
+		config.AttributeSSHPublicKey = c.String("public-ssh-key-attribute")
296
+	}
297
+	if c.IsSet("avatar-attribute") {
298
+		config.AttributeAvatar = c.String("avatar-attribute")
299
+	}
300
+	if c.IsSet("page-size") {
301
+		config.SearchPageSize = uint32(c.Uint("page-size"))
302
+	}
303
+	if c.IsSet("user-filter") {
304
+		config.Filter = c.String("user-filter")
305
+	}
306
+	if c.IsSet("admin-filter") {
307
+		config.AdminFilter = c.String("admin-filter")
308
+	}
309
+	if c.IsSet("restricted-filter") {
310
+		config.RestrictedFilter = c.String("restricted-filter")
311
+	}
312
+	if c.IsSet("allow-deactivate-all") {
313
+		config.AllowDeactivateAll = c.Bool("allow-deactivate-all")
314
+	}
315
+	if c.IsSet("enable-groups") {
316
+		config.GroupsEnabled = c.Bool("enable-groups")
317
+	}
318
+	if c.IsSet("group-search-base-dn") {
319
+		config.GroupDN = c.String("group-search-base-dn")
320
+	}
321
+	if c.IsSet("group-member-attribute") {
322
+		config.GroupMemberUID = c.String("group-member-attribute")
323
+	}
324
+	if c.IsSet("group-user-attribute") {
325
+		config.UserUID = c.String("group-user-attribute")
326
+	}
327
+	if c.IsSet("group-filter") {
328
+		config.GroupFilter = c.String("group-filter")
329
+	}
330
+	if c.IsSet("group-team-map") {
331
+		config.GroupTeamMap = c.String("group-team-map")
332
+	}
333
+	if c.IsSet("group-team-map-removal") {
334
+		config.GroupTeamMapRemoval = c.Bool("group-team-map-removal")
335
+	}
336
+	return nil
337
+}
338
+
339
+// findLdapSecurityProtocolByName finds security protocol by its name ignoring case.
340
+// It returns the value of the security protocol and if it was found.
341
+func findLdapSecurityProtocolByName(name string) (ldap.SecurityProtocol, bool) {
342
+	for i, n := range ldap.SecurityProtocolNames {
343
+		if strings.EqualFold(name, n) {
344
+			return i, true
345
+		}
346
+	}
347
+	return 0, false
348
+}
349
+
350
+// getAuthSource gets the login source by its id defined in the command line flags.
351
+// It returns an error if the id is not set, does not match any source or if the source is not of expected type.
352
+func (a *authService) getAuthSource(ctx context.Context, c *cli.Command, authType auth.Type) (*auth.Source, error) {
353
+	if err := argsSet(c, "id"); err != nil {
354
+		return nil, err
355
+	}
356
+	authSource, err := a.getAuthSourceByID(ctx, c.Int64("id"))
357
+	if err != nil {
358
+		return nil, err
359
+	}
360
+
361
+	if authSource.Type != authType {
362
+		return nil, fmt.Errorf("invalid authentication type. expected: %s, actual: %s", authType.String(), authSource.Type.String())
363
+	}
364
+
365
+	return authSource, nil
366
+}
367
+
368
+// addLdapBindDn adds a new LDAP via Bind DN authentication source.
369
+func (a *authService) addLdapBindDn(ctx context.Context, c *cli.Command) error {
370
+	if err := argsSet(c, "name", "security-protocol", "host", "port", "user-search-base", "user-filter", "email-attribute"); err != nil {
371
+		return err
372
+	}
373
+	if err := a.initDB(ctx); err != nil {
374
+		return err
375
+	}
376
+
377
+	authSource := &auth.Source{
378
+		Type:     auth.LDAP,
379
+		IsActive: true, // active by default
380
+		Cfg: &ldap.Source{
381
+			Enabled: true, // always true
382
+		},
383
+	}
384
+
385
+	parseAuthSourceLdap(c, authSource)
386
+	if err := parseLdapConfig(c, authSource.Cfg.(*ldap.Source)); err != nil {
387
+		return err
388
+	}
389
+
390
+	return a.createAuthSource(ctx, authSource)
391
+}
392
+
393
+// updateLdapBindDn updates a new LDAP via Bind DN authentication source.
394
+func (a *authService) updateLdapBindDn(ctx context.Context, c *cli.Command) error {
395
+	if err := a.initDB(ctx); err != nil {
396
+		return err
397
+	}
398
+
399
+	authSource, err := a.getAuthSource(ctx, c, auth.LDAP)
400
+	if err != nil {
401
+		return err
402
+	}
403
+
404
+	parseAuthSourceLdap(c, authSource)
405
+	if err := parseLdapConfig(c, authSource.Cfg.(*ldap.Source)); err != nil {
406
+		return err
407
+	}
408
+
409
+	return a.updateAuthSource(ctx, authSource)
410
+}
411
+
412
+// addLdapSimpleAuth adds a new LDAP (simple auth) authentication source.
413
+func (a *authService) addLdapSimpleAuth(ctx context.Context, c *cli.Command) error {
414
+	if err := argsSet(c, "name", "security-protocol", "host", "port", "user-dn", "user-filter", "email-attribute"); err != nil {
415
+		return err
416
+	}
417
+
418
+	if err := a.initDB(ctx); err != nil {
419
+		return err
420
+	}
421
+
422
+	authSource := &auth.Source{
423
+		Type:     auth.DLDAP,
424
+		IsActive: true, // active by default
425
+		Cfg: &ldap.Source{
426
+			Enabled: true, // always true
427
+		},
428
+	}
429
+
430
+	parseAuthSourceLdap(c, authSource)
431
+	if err := parseLdapConfig(c, authSource.Cfg.(*ldap.Source)); err != nil {
432
+		return err
433
+	}
434
+
435
+	return a.createAuthSource(ctx, authSource)
436
+}
437
+
438
+// updateLdapSimpleAuth updates a new LDAP (simple auth) authentication source.
439
+func (a *authService) updateLdapSimpleAuth(ctx context.Context, c *cli.Command) error {
440
+	if err := a.initDB(ctx); err != nil {
441
+		return err
442
+	}
443
+
444
+	authSource, err := a.getAuthSource(ctx, c, auth.DLDAP)
445
+	if err != nil {
446
+		return err
447
+	}
448
+
449
+	parseAuthSourceLdap(c, authSource)
450
+	if err := parseLdapConfig(c, authSource.Cfg.(*ldap.Source)); err != nil {
451
+		return err
452
+	}
453
+
454
+	return a.updateAuthSource(ctx, authSource)
455
+}

+ 1348
- 0
cmd/admin_auth_ldap_test.go
文件差異過大導致無法顯示
查看文件


+ 322
- 0
cmd/admin_auth_oauth.go 查看文件

@@ -0,0 +1,322 @@
1
+// Copyright 2023 The Gitea Authors. All rights reserved.
2
+// SPDX-License-Identifier: MIT
3
+
4
+package cmd
5
+
6
+import (
7
+	"context"
8
+	"errors"
9
+	"fmt"
10
+	"net/url"
11
+
12
+	auth_model "code.gitea.io/gitea/models/auth"
13
+	"code.gitea.io/gitea/modules/util"
14
+	"code.gitea.io/gitea/services/auth/source/oauth2"
15
+
16
+	"github.com/urfave/cli/v3"
17
+)
18
+
19
+func oauthCLIFlags() []cli.Flag {
20
+	return []cli.Flag{
21
+		&cli.StringFlag{
22
+			Name:  "name",
23
+			Value: "",
24
+			Usage: "Application Name",
25
+		},
26
+		&cli.StringFlag{
27
+			Name:  "provider",
28
+			Value: "",
29
+			Usage: "OAuth2 Provider",
30
+		},
31
+		&cli.StringFlag{
32
+			Name:  "key",
33
+			Value: "",
34
+			Usage: "Client ID (Key)",
35
+		},
36
+		&cli.StringFlag{
37
+			Name:  "secret",
38
+			Value: "",
39
+			Usage: "Client Secret",
40
+		},
41
+		&cli.StringFlag{
42
+			Name:  "auto-discover-url",
43
+			Value: "",
44
+			Usage: "OpenID Connect Auto Discovery URL (only required when using OpenID Connect as provider)",
45
+		},
46
+		&cli.StringFlag{
47
+			Name:  "use-custom-urls",
48
+			Value: "false",
49
+			Usage: "Use custom URLs for GitLab/GitHub OAuth endpoints",
50
+		},
51
+		&cli.StringFlag{
52
+			Name:  "custom-tenant-id",
53
+			Value: "",
54
+			Usage: "Use custom Tenant ID for OAuth endpoints",
55
+		},
56
+		&cli.StringFlag{
57
+			Name:  "custom-auth-url",
58
+			Value: "",
59
+			Usage: "Use a custom Authorization URL (option for GitLab/GitHub)",
60
+		},
61
+		&cli.StringFlag{
62
+			Name:  "custom-token-url",
63
+			Value: "",
64
+			Usage: "Use a custom Token URL (option for GitLab/GitHub)",
65
+		},
66
+		&cli.StringFlag{
67
+			Name:  "custom-profile-url",
68
+			Value: "",
69
+			Usage: "Use a custom Profile URL (option for GitLab/GitHub)",
70
+		},
71
+		&cli.StringFlag{
72
+			Name:  "custom-email-url",
73
+			Value: "",
74
+			Usage: "Use a custom Email URL (option for GitHub)",
75
+		},
76
+		&cli.StringFlag{
77
+			Name:  "icon-url",
78
+			Value: "",
79
+			Usage: "Custom icon URL for OAuth2 login source",
80
+		},
81
+		&cli.BoolFlag{
82
+			Name:  "skip-local-2fa",
83
+			Usage: "Set to true to skip local 2fa for users authenticated by this source",
84
+		},
85
+		&cli.StringSliceFlag{
86
+			Name:  "scopes",
87
+			Value: nil,
88
+			Usage: "Scopes to request when to authenticate against this OAuth2 source",
89
+		},
90
+		&cli.StringFlag{
91
+			Name:  "ssh-public-key-claim-name",
92
+			Usage: "Claim name that provides SSH public keys",
93
+		},
94
+		&cli.StringFlag{
95
+			Name:  "full-name-claim-name",
96
+			Usage: "Claim name that provides user's full name",
97
+		},
98
+		&cli.StringFlag{
99
+			Name:  "required-claim-name",
100
+			Value: "",
101
+			Usage: "Claim name that has to be set to allow users to login with this source",
102
+		},
103
+		&cli.StringFlag{
104
+			Name:  "required-claim-value",
105
+			Value: "",
106
+			Usage: "Claim value that has to be set to allow users to login with this source",
107
+		},
108
+		&cli.StringFlag{
109
+			Name:  "group-claim-name",
110
+			Value: "",
111
+			Usage: "Claim name providing group names for this source",
112
+		},
113
+		&cli.StringFlag{
114
+			Name:  "admin-group",
115
+			Value: "",
116
+			Usage: "Group Claim value for administrator users",
117
+		},
118
+		&cli.StringFlag{
119
+			Name:  "restricted-group",
120
+			Value: "",
121
+			Usage: "Group Claim value for restricted users",
122
+		},
123
+		&cli.StringFlag{
124
+			Name:  "group-team-map",
125
+			Value: "",
126
+			Usage: "JSON mapping between groups and org teams",
127
+		},
128
+		&cli.BoolFlag{
129
+			Name:  "group-team-map-removal",
130
+			Usage: "Activate automatic team membership removal depending on groups",
131
+		},
132
+	}
133
+}
134
+
135
+func microcmdAuthAddOauth() *cli.Command {
136
+	return &cli.Command{
137
+		Name:  "add-oauth",
138
+		Usage: "Add new Oauth authentication source",
139
+		Action: func(ctx context.Context, cmd *cli.Command) error {
140
+			return newAuthService().runAddOauth(ctx, cmd)
141
+		},
142
+		Flags: oauthCLIFlags(),
143
+	}
144
+}
145
+
146
+func microcmdAuthUpdateOauth() *cli.Command {
147
+	return &cli.Command{
148
+		Name:  "update-oauth",
149
+		Usage: "Update existing Oauth authentication source",
150
+		Action: func(ctx context.Context, cmd *cli.Command) error {
151
+			return newAuthService().runUpdateOauth(ctx, cmd)
152
+		},
153
+		Flags: append(oauthCLIFlags()[:1], append([]cli.Flag{&cli.Int64Flag{
154
+			Name:  "id",
155
+			Usage: "ID of authentication source",
156
+		}}, oauthCLIFlags()[1:]...)...),
157
+	}
158
+}
159
+
160
+func parseOAuth2Config(c *cli.Command) *oauth2.Source {
161
+	var customURLMapping *oauth2.CustomURLMapping
162
+	if c.IsSet("use-custom-urls") {
163
+		customURLMapping = &oauth2.CustomURLMapping{
164
+			TokenURL:   c.String("custom-token-url"),
165
+			AuthURL:    c.String("custom-auth-url"),
166
+			ProfileURL: c.String("custom-profile-url"),
167
+			EmailURL:   c.String("custom-email-url"),
168
+			Tenant:     c.String("custom-tenant-id"),
169
+		}
170
+	} else {
171
+		customURLMapping = nil
172
+	}
173
+	return &oauth2.Source{
174
+		Provider:                      c.String("provider"),
175
+		ClientID:                      c.String("key"),
176
+		ClientSecret:                  c.String("secret"),
177
+		OpenIDConnectAutoDiscoveryURL: c.String("auto-discover-url"),
178
+		CustomURLMapping:              customURLMapping,
179
+		IconURL:                       c.String("icon-url"),
180
+		Scopes:                        c.StringSlice("scopes"),
181
+		RequiredClaimName:             c.String("required-claim-name"),
182
+		RequiredClaimValue:            c.String("required-claim-value"),
183
+		GroupClaimName:                c.String("group-claim-name"),
184
+		AdminGroup:                    c.String("admin-group"),
185
+		RestrictedGroup:               c.String("restricted-group"),
186
+		GroupTeamMap:                  c.String("group-team-map"),
187
+		GroupTeamMapRemoval:           c.Bool("group-team-map-removal"),
188
+		SSHPublicKeyClaimName:         c.String("ssh-public-key-claim-name"),
189
+		FullNameClaimName:             c.String("full-name-claim-name"),
190
+	}
191
+}
192
+
193
+func (a *authService) runAddOauth(ctx context.Context, c *cli.Command) error {
194
+	if err := a.initDB(ctx); err != nil {
195
+		return err
196
+	}
197
+
198
+	config := parseOAuth2Config(c)
199
+	if config.Provider == "openidConnect" {
200
+		discoveryURL, err := url.Parse(config.OpenIDConnectAutoDiscoveryURL)
201
+		if err != nil || (discoveryURL.Scheme != "http" && discoveryURL.Scheme != "https") {
202
+			return fmt.Errorf("invalid Auto Discovery URL: %s (this must be a valid URL starting with http:// or https://)", config.OpenIDConnectAutoDiscoveryURL)
203
+		}
204
+	}
205
+
206
+	return a.createAuthSource(ctx, &auth_model.Source{
207
+		Type:            auth_model.OAuth2,
208
+		Name:            c.String("name"),
209
+		IsActive:        true,
210
+		Cfg:             config,
211
+		TwoFactorPolicy: util.Iif(c.Bool("skip-local-2fa"), "skip", ""),
212
+	})
213
+}
214
+
215
+func (a *authService) runUpdateOauth(ctx context.Context, c *cli.Command) error {
216
+	if !c.IsSet("id") {
217
+		return errors.New("--id flag is missing")
218
+	}
219
+
220
+	if err := a.initDB(ctx); err != nil {
221
+		return err
222
+	}
223
+
224
+	source, err := a.getAuthSourceByID(ctx, c.Int64("id"))
225
+	if err != nil {
226
+		return err
227
+	}
228
+
229
+	oAuth2Config := source.Cfg.(*oauth2.Source)
230
+
231
+	if c.IsSet("name") {
232
+		source.Name = c.String("name")
233
+	}
234
+
235
+	if c.IsSet("provider") {
236
+		oAuth2Config.Provider = c.String("provider")
237
+	}
238
+
239
+	if c.IsSet("key") {
240
+		oAuth2Config.ClientID = c.String("key")
241
+	}
242
+
243
+	if c.IsSet("secret") {
244
+		oAuth2Config.ClientSecret = c.String("secret")
245
+	}
246
+
247
+	if c.IsSet("auto-discover-url") {
248
+		oAuth2Config.OpenIDConnectAutoDiscoveryURL = c.String("auto-discover-url")
249
+	}
250
+
251
+	if c.IsSet("icon-url") {
252
+		oAuth2Config.IconURL = c.String("icon-url")
253
+	}
254
+
255
+	if c.IsSet("scopes") {
256
+		oAuth2Config.Scopes = c.StringSlice("scopes")
257
+	}
258
+
259
+	if c.IsSet("required-claim-name") {
260
+		oAuth2Config.RequiredClaimName = c.String("required-claim-name")
261
+	}
262
+	if c.IsSet("required-claim-value") {
263
+		oAuth2Config.RequiredClaimValue = c.String("required-claim-value")
264
+	}
265
+
266
+	if c.IsSet("group-claim-name") {
267
+		oAuth2Config.GroupClaimName = c.String("group-claim-name")
268
+	}
269
+	if c.IsSet("admin-group") {
270
+		oAuth2Config.AdminGroup = c.String("admin-group")
271
+	}
272
+	if c.IsSet("restricted-group") {
273
+		oAuth2Config.RestrictedGroup = c.String("restricted-group")
274
+	}
275
+	if c.IsSet("group-team-map") {
276
+		oAuth2Config.GroupTeamMap = c.String("group-team-map")
277
+	}
278
+	if c.IsSet("group-team-map-removal") {
279
+		oAuth2Config.GroupTeamMapRemoval = c.Bool("group-team-map-removal")
280
+	}
281
+	if c.IsSet("ssh-public-key-claim-name") {
282
+		oAuth2Config.SSHPublicKeyClaimName = c.String("ssh-public-key-claim-name")
283
+	}
284
+	if c.IsSet("full-name-claim-name") {
285
+		oAuth2Config.FullNameClaimName = c.String("full-name-claim-name")
286
+	}
287
+
288
+	// update custom URL mapping
289
+	customURLMapping := &oauth2.CustomURLMapping{}
290
+
291
+	if oAuth2Config.CustomURLMapping != nil {
292
+		customURLMapping.TokenURL = oAuth2Config.CustomURLMapping.TokenURL
293
+		customURLMapping.AuthURL = oAuth2Config.CustomURLMapping.AuthURL
294
+		customURLMapping.ProfileURL = oAuth2Config.CustomURLMapping.ProfileURL
295
+		customURLMapping.EmailURL = oAuth2Config.CustomURLMapping.EmailURL
296
+		customURLMapping.Tenant = oAuth2Config.CustomURLMapping.Tenant
297
+	}
298
+	if c.IsSet("use-custom-urls") && c.IsSet("custom-token-url") {
299
+		customURLMapping.TokenURL = c.String("custom-token-url")
300
+	}
301
+
302
+	if c.IsSet("use-custom-urls") && c.IsSet("custom-auth-url") {
303
+		customURLMapping.AuthURL = c.String("custom-auth-url")
304
+	}
305
+
306
+	if c.IsSet("use-custom-urls") && c.IsSet("custom-profile-url") {
307
+		customURLMapping.ProfileURL = c.String("custom-profile-url")
308
+	}
309
+
310
+	if c.IsSet("use-custom-urls") && c.IsSet("custom-email-url") {
311
+		customURLMapping.EmailURL = c.String("custom-email-url")
312
+	}
313
+
314
+	if c.IsSet("use-custom-urls") && c.IsSet("custom-tenant-id") {
315
+		customURLMapping.Tenant = c.String("custom-tenant-id")
316
+	}
317
+
318
+	oAuth2Config.CustomURLMapping = customURLMapping
319
+	source.Cfg = oAuth2Config
320
+	source.TwoFactorPolicy = util.Iif(c.Bool("skip-local-2fa"), "skip", "")
321
+	return a.updateAuthSource(ctx, source)
322
+}

+ 343
- 0
cmd/admin_auth_oauth_test.go 查看文件

@@ -0,0 +1,343 @@
1
+// Copyright 2025 The Gitea Authors. All rights reserved.
2
+// SPDX-License-Identifier: MIT
3
+
4
+package cmd
5
+
6
+import (
7
+	"context"
8
+	"testing"
9
+
10
+	auth_model "code.gitea.io/gitea/models/auth"
11
+	"code.gitea.io/gitea/services/auth/source/oauth2"
12
+
13
+	"github.com/stretchr/testify/assert"
14
+	"github.com/urfave/cli/v3"
15
+)
16
+
17
+func TestAddOauth(t *testing.T) {
18
+	testCases := []struct {
19
+		name   string
20
+		args   []string
21
+		source *auth_model.Source
22
+		errMsg string
23
+	}{
24
+		{
25
+			name: "valid config",
26
+			args: []string{
27
+				"--name", "test",
28
+				"--provider", "github",
29
+				"--key", "some_key",
30
+				"--secret", "some_secret",
31
+			},
32
+			source: &auth_model.Source{
33
+				Type:     auth_model.OAuth2,
34
+				Name:     "test",
35
+				IsActive: true,
36
+				Cfg: &oauth2.Source{
37
+					Scopes:       []string{},
38
+					Provider:     "github",
39
+					ClientID:     "some_key",
40
+					ClientSecret: "some_secret",
41
+				},
42
+				TwoFactorPolicy: "",
43
+			},
44
+		},
45
+		{
46
+			name: "valid config with openid connect",
47
+			args: []string{
48
+				"--name", "test",
49
+				"--provider", "openidConnect",
50
+				"--key", "some_key",
51
+				"--secret", "some_secret",
52
+				"--auto-discover-url", "https://example.com",
53
+			},
54
+			source: &auth_model.Source{
55
+				Type:     auth_model.OAuth2,
56
+				Name:     "test",
57
+				IsActive: true,
58
+				Cfg: &oauth2.Source{
59
+					Scopes:                        []string{},
60
+					Provider:                      "openidConnect",
61
+					ClientID:                      "some_key",
62
+					ClientSecret:                  "some_secret",
63
+					OpenIDConnectAutoDiscoveryURL: "https://example.com",
64
+				},
65
+				TwoFactorPolicy: "",
66
+			},
67
+		},
68
+		{
69
+			name: "valid config with options",
70
+			args: []string{
71
+				"--name", "test",
72
+				"--provider", "gitlab",
73
+				"--key", "some_key",
74
+				"--secret", "some_secret",
75
+				"--use-custom-urls", "true",
76
+				"--custom-token-url", "https://example.com/token",
77
+				"--custom-auth-url", "https://example.com/auth",
78
+				"--custom-profile-url", "https://example.com/profile",
79
+				"--custom-email-url", "https://example.com/email",
80
+				"--custom-tenant-id", "some_tenant",
81
+				"--icon-url", "https://example.com/icon",
82
+				"--scopes", "scope1,scope2",
83
+				"--skip-local-2fa", "true",
84
+				"--required-claim-name", "claim_name",
85
+				"--required-claim-value", "claim_value",
86
+				"--group-claim-name", "group_name",
87
+				"--admin-group", "admin",
88
+				"--restricted-group", "restricted",
89
+				"--group-team-map", `{"group1": [1,2]}`,
90
+				"--group-team-map-removal=true",
91
+				"--ssh-public-key-claim-name", "attr_ssh_pub_key",
92
+				"--full-name-claim-name", "attr_full_name",
93
+			},
94
+			source: &auth_model.Source{
95
+				Type:     auth_model.OAuth2,
96
+				Name:     "test",
97
+				IsActive: true,
98
+				Cfg: &oauth2.Source{
99
+					Provider:     "gitlab",
100
+					ClientID:     "some_key",
101
+					ClientSecret: "some_secret",
102
+					CustomURLMapping: &oauth2.CustomURLMapping{
103
+						TokenURL:   "https://example.com/token",
104
+						AuthURL:    "https://example.com/auth",
105
+						ProfileURL: "https://example.com/profile",
106
+						EmailURL:   "https://example.com/email",
107
+						Tenant:     "some_tenant",
108
+					},
109
+					IconURL:               "https://example.com/icon",
110
+					Scopes:                []string{"scope1", "scope2"},
111
+					RequiredClaimName:     "claim_name",
112
+					RequiredClaimValue:    "claim_value",
113
+					GroupClaimName:        "group_name",
114
+					AdminGroup:            "admin",
115
+					RestrictedGroup:       "restricted",
116
+					GroupTeamMap:          `{"group1": [1,2]}`,
117
+					GroupTeamMapRemoval:   true,
118
+					SSHPublicKeyClaimName: "attr_ssh_pub_key",
119
+					FullNameClaimName:     "attr_full_name",
120
+				},
121
+				TwoFactorPolicy: "skip",
122
+			},
123
+		},
124
+	}
125
+
126
+	for _, tc := range testCases {
127
+		t.Run(tc.name, func(t *testing.T) {
128
+			var createdSource *auth_model.Source
129
+			a := &authService{
130
+				initDB: func(ctx context.Context) error {
131
+					return nil
132
+				},
133
+				createAuthSource: func(ctx context.Context, source *auth_model.Source) error {
134
+					createdSource = source
135
+					return nil
136
+				},
137
+			}
138
+
139
+			app := &cli.Command{
140
+				Flags:  microcmdAuthAddOauth().Flags,
141
+				Action: a.runAddOauth,
142
+			}
143
+
144
+			args := []string{"oauth-test"}
145
+			args = append(args, tc.args...)
146
+
147
+			err := app.Run(t.Context(), args)
148
+
149
+			if tc.errMsg != "" {
150
+				assert.EqualError(t, err, tc.errMsg)
151
+			} else {
152
+				assert.NoError(t, err)
153
+				assert.Equal(t, tc.source, createdSource)
154
+			}
155
+		})
156
+	}
157
+}
158
+
159
+func TestUpdateOauth(t *testing.T) {
160
+	testCases := []struct {
161
+		name               string
162
+		args               []string
163
+		id                 int64
164
+		existingAuthSource *auth_model.Source
165
+		authSource         *auth_model.Source
166
+		errMsg             string
167
+	}{
168
+		{
169
+			name: "missing id",
170
+			args: []string{
171
+				"--name", "test",
172
+			},
173
+			errMsg: "--id flag is missing",
174
+		},
175
+		{
176
+			name: "valid config",
177
+			id:   1,
178
+			existingAuthSource: &auth_model.Source{
179
+				ID:       1,
180
+				Type:     auth_model.OAuth2,
181
+				Name:     "old name",
182
+				IsActive: true,
183
+				Cfg: &oauth2.Source{
184
+					Provider:     "github",
185
+					ClientID:     "old_key",
186
+					ClientSecret: "old_secret",
187
+				},
188
+				TwoFactorPolicy: "",
189
+			},
190
+			args: []string{
191
+				"--id", "1",
192
+				"--name", "test",
193
+				"--provider", "gitlab",
194
+				"--key", "new_key",
195
+				"--secret", "new_secret",
196
+			},
197
+			authSource: &auth_model.Source{
198
+				ID:       1,
199
+				Type:     auth_model.OAuth2,
200
+				Name:     "test",
201
+				IsActive: true,
202
+				Cfg: &oauth2.Source{
203
+					Provider:         "gitlab",
204
+					ClientID:         "new_key",
205
+					ClientSecret:     "new_secret",
206
+					CustomURLMapping: &oauth2.CustomURLMapping{},
207
+				},
208
+				TwoFactorPolicy: "",
209
+			},
210
+		},
211
+		{
212
+			name: "valid config with options",
213
+			id:   1,
214
+			existingAuthSource: &auth_model.Source{
215
+				ID:       1,
216
+				Type:     auth_model.OAuth2,
217
+				Name:     "old name",
218
+				IsActive: true,
219
+				Cfg: &oauth2.Source{
220
+					Provider:     "gitlab",
221
+					ClientID:     "old_key",
222
+					ClientSecret: "old_secret",
223
+					CustomURLMapping: &oauth2.CustomURLMapping{
224
+						TokenURL:   "https://old.example.com/token",
225
+						AuthURL:    "https://old.example.com/auth",
226
+						ProfileURL: "https://old.example.com/profile",
227
+						EmailURL:   "https://old.example.com/email",
228
+						Tenant:     "old_tenant",
229
+					},
230
+					IconURL:               "https://old.example.com/icon",
231
+					Scopes:                []string{"old_scope1", "old_scope2"},
232
+					RequiredClaimName:     "old_claim_name",
233
+					RequiredClaimValue:    "old_claim_value",
234
+					GroupClaimName:        "old_group_name",
235
+					AdminGroup:            "old_admin",
236
+					RestrictedGroup:       "old_restricted",
237
+					GroupTeamMap:          `{"old_group1": [1,2]}`,
238
+					GroupTeamMapRemoval:   true,
239
+					SSHPublicKeyClaimName: "old_ssh_pub_key",
240
+					FullNameClaimName:     "old_full_name",
241
+				},
242
+				TwoFactorPolicy: "",
243
+			},
244
+			args: []string{
245
+				"--id", "1",
246
+				"--name", "test",
247
+				"--provider", "github",
248
+				"--key", "new_key",
249
+				"--secret", "new_secret",
250
+				"--use-custom-urls", "true",
251
+				"--custom-token-url", "https://example.com/token",
252
+				"--custom-auth-url", "https://example.com/auth",
253
+				"--custom-profile-url", "https://example.com/profile",
254
+				"--custom-email-url", "https://example.com/email",
255
+				"--custom-tenant-id", "new_tenant",
256
+				"--icon-url", "https://example.com/icon",
257
+				"--scopes", "scope1,scope2",
258
+				"--skip-local-2fa=true",
259
+				"--required-claim-name", "claim_name",
260
+				"--required-claim-value", "claim_value",
261
+				"--group-claim-name", "group_name",
262
+				"--admin-group", "admin",
263
+				"--restricted-group", "restricted",
264
+				"--group-team-map", `{"group1": [1,2]}`,
265
+				"--group-team-map-removal=false",
266
+				"--ssh-public-key-claim-name", "new_ssh_pub_key",
267
+				"--full-name-claim-name", "new_full_name",
268
+			},
269
+			authSource: &auth_model.Source{
270
+				ID:       1,
271
+				Type:     auth_model.OAuth2,
272
+				Name:     "test",
273
+				IsActive: true,
274
+				Cfg: &oauth2.Source{
275
+					Provider:     "github",
276
+					ClientID:     "new_key",
277
+					ClientSecret: "new_secret",
278
+					CustomURLMapping: &oauth2.CustomURLMapping{
279
+						TokenURL:   "https://example.com/token",
280
+						AuthURL:    "https://example.com/auth",
281
+						ProfileURL: "https://example.com/profile",
282
+						EmailURL:   "https://example.com/email",
283
+						Tenant:     "new_tenant",
284
+					},
285
+					IconURL:               "https://example.com/icon",
286
+					Scopes:                []string{"scope1", "scope2"},
287
+					RequiredClaimName:     "claim_name",
288
+					RequiredClaimValue:    "claim_value",
289
+					GroupClaimName:        "group_name",
290
+					AdminGroup:            "admin",
291
+					RestrictedGroup:       "restricted",
292
+					GroupTeamMap:          `{"group1": [1,2]}`,
293
+					GroupTeamMapRemoval:   false,
294
+					SSHPublicKeyClaimName: "new_ssh_pub_key",
295
+					FullNameClaimName:     "new_full_name",
296
+				},
297
+				TwoFactorPolicy: "skip",
298
+			},
299
+		},
300
+	}
301
+
302
+	for _, tc := range testCases {
303
+		t.Run(tc.name, func(t *testing.T) {
304
+			a := &authService{
305
+				initDB: func(ctx context.Context) error {
306
+					return nil
307
+				},
308
+				getAuthSourceByID: func(ctx context.Context, id int64) (*auth_model.Source, error) {
309
+					return &auth_model.Source{
310
+						ID:       1,
311
+						Type:     auth_model.OAuth2,
312
+						Name:     "test",
313
+						IsActive: true,
314
+						Cfg: &oauth2.Source{
315
+							CustomURLMapping: &oauth2.CustomURLMapping{},
316
+						},
317
+						TwoFactorPolicy: "skip",
318
+					}, nil
319
+				},
320
+				updateAuthSource: func(ctx context.Context, source *auth_model.Source) error {
321
+					assert.Equal(t, tc.authSource, source)
322
+					return nil
323
+				},
324
+			}
325
+
326
+			app := &cli.Command{
327
+				Flags:  microcmdAuthUpdateOauth().Flags,
328
+				Action: a.runUpdateOauth,
329
+			}
330
+
331
+			args := []string{"oauth-test"}
332
+			args = append(args, tc.args...)
333
+
334
+			err := app.Run(t.Context(), args)
335
+
336
+			if tc.errMsg != "" {
337
+				assert.EqualError(t, err, tc.errMsg)
338
+			} else {
339
+				assert.NoError(t, err)
340
+			}
341
+		})
342
+	}
343
+}

+ 200
- 0
cmd/admin_auth_smtp.go 查看文件

@@ -0,0 +1,200 @@
1
+// Copyright 2023 The Gitea Authors. All rights reserved.
2
+// SPDX-License-Identifier: MIT
3
+
4
+package cmd
5
+
6
+import (
7
+	"context"
8
+	"errors"
9
+	"strings"
10
+
11
+	auth_model "code.gitea.io/gitea/models/auth"
12
+	"code.gitea.io/gitea/modules/util"
13
+	"code.gitea.io/gitea/services/auth/source/smtp"
14
+
15
+	"github.com/urfave/cli/v3"
16
+)
17
+
18
+func smtpCLIFlags() []cli.Flag {
19
+	return []cli.Flag{
20
+		&cli.StringFlag{
21
+			Name:  "name",
22
+			Value: "",
23
+			Usage: "Application Name",
24
+		},
25
+		&cli.StringFlag{
26
+			Name:  "auth-type",
27
+			Value: "PLAIN",
28
+			Usage: "SMTP Authentication Type (PLAIN/LOGIN/CRAM-MD5) default PLAIN",
29
+		},
30
+		&cli.StringFlag{
31
+			Name:  "host",
32
+			Value: "",
33
+			Usage: "SMTP Host",
34
+		},
35
+		&cli.IntFlag{
36
+			Name:  "port",
37
+			Usage: "SMTP Port",
38
+		},
39
+		&cli.BoolFlag{
40
+			Name:  "force-smtps",
41
+			Usage: "SMTPS is always used on port 465. Set this to force SMTPS on other ports.",
42
+		},
43
+		&cli.BoolFlag{
44
+			Name:  "skip-verify",
45
+			Usage: "Skip TLS verify.",
46
+		},
47
+		&cli.StringFlag{
48
+			Name:  "helo-hostname",
49
+			Value: "",
50
+			Usage: "Hostname sent with HELO. Leave blank to send current hostname",
51
+		},
52
+		&cli.BoolFlag{
53
+			Name:  "disable-helo",
54
+			Usage: "Disable SMTP helo.",
55
+		},
56
+		&cli.StringFlag{
57
+			Name:  "allowed-domains",
58
+			Value: "",
59
+			Usage: "Leave empty to allow all domains. Separate multiple domains with a comma (',')",
60
+		},
61
+		&cli.BoolFlag{
62
+			Name:  "skip-local-2fa",
63
+			Usage: "Skip 2FA to log on.",
64
+		},
65
+		&cli.BoolFlag{
66
+			Name:  "active",
67
+			Usage: "This Authentication Source is Activated.",
68
+			Value: true,
69
+		},
70
+	}
71
+}
72
+
73
+func microcmdAuthUpdateSMTP() *cli.Command {
74
+	return &cli.Command{
75
+		Name:  "update-smtp",
76
+		Usage: "Update existing SMTP authentication source",
77
+		Action: func(ctx context.Context, cmd *cli.Command) error {
78
+			return newAuthService().runUpdateSMTP(ctx, cmd)
79
+		},
80
+		Flags: append(smtpCLIFlags()[:1], append([]cli.Flag{&cli.Int64Flag{
81
+			Name:  "id",
82
+			Usage: "ID of authentication source",
83
+		}}, smtpCLIFlags()[1:]...)...),
84
+	}
85
+}
86
+
87
+func microcmdAuthAddSMTP() *cli.Command {
88
+	return &cli.Command{
89
+		Name:  "add-smtp",
90
+		Usage: "Add new SMTP authentication source",
91
+		Action: func(ctx context.Context, cmd *cli.Command) error {
92
+			return newAuthService().runAddSMTP(ctx, cmd)
93
+		},
94
+		Flags: smtpCLIFlags(),
95
+	}
96
+}
97
+
98
+func parseSMTPConfig(c *cli.Command, conf *smtp.Source) error {
99
+	if c.IsSet("auth-type") {
100
+		conf.Auth = c.String("auth-type")
101
+		validAuthTypes := []string{"PLAIN", "LOGIN", "CRAM-MD5"}
102
+		if !util.SliceContainsString(validAuthTypes, strings.ToUpper(c.String("auth-type"))) {
103
+			return errors.New("Auth must be one of PLAIN/LOGIN/CRAM-MD5")
104
+		}
105
+		conf.Auth = c.String("auth-type")
106
+	}
107
+	if c.IsSet("host") {
108
+		conf.Host = c.String("host")
109
+	}
110
+	if c.IsSet("port") {
111
+		conf.Port = c.Int("port")
112
+	}
113
+	if c.IsSet("allowed-domains") {
114
+		conf.AllowedDomains = c.String("allowed-domains")
115
+	}
116
+	if c.IsSet("force-smtps") {
117
+		conf.ForceSMTPS = c.Bool("force-smtps")
118
+	}
119
+	if c.IsSet("skip-verify") {
120
+		conf.SkipVerify = c.Bool("skip-verify")
121
+	}
122
+	if c.IsSet("helo-hostname") {
123
+		conf.HeloHostname = c.String("helo-hostname")
124
+	}
125
+	if c.IsSet("disable-helo") {
126
+		conf.DisableHelo = c.Bool("disable-helo")
127
+	}
128
+	return nil
129
+}
130
+
131
+func (a *authService) runAddSMTP(ctx context.Context, c *cli.Command) error {
132
+	if err := a.initDB(ctx); err != nil {
133
+		return err
134
+	}
135
+
136
+	if !c.IsSet("name") || len(c.String("name")) == 0 {
137
+		return errors.New("name must be set")
138
+	}
139
+	if !c.IsSet("host") || len(c.String("host")) == 0 {
140
+		return errors.New("host must be set")
141
+	}
142
+	if !c.IsSet("port") {
143
+		return errors.New("port must be set")
144
+	}
145
+	active := true
146
+	if c.IsSet("active") {
147
+		active = c.Bool("active")
148
+	}
149
+
150
+	var smtpConfig smtp.Source
151
+	if err := parseSMTPConfig(c, &smtpConfig); err != nil {
152
+		return err
153
+	}
154
+
155
+	// If not set default to PLAIN
156
+	if len(smtpConfig.Auth) == 0 {
157
+		smtpConfig.Auth = "PLAIN"
158
+	}
159
+
160
+	return a.createAuthSource(ctx, &auth_model.Source{
161
+		Type:            auth_model.SMTP,
162
+		Name:            c.String("name"),
163
+		IsActive:        active,
164
+		Cfg:             &smtpConfig,
165
+		TwoFactorPolicy: util.Iif(c.Bool("skip-local-2fa"), "skip", ""),
166
+	})
167
+}
168
+
169
+func (a *authService) runUpdateSMTP(ctx context.Context, c *cli.Command) error {
170
+	if !c.IsSet("id") {
171
+		return errors.New("--id flag is missing")
172
+	}
173
+
174
+	if err := a.initDB(ctx); err != nil {
175
+		return err
176
+	}
177
+
178
+	source, err := a.getAuthSourceByID(ctx, c.Int64("id"))
179
+	if err != nil {
180
+		return err
181
+	}
182
+
183
+	smtpConfig := source.Cfg.(*smtp.Source)
184
+
185
+	if err := parseSMTPConfig(c, smtpConfig); err != nil {
186
+		return err
187
+	}
188
+
189
+	if c.IsSet("name") {
190
+		source.Name = c.String("name")
191
+	}
192
+
193
+	if c.IsSet("active") {
194
+		source.IsActive = c.Bool("active")
195
+	}
196
+
197
+	source.Cfg = smtpConfig
198
+	source.TwoFactorPolicy = util.Iif(c.Bool("skip-local-2fa"), "skip", "")
199
+	return a.updateAuthSource(ctx, source)
200
+}

+ 271
- 0
cmd/admin_auth_smtp_test.go 查看文件

@@ -0,0 +1,271 @@
1
+// Copyright 2025 The Gitea Authors. All rights reserved.
2
+// SPDX-License-Identifier: MIT
3
+
4
+package cmd
5
+
6
+import (
7
+	"context"
8
+	"testing"
9
+
10
+	auth_model "code.gitea.io/gitea/models/auth"
11
+	"code.gitea.io/gitea/services/auth/source/smtp"
12
+
13
+	"github.com/stretchr/testify/assert"
14
+	"github.com/urfave/cli/v3"
15
+)
16
+
17
+func TestAddSMTP(t *testing.T) {
18
+	testCases := []struct {
19
+		name   string
20
+		args   []string
21
+		source *auth_model.Source
22
+		errMsg string
23
+	}{
24
+		{
25
+			name: "missing name",
26
+			args: []string{
27
+				"--host", "localhost",
28
+				"--port", "25",
29
+			},
30
+			errMsg: "name must be set",
31
+		},
32
+		{
33
+			name: "missing host",
34
+			args: []string{
35
+				"--name", "test",
36
+				"--port", "25",
37
+			},
38
+			errMsg: "host must be set",
39
+		},
40
+		{
41
+			name: "missing port",
42
+			args: []string{
43
+				"--name", "test",
44
+				"--host", "localhost",
45
+			},
46
+			errMsg: "port must be set",
47
+		},
48
+		{
49
+			name: "valid config",
50
+			args: []string{
51
+				"--name", "test",
52
+				"--host", "localhost",
53
+				"--port", "25",
54
+			},
55
+			source: &auth_model.Source{
56
+				Type:     auth_model.SMTP,
57
+				Name:     "test",
58
+				IsActive: true,
59
+				Cfg: &smtp.Source{
60
+					Auth: "PLAIN",
61
+					Host: "localhost",
62
+					Port: 25,
63
+				},
64
+				TwoFactorPolicy: "",
65
+			},
66
+		},
67
+		{
68
+			name: "valid config with options",
69
+			args: []string{
70
+				"--name", "test",
71
+				"--host", "localhost",
72
+				"--port", "25",
73
+				"--auth-type", "LOGIN",
74
+				"--force-smtps",
75
+				"--skip-verify",
76
+				"--helo-hostname", "example.com",
77
+				"--disable-helo=true",
78
+				"--allowed-domains", "example.com,example.org",
79
+				"--skip-local-2fa",
80
+				"--active=false",
81
+			},
82
+			source: &auth_model.Source{
83
+				Type:     auth_model.SMTP,
84
+				Name:     "test",
85
+				IsActive: false,
86
+				Cfg: &smtp.Source{
87
+					Auth:           "LOGIN",
88
+					Host:           "localhost",
89
+					Port:           25,
90
+					ForceSMTPS:     true,
91
+					SkipVerify:     true,
92
+					HeloHostname:   "example.com",
93
+					DisableHelo:    true,
94
+					AllowedDomains: "example.com,example.org",
95
+				},
96
+				TwoFactorPolicy: "skip",
97
+			},
98
+		},
99
+	}
100
+
101
+	for _, tc := range testCases {
102
+		t.Run(tc.name, func(t *testing.T) {
103
+			a := &authService{
104
+				initDB: func(ctx context.Context) error {
105
+					return nil
106
+				},
107
+				createAuthSource: func(ctx context.Context, source *auth_model.Source) error {
108
+					assert.Equal(t, tc.source, source)
109
+					return nil
110
+				},
111
+			}
112
+
113
+			cmd := &cli.Command{
114
+				Flags:  microcmdAuthAddSMTP().Flags,
115
+				Action: a.runAddSMTP,
116
+			}
117
+
118
+			args := []string{"smtp-test"}
119
+			args = append(args, tc.args...)
120
+
121
+			t.Log(args)
122
+			err := cmd.Run(t.Context(), args)
123
+
124
+			if tc.errMsg != "" {
125
+				assert.EqualError(t, err, tc.errMsg)
126
+			} else {
127
+				assert.NoError(t, err)
128
+			}
129
+		})
130
+	}
131
+}
132
+
133
+func TestUpdateSMTP(t *testing.T) {
134
+	testCases := []struct {
135
+		name               string
136
+		args               []string
137
+		existingAuthSource *auth_model.Source
138
+		authSource         *auth_model.Source
139
+		errMsg             string
140
+	}{
141
+		{
142
+			name: "missing id",
143
+			args: []string{
144
+				"--name", "test",
145
+				"--host", "localhost",
146
+				"--port", "25",
147
+			},
148
+			errMsg: "--id flag is missing",
149
+		},
150
+		{
151
+			name: "valid config",
152
+			existingAuthSource: &auth_model.Source{
153
+				ID:       1,
154
+				Type:     auth_model.SMTP,
155
+				Name:     "old name",
156
+				IsActive: true,
157
+				Cfg: &smtp.Source{
158
+					Auth: "PLAIN",
159
+					Host: "old host",
160
+					Port: 26,
161
+				},
162
+			},
163
+			args: []string{
164
+				"--id", "1",
165
+				"--name", "test",
166
+				"--host", "localhost",
167
+				"--port", "25",
168
+			},
169
+			authSource: &auth_model.Source{
170
+				ID:       1,
171
+				Type:     auth_model.SMTP,
172
+				Name:     "test",
173
+				IsActive: true,
174
+				Cfg: &smtp.Source{
175
+					Auth: "PLAIN",
176
+					Host: "localhost",
177
+					Port: 25,
178
+				},
179
+			},
180
+		},
181
+		{
182
+			name: "valid config with options",
183
+			existingAuthSource: &auth_model.Source{
184
+				ID:       1,
185
+				Type:     auth_model.SMTP,
186
+				Name:     "old name",
187
+				IsActive: true,
188
+				Cfg: &smtp.Source{
189
+					Auth:           "PLAIN",
190
+					Host:           "old host",
191
+					Port:           26,
192
+					HeloHostname:   "old.example.com",
193
+					AllowedDomains: "old.example.com",
194
+				},
195
+				TwoFactorPolicy: "",
196
+			},
197
+			args: []string{
198
+				"--id", "1",
199
+				"--name", "test",
200
+				"--host", "localhost",
201
+				"--port", "25",
202
+				"--auth-type", "LOGIN",
203
+				"--force-smtps",
204
+				"--skip-verify",
205
+				"--helo-hostname", "example.com",
206
+				"--disable-helo",
207
+				"--allowed-domains", "example.com,example.org",
208
+				"--skip-local-2fa",
209
+				"--active=false",
210
+			},
211
+			authSource: &auth_model.Source{
212
+				ID:       1,
213
+				Type:     auth_model.SMTP,
214
+				Name:     "test",
215
+				IsActive: false,
216
+				Cfg: &smtp.Source{
217
+					Auth:           "LOGIN",
218
+					Host:           "localhost",
219
+					Port:           25,
220
+					ForceSMTPS:     true,
221
+					SkipVerify:     true,
222
+					HeloHostname:   "example.com",
223
+					DisableHelo:    true,
224
+					AllowedDomains: "example.com,example.org",
225
+				},
226
+				TwoFactorPolicy: "skip",
227
+			},
228
+		},
229
+	}
230
+
231
+	for _, tc := range testCases {
232
+		t.Run(tc.name, func(t *testing.T) {
233
+			a := &authService{
234
+				initDB: func(ctx context.Context) error {
235
+					return nil
236
+				},
237
+				getAuthSourceByID: func(ctx context.Context, id int64) (*auth_model.Source, error) {
238
+					return &auth_model.Source{
239
+						ID:       1,
240
+						Type:     auth_model.SMTP,
241
+						Name:     "test",
242
+						IsActive: true,
243
+						Cfg: &smtp.Source{
244
+							Auth: "PLAIN",
245
+						},
246
+					}, nil
247
+				},
248
+
249
+				updateAuthSource: func(ctx context.Context, source *auth_model.Source) error {
250
+					assert.Equal(t, tc.authSource, source)
251
+					return nil
252
+				},
253
+			}
254
+
255
+			app := &cli.Command{
256
+				Flags:  microcmdAuthUpdateSMTP().Flags,
257
+				Action: a.runUpdateSMTP,
258
+			}
259
+			args := []string{"smtp-tests"}
260
+			args = append(args, tc.args...)
261
+
262
+			err := app.Run(t.Context(), args)
263
+
264
+			if tc.errMsg != "" {
265
+				assert.EqualError(t, err, tc.errMsg)
266
+			} else {
267
+				assert.NoError(t, err)
268
+			}
269
+		})
270
+	}
271
+}

+ 42
- 0
cmd/admin_regenerate.go 查看文件

@@ -0,0 +1,42 @@
1
+// Copyright 2023 The Gitea Authors. All rights reserved.
2
+// SPDX-License-Identifier: MIT
3
+
4
+package cmd
5
+
6
+import (
7
+	"context"
8
+
9
+	"code.gitea.io/gitea/modules/graceful"
10
+	asymkey_service "code.gitea.io/gitea/services/asymkey"
11
+	repo_service "code.gitea.io/gitea/services/repository"
12
+
13
+	"github.com/urfave/cli/v3"
14
+)
15
+
16
+var (
17
+	microcmdRegenHooks = &cli.Command{
18
+		Name:   "hooks",
19
+		Usage:  "Regenerate git-hooks",
20
+		Action: runRegenerateHooks,
21
+	}
22
+
23
+	microcmdRegenKeys = &cli.Command{
24
+		Name:   "keys",
25
+		Usage:  "Regenerate authorized_keys file",
26
+		Action: runRegenerateKeys,
27
+	}
28
+)
29
+
30
+func runRegenerateHooks(ctx context.Context, _ *cli.Command) error {
31
+	if err := initDB(ctx); err != nil {
32
+		return err
33
+	}
34
+	return repo_service.SyncRepositoryHooks(graceful.GetManager().ShutdownContext())
35
+}
36
+
37
+func runRegenerateKeys(ctx context.Context, _ *cli.Command) error {
38
+	if err := initDB(ctx); err != nil {
39
+		return err
40
+	}
41
+	return asymkey_service.RewriteAllPublicKeys(ctx)
42
+}

+ 21
- 0
cmd/admin_user.go 查看文件

@@ -0,0 +1,21 @@
1
+// Copyright 2023 The Gitea Authors. All rights reserved.
2
+// SPDX-License-Identifier: MIT
3
+
4
+package cmd
5
+
6
+import (
7
+	"github.com/urfave/cli/v3"
8
+)
9
+
10
+var subcmdUser = &cli.Command{
11
+	Name:  "user",
12
+	Usage: "Modify users",
13
+	Commands: []*cli.Command{
14
+		microcmdUserCreate(),
15
+		microcmdUserList,
16
+		microcmdUserChangePassword(),
17
+		microcmdUserDelete(),
18
+		microcmdUserGenerateAccessToken,
19
+		microcmdUserMustChangePassword(),
20
+	},
21
+}

+ 78
- 0
cmd/admin_user_change_password.go 查看文件

@@ -0,0 +1,78 @@
1
+// Copyright 2023 The Gitea Authors. All rights reserved.
2
+// SPDX-License-Identifier: MIT
3
+
4
+package cmd
5
+
6
+import (
7
+	"context"
8
+	"errors"
9
+	"fmt"
10
+
11
+	user_model "code.gitea.io/gitea/models/user"
12
+	"code.gitea.io/gitea/modules/auth/password"
13
+	"code.gitea.io/gitea/modules/optional"
14
+	"code.gitea.io/gitea/modules/setting"
15
+	user_service "code.gitea.io/gitea/services/user"
16
+
17
+	"github.com/urfave/cli/v3"
18
+)
19
+
20
+func microcmdUserChangePassword() *cli.Command {
21
+	return &cli.Command{
22
+		Name:   "change-password",
23
+		Usage:  "Change a user's password",
24
+		Action: runChangePassword,
25
+		Flags: []cli.Flag{
26
+			&cli.StringFlag{
27
+				Name:     "username",
28
+				Aliases:  []string{"u"},
29
+				Usage:    "The user to change password for",
30
+				Required: true,
31
+			},
32
+			&cli.StringFlag{
33
+				Name:     "password",
34
+				Aliases:  []string{"p"},
35
+				Usage:    "New password to set for user",
36
+				Required: true,
37
+			},
38
+			&cli.BoolFlag{
39
+				Name:  "must-change-password",
40
+				Usage: "User must change password (can be disabled by --must-change-password=false)",
41
+				Value: true,
42
+			},
43
+		},
44
+	}
45
+}
46
+
47
+func runChangePassword(ctx context.Context, c *cli.Command) error {
48
+	if !setting.IsInTesting {
49
+		if err := initDB(ctx); err != nil {
50
+			return err
51
+		}
52
+	}
53
+
54
+	user, err := user_model.GetUserByName(ctx, c.String("username"))
55
+	if err != nil {
56
+		return err
57
+	}
58
+
59
+	opts := &user_service.UpdateAuthOptions{
60
+		Password:           optional.Some(c.String("password")),
61
+		MustChangePassword: optional.Some(c.Bool("must-change-password")),
62
+	}
63
+	if err := user_service.UpdateAuth(ctx, user, opts); err != nil {
64
+		switch {
65
+		case errors.Is(err, password.ErrMinLength):
66
+			return fmt.Errorf("password is not long enough, needs to be at least %d characters", setting.MinPasswordLength)
67
+		case errors.Is(err, password.ErrComplexity):
68
+			return errors.New("password does not meet complexity requirements")
69
+		case errors.Is(err, password.ErrIsPwned):
70
+			return errors.New("the password is in a list of stolen passwords previously exposed in public data breaches, please try again with a different password, to see more details: https://haveibeenpwned.com/Passwords")
71
+		default:
72
+			return err
73
+		}
74
+	}
75
+
76
+	fmt.Printf("%s's password has been successfully updated!\n", user.Name)
77
+	return nil
78
+}

+ 91
- 0
cmd/admin_user_change_password_test.go 查看文件

@@ -0,0 +1,91 @@
1
+// Copyright 2025 The Gitea Authors. All rights reserved.
2
+// SPDX-License-Identifier: MIT
3
+
4
+package cmd
5
+
6
+import (
7
+	"testing"
8
+
9
+	"code.gitea.io/gitea/models/db"
10
+	"code.gitea.io/gitea/models/unittest"
11
+	user_model "code.gitea.io/gitea/models/user"
12
+
13
+	"github.com/stretchr/testify/assert"
14
+	"github.com/stretchr/testify/require"
15
+)
16
+
17
+func TestChangePasswordCommand(t *testing.T) {
18
+	ctx := t.Context()
19
+
20
+	defer func() {
21
+		require.NoError(t, db.TruncateBeans(t.Context(), &user_model.User{}))
22
+	}()
23
+
24
+	t.Run("change password successfully", func(t *testing.T) {
25
+		// defer func() {
26
+		// 	require.NoError(t, db.TruncateBeans(t.Context(), &user_model.User{}))
27
+		// }()
28
+		// Prepare test user
29
+		unittest.AssertNotExistsBean(t, &user_model.User{LowerName: "testuser"})
30
+		err := microcmdUserCreate().Run(ctx, []string{"create", "--username", "testuser", "--email", "testuser@gitea.local", "--random-password"})
31
+		require.NoError(t, err)
32
+
33
+		// load test user
34
+		userBase := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
35
+
36
+		// Change the password
37
+		err = microcmdUserChangePassword().Run(ctx, []string{"change-password", "--username", "testuser", "--password", "newpassword"})
38
+		require.NoError(t, err)
39
+
40
+		// Verify the password has been changed
41
+		user := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
42
+		assert.NotEqual(t, userBase.Passwd, user.Passwd)
43
+		assert.NotEqual(t, userBase.Salt, user.Salt)
44
+
45
+		// Additional check for must-change-password flag
46
+		require.NoError(t, microcmdUserChangePassword().Run(ctx, []string{"change-password", "--username", "testuser", "--password", "anotherpassword", "--must-change-password=false"}))
47
+		user = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
48
+		assert.False(t, user.MustChangePassword)
49
+
50
+		require.NoError(t, microcmdUserChangePassword().Run(ctx, []string{"change-password", "--username", "testuser", "--password", "yetanotherpassword", "--must-change-password"}))
51
+		user = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
52
+		assert.True(t, user.MustChangePassword)
53
+	})
54
+
55
+	t.Run("failure cases", func(t *testing.T) {
56
+		testCases := []struct {
57
+			name        string
58
+			args        []string
59
+			expectedErr string
60
+		}{
61
+			{
62
+				name:        "user does not exist",
63
+				args:        []string{"change-password", "--username", "nonexistentuser", "--password", "newpassword"},
64
+				expectedErr: "user does not exist",
65
+			},
66
+			{
67
+				name:        "missing username",
68
+				args:        []string{"change-password", "--password", "newpassword"},
69
+				expectedErr: `"username" not set`,
70
+			},
71
+			{
72
+				name:        "missing password",
73
+				args:        []string{"change-password", "--username", "testuser"},
74
+				expectedErr: `"password" not set`,
75
+			},
76
+			{
77
+				name:        "too short password",
78
+				args:        []string{"change-password", "--username", "testuser", "--password", "1"},
79
+				expectedErr: "password is not long enough",
80
+			},
81
+		}
82
+
83
+		for _, tc := range testCases {
84
+			t.Run(tc.name, func(t *testing.T) {
85
+				err := microcmdUserChangePassword().Run(ctx, tc.args)
86
+				require.Error(t, err)
87
+				require.Contains(t, err.Error(), tc.expectedErr)
88
+			})
89
+		}
90
+	})
91
+}

+ 240
- 0
cmd/admin_user_create.go 查看文件

@@ -0,0 +1,240 @@
1
+// Copyright 2023 The Gitea Authors. All rights reserved.
2
+// SPDX-License-Identifier: MIT
3
+
4
+package cmd
5
+
6
+import (
7
+	"context"
8
+	"errors"
9
+	"fmt"
10
+	"strings"
11
+
12
+	auth_model "code.gitea.io/gitea/models/auth"
13
+	"code.gitea.io/gitea/models/db"
14
+	user_model "code.gitea.io/gitea/models/user"
15
+	pwd "code.gitea.io/gitea/modules/auth/password"
16
+	"code.gitea.io/gitea/modules/optional"
17
+	"code.gitea.io/gitea/modules/setting"
18
+
19
+	"github.com/urfave/cli/v3"
20
+)
21
+
22
+func microcmdUserCreate() *cli.Command {
23
+	return &cli.Command{
24
+		Name:   "create",
25
+		Usage:  "Create a new user in database",
26
+		Action: runCreateUser,
27
+		MutuallyExclusiveFlags: []cli.MutuallyExclusiveFlags{
28
+			{
29
+				Flags: [][]cli.Flag{
30
+					{
31
+						&cli.StringFlag{
32
+							Name:  "name",
33
+							Usage: "Username. DEPRECATED: use username instead",
34
+						},
35
+						&cli.StringFlag{
36
+							Name:  "username",
37
+							Usage: "Username",
38
+						},
39
+					},
40
+				},
41
+				Required: true,
42
+			},
43
+		},
44
+		Flags: []cli.Flag{
45
+			&cli.StringFlag{
46
+				Name:  "user-type",
47
+				Usage: "Set user's type: individual or bot",
48
+				Value: "individual",
49
+			},
50
+			&cli.StringFlag{
51
+				Name:  "password",
52
+				Usage: "User password",
53
+			},
54
+			&cli.StringFlag{
55
+				Name:     "email",
56
+				Usage:    "User email address",
57
+				Required: true,
58
+			},
59
+			&cli.BoolFlag{
60
+				Name:  "admin",
61
+				Usage: "User is an admin",
62
+			},
63
+			&cli.BoolFlag{
64
+				Name:  "random-password",
65
+				Usage: "Generate a random password for the user",
66
+			},
67
+			&cli.BoolFlag{
68
+				Name:        "must-change-password",
69
+				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)",
70
+				HideDefault: true,
71
+			},
72
+			&cli.IntFlag{
73
+				Name:  "random-password-length",
74
+				Usage: "Length of the random password to be generated",
75
+				Value: 12,
76
+			},
77
+			&cli.BoolFlag{
78
+				Name:  "access-token",
79
+				Usage: "Generate access token for the user",
80
+			},
81
+			&cli.StringFlag{
82
+				Name:  "access-token-name",
83
+				Usage: `Name of the generated access token`,
84
+				Value: "gitea-admin",
85
+			},
86
+			&cli.StringFlag{
87
+				Name:  "access-token-scopes",
88
+				Usage: `Scopes of the generated access token, comma separated. Examples: "all", "public-only,read:issue", "write:repository,write:user"`,
89
+				Value: "all",
90
+			},
91
+			&cli.BoolFlag{
92
+				Name:  "restricted",
93
+				Usage: "Make a restricted user account",
94
+			},
95
+			&cli.StringFlag{
96
+				Name:  "fullname",
97
+				Usage: `The full, human-readable name of the user`,
98
+			},
99
+		},
100
+	}
101
+}
102
+
103
+func runCreateUser(ctx context.Context, c *cli.Command) error {
104
+	// this command highly depends on the many setting options (create org, visibility, etc.), so it must have a full setting load first
105
+	// duplicate setting loading should be safe at the moment, but it should be refactored & improved in the future.
106
+	setting.LoadSettings()
107
+
108
+	userTypes := map[string]user_model.UserType{
109
+		"individual": user_model.UserTypeIndividual,
110
+		"bot":        user_model.UserTypeBot,
111
+	}
112
+	userType, ok := userTypes[c.String("user-type")]
113
+	if !ok {
114
+		return fmt.Errorf("invalid user type: %s", c.String("user-type"))
115
+	}
116
+	if userType != user_model.UserTypeIndividual {
117
+		// Some other commands like "change-password" also only support individual users.
118
+		// It needs to clarify the "password" behavior for bot users in the future.
119
+		// At the moment, we do not allow setting password for bot users.
120
+		if c.IsSet("password") || c.IsSet("random-password") {
121
+			return errors.New("password can only be set for individual users")
122
+		}
123
+	}
124
+
125
+	if c.IsSet("password") && c.IsSet("random-password") {
126
+		return errors.New("cannot set both -random-password and -password flags")
127
+	}
128
+
129
+	var username string
130
+	if c.IsSet("username") {
131
+		username = c.String("username")
132
+	} else {
133
+		username = c.String("name")
134
+		_, _ = fmt.Fprintf(c.ErrWriter, "--name flag is deprecated. Use --username instead.\n")
135
+	}
136
+
137
+	if !setting.IsInTesting {
138
+		// FIXME: need to refactor the "initDB" related code later
139
+		// it doesn't make sense to call it in (almost) every command action function
140
+		if err := initDB(ctx); err != nil {
141
+			return err
142
+		}
143
+	}
144
+
145
+	var password string
146
+	if c.IsSet("password") {
147
+		password = c.String("password")
148
+	} else if c.IsSet("random-password") {
149
+		var err error
150
+		password, err = pwd.Generate(c.Int("random-password-length"))
151
+		if err != nil {
152
+			return err
153
+		}
154
+		fmt.Printf("generated random password is '%s'\n", password)
155
+	} else if userType == user_model.UserTypeIndividual {
156
+		return errors.New("must set either password or random-password flag")
157
+	}
158
+
159
+	isAdmin := c.Bool("admin")
160
+	mustChangePassword := true // always default to true
161
+	if c.IsSet("must-change-password") {
162
+		if userType != user_model.UserTypeIndividual {
163
+			return errors.New("must-change-password flag can only be set for individual users")
164
+		}
165
+		// if the flag is set, use the value provided by the user
166
+		mustChangePassword = c.Bool("must-change-password")
167
+	} else if userType == user_model.UserTypeIndividual {
168
+		// check whether there are users in the database
169
+		hasUserRecord, err := db.IsTableNotEmpty(&user_model.User{})
170
+		if err != nil {
171
+			return fmt.Errorf("IsTableNotEmpty: %w", err)
172
+		}
173
+		if !hasUserRecord {
174
+			// if this is the first one being created, don't force to change password (keep the old behavior)
175
+			mustChangePassword = false
176
+		}
177
+	}
178
+
179
+	restricted := optional.None[bool]()
180
+
181
+	if c.IsSet("restricted") {
182
+		restricted = optional.Some(c.Bool("restricted"))
183
+	}
184
+
185
+	// default user visibility in app.ini
186
+	visibility := setting.Service.DefaultUserVisibilityMode
187
+
188
+	u := &user_model.User{
189
+		Name:               username,
190
+		Email:              c.String("email"),
191
+		IsAdmin:            isAdmin,
192
+		Type:               userType,
193
+		Passwd:             password,
194
+		MustChangePassword: mustChangePassword,
195
+		Visibility:         visibility,
196
+		FullName:           c.String("fullname"),
197
+	}
198
+
199
+	overwriteDefault := &user_model.CreateUserOverwriteOptions{
200
+		IsActive:     optional.Some(true),
201
+		IsRestricted: restricted,
202
+	}
203
+
204
+	var accessTokenName string
205
+	var accessTokenScope auth_model.AccessTokenScope
206
+	if c.IsSet("access-token") {
207
+		accessTokenName = strings.TrimSpace(c.String("access-token-name"))
208
+		if accessTokenName == "" {
209
+			return errors.New("access-token-name cannot be empty")
210
+		}
211
+		var err error
212
+		accessTokenScope, err = auth_model.AccessTokenScope(c.String("access-token-scopes")).Normalize()
213
+		if err != nil {
214
+			return fmt.Errorf("invalid access token scope provided: %w", err)
215
+		}
216
+		if !accessTokenScope.HasPermissionScope() {
217
+			return errors.New("access token does not have any permission")
218
+		}
219
+	} else if c.IsSet("access-token-name") || c.IsSet("access-token-scopes") {
220
+		return errors.New("access-token-name and access-token-scopes flags are only valid when access-token flag is set")
221
+	}
222
+
223
+	// arguments should be prepared before creating the user & access token, in case there is anything wrong
224
+
225
+	// create the user
226
+	if err := user_model.CreateUser(ctx, u, &user_model.Meta{}, overwriteDefault); err != nil {
227
+		return fmt.Errorf("CreateUser: %w", err)
228
+	}
229
+	fmt.Printf("New user '%s' has been successfully created!\n", username)
230
+
231
+	// create the access token
232
+	if accessTokenScope != "" {
233
+		t := &auth_model.AccessToken{Name: accessTokenName, UID: u.ID, Scope: accessTokenScope}
234
+		if err := auth_model.NewAccessToken(ctx, t); err != nil {
235
+			return err
236
+		}
237
+		fmt.Printf("Access token was successfully created... %s\n", t.Token)
238
+	}
239
+	return nil
240
+}

+ 134
- 0
cmd/admin_user_create_test.go 查看文件

@@ -0,0 +1,134 @@
1
+// Copyright 2023 The Gitea Authors. All rights reserved.
2
+// SPDX-License-Identifier: MIT
3
+
4
+package cmd
5
+
6
+import (
7
+	"fmt"
8
+	"strings"
9
+	"testing"
10
+
11
+	auth_model "code.gitea.io/gitea/models/auth"
12
+	"code.gitea.io/gitea/models/db"
13
+	"code.gitea.io/gitea/models/unittest"
14
+	user_model "code.gitea.io/gitea/models/user"
15
+
16
+	"github.com/stretchr/testify/assert"
17
+	"github.com/stretchr/testify/require"
18
+)
19
+
20
+func TestAdminUserCreate(t *testing.T) {
21
+	reset := func() {
22
+		require.NoError(t, db.TruncateBeans(t.Context(), &user_model.User{}))
23
+		require.NoError(t, db.TruncateBeans(t.Context(), &user_model.EmailAddress{}))
24
+		require.NoError(t, db.TruncateBeans(t.Context(), &auth_model.AccessToken{}))
25
+	}
26
+
27
+	t.Run("MustChangePassword", func(t *testing.T) {
28
+		type check struct {
29
+			IsAdmin            bool
30
+			MustChangePassword bool
31
+		}
32
+
33
+		createCheck := func(name, args string) check {
34
+			require.NoError(t, microcmdUserCreate().Run(t.Context(), strings.Fields(fmt.Sprintf("create --username %s --email %s@gitea.local %s --password foobar", name, name, args))))
35
+			u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: name})
36
+			return check{IsAdmin: u.IsAdmin, MustChangePassword: u.MustChangePassword}
37
+		}
38
+		reset()
39
+		assert.Equal(t, check{IsAdmin: false, MustChangePassword: false}, createCheck("u", ""), "first non-admin user doesn't need to change password")
40
+
41
+		reset()
42
+		assert.Equal(t, check{IsAdmin: true, MustChangePassword: false}, createCheck("u", "--admin"), "first admin user doesn't need to change password")
43
+
44
+		reset()
45
+		assert.Equal(t, check{IsAdmin: true, MustChangePassword: true}, createCheck("u", "--admin --must-change-password"))
46
+		assert.Equal(t, check{IsAdmin: true, MustChangePassword: true}, createCheck("u2", "--admin"))
47
+		assert.Equal(t, check{IsAdmin: true, MustChangePassword: false}, createCheck("u3", "--admin --must-change-password=false"))
48
+		assert.Equal(t, check{IsAdmin: false, MustChangePassword: true}, createCheck("u4", ""))
49
+		assert.Equal(t, check{IsAdmin: false, MustChangePassword: false}, createCheck("u5", "--must-change-password=false"))
50
+	})
51
+
52
+	createUser := func(name string, args ...string) error {
53
+		return microcmdUserCreate().Run(t.Context(), append([]string{"create", "--username", name, "--email", name + "@gitea.local"}, args...))
54
+	}
55
+
56
+	t.Run("UserType", func(t *testing.T) {
57
+		reset()
58
+		assert.ErrorContains(t, createUser("u", "--user-type", "invalid"), "invalid user type")
59
+		assert.ErrorContains(t, createUser("u", "--user-type", "bot", "--password", "123"), "can only be set for individual users")
60
+		assert.ErrorContains(t, createUser("u", "--user-type", "bot", "--must-change-password"), "can only be set for individual users")
61
+
62
+		assert.NoError(t, createUser("u", "--user-type", "bot"))
63
+		u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "u"})
64
+		assert.Equal(t, user_model.UserTypeBot, u.Type)
65
+		assert.Empty(t, u.Passwd)
66
+	})
67
+
68
+	t.Run("AccessToken", func(t *testing.T) {
69
+		// no generated access token
70
+		reset()
71
+		assert.NoError(t, createUser("u", "--random-password"))
72
+		assert.Equal(t, 1, unittest.GetCount(t, &user_model.User{}))
73
+		assert.Equal(t, 0, unittest.GetCount(t, &auth_model.AccessToken{}))
74
+
75
+		// using "--access-token" only means "all" access
76
+		reset()
77
+		assert.NoError(t, createUser("u", "--random-password", "--access-token"))
78
+		assert.Equal(t, 1, unittest.GetCount(t, &user_model.User{}))
79
+		assert.Equal(t, 1, unittest.GetCount(t, &auth_model.AccessToken{}))
80
+		accessToken := unittest.AssertExistsAndLoadBean(t, &auth_model.AccessToken{Name: "gitea-admin"})
81
+		hasScopes, err := accessToken.Scope.HasScope(auth_model.AccessTokenScopeWriteAdmin, auth_model.AccessTokenScopeWriteRepository)
82
+		assert.NoError(t, err)
83
+		assert.True(t, hasScopes)
84
+
85
+		// using "--access-token" with name & scopes
86
+		reset()
87
+		assert.NoError(t, createUser("u", "--random-password", "--access-token", "--access-token-name", "new-token-name", "--access-token-scopes", "read:issue,read:user"))
88
+		assert.Equal(t, 1, unittest.GetCount(t, &user_model.User{}))
89
+		assert.Equal(t, 1, unittest.GetCount(t, &auth_model.AccessToken{}))
90
+		accessToken = unittest.AssertExistsAndLoadBean(t, &auth_model.AccessToken{Name: "new-token-name"})
91
+		hasScopes, err = accessToken.Scope.HasScope(auth_model.AccessTokenScopeReadIssue, auth_model.AccessTokenScopeReadUser)
92
+		assert.NoError(t, err)
93
+		assert.True(t, hasScopes)
94
+		hasScopes, err = accessToken.Scope.HasScope(auth_model.AccessTokenScopeWriteAdmin, auth_model.AccessTokenScopeWriteRepository)
95
+		assert.NoError(t, err)
96
+		assert.False(t, hasScopes)
97
+
98
+		// using "--access-token-name" without "--access-token"
99
+		reset()
100
+		err = createUser("u", "--random-password", "--access-token-name", "new-token-name")
101
+		assert.Equal(t, 0, unittest.GetCount(t, &user_model.User{}))
102
+		assert.Equal(t, 0, unittest.GetCount(t, &auth_model.AccessToken{}))
103
+		assert.ErrorContains(t, err, "access-token-name and access-token-scopes flags are only valid when access-token flag is set")
104
+
105
+		// using "--access-token-scopes" without "--access-token"
106
+		reset()
107
+		err = createUser("u", "--random-password", "--access-token-scopes", "read:issue")
108
+		assert.Equal(t, 0, unittest.GetCount(t, &user_model.User{}))
109
+		assert.Equal(t, 0, unittest.GetCount(t, &auth_model.AccessToken{}))
110
+		assert.ErrorContains(t, err, "access-token-name and access-token-scopes flags are only valid when access-token flag is set")
111
+
112
+		// empty permission
113
+		reset()
114
+		err = createUser("u", "--random-password", "--access-token", "--access-token-scopes", "public-only")
115
+		assert.Equal(t, 0, unittest.GetCount(t, &user_model.User{}))
116
+		assert.Equal(t, 0, unittest.GetCount(t, &auth_model.AccessToken{}))
117
+		assert.ErrorContains(t, err, "access token does not have any permission")
118
+	})
119
+
120
+	t.Run("UserFields", func(t *testing.T) {
121
+		reset()
122
+		assert.NoError(t, createUser("u-FullNameWithSpace", "--random-password", "--fullname", "First O'Middle Last"))
123
+		unittest.AssertExistsAndLoadBean(t, &user_model.User{
124
+			Name:      "u-FullNameWithSpace",
125
+			LowerName: "u-fullnamewithspace",
126
+			FullName:  "First O'Middle Last",
127
+			Email:     "u-FullNameWithSpace@gitea.local",
128
+		})
129
+
130
+		assert.NoError(t, createUser("u-FullNameEmpty", "--random-password", "--fullname", ""))
131
+		u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "u-fullnameempty"})
132
+		assert.Empty(t, u.FullName)
133
+	})
134
+}

+ 84
- 0
cmd/admin_user_delete.go 查看文件

@@ -0,0 +1,84 @@
1
+// Copyright 2023 The Gitea Authors. All rights reserved.
2
+// SPDX-License-Identifier: MIT
3
+
4
+package cmd
5
+
6
+import (
7
+	"context"
8
+	"errors"
9
+	"fmt"
10
+	"strings"
11
+
12
+	user_model "code.gitea.io/gitea/models/user"
13
+	"code.gitea.io/gitea/modules/setting"
14
+	"code.gitea.io/gitea/modules/storage"
15
+	user_service "code.gitea.io/gitea/services/user"
16
+
17
+	"github.com/urfave/cli/v3"
18
+)
19
+
20
+func microcmdUserDelete() *cli.Command {
21
+	return &cli.Command{
22
+		Name:  "delete",
23
+		Usage: "Delete specific user by id, name or email",
24
+		Flags: []cli.Flag{
25
+			&cli.Int64Flag{
26
+				Name:  "id",
27
+				Usage: "ID of user of the user to delete",
28
+			},
29
+			&cli.StringFlag{
30
+				Name:    "username",
31
+				Aliases: []string{"u"},
32
+				Usage:   "Username of the user to delete",
33
+			},
34
+			&cli.StringFlag{
35
+				Name:    "email",
36
+				Aliases: []string{"e"},
37
+				Usage:   "Email of the user to delete",
38
+			},
39
+			&cli.BoolFlag{
40
+				Name:  "purge",
41
+				Usage: "Purge user, all their repositories, organizations and comments",
42
+			},
43
+		},
44
+		Action: runDeleteUser,
45
+	}
46
+}
47
+
48
+func runDeleteUser(ctx context.Context, c *cli.Command) error {
49
+	if !c.IsSet("id") && !c.IsSet("username") && !c.IsSet("email") {
50
+		return errors.New("You must provide the id, username or email of a user to delete")
51
+	}
52
+
53
+	if !setting.IsInTesting {
54
+		if err := initDB(ctx); err != nil {
55
+			return err
56
+		}
57
+	}
58
+
59
+	if err := storage.Init(); err != nil {
60
+		return err
61
+	}
62
+
63
+	var err error
64
+	var user *user_model.User
65
+	if c.IsSet("email") {
66
+		user, err = user_model.GetUserByEmail(ctx, c.String("email"))
67
+	} else if c.IsSet("username") {
68
+		user, err = user_model.GetUserByName(ctx, c.String("username"))
69
+	} else {
70
+		user, err = user_model.GetUserByID(ctx, c.Int64("id"))
71
+	}
72
+	if err != nil {
73
+		return err
74
+	}
75
+	if c.IsSet("username") && user.LowerName != strings.ToLower(strings.TrimSpace(c.String("username"))) {
76
+		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"))
77
+	}
78
+
79
+	if c.IsSet("id") && user.ID != c.Int64("id") {
80
+		return fmt.Errorf("the user %s does not match the provided id %d", user.Name, c.Int64("id"))
81
+	}
82
+
83
+	return user_service.DeleteUser(ctx, user, c.Bool("purge"))
84
+}

+ 111
- 0
cmd/admin_user_delete_test.go 查看文件

@@ -0,0 +1,111 @@
1
+// Copyright 2025 The Gitea Authors. All rights reserved.
2
+// SPDX-License-Identifier: MIT
3
+
4
+package cmd
5
+
6
+import (
7
+	"strconv"
8
+	"strings"
9
+	"testing"
10
+
11
+	auth_model "code.gitea.io/gitea/models/auth"
12
+	"code.gitea.io/gitea/models/db"
13
+	"code.gitea.io/gitea/models/unittest"
14
+	user_model "code.gitea.io/gitea/models/user"
15
+
16
+	"github.com/stretchr/testify/require"
17
+)
18
+
19
+func TestAdminUserDelete(t *testing.T) {
20
+	ctx := t.Context()
21
+	defer func() {
22
+		require.NoError(t, db.TruncateBeans(t.Context(), &user_model.User{}))
23
+		require.NoError(t, db.TruncateBeans(t.Context(), &user_model.EmailAddress{}))
24
+		require.NoError(t, db.TruncateBeans(t.Context(), &auth_model.AccessToken{}))
25
+	}()
26
+
27
+	setupTestUser := func(t *testing.T) {
28
+		unittest.AssertNotExistsBean(t, &user_model.User{LowerName: "testuser"})
29
+		err := microcmdUserCreate().Run(t.Context(), []string{"create", "--username", "testuser", "--email", "testuser@gitea.local", "--random-password"})
30
+		require.NoError(t, err)
31
+	}
32
+
33
+	t.Run("delete user by id", func(t *testing.T) {
34
+		setupTestUser(t)
35
+
36
+		u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
37
+		err := microcmdUserDelete().Run(ctx, []string{"delete-test", "--id", strconv.FormatInt(u.ID, 10)})
38
+		require.NoError(t, err)
39
+		unittest.AssertNotExistsBean(t, &user_model.User{LowerName: "testuser"})
40
+	})
41
+	t.Run("delete user by username", func(t *testing.T) {
42
+		setupTestUser(t)
43
+
44
+		err := microcmdUserDelete().Run(ctx, []string{"delete-test", "--username", "testuser"})
45
+		require.NoError(t, err)
46
+		unittest.AssertNotExistsBean(t, &user_model.User{LowerName: "testuser"})
47
+	})
48
+	t.Run("delete user by email", func(t *testing.T) {
49
+		setupTestUser(t)
50
+
51
+		err := microcmdUserDelete().Run(ctx, []string{"delete-test", "--email", "testuser@gitea.local"})
52
+		require.NoError(t, err)
53
+		unittest.AssertNotExistsBean(t, &user_model.User{LowerName: "testuser"})
54
+	})
55
+	t.Run("delete user by all 3 attributes", func(t *testing.T) {
56
+		setupTestUser(t)
57
+
58
+		u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
59
+		err := microcmdUserDelete().Run(ctx, []string{"delete", "--id", strconv.FormatInt(u.ID, 10), "--username", "testuser", "--email", "testuser@gitea.local"})
60
+		require.NoError(t, err)
61
+		unittest.AssertNotExistsBean(t, &user_model.User{LowerName: "testuser"})
62
+	})
63
+}
64
+
65
+func TestAdminUserDeleteFailure(t *testing.T) {
66
+	testCases := []struct {
67
+		name        string
68
+		args        []string
69
+		expectedErr string
70
+	}{
71
+		{
72
+			name:        "no user to delete",
73
+			args:        []string{"delete", "--username", "nonexistentuser"},
74
+			expectedErr: "user does not exist",
75
+		},
76
+		{
77
+			name:        "user exists but provided username does not match",
78
+			args:        []string{"delete", "--email", "testuser@gitea.local", "--username", "wrongusername"},
79
+			expectedErr: "the user testuser who has email testuser@gitea.local does not match the provided username wrongusername",
80
+		},
81
+		{
82
+			name:        "user exists but provided id does not match",
83
+			args:        []string{"delete", "--username", "testuser", "--id", "999"},
84
+			expectedErr: "the user testuser does not match the provided id 999",
85
+		},
86
+		{
87
+			name:        "no required flags are provided",
88
+			args:        []string{"delete"},
89
+			expectedErr: "You must provide the id, username or email of a user to delete",
90
+		},
91
+	}
92
+
93
+	for _, tc := range testCases {
94
+		t.Run(tc.name, func(t *testing.T) {
95
+			ctx := t.Context()
96
+			if strings.Contains(tc.name, "user exists") {
97
+				unittest.AssertNotExistsBean(t, &user_model.User{LowerName: "testuser"})
98
+				err := microcmdUserCreate().Run(t.Context(), []string{"create", "--username", "testuser", "--email", "testuser@gitea.local", "--random-password"})
99
+				require.NoError(t, err)
100
+			}
101
+
102
+			err := microcmdUserDelete().Run(ctx, tc.args)
103
+			require.Error(t, err)
104
+			require.Contains(t, err.Error(), tc.expectedErr)
105
+		})
106
+
107
+		require.NoError(t, db.TruncateBeans(t.Context(), &user_model.User{}))
108
+		require.NoError(t, db.TruncateBeans(t.Context(), &user_model.EmailAddress{}))
109
+		require.NoError(t, db.TruncateBeans(t.Context(), &auth_model.AccessToken{}))
110
+	}
111
+}

+ 95
- 0
cmd/admin_user_generate_access_token.go 查看文件

@@ -0,0 +1,95 @@
1
+// Copyright 2023 The Gitea Authors. All rights reserved.
2
+// SPDX-License-Identifier: MIT
3
+
4
+package cmd
5
+
6
+import (
7
+	"context"
8
+	"errors"
9
+	"fmt"
10
+
11
+	auth_model "code.gitea.io/gitea/models/auth"
12
+	user_model "code.gitea.io/gitea/models/user"
13
+
14
+	"github.com/urfave/cli/v3"
15
+)
16
+
17
+var microcmdUserGenerateAccessToken = &cli.Command{
18
+	Name:  "generate-access-token",
19
+	Usage: "Generate an access token for a specific user",
20
+	Flags: []cli.Flag{
21
+		&cli.StringFlag{
22
+			Name:    "username",
23
+			Aliases: []string{"u"},
24
+			Usage:   "Username",
25
+		},
26
+		&cli.StringFlag{
27
+			Name:    "token-name",
28
+			Aliases: []string{"t"},
29
+			Usage:   "Token name",
30
+			Value:   "gitea-admin",
31
+		},
32
+		&cli.BoolFlag{
33
+			Name:  "raw",
34
+			Usage: "Display only the token value",
35
+		},
36
+		&cli.StringFlag{
37
+			Name:  "scopes",
38
+			Value: "all",
39
+			Usage: `Comma separated list of scopes to apply to access token, examples: "all", "public-only,read:issue", "write:repository,write:user"`,
40
+		},
41
+	},
42
+	Action: runGenerateAccessToken,
43
+}
44
+
45
+func runGenerateAccessToken(ctx context.Context, c *cli.Command) error {
46
+	if !c.IsSet("username") {
47
+		return errors.New("you must provide a username to generate a token for")
48
+	}
49
+
50
+	if err := initDB(ctx); err != nil {
51
+		return err
52
+	}
53
+
54
+	user, err := user_model.GetUserByName(ctx, c.String("username"))
55
+	if err != nil {
56
+		return err
57
+	}
58
+
59
+	// construct token with name and user so we can make sure it is unique
60
+	t := &auth_model.AccessToken{
61
+		Name: c.String("token-name"),
62
+		UID:  user.ID,
63
+	}
64
+
65
+	exist, err := auth_model.AccessTokenByNameExists(ctx, t)
66
+	if err != nil {
67
+		return err
68
+	}
69
+	if exist {
70
+		return errors.New("access token name has been used already")
71
+	}
72
+
73
+	// make sure the scopes are valid
74
+	accessTokenScope, err := auth_model.AccessTokenScope(c.String("scopes")).Normalize()
75
+	if err != nil {
76
+		return fmt.Errorf("invalid access token scope provided: %w", err)
77
+	}
78
+	if !accessTokenScope.HasPermissionScope() {
79
+		return errors.New("access token does not have any permission")
80
+	}
81
+	t.Scope = accessTokenScope
82
+
83
+	// create the token
84
+	if err := auth_model.NewAccessToken(ctx, t); err != nil {
85
+		return err
86
+	}
87
+
88
+	if c.Bool("raw") {
89
+		fmt.Printf("%s\n", t.Token)
90
+	} else {
91
+		fmt.Printf("Access token was successfully created: %s\n", t.Token)
92
+	}
93
+
94
+	return nil
95
+}

+ 58
- 0
cmd/admin_user_list.go 查看文件

@@ -0,0 +1,58 @@
1
+// Copyright 2023 The Gitea Authors. All rights reserved.
2
+// SPDX-License-Identifier: MIT
3
+
4
+package cmd
5
+
6
+import (
7
+	"context"
8
+	"fmt"
9
+	"os"
10
+	"text/tabwriter"
11
+
12
+	user_model "code.gitea.io/gitea/models/user"
13
+
14
+	"github.com/urfave/cli/v3"
15
+)
16
+
17
+var microcmdUserList = &cli.Command{
18
+	Name:   "list",
19
+	Usage:  "List users",
20
+	Action: runListUsers,
21
+	Flags: []cli.Flag{
22
+		&cli.BoolFlag{
23
+			Name:  "admin",
24
+			Usage: "List only admin users",
25
+		},
26
+	},
27
+}
28
+
29
+func runListUsers(ctx context.Context, c *cli.Command) error {
30
+	if err := initDB(ctx); err != nil {
31
+		return err
32
+	}
33
+
34
+	users, err := user_model.GetAllUsers(ctx)
35
+	if err != nil {
36
+		return err
37
+	}
38
+
39
+	w := tabwriter.NewWriter(os.Stdout, 5, 0, 1, ' ', 0)
40
+
41
+	if c.IsSet("admin") {
42
+		fmt.Fprintf(w, "ID\tUsername\tEmail\tIsActive\n")
43
+		for _, u := range users {
44
+			if u.IsAdmin {
45
+				fmt.Fprintf(w, "%d\t%s\t%s\t%t\n", u.ID, u.Name, u.Email, u.IsActive)
46
+			}
47
+		}
48
+	} else {
49
+		twofa := user_model.UserList(users).GetTwoFaStatus(ctx)
50
+		fmt.Fprintf(w, "ID\tUsername\tEmail\tIsActive\tIsAdmin\t2FA\n")
51
+		for _, u := range users {
52
+			fmt.Fprintf(w, "%d\t%s\t%s\t%t\t%t\t%t\n", u.ID, u.Name, u.Email, u.IsActive, u.IsAdmin, twofa[u.ID])
53
+		}
54
+	}
55
+
56
+	w.Flush()
57
+	return nil
58
+}

+ 63
- 0
cmd/admin_user_must_change_password.go 查看文件

@@ -0,0 +1,63 @@
1
+// Copyright 2023 The Gitea Authors. All rights reserved.
2
+// SPDX-License-Identifier: MIT
3
+
4
+package cmd
5
+
6
+import (
7
+	"context"
8
+	"errors"
9
+	"fmt"
10
+
11
+	user_model "code.gitea.io/gitea/models/user"
12
+	"code.gitea.io/gitea/modules/setting"
13
+
14
+	"github.com/urfave/cli/v3"
15
+)
16
+
17
+func microcmdUserMustChangePassword() *cli.Command {
18
+	return &cli.Command{
19
+		Name:   "must-change-password",
20
+		Usage:  "Set the must change password flag for the provided users or all users",
21
+		Action: runMustChangePassword,
22
+		Flags: []cli.Flag{
23
+			&cli.BoolFlag{
24
+				Name:    "all",
25
+				Aliases: []string{"A"},
26
+				Usage:   "All users must change password, except those explicitly excluded with --exclude",
27
+			},
28
+			&cli.StringSliceFlag{
29
+				Name:    "exclude",
30
+				Aliases: []string{"e"},
31
+				Usage:   "Do not change the must-change-password flag for these users",
32
+			},
33
+			&cli.BoolFlag{
34
+				Name:  "unset",
35
+				Usage: "Instead of setting the must-change-password flag, unset it",
36
+			},
37
+		},
38
+	}
39
+}
40
+
41
+func runMustChangePassword(ctx context.Context, c *cli.Command) error {
42
+	if c.NArg() == 0 && !c.IsSet("all") {
43
+		return errors.New("either usernames or --all must be provided")
44
+	}
45
+
46
+	mustChangePassword := !c.Bool("unset")
47
+	all := c.Bool("all")
48
+	exclude := c.StringSlice("exclude")
49
+
50
+	if !setting.IsInTesting {
51
+		if err := initDB(ctx); err != nil {
52
+			return err
53
+		}
54
+	}
55
+
56
+	n, err := user_model.SetMustChangePassword(ctx, all, mustChangePassword, c.Args().Slice(), exclude)
57
+	if err != nil {
58
+		return err
59
+	}
60
+
61
+	fmt.Printf("Updated %d users setting MustChangePassword to %t\n", n, mustChangePassword)
62
+	return nil
63
+}

+ 78
- 0
cmd/admin_user_must_change_password_test.go 查看文件

@@ -0,0 +1,78 @@
1
+// Copyright 2025 The Gitea Authors. All rights reserved.
2
+// SPDX-License-Identifier: MIT
3
+
4
+package cmd
5
+
6
+import (
7
+	"testing"
8
+
9
+	"code.gitea.io/gitea/models/db"
10
+	"code.gitea.io/gitea/models/unittest"
11
+	user_model "code.gitea.io/gitea/models/user"
12
+
13
+	"github.com/stretchr/testify/assert"
14
+	"github.com/stretchr/testify/require"
15
+)
16
+
17
+func TestMustChangePassword(t *testing.T) {
18
+	defer func() {
19
+		require.NoError(t, db.TruncateBeans(t.Context(), &user_model.User{}))
20
+	}()
21
+	err := microcmdUserCreate().Run(t.Context(), []string{"create", "--username", "testuser", "--email", "testuser@gitea.local", "--random-password"})
22
+	require.NoError(t, err)
23
+	err = microcmdUserCreate().Run(t.Context(), []string{"create", "--username", "testuserexclude", "--email", "testuserexclude@gitea.local", "--random-password"})
24
+	require.NoError(t, err)
25
+	// Reset password change flag
26
+	err = microcmdUserMustChangePassword().Run(t.Context(), []string{"change-test", "--all", "--unset"})
27
+	require.NoError(t, err)
28
+
29
+	testUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
30
+	assert.False(t, testUser.MustChangePassword)
31
+	testUserExclude := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuserexclude"})
32
+	assert.False(t, testUserExclude.MustChangePassword)
33
+
34
+	// Make all users change password
35
+	err = microcmdUserMustChangePassword().Run(t.Context(), []string{"change-test", "--all"})
36
+	require.NoError(t, err)
37
+
38
+	testUser = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
39
+	assert.True(t, testUser.MustChangePassword)
40
+	testUserExclude = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuserexclude"})
41
+	assert.True(t, testUserExclude.MustChangePassword)
42
+
43
+	// Reset password change flag but exclude all tested users
44
+	err = microcmdUserMustChangePassword().Run(t.Context(), []string{"change-test", "--all", "--unset", "--exclude", "testuser,testuserexclude"})
45
+	require.NoError(t, err)
46
+
47
+	testUser = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
48
+	assert.True(t, testUser.MustChangePassword)
49
+	testUserExclude = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuserexclude"})
50
+	assert.True(t, testUserExclude.MustChangePassword)
51
+
52
+	// Reset password change flag by listing multiple users
53
+	err = microcmdUserMustChangePassword().Run(t.Context(), []string{"change-test", "--unset", "testuser", "testuserexclude"})
54
+	require.NoError(t, err)
55
+
56
+	testUser = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
57
+	assert.False(t, testUser.MustChangePassword)
58
+	testUserExclude = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuserexclude"})
59
+	assert.False(t, testUserExclude.MustChangePassword)
60
+
61
+	// Exclude a user from all user
62
+	err = microcmdUserMustChangePassword().Run(t.Context(), []string{"change-test", "--all", "--exclude", "testuserexclude"})
63
+	require.NoError(t, err)
64
+
65
+	testUser = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
66
+	assert.True(t, testUser.MustChangePassword)
67
+	testUserExclude = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuserexclude"})
68
+	assert.False(t, testUserExclude.MustChangePassword)
69
+
70
+	// Unset a flag for single user
71
+	err = microcmdUserMustChangePassword().Run(t.Context(), []string{"change-test", "--unset", "testuser"})
72
+	require.NoError(t, err)
73
+
74
+	testUser = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
75
+	assert.False(t, testUser.MustChangePassword)
76
+	testUserExclude = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuserexclude"})
77
+	assert.False(t, testUserExclude.MustChangePassword)
78
+}

+ 206
- 0
cmd/cert.go 查看文件

@@ -0,0 +1,206 @@
1
+// Copyright 2009 The Go Authors. All rights reserved.
2
+// Copyright 2014 The Gogs Authors. All rights reserved.
3
+// Copyright 2016 The Gitea Authors. All rights reserved.
4
+// SPDX-License-Identifier: MIT
5
+
6
+package cmd
7
+
8
+import (
9
+	"context"
10
+	"crypto/ecdsa"
11
+	"crypto/elliptic"
12
+	"crypto/rand"
13
+	"crypto/rsa"
14
+	"crypto/x509"
15
+	"crypto/x509/pkix"
16
+	"encoding/pem"
17
+	"fmt"
18
+	"log"
19
+	"math/big"
20
+	"net"
21
+	"os"
22
+	"strings"
23
+	"time"
24
+
25
+	"github.com/urfave/cli/v3"
26
+)
27
+
28
+// cmdCert represents the available cert sub-command.
29
+func cmdCert() *cli.Command {
30
+	return &cli.Command{
31
+		Name:  "cert",
32
+		Usage: "Generate self-signed certificate",
33
+		Description: `Generate a self-signed X.509 certificate for a TLS server.
34
+Outputs to 'cert.pem' and 'key.pem' and will overwrite existing files.`,
35
+		Action: runCert,
36
+		Flags: []cli.Flag{
37
+			&cli.StringFlag{
38
+				Name:     "host",
39
+				Usage:    "Comma-separated hostnames and IPs to generate a certificate for",
40
+				Required: true,
41
+			},
42
+			&cli.StringFlag{
43
+				Name:  "ecdsa-curve",
44
+				Value: "",
45
+				Usage: "ECDSA curve to use to generate a key. Valid values are P224, P256, P384, P521",
46
+			},
47
+			&cli.IntFlag{
48
+				Name:  "rsa-bits",
49
+				Value: 3072,
50
+				Usage: "Size of RSA key to generate. Ignored if --ecdsa-curve is set",
51
+			},
52
+			&cli.StringFlag{
53
+				Name:  "start-date",
54
+				Value: "",
55
+				Usage: "Creation date formatted as Jan 1 15:04:05 2011",
56
+			},
57
+			&cli.DurationFlag{
58
+				Name:  "duration",
59
+				Value: 365 * 24 * time.Hour,
60
+				Usage: "Duration that certificate is valid for",
61
+			},
62
+			&cli.BoolFlag{
63
+				Name:  "ca",
64
+				Usage: "whether this cert should be its own Certificate Authority",
65
+			},
66
+			&cli.StringFlag{
67
+				Name:  "out",
68
+				Value: "cert.pem",
69
+				Usage: "Path to the file where there certificate will be saved",
70
+			},
71
+			&cli.StringFlag{
72
+				Name:  "keyout",
73
+				Value: "key.pem",
74
+				Usage: "Path to the file where there certificate key will be saved",
75
+			},
76
+		},
77
+	}
78
+}
79
+
80
+func publicKey(priv any) any {
81
+	switch k := priv.(type) {
82
+	case *rsa.PrivateKey:
83
+		return &k.PublicKey
84
+	case *ecdsa.PrivateKey:
85
+		return &k.PublicKey
86
+	default:
87
+		return nil
88
+	}
89
+}
90
+
91
+func pemBlockForKey(priv any) *pem.Block {
92
+	switch k := priv.(type) {
93
+	case *rsa.PrivateKey:
94
+		return &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(k)}
95
+	case *ecdsa.PrivateKey:
96
+		b, err := x509.MarshalECPrivateKey(k)
97
+		if err != nil {
98
+			log.Fatalf("Unable to marshal ECDSA private key: %v", err)
99
+		}
100
+		return &pem.Block{Type: "EC PRIVATE KEY", Bytes: b}
101
+	default:
102
+		return nil
103
+	}
104
+}
105
+
106
+func runCert(_ context.Context, c *cli.Command) error {
107
+	var priv any
108
+	var err error
109
+	switch c.String("ecdsa-curve") {
110
+	case "":
111
+		priv, err = rsa.GenerateKey(rand.Reader, c.Int("rsa-bits"))
112
+	case "P224":
113
+		priv, err = ecdsa.GenerateKey(elliptic.P224(), rand.Reader)
114
+	case "P256":
115
+		priv, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
116
+	case "P384":
117
+		priv, err = ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
118
+	case "P521":
119
+		priv, err = ecdsa.GenerateKey(elliptic.P521(), rand.Reader)
120
+	default:
121
+		err = fmt.Errorf("unrecognized elliptic curve: %q", c.String("ecdsa-curve"))
122
+	}
123
+	if err != nil {
124
+		return fmt.Errorf("failed to generate private key: %w", err)
125
+	}
126
+
127
+	var notBefore time.Time
128
+	if startDate := c.String("start-date"); startDate != "" {
129
+		notBefore, err = time.Parse("Jan 2 15:04:05 2006", startDate)
130
+		if err != nil {
131
+			return fmt.Errorf("failed to parse creation date %w", err)
132
+		}
133
+	} else {
134
+		notBefore = time.Now()
135
+	}
136
+
137
+	notAfter := notBefore.Add(c.Duration("duration"))
138
+
139
+	serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
140
+	serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
141
+	if err != nil {
142
+		return fmt.Errorf("failed to generate serial number: %w", err)
143
+	}
144
+
145
+	template := x509.Certificate{
146
+		SerialNumber: serialNumber,
147
+		Subject: pkix.Name{
148
+			Organization: []string{"Acme Co"},
149
+			CommonName:   "Gitea",
150
+		},
151
+		NotBefore: notBefore,
152
+		NotAfter:  notAfter,
153
+
154
+		KeyUsage:              x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
155
+		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
156
+		BasicConstraintsValid: true,
157
+	}
158
+
159
+	hosts := strings.SplitSeq(c.String("host"), ",")
160
+	for h := range hosts {
161
+		if ip := net.ParseIP(h); ip != nil {
162
+			template.IPAddresses = append(template.IPAddresses, ip)
163
+		} else {
164
+			template.DNSNames = append(template.DNSNames, h)
165
+		}
166
+	}
167
+
168
+	if c.Bool("ca") {
169
+		template.IsCA = true
170
+		template.KeyUsage |= x509.KeyUsageCertSign
171
+	}
172
+
173
+	derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, publicKey(priv), priv)
174
+	if err != nil {
175
+		return fmt.Errorf("failed to create certificate: %w", err)
176
+	}
177
+
178
+	certOut, err := os.Create(c.String("out"))
179
+	if err != nil {
180
+		return fmt.Errorf("failed to open %s for writing: %w", c.String("keyout"), err)
181
+	}
182
+	err = pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
183
+	if err != nil {
184
+		return fmt.Errorf("failed to encode certificate: %w", err)
185
+	}
186
+	err = certOut.Close()
187
+	if err != nil {
188
+		return fmt.Errorf("failed to write cert: %w", err)
189
+	}
190
+	fmt.Fprintf(c.Writer, "Written cert to %s\n", c.String("out"))
191
+
192
+	keyOut, err := os.OpenFile(c.String("keyout"), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600)
193
+	if err != nil {
194
+		return fmt.Errorf("failed to open %s for writing: %w", c.String("keyout"), err)
195
+	}
196
+	err = pem.Encode(keyOut, pemBlockForKey(priv))
197
+	if err != nil {
198
+		return fmt.Errorf("failed to encode key: %w", err)
199
+	}
200
+	err = keyOut.Close()
201
+	if err != nil {
202
+		return fmt.Errorf("failed to write key: %w", err)
203
+	}
204
+	fmt.Fprintf(c.Writer, "Written key to %s\n", c.String("keyout"))
205
+	return nil
206
+}

+ 123
- 0
cmd/cert_test.go 查看文件

@@ -0,0 +1,123 @@
1
+// Copyright 2025 The Gitea Authors. All rights reserved.
2
+// SPDX-License-Identifier: MIT
3
+
4
+package cmd
5
+
6
+import (
7
+	"path/filepath"
8
+	"testing"
9
+
10
+	"github.com/stretchr/testify/assert"
11
+	"github.com/stretchr/testify/require"
12
+)
13
+
14
+func TestCertCommand(t *testing.T) {
15
+	cases := []struct {
16
+		name string
17
+		args []string
18
+	}{
19
+		{
20
+			name: "RSA cert generation",
21
+			args: []string{
22
+				"cert-test",
23
+				"--host", "localhost",
24
+				"--rsa-bits", "2048",
25
+				"--duration", "1h",
26
+				"--start-date", "Jan 1 00:00:00 2024",
27
+			},
28
+		},
29
+		{
30
+			name: "ECDSA cert generation",
31
+			args: []string{
32
+				"cert-test",
33
+				"--host", "localhost",
34
+				"--ecdsa-curve", "P256",
35
+				"--duration", "1h",
36
+				"--start-date", "Jan 1 00:00:00 2024",
37
+			},
38
+		},
39
+		{
40
+			name: "mixed host, certificate authority",
41
+			args: []string{
42
+				"cert-test",
43
+				"--host", "localhost,127.0.0.1",
44
+				"--duration", "1h",
45
+				"--start-date", "Jan 1 00:00:00 2024",
46
+			},
47
+		},
48
+	}
49
+
50
+	for _, c := range cases {
51
+		t.Run(c.name, func(t *testing.T) {
52
+			app := cmdCert()
53
+			tempDir := t.TempDir()
54
+
55
+			certFile := filepath.Join(tempDir, "cert.pem")
56
+			keyFile := filepath.Join(tempDir, "key.pem")
57
+
58
+			err := app.Run(t.Context(), append(c.args, "--out", certFile, "--keyout", keyFile))
59
+			require.NoError(t, err)
60
+
61
+			assert.FileExists(t, certFile)
62
+			assert.FileExists(t, keyFile)
63
+		})
64
+	}
65
+}
66
+
67
+func TestCertCommandFailures(t *testing.T) {
68
+	cases := []struct {
69
+		name   string
70
+		args   []string
71
+		errMsg string
72
+	}{
73
+		{
74
+			name: "Start Date Parsing failure",
75
+			args: []string{
76
+				"cert-test",
77
+				"--host", "localhost",
78
+				"--start-date", "invalid-date",
79
+			},
80
+			errMsg: "parsing time",
81
+		},
82
+		{
83
+			name: "Unknown curve",
84
+			args: []string{
85
+				"cert-test",
86
+				"--host", "localhost",
87
+				"--ecdsa-curve", "invalid-curve",
88
+			},
89
+			errMsg: "unrecognized elliptic curve",
90
+		},
91
+		{
92
+			name: "Key generation failure",
93
+			args: []string{
94
+				"cert-test",
95
+				"--host", "localhost",
96
+				"--rsa-bits", "invalid-bits",
97
+			},
98
+		},
99
+		{
100
+			name: "Missing parameters",
101
+			args: []string{
102
+				"cert-test",
103
+			},
104
+			errMsg: `"host" not set`,
105
+		},
106
+	}
107
+	for _, c := range cases {
108
+		t.Run(c.name, func(t *testing.T) {
109
+			app := cmdCert()
110
+			tempDir := t.TempDir()
111
+
112
+			certFile := filepath.Join(tempDir, "cert.pem")
113
+			keyFile := filepath.Join(tempDir, "key.pem")
114
+			err := app.Run(t.Context(), append(c.args, "--out", certFile, "--keyout", keyFile))
115
+			require.Error(t, err)
116
+			if c.errMsg != "" {
117
+				assert.ErrorContains(t, err, c.errMsg)
118
+			}
119
+			assert.NoFileExists(t, certFile)
120
+			assert.NoFileExists(t, keyFile)
121
+		})
122
+	}
123
+}

+ 144
- 0
cmd/cmd.go 查看文件

@@ -0,0 +1,144 @@
1
+// Copyright 2018 The Gitea Authors. All rights reserved.
2
+// SPDX-License-Identifier: MIT
3
+
4
+// Package cmd provides subcommands to the gitea binary - such as "web" or
5
+// "admin".
6
+package cmd
7
+
8
+import (
9
+	"context"
10
+	"errors"
11
+	"fmt"
12
+	"io"
13
+	"os"
14
+	"os/signal"
15
+	"strings"
16
+	"syscall"
17
+
18
+	"code.gitea.io/gitea/models/db"
19
+	"code.gitea.io/gitea/modules/log"
20
+	"code.gitea.io/gitea/modules/setting"
21
+
22
+	"github.com/urfave/cli/v3"
23
+)
24
+
25
+// argsSet checks that all the required arguments are set. args is a list of
26
+// arguments that must be set in the passed Context.
27
+func argsSet(c *cli.Command, args ...string) error {
28
+	for _, a := range args {
29
+		if !c.IsSet(a) {
30
+			return errors.New(a + " is not set")
31
+		}
32
+
33
+		if c.Value(a) == nil {
34
+			return errors.New(a + " is required")
35
+		}
36
+	}
37
+	return nil
38
+}
39
+
40
+// confirm waits for user input which confirms an action
41
+func confirm() (bool, error) {
42
+	var response string
43
+
44
+	_, err := fmt.Scanln(&response)
45
+	if err != nil {
46
+		return false, err
47
+	}
48
+
49
+	switch strings.ToLower(response) {
50
+	case "y", "yes":
51
+		return true, nil
52
+	case "n", "no":
53
+		return false, nil
54
+	default:
55
+		return false, errors.New(response + " isn't a correct confirmation string")
56
+	}
57
+}
58
+
59
+func initDB(ctx context.Context) error {
60
+	setting.MustInstalled()
61
+	setting.LoadDBSetting()
62
+	setting.InitSQLLoggersForCli(log.INFO)
63
+
64
+	if setting.Database.Type == "" {
65
+		log.Fatal(`Database settings are missing from the configuration file: %q.
66
+Ensure you are running in the correct environment or set the correct configuration file with -c.
67
+If this is the intended configuration file complete the [database] section.`, setting.CustomConf)
68
+	}
69
+	if err := db.InitEngine(ctx); err != nil {
70
+		return fmt.Errorf("unable to initialize the database using the configuration in %q. Error: %w", setting.CustomConf, err)
71
+	}
72
+	return nil
73
+}
74
+
75
+func installSignals() (context.Context, context.CancelFunc) {
76
+	ctx, cancel := context.WithCancel(context.Background())
77
+	go func() {
78
+		// install notify
79
+		signalChannel := make(chan os.Signal, 1)
80
+
81
+		signal.Notify(
82
+			signalChannel,
83
+			syscall.SIGINT,
84
+			syscall.SIGTERM,
85
+		)
86
+		select {
87
+		case <-signalChannel:
88
+		case <-ctx.Done():
89
+		}
90
+		cancel()
91
+		signal.Reset()
92
+	}()
93
+
94
+	return ctx, cancel
95
+}
96
+
97
+func setupConsoleLogger(level log.Level, colorize bool, out io.Writer) {
98
+	if out != os.Stdout && out != os.Stderr {
99
+		panic("setupConsoleLogger can only be used with os.Stdout or os.Stderr")
100
+	}
101
+
102
+	writeMode := log.WriterMode{
103
+		Level:        level,
104
+		Colorize:     colorize,
105
+		WriterOption: log.WriterConsoleOption{Stderr: out == os.Stderr},
106
+	}
107
+	writer := log.NewEventWriterConsole("console-default", writeMode)
108
+	log.GetManager().GetLogger(log.DEFAULT).ReplaceAllWriters(writer)
109
+}
110
+
111
+func globalBool(c *cli.Command, name string) bool {
112
+	for _, ctx := range c.Lineage() {
113
+		if ctx.Bool(name) {
114
+			return true
115
+		}
116
+	}
117
+	return false
118
+}
119
+
120
+// PrepareConsoleLoggerLevel by default, use INFO level for console logger, but some sub-commands (for git/ssh protocol) shouldn't output any log to stdout.
121
+// Any log appears in git stdout pipe will break the git protocol, eg: client can't push and hangs forever.
122
+func PrepareConsoleLoggerLevel(defaultLevel log.Level) func(context.Context, *cli.Command) (context.Context, error) {
123
+	return func(ctx context.Context, c *cli.Command) (context.Context, error) {
124
+		level := defaultLevel
125
+		if globalBool(c, "quiet") {
126
+			level = log.FATAL
127
+		}
128
+		if globalBool(c, "debug") || globalBool(c, "verbose") {
129
+			level = log.TRACE
130
+		}
131
+		log.SetConsoleLogger(log.DEFAULT, "console-default", level)
132
+		return ctx, nil
133
+	}
134
+}
135
+
136
+func isValidDefaultSubCommand(cmd *cli.Command) (string, bool) {
137
+	// Dirty patch for urfave/cli's strange design.
138
+	// "./gitea bad-cmd" should not start the web server.
139
+	rootArgs := cmd.Root().Args().Slice()
140
+	if len(rootArgs) != 0 && rootArgs[0] != cmd.Name {
141
+		return rootArgs[0], false
142
+	}
143
+	return "", true
144
+}

+ 38
- 0
cmd/cmd_test.go 查看文件

@@ -0,0 +1,38 @@
1
+// Copyright 2025 The Gitea Authors. All rights reserved.
2
+// SPDX-License-Identifier: MIT
3
+
4
+package cmd
5
+
6
+import (
7
+	"context"
8
+	"testing"
9
+
10
+	"github.com/stretchr/testify/assert"
11
+	"github.com/urfave/cli/v3"
12
+)
13
+
14
+func TestDefaultCommand(t *testing.T) {
15
+	test := func(t *testing.T, args []string, expectedRetName string, expectedRetValid bool) {
16
+		called := false
17
+		cmd := &cli.Command{
18
+			DefaultCommand: "test",
19
+			Commands: []*cli.Command{
20
+				{
21
+					Name: "test",
22
+					Action: func(ctx context.Context, command *cli.Command) error {
23
+						retName, retValid := isValidDefaultSubCommand(command)
24
+						assert.Equal(t, expectedRetName, retName)
25
+						assert.Equal(t, expectedRetValid, retValid)
26
+						called = true
27
+						return nil
28
+					},
29
+				},
30
+			},
31
+		}
32
+		assert.NoError(t, cmd.Run(t.Context(), args))
33
+		assert.True(t, called)
34
+	}
35
+	test(t, []string{"./gitea"}, "", true)
36
+	test(t, []string{"./gitea", "test"}, "", true)
37
+	test(t, []string{"./gitea", "other"}, "other", false)
38
+}

+ 67
- 0
cmd/docs.go 查看文件

@@ -0,0 +1,67 @@
1
+// Copyright 2020 The Gitea Authors. All rights reserved.
2
+// SPDX-License-Identifier: MIT
3
+
4
+package cmd
5
+
6
+import (
7
+	"context"
8
+	"fmt"
9
+	"os"
10
+	"strings"
11
+
12
+	cli_docs "github.com/urfave/cli-docs/v3"
13
+	"github.com/urfave/cli/v3"
14
+)
15
+
16
+// CmdDocs represents the available docs sub-command.
17
+var CmdDocs = &cli.Command{
18
+	Name:        "docs",
19
+	Usage:       "Output CLI documentation",
20
+	Description: "A command to output Gitea's CLI documentation, optionally to a file.",
21
+	Action:      runDocs,
22
+	Flags: []cli.Flag{
23
+		&cli.BoolFlag{
24
+			Name:  "man",
25
+			Usage: "Output man pages instead",
26
+		},
27
+		&cli.StringFlag{
28
+			Name:    "output",
29
+			Aliases: []string{"o"},
30
+			Usage:   "Path to output to instead of stdout (will overwrite if exists)",
31
+		},
32
+	},
33
+}
34
+
35
+func runDocs(_ context.Context, cmd *cli.Command) error {
36
+	docs, err := cli_docs.ToMarkdown(cmd.Root())
37
+	if cmd.Bool("man") {
38
+		docs, err = cli_docs.ToMan(cmd.Root())
39
+	}
40
+	if err != nil {
41
+		return err
42
+	}
43
+
44
+	if !cmd.Bool("man") {
45
+		// Clean up markdown. The following bug was fixed in v2, but is present in v1.
46
+		// It affects markdown output (even though the issue is referring to man pages)
47
+		// https://github.com/urfave/cli/issues/1040
48
+		firstHashtagIndex := strings.Index(docs, "#")
49
+
50
+		if firstHashtagIndex > 0 {
51
+			docs = docs[firstHashtagIndex:]
52
+		}
53
+	}
54
+
55
+	out := os.Stdout
56
+	if cmd.String("output") != "" {
57
+		fi, err := os.Create(cmd.String("output"))
58
+		if err != nil {
59
+			return err
60
+		}
61
+		defer fi.Close()
62
+		out = fi
63
+	}
64
+
65
+	_, err = fmt.Fprintln(out, docs)
66
+	return err
67
+}

+ 215
- 0
cmd/doctor.go 查看文件

@@ -0,0 +1,215 @@
1
+// Copyright 2019 The Gitea Authors. All rights reserved.
2
+// SPDX-License-Identifier: MIT
3
+
4
+package cmd
5
+
6
+import (
7
+	"context"
8
+	"fmt"
9
+	golog "log"
10
+	"os"
11
+	"path/filepath"
12
+	"strings"
13
+	"text/tabwriter"
14
+
15
+	"code.gitea.io/gitea/models/db"
16
+	"code.gitea.io/gitea/models/migrations"
17
+	migrate_base "code.gitea.io/gitea/models/migrations/base"
18
+	"code.gitea.io/gitea/modules/container"
19
+	"code.gitea.io/gitea/modules/log"
20
+	"code.gitea.io/gitea/modules/setting"
21
+	"code.gitea.io/gitea/services/doctor"
22
+
23
+	"github.com/urfave/cli/v3"
24
+	"xorm.io/xorm"
25
+)
26
+
27
+// CmdDoctor represents the available doctor sub-command.
28
+var CmdDoctor = &cli.Command{
29
+	Name:        "doctor",
30
+	Usage:       "Diagnose and optionally fix problems, convert or re-create database tables",
31
+	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.",
32
+
33
+	Commands: []*cli.Command{
34
+		cmdDoctorCheck,
35
+		cmdRecreateTable,
36
+		cmdDoctorConvert,
37
+	},
38
+}
39
+
40
+var cmdDoctorCheck = &cli.Command{
41
+	Name:        "check",
42
+	Usage:       "Diagnose and optionally fix problems",
43
+	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.",
44
+	Action:      runDoctorCheck,
45
+	Flags: []cli.Flag{
46
+		&cli.BoolFlag{
47
+			Name:  "list",
48
+			Usage: "List the available checks",
49
+		},
50
+		&cli.BoolFlag{
51
+			Name:  "default",
52
+			Usage: "Run the default checks (if neither --run or --all is set, this is the default behaviour)",
53
+		},
54
+		&cli.StringSliceFlag{
55
+			Name:  "run",
56
+			Usage: "Run the provided checks - (if --default is set, the default checks will also run)",
57
+		},
58
+		&cli.BoolFlag{
59
+			Name:  "all",
60
+			Usage: "Run all the available checks",
61
+		},
62
+		&cli.BoolFlag{
63
+			Name:  "fix",
64
+			Usage: "Automatically fix what we can",
65
+		},
66
+		&cli.StringFlag{
67
+			Name:  "log-file",
68
+			Usage: `Name of the log file (no verbose log output by default). Set to "-" to output to stdout`,
69
+		},
70
+		&cli.BoolFlag{
71
+			Name:    "color",
72
+			Aliases: []string{"H"},
73
+			Usage:   "Use color for outputted information",
74
+		},
75
+	},
76
+}
77
+
78
+var cmdRecreateTable = &cli.Command{
79
+	Name:      "recreate-table",
80
+	Usage:     "Recreate tables from XORM definitions and copy the data.",
81
+	ArgsUsage: "[TABLE]... : (TABLEs to recreate - leave blank for all)",
82
+	Flags: []cli.Flag{
83
+		&cli.BoolFlag{
84
+			Name:  "debug",
85
+			Usage: "Print SQL commands sent",
86
+		},
87
+	},
88
+	Description: `The database definitions Gitea uses change across versions, sometimes changing default values and leaving old unused columns.
89
+
90
+This command will cause Xorm to recreate tables, copying over the data and deleting the old table.
91
+
92
+You should back-up your database before doing this and ensure that your database is up-to-date first.`,
93
+	Action: runRecreateTable,
94
+}
95
+
96
+func runRecreateTable(ctx context.Context, cmd *cli.Command) error {
97
+	// Redirect the default golog to here
98
+	golog.SetFlags(0)
99
+	golog.SetPrefix("")
100
+	golog.SetOutput(log.LoggerToWriter(log.GetLogger(log.DEFAULT).Info))
101
+
102
+	debug := cmd.Bool("debug")
103
+	setting.MustInstalled()
104
+	setting.LoadDBSetting()
105
+
106
+	if debug {
107
+		setting.InitSQLLoggersForCli(log.DEBUG)
108
+	} else {
109
+		setting.InitSQLLoggersForCli(log.INFO)
110
+	}
111
+
112
+	setting.Database.LogSQL = debug
113
+	if err := db.InitEngine(ctx); err != nil {
114
+		fmt.Println(err)
115
+		fmt.Println("Check if you are using the right config file. You can use a --config directive to specify one.")
116
+		return nil
117
+	}
118
+
119
+	args := cmd.Args()
120
+	names := make([]string, 0, cmd.NArg())
121
+	for i := 0; i < cmd.NArg(); i++ {
122
+		names = append(names, args.Get(i))
123
+	}
124
+
125
+	beans, err := db.NamesToBean(names...)
126
+	if err != nil {
127
+		return err
128
+	}
129
+	recreateTables := migrate_base.RecreateTables(beans...)
130
+
131
+	return db.InitEngineWithMigration(context.Background(), func(ctx context.Context, x *xorm.Engine) error {
132
+		if err := migrations.EnsureUpToDate(ctx, x); err != nil {
133
+			return err
134
+		}
135
+		return recreateTables(x)
136
+	})
137
+}
138
+
139
+func setupDoctorDefaultLogger(cmd *cli.Command, colorize bool) {
140
+	// Silence the default loggers
141
+	setupConsoleLogger(log.FATAL, log.CanColorStderr, os.Stderr)
142
+
143
+	logFile := cmd.String("log-file")
144
+	switch logFile {
145
+	case "":
146
+		return // if no doctor log-file is set, do not show any log from default logger
147
+	case "-":
148
+		setupConsoleLogger(log.TRACE, colorize, os.Stdout)
149
+	default:
150
+		logFile, _ = filepath.Abs(logFile)
151
+		writeMode := log.WriterMode{Level: log.TRACE, WriterOption: log.WriterFileOption{FileName: logFile}}
152
+		writer, err := log.NewEventWriter("console-to-file", "file", writeMode)
153
+		if err != nil {
154
+			log.FallbackErrorf("unable to create file log writer: %v", err)
155
+			return
156
+		}
157
+		log.GetManager().GetLogger(log.DEFAULT).ReplaceAllWriters(writer)
158
+	}
159
+}
160
+
161
+func runDoctorCheck(ctx context.Context, cmd *cli.Command) error {
162
+	colorize := log.CanColorStdout
163
+	if cmd.IsSet("color") {
164
+		colorize = cmd.Bool("color")
165
+	}
166
+
167
+	setupDoctorDefaultLogger(cmd, colorize)
168
+
169
+	// Finally redirect the default golang's log to here
170
+	golog.SetFlags(0)
171
+	golog.SetPrefix("")
172
+	golog.SetOutput(log.LoggerToWriter(log.GetLogger(log.DEFAULT).Info))
173
+
174
+	if cmd.IsSet("list") {
175
+		w := tabwriter.NewWriter(os.Stdout, 0, 8, 1, '\t', 0)
176
+		_, _ = w.Write([]byte("Default\tName\tTitle\n"))
177
+		doctor.SortChecks(doctor.Checks)
178
+		for _, check := range doctor.Checks {
179
+			if check.IsDefault {
180
+				_, _ = w.Write([]byte{'*'})
181
+			}
182
+			_, _ = w.Write([]byte{'\t'})
183
+			_, _ = w.Write([]byte(check.Name))
184
+			_, _ = w.Write([]byte{'\t'})
185
+			_, _ = w.Write([]byte(check.Title))
186
+			_, _ = w.Write([]byte{'\n'})
187
+		}
188
+		return w.Flush()
189
+	}
190
+
191
+	var checks []*doctor.Check
192
+	if cmd.Bool("all") {
193
+		checks = make([]*doctor.Check, len(doctor.Checks))
194
+		copy(checks, doctor.Checks)
195
+	} else if cmd.IsSet("run") {
196
+		addDefault := cmd.Bool("default")
197
+		runNamesSet := container.SetOf(cmd.StringSlice("run")...)
198
+		for _, check := range doctor.Checks {
199
+			if (addDefault && check.IsDefault) || runNamesSet.Contains(check.Name) {
200
+				checks = append(checks, check)
201
+				runNamesSet.Remove(check.Name)
202
+			}
203
+		}
204
+		if len(runNamesSet) > 0 {
205
+			return fmt.Errorf("unknown checks: %q", strings.Join(runNamesSet.Values(), ","))
206
+		}
207
+	} else {
208
+		for _, check := range doctor.Checks {
209
+			if check.IsDefault {
210
+				checks = append(checks, check)
211
+			}
212
+		}
213
+	}
214
+	return doctor.RunChecks(ctx, colorize, cmd.Bool("fix"), checks)
215
+}

+ 54
- 0
cmd/doctor_convert.go 查看文件

@@ -0,0 +1,54 @@
1
+// Copyright 2019 The Gitea Authors. All rights reserved.
2
+// SPDX-License-Identifier: MIT
3
+
4
+package cmd
5
+
6
+import (
7
+	"context"
8
+	"fmt"
9
+
10
+	"code.gitea.io/gitea/models/db"
11
+	"code.gitea.io/gitea/modules/log"
12
+	"code.gitea.io/gitea/modules/setting"
13
+
14
+	"github.com/urfave/cli/v3"
15
+)
16
+
17
+// cmdDoctorConvert represents the available convert sub-command.
18
+var cmdDoctorConvert = &cli.Command{
19
+	Name:        "convert",
20
+	Usage:       "Convert the database",
21
+	Description: "A command to convert an existing MySQL database from utf8 to utf8mb4 or MSSQL database from varchar to nvarchar",
22
+	Action:      runDoctorConvert,
23
+}
24
+
25
+func runDoctorConvert(ctx context.Context, cmd *cli.Command) error {
26
+	if err := initDB(ctx); err != nil {
27
+		return err
28
+	}
29
+
30
+	log.Info("AppPath: %s", setting.AppPath)
31
+	log.Info("AppWorkPath: %s", setting.AppWorkPath)
32
+	log.Info("Custom path: %s", setting.CustomPath)
33
+	log.Info("Log path: %s", setting.Log.RootPath)
34
+	log.Info("Configuration file: %s", setting.CustomConf)
35
+
36
+	switch {
37
+	case setting.Database.Type.IsMySQL():
38
+		if err := db.ConvertDatabaseTable(); err != nil {
39
+			log.Fatal("Failed to convert database & table: %v", err)
40
+			return err
41
+		}
42
+		fmt.Println("Converted successfully, please confirm your database's character set is now utf8mb4")
43
+	case setting.Database.Type.IsMSSQL():
44
+		if err := db.ConvertVarcharToNVarchar(); err != nil {
45
+			log.Fatal("Failed to convert database from varchar to nvarchar: %v", err)
46
+			return err
47
+		}
48
+		fmt.Println("Converted successfully, please confirm your database's all columns character is NVARCHAR now")
49
+	default:
50
+		fmt.Println("This command can only be used with a MySQL or MSSQL database")
51
+	}
52
+
53
+	return nil
54
+}

+ 34
- 0
cmd/doctor_test.go 查看文件

@@ -0,0 +1,34 @@
1
+// Copyright 2023 The Gitea Authors. All rights reserved.
2
+// SPDX-License-Identifier: MIT
3
+
4
+package cmd
5
+
6
+import (
7
+	"context"
8
+	"testing"
9
+
10
+	"code.gitea.io/gitea/modules/log"
11
+	"code.gitea.io/gitea/services/doctor"
12
+
13
+	"github.com/stretchr/testify/assert"
14
+	"github.com/urfave/cli/v3"
15
+)
16
+
17
+func TestDoctorRun(t *testing.T) {
18
+	doctor.Register(&doctor.Check{
19
+		Title: "Test Check",
20
+		Name:  "test-check",
21
+		Run:   func(ctx context.Context, logger log.Logger, autofix bool) error { return nil },
22
+
23
+		SkipDatabaseInitialization: true,
24
+	})
25
+	app := &cli.Command{
26
+		Commands: []*cli.Command{cmdDoctorCheck},
27
+	}
28
+	err := app.Run(t.Context(), []string{"./gitea", "check", "--run", "test-check"})
29
+	assert.NoError(t, err)
30
+	err = app.Run(t.Context(), []string{"./gitea", "check", "--run", "no-such"})
31
+	assert.ErrorContains(t, err, `unknown checks: "no-such"`)
32
+	err = app.Run(t.Context(), []string{"./gitea", "check", "--run", "test-check,no-such"})
33
+	assert.ErrorContains(t, err, `unknown checks: "no-such"`)
34
+}

+ 327
- 0
cmd/dump.go 查看文件

@@ -0,0 +1,327 @@
1
+// Copyright 2014 The Gogs Authors. All rights reserved.
2
+// Copyright 2016 The Gitea Authors. All rights reserved.
3
+// SPDX-License-Identifier: MIT
4
+
5
+package cmd
6
+
7
+import (
8
+	"context"
9
+	"os"
10
+	"path"
11
+	"path/filepath"
12
+	"strings"
13
+
14
+	"code.gitea.io/gitea/models/db"
15
+	"code.gitea.io/gitea/modules/dump"
16
+	"code.gitea.io/gitea/modules/json"
17
+	"code.gitea.io/gitea/modules/log"
18
+	"code.gitea.io/gitea/modules/setting"
19
+	"code.gitea.io/gitea/modules/storage"
20
+	"code.gitea.io/gitea/modules/util"
21
+
22
+	"gitea.com/go-chi/session"
23
+	"github.com/urfave/cli/v3"
24
+)
25
+
26
+// CmdDump represents the available dump sub-command.
27
+var CmdDump = &cli.Command{
28
+	Name:        "dump",
29
+	Usage:       "Dump Gitea files and database",
30
+	Description: `Dump compresses all related files and database into zip file. It can be used for backup and capture Gitea server image to send to maintainer`,
31
+	Action:      runDump,
32
+	Flags: []cli.Flag{
33
+		&cli.StringFlag{
34
+			Name:    "file",
35
+			Aliases: []string{"f"},
36
+			Usage:   `Name of the dump file which will be created, default to "gitea-dump-{time}.zip". Supply '-' for stdout. See type for available types.`,
37
+		},
38
+		&cli.BoolFlag{
39
+			Name:    "verbose",
40
+			Aliases: []string{"V"},
41
+			Usage:   "Show process details",
42
+		},
43
+		&cli.BoolFlag{
44
+			Name:    "quiet",
45
+			Aliases: []string{"q"},
46
+			Usage:   "Only display warnings and errors",
47
+		},
48
+		&cli.StringFlag{
49
+			Name:    "tempdir",
50
+			Aliases: []string{"t"},
51
+			Value:   os.TempDir(),
52
+			Usage:   "Temporary dir path",
53
+		},
54
+		&cli.StringFlag{
55
+			Name:    "database",
56
+			Aliases: []string{"d"},
57
+			Usage:   "Specify the database SQL syntax: sqlite3, mysql, mssql, postgres",
58
+		},
59
+		&cli.BoolFlag{
60
+			Name:    "skip-repository",
61
+			Aliases: []string{"R"},
62
+			Usage:   "Skip the repository dumping",
63
+		},
64
+		&cli.BoolFlag{
65
+			Name:    "skip-log",
66
+			Aliases: []string{"L"},
67
+			Usage:   "Skip the log dumping",
68
+		},
69
+		&cli.BoolFlag{
70
+			Name:  "skip-custom-dir",
71
+			Usage: "Skip custom directory",
72
+		},
73
+		&cli.BoolFlag{
74
+			Name:  "skip-lfs-data",
75
+			Usage: "Skip LFS data",
76
+		},
77
+		&cli.BoolFlag{
78
+			Name:  "skip-attachment-data",
79
+			Usage: "Skip attachment data",
80
+		},
81
+		&cli.BoolFlag{
82
+			Name:  "skip-package-data",
83
+			Usage: "Skip package data",
84
+		},
85
+		&cli.BoolFlag{
86
+			Name:  "skip-index",
87
+			Usage: "Skip bleve index data",
88
+		},
89
+		&cli.BoolFlag{
90
+			Name:  "skip-db",
91
+			Usage: "Skip database",
92
+		},
93
+		&cli.StringFlag{
94
+			Name:  "type",
95
+			Usage: `Dump output format, default to "zip", supported types: ` + strings.Join(dump.SupportedOutputTypes, ", "),
96
+		},
97
+	},
98
+}
99
+
100
+func fatal(format string, args ...any) {
101
+	log.Fatal(format, args...)
102
+}
103
+
104
+func runDump(ctx context.Context, cmd *cli.Command) error {
105
+	setting.MustInstalled()
106
+
107
+	quite := cmd.Bool("quiet")
108
+	verbose := cmd.Bool("verbose")
109
+	if verbose && quite {
110
+		fatal("Option --quiet and --verbose cannot both be set")
111
+	}
112
+
113
+	// outFileName is either "-" or a file name (will be made absolute)
114
+	outFileName, outType := dump.PrepareFileNameAndType(cmd.String("file"), cmd.String("type"))
115
+	if outType == "" {
116
+		fatal("Invalid output type")
117
+	}
118
+
119
+	outFile := os.Stdout
120
+	if outFileName != "-" {
121
+		var err error
122
+		if outFileName, err = filepath.Abs(outFileName); err != nil {
123
+			fatal("Unable to get absolute path of dump file: %v", err)
124
+		}
125
+		if exist, _ := util.IsExist(outFileName); exist {
126
+			fatal("Dump file %q exists", outFileName)
127
+		}
128
+		if outFile, err = os.Create(outFileName); err != nil {
129
+			fatal("Unable to create dump file %q: %v", outFileName, err)
130
+		}
131
+		defer outFile.Close()
132
+	}
133
+
134
+	setupConsoleLogger(util.Iif(quite, log.WARN, log.INFO), log.CanColorStderr, os.Stderr)
135
+
136
+	setting.DisableLoggerInit()
137
+	setting.LoadSettings() // cannot access session settings otherwise
138
+
139
+	err := db.InitEngine(ctx)
140
+	if err != nil {
141
+		return err
142
+	}
143
+
144
+	if err = storage.Init(); err != nil {
145
+		return err
146
+	}
147
+
148
+	dumper, err := dump.NewDumper(ctx, outType, outFile)
149
+	if err != nil {
150
+		fatal("Failed to create archive %q: %v", outFile, err)
151
+		return err
152
+	}
153
+	dumper.Verbose = verbose
154
+	dumper.GlobalExcludeAbsPath(outFileName)
155
+	defer func() {
156
+		if err := dumper.Close(); err != nil {
157
+			fatal("Failed to save archive %q: %v", outFileName, err)
158
+		}
159
+	}()
160
+
161
+	if cmd.IsSet("skip-repository") && cmd.Bool("skip-repository") {
162
+		log.Info("Skip dumping local repositories")
163
+	} else {
164
+		log.Info("Dumping local repositories... %s", setting.RepoRootPath)
165
+		if err := dumper.AddRecursiveExclude("repos", setting.RepoRootPath, nil); err != nil {
166
+			fatal("Failed to include repositories: %v", err)
167
+		}
168
+
169
+		if cmd.IsSet("skip-lfs-data") && cmd.Bool("skip-lfs-data") {
170
+			log.Info("Skip dumping LFS data")
171
+		} else if !setting.LFS.StartServer {
172
+			log.Info("LFS isn't enabled. Skip dumping LFS data")
173
+		} else if err := storage.LFS.IterateObjects("", func(objPath string, object storage.Object) error {
174
+			info, err := object.Stat()
175
+			if err != nil {
176
+				return err
177
+			}
178
+			return dumper.AddFileByReader(object, info, path.Join("data", "lfs", objPath))
179
+		}); err != nil {
180
+			fatal("Failed to dump LFS objects: %v", err)
181
+		}
182
+	}
183
+
184
+	if cmd.Bool("skip-db") {
185
+		// Ensure that we don't dump the database file that may reside in setting.AppDataPath or elsewhere.
186
+		dumper.GlobalExcludeAbsPath(setting.Database.Path)
187
+		log.Info("Skipping database")
188
+	} else {
189
+		tmpDir := cmd.String("tempdir")
190
+		if _, err := os.Stat(tmpDir); os.IsNotExist(err) {
191
+			fatal("Path does not exist: %s", tmpDir)
192
+		}
193
+
194
+		dbDump, err := os.CreateTemp(tmpDir, "gitea-db.sql")
195
+		if err != nil {
196
+			fatal("Failed to create tmp file: %v", err)
197
+		}
198
+		defer func() {
199
+			_ = dbDump.Close()
200
+			if err := util.Remove(dbDump.Name()); err != nil {
201
+				log.Warn("Unable to remove temporary file: %s: Error: %v", dbDump.Name(), err)
202
+			}
203
+		}()
204
+
205
+		targetDBType := cmd.String("database")
206
+		if len(targetDBType) > 0 && targetDBType != setting.Database.Type.String() {
207
+			log.Info("Dumping database %s => %s...", setting.Database.Type, targetDBType)
208
+		} else {
209
+			log.Info("Dumping database...")
210
+		}
211
+
212
+		if err := db.DumpDatabase(dbDump.Name(), targetDBType); err != nil {
213
+			fatal("Failed to dump database: %v", err)
214
+		}
215
+
216
+		if err = dumper.AddFileByPath("gitea-db.sql", dbDump.Name()); err != nil {
217
+			fatal("Failed to include gitea-db.sql: %v", err)
218
+		}
219
+	}
220
+
221
+	log.Info("Adding custom configuration file from %s", setting.CustomConf)
222
+	if err = dumper.AddFileByPath("app.ini", setting.CustomConf); err != nil {
223
+		fatal("Failed to include specified app.ini: %v", err)
224
+	}
225
+
226
+	if cmd.IsSet("skip-custom-dir") && cmd.Bool("skip-custom-dir") {
227
+		log.Info("Skipping custom directory")
228
+	} else {
229
+		customDir, err := os.Stat(setting.CustomPath)
230
+		if err == nil && customDir.IsDir() {
231
+			if is, _ := dump.IsSubdir(setting.AppDataPath, setting.CustomPath); !is {
232
+				if err := dumper.AddRecursiveExclude("custom", setting.CustomPath, nil); err != nil {
233
+					fatal("Failed to include custom: %v", err)
234
+				}
235
+			} else {
236
+				log.Info("Custom dir %s is inside data dir %s, skipped", setting.CustomPath, setting.AppDataPath)
237
+			}
238
+		} else {
239
+			log.Info("Custom dir %s doesn't exist, skipped", setting.CustomPath)
240
+		}
241
+	}
242
+
243
+	isExist, err := util.IsExist(setting.AppDataPath)
244
+	if err != nil {
245
+		log.Error("Unable to check if %s exists. Error: %v", setting.AppDataPath, err)
246
+	}
247
+	if isExist {
248
+		log.Info("Packing data directory...%s", setting.AppDataPath)
249
+
250
+		var excludes []string
251
+		if setting.SessionConfig.OriginalProvider == "file" {
252
+			var opts session.Options
253
+			if err = json.Unmarshal([]byte(setting.SessionConfig.ProviderConfig), &opts); err != nil {
254
+				return err
255
+			}
256
+			excludes = append(excludes, opts.ProviderConfig)
257
+		}
258
+
259
+		if cmd.IsSet("skip-index") && cmd.Bool("skip-index") {
260
+			excludes = append(excludes, setting.Indexer.RepoPath)
261
+			excludes = append(excludes, setting.Indexer.IssuePath)
262
+		}
263
+
264
+		excludes = append(excludes, setting.RepoRootPath)
265
+		excludes = append(excludes, setting.LFS.Storage.Path)
266
+		excludes = append(excludes, setting.Attachment.Storage.Path)
267
+		excludes = append(excludes, setting.Packages.Storage.Path)
268
+		excludes = append(excludes, setting.RepoArchive.Storage.Path)
269
+		excludes = append(excludes, setting.Log.RootPath)
270
+		if err := dumper.AddRecursiveExclude("data", setting.AppDataPath, excludes); err != nil {
271
+			fatal("Failed to include data directory: %v", err)
272
+		}
273
+	}
274
+
275
+	if cmd.IsSet("skip-attachment-data") && cmd.Bool("skip-attachment-data") {
276
+		log.Info("Skip dumping attachment data")
277
+	} else if err := storage.Attachments.IterateObjects("", func(objPath string, object storage.Object) error {
278
+		info, err := object.Stat()
279
+		if err != nil {
280
+			return err
281
+		}
282
+		return dumper.AddFileByReader(object, info, path.Join("data", "attachments", objPath))
283
+	}); err != nil {
284
+		fatal("Failed to dump attachments: %v", err)
285
+	}
286
+
287
+	if cmd.IsSet("skip-package-data") && cmd.Bool("skip-package-data") {
288
+		log.Info("Skip dumping package data")
289
+	} else if !setting.Packages.Enabled {
290
+		log.Info("Packages isn't enabled. Skip dumping package data")
291
+	} else if err := storage.Packages.IterateObjects("", func(objPath string, object storage.Object) error {
292
+		info, err := object.Stat()
293
+		if err != nil {
294
+			return err
295
+		}
296
+		return dumper.AddFileByReader(object, info, path.Join("data", "packages", objPath))
297
+	}); err != nil {
298
+		fatal("Failed to dump packages: %v", err)
299
+	}
300
+
301
+	// Doesn't check if LogRootPath exists before processing --skip-log intentionally,
302
+	// ensuring that it's clear the dump is skipped whether the directory's initialized
303
+	// yet or not.
304
+	if cmd.IsSet("skip-log") && cmd.Bool("skip-log") {
305
+		log.Info("Skip dumping log files")
306
+	} else {
307
+		isExist, err := util.IsExist(setting.Log.RootPath)
308
+		if err != nil {
309
+			log.Error("Unable to check if %s exists. Error: %v", setting.Log.RootPath, err)
310
+		}
311
+		if isExist {
312
+			if err := dumper.AddRecursiveExclude("log", setting.Log.RootPath, nil); err != nil {
313
+				fatal("Failed to include log: %v", err)
314
+			}
315
+		}
316
+	}
317
+
318
+	if outFileName == "-" {
319
+		log.Info("Finish dumping to stdout")
320
+	} else {
321
+		if err = os.Chmod(outFileName, 0o600); err != nil {
322
+			log.Info("Can't change file access permissions mask to 0600: %v", err)
323
+		}
324
+		log.Info("Finish dumping in file %s", outFileName)
325
+	}
326
+	return nil
327
+}

+ 0
- 0
cmd/dump_repo.go 查看文件


部分文件因文件數量過多而無法顯示