Compare commits

..

38 Commits

Author SHA1 Message Date
Lunny Xiao 34182c87ec release notes for v1.1.4 2017-09-04 15:34:29 +02:00
Lunny Xiao c401788383 bug fixed 2017-09-04 14:27:53 +08:00
Ethan Koenig 8335b556d1 Fix rendering of external links (#2292) (#2315) 2017-08-17 15:03:41 +08:00
Lunny Xiao 09fff9e1c1 fix 500 error when view an issue which's milestone deleted (#2297) (#2299) 2017-08-14 10:45:06 +08:00
Lunny Xiao 622552b709 Fix deleted milestone bug (#1942) (#2300)
* Fix deleted milestone bug

* Use locale for ghost milestone name

* Fix pointer bug
2017-08-13 19:18:57 -03:00
Lunny Xiao 1709297701 Fix SHA1 hash linking (#2143) (#2293)
This changes the regex to look for a hash from 7 to 40 characters,
to match the use of abbreviated hash lookups in both git and github.
The restriction of not being a pure number is also removed because
1234567 is now considered a valid abbreviated hash, as is deadbeef.

A note has been added to the top of the code to state that the
literal regex match is fine, but no extra validation is currently
performed so some false positives are expected.

A future change could ensure that the hash exists in the repository
before rendering it as a link, although this might incur a slight
performance penalty.

Reverts part of commit 4a46613 and fixes #2053.
2017-08-12 09:32:56 +08:00
Bo-Yi Wu 5fe8fee933 back port from #1709 (#2291)
Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2017-08-11 10:32:26 +08:00
Bo-Yi Wu 5bb20be8b2 update sig file (#2269)
Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2017-08-06 21:56:17 +03:00
Lunny Xiao 06a554c22a ignore coverage for release (#2263) 2017-08-05 12:14:59 +03:00
Lunny Xiao 00bd47ae5c add changelog of release v1.1.3 (#2251) 2017-08-03 16:50:21 +08:00
Lunny Xiao b20f1ab47f fix some bug 2017-07-28 14:36:18 +08:00
Ethan Koenig 9a7f59ef35 Fix fast-forward PR bug (#2137)
* Fix fast-forward PR bug

* Don't ignore error in getMergeCommit (#1843)
2017-07-11 09:51:39 -05:00
Ethan Koenig 6a6f0616f2 Fix diff of renamed and modified file (#2136) 2017-07-11 02:19:10 -05:00
Ethan Koenig 6caf04c129 Don't ignore gravatar error (#2138) 2017-07-11 01:00:43 -05:00
Antoine GIRARD 406f5de18c Fix missing less sources for oauth (backport #1288) (#2135) 2017-07-11 09:25:30 +08:00
Andrey Nering 39cb1ac517 Merge pull request #2043 from lunny/lunny/fix_markdown_render_list
Fix markdown rendering (#1530)
2017-06-23 10:17:45 -03:00
Morlinest 58dcbaf20b Fix markdown rendering (#1530) 2017-06-23 17:19:38 +08:00
Kim "BKC" Carlbäcker 5f212ff4e9 Fix PR template error (#2008) 2017-06-19 09:18:53 +02:00
Ethan Koenig 45fa822ac4 Fix PR template error 2017-06-18 22:01:20 -04:00
Kim "BKC" Carlbäcker 1ac8646845 Merge pull request #1944 from appleboy/release/v1.1
[ci skip] add 1.1.2 change log.
2017-06-12 15:26:43 +02:00
Bo-Yi Wu 13e284c7cf [ci skip] add 1.1.2 change log.
Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2017-06-12 17:04:29 +08:00
Lunny Xiao bbe6aa349f fix bug not to trim space of login username (#1806) 2017-05-29 06:10:11 -05:00
agrn 4fd55d8796 Backport bugfixes #1220 and #1393 to v1.1 (#1758)
* Fix "Error: No issue number specified"  when pushing

* fix wrong log when push to a new branch (#1220)

* Removing empty file modules/github/issues.go
2017-05-20 12:43:43 +08:00
Jonas Östanbäck daaabaa1d9 Enforce netgo build tag while cross-compilation (Backport of #1690) (#1731) 2017-05-18 09:04:12 +08:00
Lunny Xiao fa059debca fix delete user failed on sqlite (#1321) 2017-05-17 12:43:22 +02:00
Kim "BKC" Carlbäcker 2854c8aa47 Merge pull request #1724 from lunny/lunny/fix_avatar_update
fix update avatar
2017-05-15 17:16:31 +02:00
Lunny Xiao 506c98df5b fix update avatar 2017-05-14 13:39:53 +08:00
Kim "BKC" Carlbäcker f9859a2991 Update changelog with v1.1.1 2017-05-04 18:29:26 +02:00
Kim "BKC" Carlbäcker 473df53533 [backport] fix sanitation (#1646)
* Santiation fix from Gogs

- Add code-injection checks

* Fix SSH-breakage
2017-04-30 17:55:16 +08:00
Philippe Kueck 2482c67e2b backport of PR #1257 for release/v1.1: rewrite pre-commit, post-commit and options hooks (#1376)
* issue #1250, replace {pre,post}-receive and update hooks with a single shell script that does not require custom hooks to be a sh-script

* issue #1250, make script posix compilant

* v23, add migration script to update {pre,post}-receive and update hooks

* migration: use a more common name and rename v23 to v26 to avoid conflicts

* gofmt'ed and added copyright header

* fix SyncRepositoryHooks to also sync wiki repos

* no migration for you.
2017-03-28 15:47:59 +08:00
Lunny Xiao d9bdf7a65d fix migration failed when authorized_keys is not exist (#1375)
* fix wiki bugs (#1294)

* fix migration failed when authorized_keys is not exist
2017-03-23 23:07:30 +08:00
Lunny Xiao 11ad296347 fix wiki bugs (#1294) (#1338) 2017-03-20 22:54:33 +08:00
Lunny Xiao 5fad54248f fix forget migration for wiki hooks (#1227) (#1237) 2017-03-17 13:01:33 +08:00
Philippe Kueck 2b5e4b4d96 fix #1189, commit messages containing a pipe (#1218)
(d680164f11)
2017-03-16 19:41:19 +08:00
Lunny Xiao 65eea82c3e fix repo settings external tracker failed and check external urls (#1215) (#1236) 2017-03-13 16:23:00 +08:00
Lunny Xiao fac75b8086 Fix upgrade failed after ever rollback (#1194) (#1197)
* fix upgrade failed after rollback

* fix upgrade failed after rollback

* fix possible upgrade twice fail after rollback
2017-03-11 17:01:22 +08:00
Lunny Xiao e4706127f9 bug fixed for delete repo failed (#1193) (#1195) 2017-03-10 22:28:00 +08:00
Lunny Xiao c9baf9d14b fix ini format incomiptable with crowdin (#1177) (#1192) 2017-03-10 20:59:51 +08:00
9498 changed files with 1341732 additions and 790965 deletions
-26
View File
@@ -1,26 +0,0 @@
root = "."
tmp_dir = ".air"
[build]
pre_cmd = ["killall -9 gitea 2>/dev/null || true"] # kill off potential zombie processes from previous runs
cmd = "make --no-print-directory backend"
entrypoint = ["./gitea"]
delay = 2000
include_ext = ["go", "tmpl"]
include_file = ["main.go"]
include_dir = ["cmd", "models", "modules", "options", "routers", "services"]
exclude_dir = [
"models/fixtures",
"models/migrations/fixtures",
"modules/avatar/identicon/testdata",
"modules/avatar/testdata",
"modules/git/tests",
"modules/migration/file_format_testdata",
"routers/private/tests",
"services/gitdiff/testdata",
]
exclude_regex = ["_test.go$", "_gen.go$"]
stop_on_error = true
[log]
main_only = true
-59
View File
@@ -1,59 +0,0 @@
# The full repository name
repo: go-gitea/gitea
# Service type (gitea or github)
service: github
# Base URL for Gitea instance if using gitea service type (optional)
# Default: https://gitea.com
base-url:
# Changelog groups and which labeled PRs to add to each group
groups:
-
name: BREAKING
labels:
- pr/breaking
-
name: SECURITY
labels:
- topic/security
-
name: FEATURES
labels:
- type/feature
-
name: ENHANCEMENTS
labels:
- type/enhancement
-
name: PERFORMANCE
labels:
- performance/memory
- performance/speed
- performance/bigrepo
- performance/cpu
-
name: BUGFIXES
labels:
- type/bug
-
name: TESTING
labels:
- type/testing
-
name: BUILD
labels:
- topic/build
- topic/code-linting
-
name: DOCS
labels:
- type/docs
-
name: MISC
default: true
# regex indicating which labels to skip for the changelog
skip-labels: skip-changelog|backport\/.+
-45
View File
@@ -1,45 +0,0 @@
{
"name": "Gitea DevContainer",
"image": "mcr.microsoft.com/devcontainers/go:1.26-trixie",
"containerEnv": {
// override "local" from packaged version
"GOTOOLCHAIN": "auto"
},
"features": {
// installs nodejs into container
"ghcr.io/devcontainers/features/node:1": {
"version": "latest"
},
"ghcr.io/devcontainers/features/git-lfs:1.2.5": {},
"ghcr.io/jsburckhardt/devcontainer-features/uv:1": {},
"ghcr.io/devcontainers/features/python:1": {
"version": "3.14"
},
"ghcr.io/warrenbuckley/codespace-features/sqlite:1": {}
},
"customizations": {
"vscode": {
"settings": {},
"extensions": [
"editorconfig.editorconfig",
"dbaeumer.vscode-eslint",
"golang.go",
"stylelint.vscode-stylelint",
"DavidAnson.vscode-markdownlint",
"Vue.volar",
"ms-azuretools.vscode-docker",
"vitest.explorer",
"cweijan.vscode-database-client2",
"GitHub.vscode-pull-request-github",
"Azurite.azurite"
]
}
},
"portsAttributes": {
"3000": {
"label": "Gitea Web",
"onAutoForward": "notify"
}
},
"postCreateCommand": "make deps"
}
+5 -90
View File
@@ -1,90 +1,5 @@
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
# Folders
_obj
_test
# IntelliJ
.idea
# Goland's output filename can not be set manually
/go_build_*
# MS VSCode
.vscode
__debug_bin*
# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe
*.test
*.prof
*coverage.out
coverage.all
cpu.out
*.db
*.log
/gitea
/debug
/bin
/dist
/custom/*
!/custom/conf
/custom/conf/*
!/custom/conf/app.example.ini
/data
/indexers
/log
/tests/integration/gitea-integration-*
/tests/*.ini
/node_modules
/yarn.lock
/yarn-error.log
/npm-debug.log*
/pnpm-debug.log*
/public/assets/js
/public/assets/css
/public/assets/fonts
/public/assets/img/avatar
/vendor
/VERSION
/.air
/.go-licenses
/Dockerfile
/Dockerfile.rootless
/.venv
# Files and folders that were previously generated
/public/assets/img/webpack
# Snapcraft
snap/.snapcraft/
parts/
stage/
prime/
*.snap
*.snap-build
*_source.tar.bz2
.DS_Store
# Make evidence files
/.make_evidence
# Manpage
/man
*
!gitea
!docker
!public
!templates
+145
View File
@@ -0,0 +1,145 @@
workspace:
base: /srv/app
path: src/code.gitea.io/gitea
pipeline:
clone:
image: plugins/git
tags: true
test:
image: webhippie/golang:edge
pull: true
environment:
TAGS: bindata sqlite
GOPATH: /srv/app
commands:
- apk -U add openssh-client
- make clean
- make generate
- make vet
- make lint
- make test
- make build
when:
event: [ push, tag, pull_request ]
test-mysql:
image: webhippie/golang:edge
pull: true
environment:
TAGS: bindata
GOPATH: /srv/app
commands:
- make test-mysql
when:
event: [ push, tag, pull_request ]
test-pgsql:
image: webhippie/golang:edge
pull: true
environment:
TAGS: bindata
GOPATH: /srv/app
commands:
- make test-pgsql
when:
event: [ push, tag, pull_request ]
static:
image: karalabe/xgo-latest:latest
pull: true
environment:
TAGS: bindata sqlite
GOPATH: /srv/app
commands:
- make release
when:
event: [ push, tag, pull_request ]
# coverage:
# image: plugins/coverage
# server: https://coverage.gitea.io
# when:
# event: [ push, tag, pull_request ]
docker:
image: plugins/docker
repo: gitea/gitea
tags: [ '${DRONE_TAG##v}' ]
when:
event: [ tag ]
branch: [ refs/tags/* ]
docker:
image: plugins/docker
repo: gitea/gitea
tags: [ '${DRONE_BRANCH##release/v}' ]
when:
event: [ push ]
branch: [ release/* ]
docker:
image: plugins/docker
repo: gitea/gitea
tags: [ 'latest' ]
when:
event: [ push ]
branch: [ master ]
release:
image: plugins/s3
path_style: true
strip_prefix: dist/release/
source: dist/release/*
target: /gitea/${DRONE_TAG##v}
when:
event: [ tag ]
branch: [ refs/tags/* ]
release:
image: plugins/s3
path_style: true
strip_prefix: dist/release/
source: dist/release/*
target: /gitea/${DRONE_BRANCH##release/v}
when:
event: [ push ]
branch: [ release/* ]
release:
image: plugins/s3
path_style: true
strip_prefix: dist/release/
source: dist/release/*
target: /gitea/master
when:
event: [ push ]
branch: [ master ]
github:
image: plugins/github-release
files:
- dist/release/*
when:
event: [ tag ]
branch: [ refs/tags/* ]
gitter:
image: plugins/gitter
services:
mysql:
image: mysql:5.7
environment:
- MYSQL_DATABASE=test
- MYSQL_ALLOW_EMPTY_PASSWORD=yes
when:
event: [ push, tag, pull_request ]
pgsql:
image: postgres:9.5
environment:
- POSTGRES_DB=test
when:
event: [ push, tag, pull_request ]
+1
View File
@@ -0,0 +1 @@
eyJhbGciOiJIUzI1NiJ9.d29ya3NwYWNlOgogIGJhc2U6IC9zcnYvYXBwCiAgcGF0aDogc3JjL2NvZGUuZ2l0ZWEuaW8vZ2l0ZWEKCnBpcGVsaW5lOgogIGNsb25lOgogICAgaW1hZ2U6IHBsdWdpbnMvZ2l0CiAgICB0YWdzOiB0cnVlCgogIHRlc3Q6CiAgICBpbWFnZTogd2ViaGlwcGllL2dvbGFuZzplZGdlCiAgICBwdWxsOiB0cnVlCiAgICBlbnZpcm9ubWVudDoKICAgICAgVEFHUzogYmluZGF0YSBzcWxpdGUKICAgICAgR09QQVRIOiAvc3J2L2FwcAogICAgY29tbWFuZHM6CiAgICAgIC0gYXBrIC1VIGFkZCBvcGVuc3NoLWNsaWVudAogICAgICAtIG1ha2UgY2xlYW4KICAgICAgLSBtYWtlIGdlbmVyYXRlCiAgICAgIC0gbWFrZSB2ZXQKICAgICAgLSBtYWtlIGxpbnQKICAgICAgLSBtYWtlIHRlc3QKICAgICAgLSBtYWtlIGJ1aWxkCiAgICB3aGVuOgogICAgICBldmVudDogWyBwdXNoLCB0YWcsIHB1bGxfcmVxdWVzdCBdCgogIHRlc3QtbXlzcWw6CiAgICBpbWFnZTogd2ViaGlwcGllL2dvbGFuZzplZGdlCiAgICBwdWxsOiB0cnVlCiAgICBlbnZpcm9ubWVudDoKICAgICAgVEFHUzogYmluZGF0YQogICAgICBHT1BBVEg6IC9zcnYvYXBwCiAgICBjb21tYW5kczoKICAgICAgLSBtYWtlIHRlc3QtbXlzcWwKICAgIHdoZW46CiAgICAgIGV2ZW50OiBbIHB1c2gsIHRhZywgcHVsbF9yZXF1ZXN0IF0KCiAgdGVzdC1wZ3NxbDoKICAgIGltYWdlOiB3ZWJoaXBwaWUvZ29sYW5nOmVkZ2UKICAgIHB1bGw6IHRydWUKICAgIGVudmlyb25tZW50OgogICAgICBUQUdTOiBiaW5kYXRhCiAgICAgIEdPUEFUSDogL3Nydi9hcHAKICAgIGNvbW1hbmRzOgogICAgICAtIG1ha2UgdGVzdC1wZ3NxbAogICAgd2hlbjoKICAgICAgZXZlbnQ6IFsgcHVzaCwgdGFnLCBwdWxsX3JlcXVlc3QgXQoKICBzdGF0aWM6CiAgICBpbWFnZToga2FyYWxhYmUveGdvLWxhdGVzdDpsYXRlc3QKICAgIHB1bGw6IHRydWUKICAgIGVudmlyb25tZW50OgogICAgICBUQUdTOiBiaW5kYXRhIHNxbGl0ZQogICAgICBHT1BBVEg6IC9zcnYvYXBwCiAgICBjb21tYW5kczoKICAgICAgLSBtYWtlIHJlbGVhc2UKICAgIHdoZW46CiAgICAgIGV2ZW50OiBbIHB1c2gsIHRhZywgcHVsbF9yZXF1ZXN0IF0KCiAgIyBjb3ZlcmFnZToKICAjICAgaW1hZ2U6IHBsdWdpbnMvY292ZXJhZ2UKICAjICAgc2VydmVyOiBodHRwczovL2NvdmVyYWdlLmdpdGVhLmlvCiAgIyAgIHdoZW46CiAgIyAgICAgZXZlbnQ6IFsgcHVzaCwgdGFnLCBwdWxsX3JlcXVlc3QgXQoKICBkb2NrZXI6CiAgICBpbWFnZTogcGx1Z2lucy9kb2NrZXIKICAgIHJlcG86IGdpdGVhL2dpdGVhCiAgICB0YWdzOiBbICcke0RST05FX1RBRyMjdn0nIF0KICAgIHdoZW46CiAgICAgIGV2ZW50OiBbIHRhZyBdCiAgICAgIGJyYW5jaDogWyByZWZzL3RhZ3MvKiBdCgogIGRvY2tlcjoKICAgIGltYWdlOiBwbHVnaW5zL2RvY2tlcgogICAgcmVwbzogZ2l0ZWEvZ2l0ZWEKICAgIHRhZ3M6IFsgJyR7RFJPTkVfQlJBTkNIIyNyZWxlYXNlL3Z9JyBdCiAgICB3aGVuOgogICAgICBldmVudDogWyBwdXNoIF0KICAgICAgYnJhbmNoOiBbIHJlbGVhc2UvKiBdCgogIGRvY2tlcjoKICAgIGltYWdlOiBwbHVnaW5zL2RvY2tlcgogICAgcmVwbzogZ2l0ZWEvZ2l0ZWEKICAgIHRhZ3M6IFsgJ2xhdGVzdCcgXQogICAgd2hlbjoKICAgICAgZXZlbnQ6IFsgcHVzaCBdCiAgICAgIGJyYW5jaDogWyBtYXN0ZXIgXQoKICByZWxlYXNlOgogICAgaW1hZ2U6IHBsdWdpbnMvczMKICAgIHBhdGhfc3R5bGU6IHRydWUKICAgIHN0cmlwX3ByZWZpeDogZGlzdC9yZWxlYXNlLwogICAgc291cmNlOiBkaXN0L3JlbGVhc2UvKgogICAgdGFyZ2V0OiAvZ2l0ZWEvJHtEUk9ORV9UQUcjI3Z9CiAgICB3aGVuOgogICAgICBldmVudDogWyB0YWcgXQogICAgICBicmFuY2g6IFsgcmVmcy90YWdzLyogXQoKICByZWxlYXNlOgogICAgaW1hZ2U6IHBsdWdpbnMvczMKICAgIHBhdGhfc3R5bGU6IHRydWUKICAgIHN0cmlwX3ByZWZpeDogZGlzdC9yZWxlYXNlLwogICAgc291cmNlOiBkaXN0L3JlbGVhc2UvKgogICAgdGFyZ2V0OiAvZ2l0ZWEvJHtEUk9ORV9CUkFOQ0gjI3JlbGVhc2Uvdn0KICAgIHdoZW46CiAgICAgIGV2ZW50OiBbIHB1c2ggXQogICAgICBicmFuY2g6IFsgcmVsZWFzZS8qIF0KCiAgcmVsZWFzZToKICAgIGltYWdlOiBwbHVnaW5zL3MzCiAgICBwYXRoX3N0eWxlOiB0cnVlCiAgICBzdHJpcF9wcmVmaXg6IGRpc3QvcmVsZWFzZS8KICAgIHNvdXJjZTogZGlzdC9yZWxlYXNlLyoKICAgIHRhcmdldDogL2dpdGVhL21hc3RlcgogICAgd2hlbjoKICAgICAgZXZlbnQ6IFsgcHVzaCBdCiAgICAgIGJyYW5jaDogWyBtYXN0ZXIgXQoKICBnaXRodWI6CiAgICBpbWFnZTogcGx1Z2lucy9naXRodWItcmVsZWFzZQogICAgZmlsZXM6CiAgICAgIC0gZGlzdC9yZWxlYXNlLyoKICAgIHdoZW46CiAgICAgIGV2ZW50OiBbIHRhZyBdCiAgICAgIGJyYW5jaDogWyByZWZzL3RhZ3MvKiBdCgogIGdpdHRlcjoKICAgIGltYWdlOiBwbHVnaW5zL2dpdHRlcgoKc2VydmljZXM6CiAgbXlzcWw6CiAgICBpbWFnZTogbXlzcWw6NS43CiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBNWVNRTF9EQVRBQkFTRT10ZXN0CiAgICAgIC0gTVlTUUxfQUxMT1dfRU1QVFlfUEFTU1dPUkQ9eWVzCiAgICB3aGVuOgogICAgICBldmVudDogWyBwdXNoLCB0YWcsIHB1bGxfcmVxdWVzdCBdCgogIHBnc3FsOgogICAgaW1hZ2U6IHBvc3RncmVzOjkuNQogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gUE9TVEdSRVNfREI9dGVzdAogICAgd2hlbjoKICAgICAgZXZlbnQ6IFsgcHVzaCwgdGFnLCBwdWxsX3JlcXVlc3QgXQo.uf02h57dWfCrxG3rcNcYlZPQP2XsFhKvcF2geGTpG50
+11 -20
View File
@@ -1,36 +1,27 @@
# http://editorconfig.org
root = true
[*]
indent_style = space
indent_size = 2
tab_width = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
trim_trailing_whitespace = true
[*.{go,tmpl,html}]
[*.go]
indent_style = tab
indent_size = 8
[go.*]
[*.{tmpl,html}]
indent_style = tab
indent_size = 4
[templates/custom/*.tmpl]
insert_final_newline = false
[templates/swagger/*_json.tmpl]
[*.{less,yml}]
indent_style = space
insert_final_newline = false
indent_size = 4
[templates/user/auth/oidc_wellknown.tmpl]
[*.js]
indent_style = space
[templates/shared/actions/runner_badge_*.tmpl]
# editconfig lint requires these XML-like files to have charset defined, but the files don't have.
charset = unset
indent_size = 4
[Makefile]
indent_style = tab
[*.svg]
insert_final_newline = false
-1
View File
@@ -1 +0,0 @@
use flake
+6 -11
View File
@@ -1,11 +1,6 @@
* text=auto eol=lf
*.tmpl linguist-language=Handlebars
*.pb.go linguist-generated
/assets/*.json linguist-generated
/public/assets/img/svg/*.svg linguist-generated
/templates/swagger/v1_json.tmpl linguist-generated
/options/fileicon/** linguist-generated
/vendor/** -text -eol linguist-vendored
/web_src/js/vendor/** -text -eol linguist-vendored
Dockerfile.* linguist-language=Dockerfile
Makefile.* linguist-language=Makefile
conf/* linguist-vendored
docker/* linguist-vendored
options/* linguist-vendored
public/* linguist-vendored
scripts/* linguist-vendored
templates/* linguist-vendored
-1
View File
@@ -1 +0,0 @@
open_collective: gitea
-91
View File
@@ -1,91 +0,0 @@
name: Bug Report
description: Found something you weren't expecting? Report it here!
labels: ["type/bug"]
body:
- type: markdown
attributes:
value: |
NOTE: If your issue is a security concern, please send an email to security@gitea.io instead of opening a public issue.
- type: markdown
attributes:
value: |
1. Please speak English, this is the language all maintainers can speak and write.
2. Please ask questions or configuration/deploy problems on our Discord
server (https://discord.gg/gitea) or forum (https://forum.gitea.com).
3. Make sure you are using the latest release and
take a moment to check that your issue hasn't been reported before.
4. Make sure it's not mentioned in the FAQ (https://docs.gitea.com/help/faq)
5. It's really important to provide pertinent details and logs (https://docs.gitea.com/help/support),
incomplete details will be handled as an invalid report.
- type: textarea
id: description
attributes:
label: Description
description: |
Please provide a description of your issue here, with a URL if you were able to reproduce the issue (see below)
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.
- type: input
id: gitea-ver
attributes:
label: Gitea Version
description: Gitea version (or commit reference) of your instance
validations:
required: true
- type: dropdown
id: can-reproduce
attributes:
label: Can you reproduce the bug on the Gitea demo site?
description: |
If so, please provide a URL in the Description field
URL of Gitea demo: https://demo.gitea.com
options:
- "Yes"
- "No"
validations:
required: true
- type: markdown
attributes:
value: |
It's really important to provide pertinent logs
Please read https://docs.gitea.com/administration/logging-config#collecting-logs-for-help
In addition, if your problem relates to git commands set `RUN_MODE=dev` at the top of app.ini
- type: input
id: logs
attributes:
label: Log Gist
description: Please provide a gist URL of your logs, with any sensitive information (e.g. API keys) removed/hidden
- type: textarea
id: screenshots
attributes:
label: Screenshots
description: If this issue involves the Web Interface, please provide one or more screenshots
- type: input
id: git-ver
attributes:
label: Git Version
description: The version of git running on the server
- type: input
id: os-ver
attributes:
label: Operating System
description: The operating system you are using to run Gitea
- type: textarea
id: run-info
attributes:
label: How are you running Gitea?
description: |
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
Please also tell us how you are running Gitea, e.g. if it is being run from docker, a command-line, systemd etc.
If you are using a package or systemd tell us what distribution you are using
validations:
required: true
- type: dropdown
id: database
attributes:
label: Database
description: What database system are you running?
options:
- PostgreSQL
- MySQL/MariaDB
- MSSQL
- SQLite
-17
View File
@@ -1,17 +0,0 @@
blank_issues_enabled: false
contact_links:
- name: Security Concern
url: https://tinyurl.com/security-gitea
about: For security concerns, please send a mail to security@gitea.io instead of opening a public issue.
- name: Discord Server
url: https://discord.gg/Gitea
about: Please ask questions and discuss configuration or deployment problems here.
- name: Discourse Forum
url: https://forum.gitea.com
about: Questions and configuration or deployment problems can also be discussed on our forum.
- name: Frequently Asked Questions
url: https://docs.gitea.com/help/faq
about: Please check if your question isn't mentioned here.
- name: Crowdin Translations
url: https://translate.gitea.com
about: Translations are managed here.
@@ -1,24 +0,0 @@
name: Feature Request
description: Got an idea for a feature that Gitea doesn't have currently? Submit your idea here!
labels: ["type/proposal"]
body:
- type: markdown
attributes:
value: |
1. Please speak English, this is the language all maintainers can speak and write.
2. Please ask questions or configuration/deploy problems on our Discord
server (https://discord.gg/gitea) or forum (https://forum.gitea.com).
3. Please take a moment to check that your feature hasn't already been suggested.
- type: textarea
id: description
attributes:
label: Feature Description
placeholder: |
I think it would be great if Gitea had...
validations:
required: true
- type: textarea
id: screenshots
attributes:
label: Screenshots
description: If you can, provide screenshots of an implementation on another site e.g. GitHub
-66
View File
@@ -1,66 +0,0 @@
name: Web Interface Bug Report
description: Something doesn't look quite as it should? Report it here!
labels: ["type/bug", "topic/ui"]
body:
- type: markdown
attributes:
value: |
NOTE: If your issue is a security concern, please send an email to security@gitea.io instead of opening a public issue.
- type: markdown
attributes:
value: |
1. Please speak English, this is the language all maintainers can speak and write.
2. Please ask questions or configuration/deploy problems on our Discord
server (https://discord.gg/gitea) or forum (https://forum.gitea.com).
3. Please take a moment to check that your issue doesn't already exist.
4. Make sure it's not mentioned in the FAQ (https://docs.gitea.com/help/faq)
5. Please give all relevant information below for bug reports, because
incomplete details will be handled as an invalid report.
6. In particular it's really important to provide pertinent logs. If you are certain that this is a javascript
error, show us the javascript console. If the error appears to relate to Gitea the server you must also give us
DEBUG level logs. (See https://docs.gitea.com/administration/logging-config#collecting-logs-for-help)
- type: textarea
id: description
attributes:
label: Description
description: |
Please provide a description of your issue here, with a URL if you were able to reproduce the issue (see below)
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.
- type: textarea
id: screenshots
attributes:
label: Screenshots
description: Please provide at least 1 screenshot showing the issue.
validations:
required: true
- type: input
id: gitea-ver
attributes:
label: Gitea Version
description: Gitea version (or commit reference) your instance is running
validations:
required: true
- type: dropdown
id: can-reproduce
attributes:
label: Can you reproduce the bug on the Gitea demo site?
description: |
If so, please provide a URL in the Description field
URL of Gitea demo: https://demo.gitea.com
options:
- "Yes"
- "No"
validations:
required: true
- type: input
id: os-ver
attributes:
label: Operating System
description: The operating system you are using to access Gitea
- type: input
id: browser-ver
attributes:
label: Browser Version
description: The browser and version that you are using to access Gitea
validations:
required: true
-7
View File
@@ -1,7 +0,0 @@
self-hosted-runner:
labels:
- actuated-4cpu-8gb
- actuated-4cpu-16gb
- nscloud
- namespace-profile-gitea-release-docker
- namespace-profile-gitea-release-binary
-29
View File
@@ -1,29 +0,0 @@
name: docker-dryrun
description: Composite action that performs the container build steps for a single platform.
inputs:
platform:
description: "The target platform: linux/amd64, linux/arm64, linux/riscv64."
required: true
runs:
using: composite
steps:
- uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
- uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- name: Build regular image
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with:
context: .
platforms: ${{ inputs.platform }}
push: false
file: Dockerfile
cache-from: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootful
- name: Build rootless image
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with:
context: .
platforms: ${{ inputs.platform }}
push: false
file: Dockerfile.rootless
cache-from: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootless
-47
View File
@@ -1,47 +0,0 @@
name: go-caches
description: Restore and save go module, build, and golangci-lint caches
inputs:
cache-name:
description: Short identifier used in the per-caller build cache key
required: true
build-cache:
description: Whether to include ~/.cache/go-build
default: "true"
build-cache-rotate:
description: Whether to rotate the build cache key per run so Go's test result cache can accumulate across runs
default: "false"
lint-cache:
description: Whether to include ~/.cache/golangci-lint
default: "false"
runs:
using: composite
steps:
- uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ~/go/pkg/mod
key: gomod-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('go.sum') }}
restore-keys: gomod-${{ runner.os }}-${{ runner.arch }}
- if: ${{ inputs.build-cache == 'true' && inputs.build-cache-rotate == 'true' }}
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ~/.cache/go-build
key: gobuild-${{ runner.os }}-${{ runner.arch }}-${{ inputs.cache-name }}-${{ hashFiles('go.sum') }}-${{ github.run_id }}
restore-keys: |
gobuild-${{ runner.os }}-${{ runner.arch }}-${{ inputs.cache-name }}-${{ hashFiles('go.sum') }}
gobuild-${{ runner.os }}-${{ runner.arch }}-${{ inputs.cache-name }}
gobuild-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('go.sum') }}
gobuild-${{ runner.os }}-${{ runner.arch }}
- if: ${{ inputs.build-cache == 'true' && inputs.build-cache-rotate != 'true' }}
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ~/.cache/go-build
key: gobuild-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('go.sum') }}
restore-keys: gobuild-${{ runner.os }}-${{ runner.arch }}
- if: ${{ inputs.lint-cache == 'true' }}
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ~/.cache/golangci-lint
key: golangci-${{ runner.os }}-${{ runner.arch }}-${{ inputs.cache-name }}-${{ hashFiles('go.sum', '.golangci.yml') }}
restore-keys: golangci-${{ runner.os }}-${{ runner.arch }}-${{ inputs.cache-name }}
+28
View File
@@ -0,0 +1,28 @@
1. Please speak English, this is the language all of us can speak and write.
2. Please ask questions or configuration/deploy problems on our Gitter channel: https://gitter.im/go-gitea/gitea
3. Please take a moment to search check that your issue doesn't already exist.
4. Please give all relevant information below for bug reports, because incomplete details will be handled as an invalid report.
**You MUST delete the content above including this line before posting, otherwise your issue will be invalid.**
- Gitea version (or commit ref):
- Git version:
- Operating system:
- Database (use `[x]`):
- [ ] PostgreSQL
- [ ] MySQL
- [ ] SQLite
- Can you reproduce the bug at https://try.gitea.io:
- [ ] Yes (provide example URL)
- [ ] No
- [ ] Not relevant
- Log gist:
## Description
...
## Screenshots
**If this issue involves the Web Interface, please include a screenshot**
-14
View File
@@ -1,14 +0,0 @@
docs-update-needed:
- changed-files:
- any-glob-to-any-file:
- "custom/conf/app.example.ini"
topic/code-linting:
- changed-files:
- any-glob-to-any-file:
- ".golangci.yml"
- ".markdownlint.yaml"
- ".spectral.yaml"
- ".yamllint.yaml"
- "eslint*.config.*"
- "stylelint.config.*"
+6 -10
View File
@@ -1,11 +1,7 @@
<!-- start tips -->
Please check the following:
1. Make sure you are targeting the `main` branch, pull requests on release branches are only allowed for backports.
2. Use a Conventional Commits PR title, for example `fix(repo): handle empty branch names`.
3. Make sure you have read contributing guidelines: https://github.com/go-gitea/gitea/blob/main/CONTRIBUTING.md .
4. For documentations contribution, please go to https://gitea.com/gitea/docs
5. Describe what your pull request does and which issue you're targeting (if any).
6. It is recommended to enable "Allow edits by maintainers", so maintainers can help more easily.
7. 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 `---`.
8. Delete all these tips before posting.
<!-- end tips -->
1. Make sure you are targeting the `master` branch, pull requests on release branches are only allowed for bug fixes.
2. Read contributing guidelines: https://github.com/go-gitea/gitea/blob/master/CONTRIBUTING.md
3. Describe what your pull request does and which issue you're targeting (if any)
**You MUST delete the content above including this line before posting, otherwise your pull request will be invalid.**
+9 -88
View File
@@ -9,24 +9,10 @@ _test
# IntelliJ
.idea
.run
# IntelliJ Gateway
.uuid
# Goland's output filename can not be set manually
/go_build_*
/gitea_*
# MS VSCode
.vscode
__debug_bin*
# Visual Studio
/.vs/
# mise version managment tool
mise.toml
# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
@@ -39,87 +25,22 @@ _testmain.go
*.exe
*.test
*.prof
*.tsbuildinfo
*coverage.out
coverage.all
cpu.out
coverage.out
/modules/migration/bindata.*
/modules/options/bindata.*
/modules/public/bindata.*
/modules/templates/bindata.*
/modules/options/bindata.go
/modules/public/bindata.go
/modules/templates/bindata.go
*.db
*.log
*.log.*.gz
/gitea
/debug
/bin
/dist
/custom/*
!/custom/conf/app.example.ini
/custom
/data
/indexers
/log
/public/assets/img/avatar
/tests/e2e-output
/tests/integration/gitea-integration-*
/tests/integration/indexers-*
/tests/*.ini
/tests/**/*.git/**/*.sample
/node_modules
/.venv
/yarn.lock
/yarn-error.log
/npm-debug.log*
/.pnpm-store
/public/assets/.vite
/public/assets/js
/public/assets/css
/public/assets/fonts
/public/assets/licenses.txt
/vendor
/VERSION
/.air
# Snapcraft
/gitea_a*.txt
snap/.snapcraft/
parts/
stage/
prime/
*.snap
*.snap-build
*_source.tar.bz2
.DS_Store
# nix-direnv generated files
.direnv/
# Make evidence files
/.make_evidence
# Manpage
/man
# Ignore AI/LLM instruction files
/.claude/
/.cursorrules
/.cursor/
/.goosehints
/.windsurfrules
/.github/copilot-instructions.md
/llms.txt
# Ignore worktrees when working on multiple branches
.worktrees/
# A Makefile for custom make targets
Makefile.local
# Local clone of the MCP server (separate repo, not a submodule of this project)
/mcp-mokogitea-api/
/public/img/avatar
-196
View File
@@ -1,196 +0,0 @@
version: "2"
output:
sort-order:
- file
linters:
default: none
enable:
- bidichk
- bodyclose
- depguard
- dupl
- errcheck
- forbidigo
- gocheckcompilerdirectives
- gocritic
- goheader
- govet
- ineffassign
- mirror
- modernize
- nakedret
- nilnil
- nolintlint
- perfsprint
- revive
- staticcheck
- testifylint
- unconvert
- unparam
- unused
- usestdlibvars
- usetesting
- wastedassign
settings:
depguard:
rules:
main:
deny:
- pkg: encoding/json
desc: use gitea's modules/json instead of encoding/json
- pkg: github.com/unknwon/com
desc: use gitea's util and replacements
- pkg: io/ioutil
desc: use os or io instead
- pkg: golang.org/x/exp
desc: it's experimental and unreliable
- pkg: code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/git/internal
desc: do not use the internal package, use AddXxx function instead
- pkg: gopkg.in/ini.v1
desc: do not use the ini package, use gitea's config system instead
- pkg: gitea.com/go-chi/cache
desc: do not use the go-chi cache package, use gitea's cache system
- pkg: github.com/pkg/errors
desc: use builtin errors package instead
migrations:
files:
- '**/models/migrations/**/*.go'
deny:
- pkg: code.mokoconsulting.tech/MokoConsulting/MokoGitea/models$
desc: migrations must not depend on the models package
- pkg: code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs
desc: migrations must not depend on modules/structs (API structures change over time)
nolintlint:
allow-unused: false
require-explanation: true
require-specific: true
gocritic:
enabled-checks:
- equalFold
disabled-checks:
- ifElseChain
- singleCaseSwitch # Every time this occurred in the code, there was no other way.
- deprecatedComment # conflicts with go-swagger comments
revive:
severity: error
rules:
- name: blank-imports
- name: constant-logical-expr
- name: context-as-argument
- name: context-keys-type
- name: dot-imports
- name: empty-lines
- name: error-return
- name: error-strings
- name: exported
- name: identical-branches
- name: if-return
- name: increment-decrement
- name: modifies-value-receiver
- name: package-comments
- name: redefines-builtin-id
- name: superfluous-else
- name: time-naming
- name: unexported-return
- name: var-declaration
- name: var-naming
arguments:
- [] # AllowList - do not remove as args for the rule are positional and won't work without lists first
- [] # DenyList
- - skip-package-name-checks: true # supress errors from underscore in migration packages
staticcheck:
checks:
- all
- -ST1003
- -ST1005
- -QF1001
- -QF1006
- -QF1008
testifylint:
disable:
- go-require
- require-error
usetesting:
os-temp-dir: true
perfsprint:
concat-loop: false
govet:
enable:
- nilness
- unusedwrite
goheader:
values:
regexp:
HEADER: '((Copyright [^\n]+|All rights reserved\.)\n)*Copyright \d{4} (The (Gogs|Gitea) Authors|Gitea Authors|Gitea)\.( All rights reserved\.)?(\n(Copyright [^\n]+|All rights reserved\.))*\nSPDX-License-Identifier: [\w.-]+'
template: '{{ HEADER }}'
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
rules:
- linters:
- dupl
- errcheck
- staticcheck
- unparam
path: _test\.go
- linters:
- dupl
- errcheck
path: models/migrations/v
- linters:
- forbidigo
path: cmd
- linters:
- dupl
text: (?i)webhook
- linters:
- gocritic
text: (?i)`ID' should not be capitalized
- linters:
- unused
text: (?i)swagger
- linters:
- gocritic
text: '(?i)commentFormatting: put a space between `//` and comment text'
- linters:
- gocritic
text: '(?i)exitAfterDefer:'
paths:
- node_modules
- .venv
- public
- web_src
- third_party$
- builtin$
- examples$
issues:
max-issues-per-linter: 0
max-same-issues: 0
formatters:
enable:
- gci
- gofumpt
settings:
gci:
custom-order: true
sections:
- standard
- prefix(code.mokoconsulting.tech/MokoConsulting/MokoGitea)
- blank
- default
gofumpt:
extra-rules: true
exclusions:
generated: lax
paths:
- node_modules
- .venv
- public
- web_src
run:
timeout: 10m
-8
View File
@@ -1,8 +0,0 @@
*.min.css
*.min.js
/assets/*.json
/options/gitignore
/options/license
/public/assets
/vendor
node_modules
+2
View File
@@ -0,0 +1,2 @@
self_approval_off = false
ignore_maintainers_file = true
-2
View File
@@ -1,2 +0,0 @@
Unknwon <u@gogs.io> <joe2010xtmf@163.com>
Unknwon <u@gogs.io> 无闻 <u@gogs.io>
-15
View File
@@ -1,15 +0,0 @@
commands-show-output: false
fenced-code-language: false
first-line-h1: false
heading-increment: false
line-length: {code_blocks: false, tables: false, stern: true, line_length: -1}
no-alt-text: false
no-bare-urls: false
no-emphasis-as-heading: false
no-empty-links: false
no-hard-tabs: {code_blocks: false}
no-inline-html: false
no-space-in-code: false
no-space-in-emphasis: false
no-trailing-spaces: {br_spaces: 0}
single-h1: false
-25
View File
@@ -1,25 +0,0 @@
{
"mcpServers": {
"ssh": {
"type": "stdio",
"command": "node",
"args": [
"A:/ssh-mcp/src/index.js"
]
},
"wiki": {
"type": "stdio",
"command": "node",
"args": [
"A:/wiki-mcp/dist/index.js"
]
},
"project": {
"type": "stdio",
"command": "node",
"args": [
"A:/project-mcp/dist/index.js"
]
}
}
}
-7
View File
@@ -1,7 +0,0 @@
audit=false
fund=false
update-notifier=false
save-exact=true
auto-install-peers=true
dedupe-peer-dependents=false
enable-pre-post-scripts=true
-12
View File
@@ -1,12 +0,0 @@
extends: [[spectral:oas, all]]
rules:
info-contact: off
oas2-api-host: off
oas2-parameter-description: off
oas2-schema: off
oas2-valid-schema-example: off
openapi-tags: off
operation-description: off
operation-singular-tag: off
operation-tag-defined: off
-42
View File
@@ -1,42 +0,0 @@
extends: default
rules:
braces:
min-spaces-inside: 0
max-spaces-inside: 1
min-spaces-inside-empty: 0
max-spaces-inside-empty: 0
brackets:
min-spaces-inside: 0
max-spaces-inside: 1
min-spaces-inside-empty: 0
max-spaces-inside-empty: 0
comments:
require-starting-space: true
ignore-shebangs: true
min-spaces-from-content: 1
comments-indentation:
level: error
document-start: disable
document-end:
present: false
empty-lines:
max: 1
indentation:
spaces: 2
line-length: disable
truthy:
allowed-values: ["true", "false", "on", "off"]
ignore: |
.venv
node_modules
-58
View File
@@ -1,58 +0,0 @@
# GNU makefile proxy script for BSD make
#
# Written and maintained by Mahmoud Al-Qudsi <mqudsi@neosmart.net>
# Copyright NeoSmart Technologies <https://neosmart.net/> 2014-2019
# Obtain updates from <https://github.com/neosmart/gmake-proxy>
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
JARG =
GMAKE = "gmake"
# When gmake is called from another make instance, -w is automatically added
# which causes extraneous messages about directory changes to be emitted.
# Running with --no-print-directory silences these messages.
GARGS = "--no-print-directory"
.if "$(.MAKE.JOBS)" != ""
JARG = -j$(.MAKE.JOBS)
.endif
# bmake prefers out-of-source builds and tries to cd into ./obj (among others)
# where possible. GNU Make doesn't, so override that value.
.OBJDIR: ./
# The GNU convention is to use the lowercased `prefix` variable/macro to
# specify the installation directory. Humor them.
GPREFIX =
.if defined(PREFIX) && ! defined(prefix)
GPREFIX = 'prefix = "$(PREFIX)"'
.endif
.BEGIN: .SILENT
which $(GMAKE) || (printf "Error: GNU Make is required!\n\n" 1>&2 && false)
.PHONY: FRC
$(.TARGETS): FRC
$(GMAKE) $(GPREFIX) $(GARGS) $(.TARGETS:S,.DONE,,) $(JARG)
.DONE .DEFAULT: .SILENT
$(GMAKE) $(GPREFIX) $(GARGS) $(.TARGETS:S,.DONE,,) $(JARG)
File diff suppressed because it is too large Load Diff
+189 -104
View File
@@ -1,115 +1,200 @@
# Changelog
## [Unreleased]
## [1.1.4](https://github.com/go-gitea/gitea/releases/tag/v1.1.4) - 2017-09-04
### Added
- Org branch protection: repositories now show the inherited organization rules read-only in their Branch Protection settings, with an expandable detail (direct push, force-push, branch deletion, merge restrictions, required approvals, status checks, protected files, and whitelisted teams) — like GitHub surfaces org rulesets in a repo (#727)
- Org branch protection: org-level rules can now also protect against branch deletion (`enable_delete` + delete allowlist teams), mirroring the per-repo delete allowlist (#727)
- Org-level tag protection: protect tag patterns org-wide (e.g. `v*`) with a team allowlist, layered on top of each repo's own protected tags — a tag is controllable only if allowed at both levels (fail-closed). API at `/orgs/{org}/tag_protections`; enforced at the git push/delete hook and the release create/delete paths; shown read-only in the repo Tag settings (#727)
- Org-level push policy: one policy per org, enforced in the pre-receive hook across all its repositories — branch/tag name conventions (glob), a mandatory secret-scanning block-on-push that repos cannot disable, a max pushed-file size, and blocked file-path patterns. API at `/orgs/{org}/push_policy`. Naming is fail-closed; the content checks (blocked paths, max size) fail open on error so a policy bug can never block every push (#727)
- Org-level repository defaults: an org can force new/transferred repositories private and set default pull-request settings (allowed merge styles, default merge style, auto-delete branch after merge), applied via a notifier when a repo is created in or transferred into the org (best-effort — never blocks repo creation). API at `/orgs/{org}/repo_defaults` (#727)
- Org-level email domain policy: restrict which email domains an organization's members may have — a user can only be added to the org (via any team) if their primary email matches one of the allowed domain globs. Enforced at the single membership-add choke point (`AddTeamMember`); API at `/orgs/{org}/email_domain_policy` (#727)
- Code security scanner: pattern-based detection of SQL injection, XSS, command injection, path traversal, insecure deserialization, hardcoded credentials, and weak cryptography across Go/PHP/Python/JS/TS (#552)
- Cascade merge: auto-create PRs to downstream branches after merge with configurable rules per repo (#460)
- Issue status presets: 4 built-in templates (default, software-development, support-tickets, bug-tracking) with API + web UI (#507)
- Cross-org status migration: copy status definitions from one org to another via API (#507)
- Auto-create default teams on org creation: Developers (write), Reviewers (read), CI/CD (actions+packages) (#513)
- Branch protection delete allowlist: configurable per-user/team/deploy-key allowlist for deleting protected branches (#696)
- Workflow subdirectory discovery: workflows in subdirectories of `.mokogitea/workflows/` are now auto-discovered (#693)
- API token scope `read:licensing` / `write:licensing` for licensing endpoints (#697)
- Edit API token scopes: PATCH /users/{username}/tokens/{id} API endpoint + web UI edit button (#697)
- Wiki full-text search: case-insensitive search across all wiki page titles and content (#550)
- Wiki search API: GET /wiki/search?q=term with paginated JSON results (#550)
- Metadata deploy fields: deploy_host, deploy_port, deploy_user, deploy_path, docker_image, docker_registry, container_name, health_url (#692)
- Security scanning API: REST endpoints for alerts, config, and on-demand scans (GET/PATCH /security/alerts, /security/config, POST /security/scan) (#692)
- Pre-receive hook secret blocking: push rejection when block_on_push enabled and secrets detected in commits (#692)
- Metadata API partial updates: PUT /metadata now merges only sent fields instead of replacing all
- Wiki revision diff: line-by-line diff view per commit in wiki page history (#667)
- Wiki categories: YAML frontmatter `categories:` with category index page (#668)
- Wiki template transclusion: `{{template:Name|key=val}}` with `_Template/` folder (#671)
- Wiki enhanced ToC: collapsible, inline via frontmatter, sticky sidebar (#673)
- Wiki folder ACL: `_access.yml` per-folder write protection (#674)
- Wiki print view and ZIP export of all wiki pages (#675)
- Wiki features documentation page in org wiki (standards/Wiki-Features)
- DLID licensing system: license, entitlement, activation, product_tier, audit_log tables (v359 migration)
- License CRUD with CRC32-checksummed DLID generation and format validation
- Entitlement model with tier-based rebuild and custom entitlement preservation
- Domain activation tracking with limit enforcement and auto-activate on first use
- 13 seeded product tiers from base to enterprise
- DLID-gated update XML endpoint: GET /api/v1/licensing/updates/{product}.xml
- Profile repo fallback chain: .mokogitea > .profile > .github
- Metadata/manifest GET endpoint publicly accessible without auth (#676)
- Org wiki: folder-based collapsible tree sidebar, _Sidebar.md overrides (#680)
- Wiki backlinks: "What links here" page showing all pages referencing current page (#669)
- Wiki wikilinks: [[Page Name]] and [[Page|Display Text]] syntax with red links for missing pages (#666)
- Required baseline issue statuses: Open and Closed are indestructible (is_required flag) (#681)
- Issue status API response includes is_required field
- Wiki recent changes page: cross-page edit activity with pagination (#670)
- Wiki page rename with automatic redirects via YAML frontmatter (#672)
* BUGFIXES
* Fix rendering of external links (#2292) (#2315)
* Fix deleted milestone bug (#1942) (#2300)
* fix 500 error when view an issue which's milestone deleted (#2297) (#2299)
* Fix SHA1 hash linking (#2143) (#2293)
* back port from #1709 (#2291)
### Security
- Cherry-pick upstream v1.26.3: LFS reject unknown SSH sub-verbs to prevent auth bypass (#38015)
- Cherry-pick upstream v1.26.3: bound CODEOWNERS regex match time — ReDoS prevention (#38025)
- Cherry-pick upstream v1.26.3: require merged PR to bypass fork PR approval gate (#38041)
- Cherry-pick upstream v1.26.3: LFS require Code-unit access for cross-repo object reuse (#38050)
- Cherry-pick upstream v1.26.3: hostmatcher block reserved IP ranges — SSRF prevention (#38059)
- Cherry-pick upstream v1.26.3: bound debian ParseControlFile — DoS prevention (#38055)
- Cherry-pick upstream v1.26.3: feed token scope, migration SSRF, notification redaction (#38147)
- Cherry-pick upstream v1.26.3: OIDC ignore stale external login links to organizations (#38141)
- Cherry-pick upstream v1.26.3: 2FA timing, branch delete auth, org labels visibility, merge upstream auth (#38151)
- Cherry-pick upstream v1.26.3: allow git clone of private repos with anonymous code access (#38146)
- Cherry-pick upstream v1.26.3: hostmatcher patch incorrect private IP list (#38173)
- Cherry-pick upstream v1.26.4: do not auto-reactivate disabled users on OAuth2 callback (#38183)
- Cherry-pick upstream v1.26.4: walk git log context error handling — regression fix (#38185)
## [1.1.3](https://github.com/go-gitea/gitea/releases/tag/v1.1.3) - 2017-08-03
### Fixed
- Fork server binary now compiles: `routers/api/v1/api.go` called `organization.HasOrgOrUserVisible`, which had been renamed to `IsOwnerVisibleToDoer`; the one missed call site broke `go build` of the entire `routers/api/v1` package (CI's Lint & Validate does not run a full build, so it went unnoticed) (#735)
- Dev deploy workflow: the build/deploy step referenced runner-side values as `\$TAG` / `\$REGISTRY_TOKEN` inside an unquoted SSH heredoc, deferring expansion to the remote shell where those names are unset — the Docker tag collapsed to an empty `mokogitea:` and every dev deploy failed with `invalid reference format`. Runner values are now injected via an ssh env-prefix and the heredoc is quoted so each `$var` expands in exactly one place (#737)
- Repaired unit-test compile and `go vet` failures: `CryptoRandomInt/String/Bytes` now return two values (updated `modules/util/util_test.go`), removed a redundant `&&` condition in `issue_comment.go`, and cleaned up isolated integration-test compile errors (#736)
- Removed a stray `package-lock.json` (13.9k lines) that a `git add -A` had accidentally swept into the org-push-policy branch (#734)
- Org-level branch protection now **layers** with per-repo rules instead of being ignored whenever a repo rule exists. When both an org rule and a repo rule match a branch, the effective rule is the most-restrictive (fail-closed) combination — the org rule is a mandatory floor a repo cannot weaken: allow flags AND'd, gate/require/block flags OR'd, required approvals max'd, status checks and protected-file patterns unioned, whitelists intersected. Previously a repo rule shadowed the org rule entirely at the enforcement choke point (`GetFirstMatchProtectedBranchRule`), letting a repo opt out of org protection (#727)
- Org Teams page: list now renders — the handler wrote `ctx.Data["OrgListTeams"]` but the template reads `.Teams`, so the page showed header/nav but no teams (#720)
- Issue type: now editable after creation for users with issue write permission — the sidebar gated editing on a `FieldEditFlags` data key that was never populated (always read-only); now uses `HasIssuesOrPullsWritePermission` like the priority field (#721)
- Admin config form: radio inputs (e.g. instance landing page Mode) no longer throw "Unsupported config form value mapping", which had aborted all JS init on the admin settings page
- PR check branch policy: allow `fix/*``main` and `patch/*``main` to match documented policy (was rejecting fix/patch PRs to main)
- PR check platform detection: guard for missing `.mokogitea/manifest.xml` so the Validate PR job no longer aborts under `set -e` (manifest replaced by metadata API)
- Remove dangling `mcp-mokogitea-api` submodule gitlink (no `.gitmodules` entry) that broke `submodule update --init` at checkout, failing all PR build/release jobs; ignore the local clone path
- PR RC Release workflow: no-op cleanly when `updates.xml` is absent (generic repos) instead of aborting the "Determine RC version" step under `set -e`
- PR check: platform detection now queries metadata API instead of removed manifest.xml
- Cherry-pick upstream v1.26.2: handle empty pull request files view to allow reviews (#37783)
- Cherry-pick upstream v1.26.2: fix "run as root" check with snap container detection (#37622)
- Cherry-pick upstream: ack re-sent UpdateLog finalize idempotently (#37885)
- Cherry-pick upstream: reject workflow_dispatch for workflows without that trigger (#37660)
- Cherry-pick upstream: keep action run title clickable when commit subject is a URL (#37867)
- Cherry-pick upstream: exclude workflow_call from workflow trigger detection (#37894)
- API token edit: reject empty scope update requests with 400 instead of silently succeeding
- Workflow token auth: pr-check.yml pre-release dispatch was silently failing due to env var / curl reference mismatch
- Workflow tokens: standardize all GA_TOKEN/GITEA_TOKEN/GITEA_URL env vars to MOKOGITEA_TOKEN/MOKOGITEA_URL across all workflow files in 5 template repos + MokoCLI (65+ files)
- CI issue reporter: rename GITEA_TOKEN/GITEA_URL to MOKOGITEA_TOKEN/MOKOGITEA_URL in automation/ci-issue-reporter.sh
- Workflow sync trigger: add workflow_dispatch event, fix if-condition to allow manual dispatch, add PHP install step for non-PHP runners
- Deploy workflow: merge dev health check into deploy job to avoid runner status reporting failures on inter-job handoff
- Licensing API: handle DB write errors in UpdateLicense, UpdateTier, DeleteTier instead of silently discarding
- Wiki API: fix findEntryForFile URL-decode fallback for non-ASCII page names
- Metadata settings template 500 error: removed reference to deleted Version field
- Wiki recent changes: use commit.MessageTitle() instead of commit.Message()
- Wiki backlinks: proper URL encoding for subdirectory pages
- Wiki wikilinks: page existence lookup normalizes spaces and hyphens
- Issue statuses template: garbled em-dash character replaced
* BUGFIXES
* Fix PR template error (#2008)
* Fix markdown rendering (fix #1530) (#2043)
* Fix missing less sources for oauth (backport #1288) (#2135)
* Don't ignore gravatar error (#2138)
* Fix diff of renamed and modified file (#2136)
* Fix fast-forward PR bug (#2137)
* Fix some security bugs
### Changed
- Custom workflows moved to `.mokogitea/workflows/custom/`: deploy-mokogitea, deploy-dev, cascade-dev, pr-rc-release, test-mokogitea, upstream-bug-sync
- Issue status seed defaults: Open, In Progress, Waiting, In Review, Closed, Won't Fix
- Pre-release workflow: auto-bump skipped for non-Joomla repos (platform check)
- CI issue reporter: moved to MokoCLI (cli/ci_issue_reporter.sh), pr-check and repo-health now use ci-issue-reporter.yml reusable workflow
## [1.1.2](https://github.com/go-gitea/gitea/releases/tag/v1.1.2) - 2017-06-13
### Removed
- Workflows: gitleaks.yml, npm-publish.yml, notify.yml, workflow-sync-trigger.yml, composer-publish.yml, deploy-manual.yml, security-audit.yml (not applicable to Go repo)
- automation/ci-issue-reporter.sh: moved to MokoCLI as centralized CLI tool
* BUGFIXES
* Enforce netgo build tag while cross-compilation (Backport of #1690) (#1731)
* fix update avatar
* fix delete user failed on sqlite (#1321)
* fix bug not to trim space of login username (#1806)
* Backport bugfixes #1220 and #1393 to v1.1 (#1758)
## [06.19.00] --- 2026-06-20
## [1.1.1](https://github.com/go-gitea/gitea/releases/tag/v1.1.1) - 2017-05-04
## [06.19.00] --- 2026-06-20
* BUGFIXES
* Markdown Sanitation Fix [#1646](https://github.com/go-gitea/gitea/pull/1646)
* Fix broken hooks [#1376](https://github.com/go-gitea/gitea/pull/1376)
* Fix migration issue [#1375](https://github.com/go-gitea/gitea/pull/1375)
* Fix Wiki Issues [#1338](https://github.com/go-gitea/gitea/pull/1338)
* Forgotten migration for wiki githooks [#1237](https://github.com/go-gitea/gitea/pull/1237)
* Commit messages can contain pipes [#1218](https://github.com/go-gitea/gitea/pull/1218)
* Verify external tracker URLs [#1236](https://github.com/go-gitea/gitea/pull/1236)
* Allow upgrade after downgrade [#1197](https://github.com/go-gitea/gitea/pull/1197)
* 500 on delete repo with issue [#1195](https://github.com/go-gitea/gitea/pull/1195)
* INI compat with CrowdIn [#1192](https://github.com/go-gitea/gitea/pull/1192)
## [06.19.00] --- 2026-06-20
## [1.1.0](https://github.com/go-gitea/gitea/releases/tag/v1.1.0) - 2017-03-09
## [06.19.00] --- 2026-06-19
* BREAKING
* The SSH keys can potentially break, make sure to regenerate the authorized keys
* FEATURE
* Git LFSv2 support [#122](https://github.com/go-gitea/gitea/pull/122)
* API endpoints for repo watching [#191](https://github.com/go-gitea/gitea/pull/191)
* Search within private repos [#222](https://github.com/go-gitea/gitea/pull/222)
* Hide user email address on explore page [#336](https://github.com/go-gitea/gitea/pull/336)
* Protected branch system [#339](https://github.com/go-gitea/gitea/pull/339)
* Sendmail for mail delivery [#355](https://github.com/go-gitea/gitea/pull/355)
* API endpoints for org webhooks [#372](https://github.com/go-gitea/gitea/pull/372)
* Enabled MSSQL support [#383](https://github.com/go-gitea/gitea/pull/383)
* API endpoints for org teams [#370](https://github.com/go-gitea/gitea/pull/370)
* API endpoints for collaborators [#375](https://github.com/go-gitea/gitea/pull/375)
* Graceful server restart [#416](https://github.com/go-gitea/gitea/pull/416)
* Commitgraph / timeline on commits page [#428](https://github.com/go-gitea/gitea/pull/428)
* API endpoints for repo forks [#509](https://github.com/go-gitea/gitea/pull/509)
* API endpoints for releases [#510](https://github.com/go-gitea/gitea/pull/510)
* Folder jumping [#511](https://github.com/go-gitea/gitea/pull/511)
* Stars tab on profile page [#519](https://github.com/go-gitea/gitea/pull/519)
* Notification system [#523](https://github.com/go-gitea/gitea/pull/523)
* Push and pull through reverse proxy basic auth [#524](https://github.com/go-gitea/gitea/pull/524)
* Search for issues and pull requests [#530](https://github.com/go-gitea/gitea/pull/530)
* API endpoint for stargazers [#597](https://github.com/go-gitea/gitea/pull/597)
* API endpoints for subscribers [#598](https://github.com/go-gitea/gitea/pull/598)
* PID file support [#610](https://github.com/go-gitea/gitea/pull/610)
* Two factor authentication (2FA) [#630](https://github.com/go-gitea/gitea/pull/630)
* API endpoints for org users [#645](https://github.com/go-gitea/gitea/pull/645)
* Release attachments [#673](https://github.com/go-gitea/gitea/pull/673)
* OAuth2 consumer [#679](https://github.com/go-gitea/gitea/pull/679)
* Add ability to fork your own repos [#761](https://github.com/go-gitea/gitea/pull/761)
* Search repository on dashboard [#773](https://github.com/go-gitea/gitea/pull/773)
* Search bar on user profile [#787](https://github.com/go-gitea/gitea/pull/787)
* Track label changes on issue view [#788](https://github.com/go-gitea/gitea/pull/788)
* Allow using custom time format [#798](https://github.com/go-gitea/gitea/pull/798)
* Redirects for renamed repos [#807](https://github.com/go-gitea/gitea/pull/807)
* Track assignee changes on issue view [#808](https://github.com/go-gitea/gitea/pull/808)
* Track title changes on issue view [#841](https://github.com/go-gitea/gitea/pull/841)
* Archive cleanup action [#885](https://github.com/go-gitea/gitea/pull/885)
* Basic Open Graph support [#901](https://github.com/go-gitea/gitea/pull/901)
* Take back control of Git hooks [#1006](https://github.com/go-gitea/gitea/pull/1006)
* API endpoints for user repos [#1059](https://github.com/go-gitea/gitea/pull/1059)
* BUGFIXES
* Fixed counting issues for issue filters [#413](https://github.com/go-gitea/gitea/pull/413)
* Added back default settings for SSH [#500](https://github.com/go-gitea/gitea/pull/500)
* Fixed repo permissions [#513](https://github.com/go-gitea/gitea/pull/513)
* Issues cannot be created with labels [#622](https://github.com/go-gitea/gitea/pull/622)
* Add a reserved wiki paths check to the wiki [#720](https://github.com/go-gitea/gitea/pull/720)
* Update website binding MaxSize to 255 [#722](https://github.com/go-gitea/gitea/pull/722)
* User can see the private activity on public history [#818](https://github.com/go-gitea/gitea/pull/818)
* Wrong pages number which includes private repositories [#844](https://github.com/go-gitea/gitea/pull/844)
* Trim whitespaces for search keyword [#893](https://github.com/go-gitea/gitea/pull/893)
* Don't rewrite non-gitea public keys [#906](https://github.com/go-gitea/gitea/pull/906)
* Use fingerprint to check instead content for public key [#911](https://github.com/go-gitea/gitea/pull/911)
* Fix random avatars [#1147](https://github.com/go-gitea/gitea/pull/1147)
* ENHANCEMENT
* Refactored process manager [#75](https://github.com/go-gitea/gitea/pull/75)
* Restrict rights to create new orgs [#193](https://github.com/go-gitea/gitea/pull/193)
* Added label and milestone sorting [#199](https://github.com/go-gitea/gitea/pull/199)
* Make minimum password length configurable [#223](https://github.com/go-gitea/gitea/pull/223)
* Speedup conflict checking on pull requests [#276](https://github.com/go-gitea/gitea/pull/276)
* Added button to delete merged pull request branches [#441](https://github.com/go-gitea/gitea/pull/441)
* Improved issue references within markdown [#471](https://github.com/go-gitea/gitea/pull/471)
* Dutch translation for the landingpage [#487](https://github.com/go-gitea/gitea/pull/487)
* Added Gogs migration script [#532](https://github.com/go-gitea/gitea/pull/532)
* Support a .gitea folder for issue templates [#582](https://github.com/go-gitea/gitea/pull/582)
* Enhanced diff-view coloring [#584](https://github.com/go-gitea/gitea/pull/584)
* Added ETag header to avatars [#721](https://github.com/go-gitea/gitea/pull/721)
* Added option to config to disable local path imports [#724](https://github.com/go-gitea/gitea/pull/724)
* Allow custom public files [#782](https://github.com/go-gitea/gitea/pull/782)
* Added pprof endpoint for debugging [#801](https://github.com/go-gitea/gitea/pull/801)
* Added X-GitHub-* headers [#809](https://github.com/go-gitea/gitea/pull/809)
* Fill SSH key title automatically [#863](https://github.com/go-gitea/gitea/pull/863)
* Display Git version on admin panel [#921](https://github.com/go-gitea/gitea/pull/921)
* Expose URL field on issue API [#982](https://github.com/go-gitea/gitea/pull/982)
* Statically compile the binaries [#985](https://github.com/go-gitea/gitea/pull/985)
* Embed build tags into version string [#1051](https://github.com/go-gitea/gitea/pull/1051)
* Gitignore support for FSharp and Clojure [#1072](https://github.com/go-gitea/gitea/pull/1072)
* Custom templates for static builds [#1087](https://github.com/go-gitea/gitea/pull/1087)
* Add ProxyFromEnvironment if none set [#1096](https://github.com/go-gitea/gitea/pull/1096)
* MISC
* Replaced remaining Gogs references
* Added more tests on various packages
* Use Crowdin for translations again
* Resolved some XSS attack vectors
* Optimized and reduced number of database queries
## [1.0.2](https://github.com/go-gitea/gitea/releases/tag/v1.0.2) - 2017-02-21
* BUGFIXES
* Fixed issue counter [#882](https://github.com/go-gitea/gitea/pull/882)
* Fixed XSS vulnerability on wiki page [#955](https://github.com/go-gitea/gitea/pull/955)
* Add data dir without session to dump [#587](https://github.com/go-gitea/gitea/pull/587)
* Fixed wiki page renaming [#958](https://github.com/go-gitea/gitea/pull/958)
* Drop default console logger if not required [#960](https://github.com/go-gitea/gitea/pull/960)
* Fixed docker docs link on install page [#972](https://github.com/go-gitea/gitea/pull/972)
* Handle SetModel errors [#957](https://github.com/go-gitea/gitea/pull/957)
* Fixed XSS vulnerability on milestones [#977](https://github.com/go-gitea/gitea/pull/977)
* Fixed XSS vulnerability on alerts [#981](https://github.com/go-gitea/gitea/pull/981)
## [1.0.1](https://github.com/go-gitea/gitea/releases/tag/v1.0.1) - 2017-01-05
* BUGFIXES
* Fixed localized MIN_PASSWORD_LENGTH [#501](https://github.com/go-gitea/gitea/pull/501)
* Fixed 500 error on organization delete [#507](https://github.com/go-gitea/gitea/pull/507)
* Ignore empty wiki repo on migrate [#544](https://github.com/go-gitea/gitea/pull/544)
* Proper check access for forking [#563](https://github.com/go-gitea/gitea/pull/563)
* Fix SSH domain on installer [#506](https://github.com/go-gitea/gitea/pull/506)
* Fix missing data rows on admin UI [#580](https://github.com/go-gitea/gitea/pull/580)
* Do not delete tags with releases by default [#579](https://github.com/go-gitea/gitea/pull/579)
* Fix missing session config data on admin UI [#578](https://github.com/go-gitea/gitea/pull/578)
* Properly show the version within footer on the UI [#593](https://github.com/go-gitea/gitea/pull/593)
## [1.0.0](https://github.com/go-gitea/gitea/releases/tag/v1.0.0) - 2016-12-23
* BREAKING
* We have various changes on the API, scripting against API must be updated
* FEATURE
* Show last login for admins [#121](https://github.com/go-gitea/gitea/pull/121)
* BUGFIXES
* Fixed sender of notifications [#2](https://github.com/go-gitea/gitea/pull/2)
* Fixed keyword hijacking vulnerability [#20](https://github.com/go-gitea/gitea/pull/20)
* Fixed non-markdown readme rendering [#95](https://github.com/go-gitea/gitea/pull/95)
* Allow updating draft releases [#169](https://github.com/go-gitea/gitea/pull/169)
* GitHub API compliance [#227](https://github.com/go-gitea/gitea/pull/227)
* Added commit SHA to tag webhook [#286](https://github.com/go-gitea/gitea/issues/286)
* Secured links via noopener [#315](https://github.com/go-gitea/gitea/issues/315)
* Replace tabs with spaces on wiki title [#371](https://github.com/go-gitea/gitea/pull/371)
* Fixed vulnerability on labels and releases [#409](https://github.com/go-gitea/gitea/pull/409)
* Fixed issue comment API [#449](https://github.com/go-gitea/gitea/pull/449)
* ENHANCEMENT
* Use proper import path for libravatar [#3](https://github.com/go-gitea/gitea/pull/3)
* Integrated DroneCI for tests and builds [#24](https://github.com/go-gitea/gitea/issues/24)
* Integrated dependency manager [#29](https://github.com/go-gitea/gitea/issues/29)
* Embedded bindata optionally [#30](https://github.com/go-gitea/gitea/issues/30)
* Integrated pagination for releases [#73](https://github.com/go-gitea/gitea/pull/73)
* Autogenerate version on every build [#91](https://github.com/go-gitea/gitea/issues/91)
* Refactored Docker container [#104](https://github.com/go-gitea/gitea/issues/104)
* Added short-hash support for downloads [#211](https://github.com/go-gitea/gitea/issues/211)
* Display tooltip for downloads [#221](https://github.com/go-gitea/gitea/issues/221)
* Improved HTTP headers for issue attachments [#270](https://github.com/go-gitea/gitea/pull/270)
* Integrate public as bindata optionally [#293](https://github.com/go-gitea/gitea/pull/293)
* Integrate templates as bindata optionally [#314](https://github.com/go-gitea/gitea/pull/314)
* Inject more ENV variables into custom hooks [#316](https://github.com/go-gitea/gitea/issues/316)
* Correct LDAP login validation [#342](https://github.com/go-gitea/gitea/pull/342)
* Integrate conf as bindata optionally [#354](https://github.com/go-gitea/gitea/pull/354)
* Serve video files in browser [#418](https://github.com/go-gitea/gitea/pull/418)
* Configurable SSH host binding [#431](https://github.com/go-gitea/gitea/issues/431)
* MISC
* Forked from Gogs and renamed to Gitea
* Catching more errors with logs
* Fixed all linting errors
* Made the go linter entirely happy
* Really integrated vendoring
-96
View File
@@ -1,96 +0,0 @@
# Gitea Community Code of Conduct
## About
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.
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.
The second goal is to provide a mechanism for resolving conflicts in the community when they arise.
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.
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.
## Community values
These are the values to which people in the Gitea community should aspire.
- **Be friendly and welcoming.**
- **Be patient.**
- Remember that people have varying communication styles and that not everyone is using their native language. (Meaning and tone can be lost in translation.)
- **Be thoughtful.**
- Productive communication requires effort. Think about how your words will be interpreted.
- Remember that sometimes it is best to refrain entirely from commenting.
- **Be respectful.**
- In particular, respect differences of opinion.
- **Be charitable.**
- Interpret the arguments of others in good faith, do not seek to disagree.
- When we do disagree, try to understand why.
- **Be constructive.**
- Avoid derailing: stay on topic; if you want to talk about something else, start a new conversation.
- Avoid unconstructive criticism: don't merely decry the current state of affairs; offer—or at least solicit—suggestions as to how things may be improved.
- Avoid snarking (pithy, unproductive, sniping comments).
- Avoid discussing potentially offensive or sensitive issues; this all too often leads to unnecessary conflict.
- Avoid microaggressions (brief and commonplace verbal, behavioral and environmental indignities that communicate hostile, derogatory or negative slights and insults to a person or group).
- **Be responsible.**
- What you say and do matters. Take responsibility for your words and actions, including their consequences, whether intended or otherwise.
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.
## Code of Conduct
### Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to make participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
### Our Standards
Examples of behavior that contributes to creating a positive environment include:
- Using welcoming and inclusive language
- Being respectful of differing viewpoints and experiences
- Gracefully accepting constructive criticism
- Focusing on what is best for the community
- Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
- The use of sexualized language or imagery and unwelcome sexual attention or advances
- Trolling, insulting/derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others private information, such as a physical or electronic address, without explicit permission
- Other conduct which could reasonably be considered inappropriate in a professional setting
### Our Responsibilities
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.
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.
### Scope
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.
This Code of Conduct also applies outside the project spaces when the Project Stewards have a reasonable belief that an individuals behavior may have a negative impact on the project or its community.
### Conflict Resolution
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 projects code of conduct.
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.
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.
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 anyones safety, we may take action without notice.
### Attribution
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
## Summary
- Treat everyone with respect and kindness.
- Be thoughtful in how you communicate.
- Dont be destructive or inflammatory.
- If you encounter an issue, please mail conduct@gitea.com.
+104 -146
View File
@@ -1,161 +1,119 @@
# Contributing to Moko Consulting Projects
# Contribution Guidelines
Thank you for your interest in contributing. All Moko Consulting repositories follow this universal workflow and version policy.
## Introduction
## Branching Workflow
This document explains how to contribute changes to the Gitea project. It assumes you have followed the [installation instructions](https://docs.gitea.io/en-us/). Sensitive security-related issues should be reported to [security@gitea.io](mailto:security@gitea.io).
```
feature/* ──PR──> dev ──draft PR──> (renamed to rc) ──merge──> main
## Bug reports
Please search the issues on the issue tracker with a variety of keywords to ensure your bug is not already reported.
If unique, [open an issue](https://github.com/go-gitea/gitea/issues/new) and answer the questions so we can understand and reproduce the problematic behavior.
To show us that the issue you are having is in Gitea itself, please write clear, concise instructions so we can reproduce the behavior (even if it seems obvious). The more detailed and specific you are, the faster we can fix the issue. Check out [How to Report Bugs Effectively](http://www.chiark.greenend.org.uk/~sgtatham/bugs.html).
Please be kind, remember that Gitea comes at no cost to you, and you're getting free help.
## Discuss your design
The project welcomes submissions but please let everyone know what you're working on if you want to change or add something to the Gitea repositories.
Before starting to write something new for the Gitea project, please [file an issue](https://github.com/go-gitea/gitea/issues/new). Significant changes must go through the [change proposal process](https://github.com/go-gitea/proposals) before they can be accepted.
This process gives everyone a chance to validate the design, helps prevent duplication of effort, and ensures that the idea fits inside the goals for the project and tools. It also checks that the design is sound before code is written; the code review tool is not the place for high-level discussions.
## Testing redux
Before sending code out for review, run all the tests for the whole tree to make sure the changes don't break other usage and keep the compatibility on upgrade. To make sure you are running the test suite exactly like we do, you should install the CLI for [Drone CI](https://github.com/drone/drone), as we are using the server for continous testing, following [these instructions](http://readme.drone.io/usage/getting-started-cli). After that you can simply call `drone exec` within your working directory and it will try to run the test suite locally.
## Vendoring
We keep a cached copy of dependencies within the `vendor/` directory, managing updates via [govendor](http://github.com/kardianos/govendor).
Pull requests should only include `vendor/` updates if they are part of the same change, be it a bugfix or a feature addition.
The `vendor/` update needs to be justified as part of the PR description, and must be verified by the reviewers and/or merger to always reference an existing upstream commit.
## Code review
Changes to Gitea must be reviewed before they are accepted, no matter who makes the change even if it is an owner or a maintainer. We use GitHub's pull request workflow to do that and we also use [LGTM](http://lgtm.co) to ensure every PR is reviewed by at least 2 maintainers.
Please try to make your pull request easy to review for us. Please read the "[How to get faster PR reviews](https://github.com/kubernetes/community/blob/master/contributors/devel/faster_reviews.md)" guide, it has lots of useful tips for any project you may want to contribute. Some of the key points:
* Make small pull requests. The smaller, the faster to review and the more likely it will be merged soon.
* Don't make changes unrelated to your PR. Maybe there are typos on some comments, maybe refactoring would be welcome on a function... but if that is not related to your PR, please make *another* PR for that.
* Split big pull requests into multiple small ones. An incremental change will be faster to review than a huge PR.
## Styleguide
For imports you should use the following format (_without_ the comments)
```go
import (
// stdlib
"encoding/json"
"fmt"
// local packages
"code.gitea.io/gitea/models"
"code.gitea.io/sdk/gitea"
// external packages
"github.com/foo/bar"
"gopkg.io/baz.v1"
)
```
### Step by step
## Sign your work
1. **Create a feature branch** from `dev`:
```bash
git checkout dev && git pull
git checkout -b feature/my-change
```
2. **Work and commit** on your feature branch. Push to origin.
3. **Open a PR**: `feature/my-change` → `dev`. After review and checks, merge it.
4. **When ready for release**, open a **draft PR**: `dev` → `main`.
- This automatically renames the source branch to `rc` (release candidate)
- An RC pre-release is built and uploaded
5. **Alpha and beta branches** are created by manually renaming the branch before the RC stage:
- Rename `dev` to `alpha` for early testing → alpha pre-release is built
- Rename `alpha` to `beta` for feature-complete testing → beta pre-release is built
- When the draft PR is created, the branch is renamed to `rc`
6. **Once PR checks pass** on the `rc` branch, mark the PR as ready and merge to `main`.
7. **Merging to main** triggers the stable release pipeline:
- Minor version bump (e.g., `02.09.xx` → `02.10.00`)
- Stability suffix stripped (clean version)
- Gitea release created with ZIP/tar.gz packages
- `updates.xml` updated (Joomla extensions)
- `dev` branch recreated from `main`
### Branch summary
| Branch | Purpose | Created by |
|--------|---------|-----------|
| `feature/*` | New features and fixes | Developer |
| `dev` | Integration branch | Auto-recreated after release |
| `alpha` | Alpha pre-release testing | Manual rename from `dev` |
| `beta` | Beta pre-release testing | Manual rename from `alpha` |
| `rc` | Release candidate | Auto-renamed on draft PR to main |
| `main` | Stable releases | Protected, merge only |
| `version/XX.YY.ZZ` | Archived release snapshots | Auto-created by CI |
### Protected branches
| Branch | Direct push | Merge via |
|--------|------------|-----------|
| `main` | Blocked (CI bot whitelisted) | PR merge only |
| `dev` | Blocked (CI bot whitelisted) | PR merge from feature/* |
| `rc` | Blocked (CI bot whitelisted) | Auto-created on draft PR |
| `alpha` | Blocked (CI bot whitelisted) | Manual rename |
| `beta` | Blocked (CI bot whitelisted) | Manual rename |
| `feature/*` | Open | N/A (source branch) |
## Version Policy
### Format
All versions use `XX.YY.ZZ` — three two-digit segments, zero-padded:
- **XX** — Major version (breaking changes)
- **YY** — Minor version (new features, bumped on release to main)
- **ZZ** — Patch version (auto-incremented on every push to dev/feature branches)
Rollover: patch `99` → `00` increments minor; minor `99` → `00` increments major.
### Stability suffixes
Each branch appends a suffix to indicate stability:
| Branch | Suffix | Example |
|--------|--------|---------|
| `main` | (none) | `02.09.00` |
| `dev` | `-dev` | `02.09.01-dev` |
| `feature/*` | `-dev` | `02.09.01-dev` |
| `alpha` | `-alpha` | `02.09.01-alpha` |
| `beta` | `-beta` | `02.09.01-beta` |
| `rc` | `-rc` | `02.09.01-rc` |
### Auto version bump
On every push to `dev`, `feature/*`, or `patch/*`:
1. Patch version incremented
2. Stability suffix `-dev` applied
3. All version-bearing files updated (manifests, CHANGELOG, PHP headers, etc.)
4. Commit created with `[skip ci]` to avoid loops
### Release version flow
Version bumps happen at specific release events:
| Event | Bump | Example |
|-------|------|---------|
| Feature merged to dev | Patch bump after dev release | `02.09.01-dev` → release → `02.09.02-dev` |
| Dev promoted to RC | Minor bump | `02.09.02-dev` → `02.10.00-rc` |
| RC merged to main | Minor bump | `02.10.00-rc` → `02.11.00` (stable) |
| Dev recreated from main | Patch bump | `02.11.00` → `02.11.01-dev` |
### Release stream copies
When a higher-stability release is published, copies are created for all lesser streams with the same base version:
- **RC `02.10.00-rc`** also creates: `02.10.00-dev`, `02.10.00-alpha`, `02.10.00-beta`
- **Stable `02.11.00`** also creates: `02.11.00-dev`, `02.11.00-alpha`, `02.11.00-beta`, `02.11.00-rc`
This ensures Joomla sites on ANY stability channel see the update (Joomla only shows versions higher than what's installed).
### Version files
The version tools update all files containing version stamps:
- `.mokogitea/manifest.xml` (canonical source)
- Joomla XML manifests (`<version>` tag)
- `README.md`, `CHANGELOG.md` (`VERSION:` pattern)
- `package.json`, `pyproject.toml`
- Any text file with a `VERSION: XX.YY.ZZ` label
Files synced from other repos (with a `# REPO:` header) are not touched.
## Code Standards
- **PHP**: PSR-12, tabs for indentation
- **Copyright**: all files must include the Moko Consulting copyright header
- **License**: SPDX identifier `GPL-3.0-or-later` (or as specified per repo)
- **Attribution**: use `Authored-by: Moko Consulting` in commits, not individual names
## Commit Messages
Use conventional commit format:
The sign-off is a simple line at the end of the explanation for the patch. Your signature certifies that you wrote the patch or otherwise have the right to pass it on as an open-source patch. The rules are pretty simple: If you can certify [DCO](DCO), then you just add a line to every git commit message:
```
type(scope): short description
Optional body with context.
Authored-by: Moko Consulting
Signed-off-by: Joe Smith <joe.smith@email.com>
```
Types: `feat`, `fix`, `chore`, `docs`, `style`, `refactor`, `test`, `ci`
Please use your real name, we really dislike pseudonyms or anonymous contributions. We are in the open-source world without secrets. If you set your `user.name` and `user.email` git configs, you can sign your commit automatically with `git commit -s`.
Special flags in commit messages:
- `[skip ci]` — skip all CI workflows
- `[skip bump]` — skip auto version bump only
## Release Cycle
## Reporting Issues
We adopted a release schedule to streamline the process of working on, finishing, and issuing releases. The overall goal is to make a major release every two months, which breaks down into one month of general development followed by one month of testing and polishing known as the release freeze. A release is maintained by issuing minor releases to only correct critical problems such as crashes or security issues. All the feature pull requests should be merged in the first month of one release period.
Use the repository's issue tracker with the appropriate template.
The current release cycle is aligned to start on December 25 to February 24, next is February 25 to April 24, and etc. On this cycle, we also maybe publish the previous release minor version. For example, the current release version is v1.1, but we maybe also publish v1.0.2. When we publish v1.2, then we will stop publish v1.0.3.
---
## Maintainers
*Moko Consulting <hello@mokoconsulting.tech>*
To make sure every PR is checked, we have [team maintainers](https://github.com/orgs/go-gitea/teams/maintainers). Every PR **MUST** be reviewed by at least two maintainers (or owners) before it can get merged. A maintainer should be a contributor of Gitea (or Gogs) and contributed at least 4 accepted PRs. A contributor should apply as a maintainer in the [Gitter develop channel](https://gitter.im/go-gitea/develop). The owners or the team maintainers may invite the contributor. A maintainer should spend some time on code reviews. If a maintainer has no time to do that, they should apply to leave the maintainers team and we will give them the honor of being a member of the [advisors team](https://github.com/orgs/go-gitea/teams/advisors). Of course, if an advisor has time to code review, we will gladly welcome them back to the maintainers team. If a maintainer is inactive for more than 3 months and forgets to leave the maintainers team, the owners may move him or her from the maintainers team to the advisors team.
## Owners
Since Gitea is a pure community organization without any company support, to keep the development healthy we will elect three owners every year. All contributors may vote to elect up to three candidates, one of which will be the main owner, and the other two the assistant owners. When the new owners have been elected, the old owners will give up ownership to the newly elected owners. If an owner is unable to do so, the other owners will assist in ceding ownership to the newly elected owners.
After the election, the new owners should proactively agree with our [CONTRIBUTING](CONTRIBUTING.md) requirements on the [Gitter main channel](https://gitter.im/go-gitea/gitea). Below are the words to speak:
```
I'm honored to having been elected an owner of Gitea, I agree with [CONTRIBUTING](CONTRIBUTING.md). I will spend part of my time on Gitea and lead the development of Gitea.
```
To honor the past owners, here's the history of the owners and the time they served:
* 2016-11-04 ~ 2017-12-31
* [Lunny Xiao](https://github.com/lunny) <xiaolunwen@gmail.com>
* [Thomas Boerger](https://github.com/tboerger) <thomas@webhippie.de>
* [Kim Carlbäcker](https://github.com/bkcsoft) <kim.carlbacker@gmail.com>
## Versions
Gitea has the `master` branch as a tip branch and has version branches such as `v0.9`. `v0.9` is a release branch and we will tag `v0.9.0` for binary download. If `v0.9.0` has bugs, we will accept pull requests on the `v0.9` branch and publish a `v0.9.1` tag, after bringing the bug fix also to the master branch.
Since the `master` branch is a tip version, if you wish to use Gitea in production, please download the latest release tag version. All the branches will be protected via GitHub, all the PRs to every branch must be reviewed by two maintainers and must pass the automatic tests.
## Copyright
Code that you contribute should use the standard copyright header:
```
// Copyright 2017 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
```
Files in the repository contain copyright from the year they are added to the year they are last changed. If the copyright author is changed, just paste the header below the old one.
+3 -1
View File
@@ -2,6 +2,8 @@ Developer Certificate of Origin
Version 1.1
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
660 York Street, Suite 102,
San Francisco, CA 94110 USA
Everyone is permitted to copy and distribute verbatim copies of this
license document, but changing it is not allowed.
@@ -31,4 +33,4 @@ By making a contribution to this project, I certify that:
are public and that a record of the contribution (including all
personal information I submit with it, including my sign-off) is
maintained indefinitely and may be redistributed consistent with
this project or the open source license(s) involved.
this project or the open source license(s) involved.
+21 -69
View File
@@ -1,68 +1,23 @@
# syntax=docker/dockerfile:1
# Build frontend on the native platform to avoid QEMU-related issues with nodejs ecosystem
FROM --platform=$BUILDPLATFORM docker.io/library/golang:1.26-alpine3.23 AS frontend-build
RUN apk --no-cache add build-base git nodejs pnpm
WORKDIR /src
COPY package.json pnpm-lock.yaml .npmrc ./
RUN --mount=type=cache,target=/root/.local/share/pnpm/store pnpm install --frozen-lockfile
COPY --exclude=.git/ . .
RUN make frontend
# Build backend for each target platform
FROM docker.io/library/golang:1.26-alpine3.23 AS build-env
ARG GITEA_VERSION
ARG TAGS=""
ENV TAGS="bindata timetzdata $TAGS"
ARG CGO_EXTRA_CFLAGS
# Build deps
RUN apk --no-cache add \
build-base \
git
WORKDIR ${GOPATH}/src/code.mokoconsulting.tech/MokoConsulting/MokoGitea
COPY go.mod go.sum ./
RUN go mod download
# Use COPY instead of bind mount as read-only one breaks makefile state tracking
COPY --exclude=.git/ . .
COPY --from=frontend-build /src/public/assets public/assets
# Build gitea, .git mount is required for version data
# GOFLAGS=-p 1 serializes compilation to prevent OOM on low-memory servers
ARG GOFLAGS="-p 1"
RUN --mount=type=cache,target="/root/.cache/go-build" \
--mount=type=bind,source=".git/",target=".git/" \
GOFLAGS="${GOFLAGS}" make backend
COPY docker/root /tmp/local
# Set permissions for builds that made under windows which strips the executable bit from file
RUN chmod 755 /tmp/local/usr/bin/entrypoint \
/tmp/local/usr/local/bin/* \
/tmp/local/etc/s6/gitea/* \
/tmp/local/etc/s6/openssh/* \
/tmp/local/etc/s6/.s6-svscan/* \
/go/src/code.mokoconsulting.tech/MokoConsulting/MokoGitea/gitea
FROM docker.io/library/alpine:3.23 AS gitea
FROM alpine:3.4
MAINTAINER Thomas Boerger <thomas@webhippie.de>
EXPOSE 22 3000
RUN apk --no-cache add \
bash \
RUN apk update && \
apk add \
su-exec \
ca-certificates \
curl \
gettext \
sqlite \
bash \
git \
linux-pam \
openssh \
s6 \
sqlite \
su-exec \
gnupg
RUN addgroup \
curl \
openssh \
tzdata && \
rm -rf \
/var/cache/apk/* && \
addgroup \
-S -g 1000 \
git && \
adduser \
@@ -72,19 +27,16 @@ RUN addgroup \
-u 1000 \
-G git \
git && \
echo "git:*" | chpasswd -e
echo "git:$(date +%s | sha256sum | base64 | head -c 32)" | chpasswd
COPY --from=build-env /tmp/local /
COPY --from=build-env /go/src/code.mokoconsulting.tech/MokoConsulting/MokoGitea/gitea /app/gitea/gitea
# Disable openssh s6 service — we use external SSH (port 2222 via host).
RUN printf '#!/bin/sh\nexec sleep infinity\n' > /etc/s6/openssh/run && chmod 755 /etc/s6/openssh/run
ENV USER=git
ENV GITEA_CUSTOM=/data/gitea
ENV USER git
ENV GITEA_CUSTOM /data/gitea
ENV GODEBUG=netdns=go
VOLUME ["/data"]
# HINT: HEALTH-CHECK-ENDPOINT: don't use HEALTHCHECK, search this hint keyword for more information
ENTRYPOINT ["/usr/bin/entrypoint"]
CMD ["/usr/bin/s6-svscan", "/etc/s6"]
CMD ["/bin/s6-svscan", "/etc/s6"]
COPY docker /
COPY gitea /app/gitea/gitea
-88
View File
@@ -1,88 +0,0 @@
# syntax=docker/dockerfile:1
# Build frontend on the native platform to avoid QEMU-related issues with nodejs ecosystem
FROM --platform=$BUILDPLATFORM docker.io/library/golang:1.26-alpine3.23 AS frontend-build
RUN apk --no-cache add build-base git nodejs pnpm
WORKDIR /src
COPY package.json pnpm-lock.yaml .npmrc ./
RUN --mount=type=cache,target=/root/.local/share/pnpm/store pnpm install --frozen-lockfile
COPY --exclude=.git/ . .
RUN make frontend
# Build backend for each target platform
FROM docker.io/library/golang:1.26-alpine3.23 AS build-env
ARG GITEA_VERSION
ARG TAGS=""
ENV TAGS="bindata timetzdata $TAGS"
ARG CGO_EXTRA_CFLAGS
# Build deps
RUN apk --no-cache add \
build-base \
git
WORKDIR ${GOPATH}/src/code.gitea.io/gitea
COPY go.mod go.sum ./
RUN go mod download
# See the comments in Dockerfile
COPY --exclude=.git/ . .
COPY --from=frontend-build /src/public/assets public/assets
# Build gitea, .git mount is required for version data
RUN --mount=type=cache,target="/root/.cache/go-build" \
--mount=type=bind,source=".git/",target=".git/" \
make backend
COPY docker/rootless /tmp/local
# Set permissions for builds that made under windows which strips the executable bit from file
RUN chmod 755 /tmp/local/usr/local/bin/* \
/go/src/code.gitea.io/gitea/gitea
FROM docker.io/library/alpine:3.23 AS gitea-rootless
EXPOSE 2222 3000
RUN apk --no-cache add \
bash \
ca-certificates \
dumb-init \
gettext \
git \
curl \
gnupg \
openssh-keygen
RUN addgroup \
-S -g 1000 \
git && \
adduser \
-S -H -D \
-h /var/lib/gitea/git \
-s /bin/bash \
-u 1000 \
-G git \
git
RUN mkdir -p /var/lib/gitea /etc/gitea
RUN chown git:git /var/lib/gitea /etc/gitea
COPY --from=build-env /tmp/local /
COPY --from=build-env --chown=root:root /go/src/code.gitea.io/gitea/gitea /app/gitea/gitea
# git:git
USER 1000:1000
ENV GITEA_WORK_DIR=/var/lib/gitea
ENV GITEA_CUSTOM=/var/lib/gitea/custom
ENV GITEA_TEMP=/tmp/gitea
ENV TMPDIR=/tmp/gitea
# TODO add to docs the ability to define the ini to load (useful to test and revert a config)
ENV GITEA_APP_INI=/etc/gitea/app.ini
ENV HOME="/var/lib/gitea/git"
VOLUME ["/var/lib/gitea", "/etc/gitea"]
WORKDIR /var/lib/gitea
# HINT: HEALTH-CHECK-ENDPOINT: don't use HEALTHCHECK, search this hint keyword for more information
ENTRYPOINT ["/usr/bin/dumb-init", "--", "/usr/local/bin/docker-entrypoint.sh"]
CMD []
+42
View File
@@ -0,0 +1,42 @@
FROM hypriot/rpi-alpine-scratch:v3.4
MAINTAINER Thomas Boerger <thomas@webhippie.de>
EXPOSE 22 3000
RUN apk update && \
apk add \
su-exec \
ca-certificates \
sqlite \
bash \
git \
linux-pam \
s6 \
curl \
openssh \
tzdata && \
rm -rf \
/var/cache/apk/* && \
addgroup \
-S -g 1000 \
git && \
adduser \
-S -H -D \
-h /data/git \
-s /bin/bash \
-u 1000 \
-G git \
git && \
echo "git:$(date +%s | sha256sum | base64 | head -c 32)" | chpasswd
ENV USER git
ENV GITEA_CUSTOM /data/gitea
ENV GODEBUG=netdns=go
VOLUME ["/data"]
ENTRYPOINT ["/usr/bin/entrypoint"]
CMD ["/bin/s6-svscan", "/etc/s6"]
COPY docker /
COPY gitea /app/gitea/gitea
+2 -55
View File
@@ -1,67 +1,14 @@
Alexey Makhov <amakhov@avito.ru> (@makhov)
Andrey Nering <andrey.nering@gmail.com> (@andreynering)
Bo-Yi Wu <appleboy.tw@gmail.com> (@appleboy)
Ethan Koenig <ethantkoenig@gmail.com> (@ethantkoenig)
Kees de Vries <bouwko@gmail.com> (@Bwko)
Kim Carlbäcker <kim.carlbacker@gmail.com> (@bkcsoft)
LefsFlare <nobody@nobody.tld> (@LefsFlarey)
Lunny Xiao <xiaolunwen@gmail.com> (@lunny)
Matthias Loibl <mail@matthiasloibl.com> (@metalmatze)
Rachid Zarouali <nobody@nobody.tld> (@xinity)
Rémy Boulanouar <admin@dblk.org> (@DblK)
Sandro Santilli <strk@kbt.io> (@strk)
Thibault Meyer <meyer.thibault@gmail.com> (@0xbaadf00d)
Thomas Boerger <thomas@webhippie.de> (@tboerger)
Patrick G <geek1011@outlook.com> (@geek1011)
Antoine Girard <sapk@sapk.fr> (@sapk)
Lauris Bukšis-Haberkorns <lauris@nix.lv> (@lafriks)
Jonas Östanbäck <jonas.ostanback@gmail.com> (@cez81)
David Schneiderbauer <dschneiderbauer@gmail.com> (@daviian)
Peter Žeby <morlinest@gmail.com> (@morlinest)
Matti Ranta <techknowlogick@gitea.io> (@techknowlogick)
Jonas Franz <info@jonasfranz.software> (@jonasfranz)
Alexey Terentyev <axifnx@gmail.com> (@axifive)
Lanre Adelowo <yo@lanre.wtf> (@adelowo)
Konrad Langenberg <k@knt.li> (@kolaente)
He-Long Zhang <outman99@hotmail.com> (@BetaCat0)
Andrew Thornton <art27@cantab.net> (@zeripath)
John Olheiser <john.olheiser@gmail.com> (@jolheiser)
Richard Mahn <rich.mahn@unfoldingword.org> (@richmahn)
Mrsdizzie <info@mrsdizzie.com> (@mrsdizzie)
silverwind <me@silverwind.io> (@silverwind)
Gary Kim <gary@garykim.dev> (@gary-kim)
Guillermo Prandi <gitea.maint@mailfilter.com.ar> (@guillep2k)
Mura Li <typeless@ctli.io> (@typeless)
6543 <6543@obermui.de> (@6543)
David Svantesson <davidsvantesson@gmail.com> (@davidsvantesson)
a1012112796 <1012112796@qq.com> (@a1012112796)
Karl Heinz Marbaise <kama@soebes.de> (@khmarbaise)
Norwin Roosen <git@nroo.de> (@noerw)
Kyle Dumont <kdumontnu@gmail.com> (@kdumontnu)
Janis Estelmann <admin@oldschoolhack.me> (@KN4CK3R)
Jimmy Praet <jimmy.praet@telenet.be> (@jpraet)
Leon Hofmeister <dev.lh@web.de> (@delvh)
Wim <wim@42.be> (@42wim)
Jason Song <i@wolfogre.com> (@wolfogre)
Yarden Shoham <git@yardenshoham.com> (@yardenshoham)
Yu Tian <zettat123@gmail.com> (@Zettat123)
Dong Ge <gedong_1994@163.com> (@sillyguodong)
Xinyi Gong <hestergong@gmail.com> (@HesterG)
wxiaoguang <wxiaoguang@gmail.com> (@wxiaoguang)
Gary Moon <gary@garymoon.net> (@garymoon)
Philip Peterson <philip.c.peterson@gmail.com> (@philip-peterson)
Denys Konovalov <kontakt@denyskon.de> (@denyskon)
Punit Inani <punitinani1@gmail.com> (@puni9869)
CaiCandong <1290147055@qq.com> (@caicandong)
Rui Chen <rui@chenrui.dev> (@chenrui333)
Nanguan Lin <nanguanlin6@gmail.com> (@lng2020)
kerwin612 <kerwin612@qq.com> (@kerwin612)
Gary Wang <git@blumia.net> (@BLumia)
Tim-Niclas Oelschläger <zokki.softwareschmiede@gmail.com> (@zokkis)
Yu Liu <1240335630@qq.com> (@HEREYUA)
Kemal Zebari <kemalzebra@gmail.com> (@kemzeb)
Rowan Bohde <rowan.bohde@gmail.com> (@bohde)
hiifong <i@hiif.ong> (@hiifong)
metiftikci <metiftikci@hotmail.com> (@metiftikci)
Christopher Homberger <christopher.homberger@web.de> (@ChristopherHX)
Tobias Balle-Petersen <tobiasbp@gmail.com> (@tobiasbp)
TheFox <thefox0x7@gmail.com> (@TheFox0x7)
Nicolas <bircni@icloud.com> (@bircni)
+108 -626
View File
@@ -1,681 +1,163 @@
DIST := dist
DIST_DIRS := $(DIST)/binaries $(DIST)/release
IMPORT := code.gitea.io/gitea
# By default use go's 1.25 experimental json v2 library when building
# TODO: remove when no longer experimental
export GOEXPERIMENT ?= jsonv2
GO ?= go
SHASUM ?= shasum -a 256
COMMA := ,
XGO_VERSION := go-1.26.x
AIR_PACKAGE ?= github.com/air-verse/air@v1 # renovate: datasource=go
EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/v3/cmd/editorconfig-checker@v3 # renovate: datasource=go
GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.11.4 # renovate: datasource=go
GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.15 # renovate: datasource=go
MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.8.0 # renovate: datasource=go
SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.33.1 # renovate: datasource=go
XGO_PACKAGE ?= src.techknowlogick.com/xgo@latest
GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1 # renovate: datasource=go
ACTIONLINT_PACKAGE ?= github.com/rhysd/actionlint/cmd/actionlint@v1.7.11 # renovate: datasource=go
HAS_GO := $(shell hash $(GO) > /dev/null 2>&1 && echo yes)
ifeq ($(HAS_GO), yes)
CGO_EXTRA_CFLAGS := -DSQLITE_MAX_VARIABLE_NUMBER=32766
CGO_CFLAGS ?= $(shell $(GO) env CGO_CFLAGS) $(CGO_EXTRA_CFLAGS)
ifeq ($(OS), Windows_NT)
EXECUTABLE := gitea.exe
else
EXECUTABLE := gitea
endif
MAKE_EVIDENCE_DIR := .make_evidence
BINDATA := modules/{options,public,templates}/bindata.go
STYLESHEETS := $(wildcard public/less/index.less public/less/_*.less)
JAVASCRIPTS :=
# Use sqlite as default database if running tests, only do so for local tests, not in CI.
# CI should explicitly set the database to avoid unexpected results.
ifneq ($(findstring test-,$(MAKECMDGOALS)),)
ifeq ($(CI),)
GITEA_TEST_DATABASE ?= sqlite
endif
endif
LDFLAGS := -X "main.Version=$(shell git describe --tags --always | sed 's/-/+/' | sed 's/^v//')" -X "main.Tags=$(TAGS)"
PACKAGES ?= $(filter-out code.gitea.io/gitea/integrations,$(shell go list ./... | grep -v /vendor/))
SOURCES ?= $(shell find . -name "*.go" -type f)
TAGS ?=
TAGS_EVIDENCE := $(MAKE_EVIDENCE_DIR)/tags
CGO_ENABLED ?= 0
ifneq (,$(findstring sqlite_mattn,$(TAGS))$(findstring pam,$(TAGS)))
CGO_ENABLED = 1
endif
STATIC ?=
EXTLDFLAGS ?=
ifneq ($(STATIC),)
EXTLDFLAGS = -extldflags "-static"
endif
ifeq ($(GOOS),windows)
IS_WINDOWS := yes
else ifeq ($(patsubst Windows%,Windows,$(OS)),Windows)
ifeq ($(GOOS),)
IS_WINDOWS := yes
endif
endif
# GOFLAGS and EXTRA_GOFLAGS are for the 'go build' command only
ifeq ($(IS_WINDOWS),yes)
GOFLAGS := -v -buildmode=exe
EXECUTABLE ?= gitea.exe
ifneq ($(DRONE_TAG),)
VERSION ?= $(subst v,,$(DRONE_TAG))
else
GOFLAGS := -v
EXECUTABLE ?= gitea
endif
EXTRA_GOFLAGS ?=
ifeq ($(shell sed --version 2>/dev/null | grep -q GNU && echo gnu),gnu)
SED_INPLACE := sed -i
else
SED_INPLACE := sed -i ''
endif
# GOTEST_FLAGS is for unit test and integration test
GOTEST_FLAGS ?= -timeout 40m
STORED_VERSION_FILE := VERSION
GITHUB_REF_TYPE ?= branch
GITHUB_REF_NAME ?= $(shell git rev-parse --abbrev-ref HEAD)
ifneq ($(GITHUB_REF_TYPE),branch)
VERSION ?= $(subst v,,$(GITHUB_REF_NAME))
GITEA_VERSION ?= $(VERSION)
else
ifneq ($(GITHUB_REF_NAME),)
VERSION ?= $(subst release/v,,$(GITHUB_REF_NAME))-nightly
ifneq ($(DRONE_BRANCH),)
VERSION ?= $(subst release/v,,$(DRONE_BRANCH))
else
VERSION ?= main
endif
STORED_VERSION=$(shell cat $(STORED_VERSION_FILE) 2>/dev/null)
ifneq ($(STORED_VERSION),)
GITEA_VERSION ?= $(STORED_VERSION)
else
GITEA_VERSION ?= $(shell git describe --tags --always | sed 's/-/+/' | sed 's/^v//')
VERSION ?= master
endif
endif
# if version = "main" then update version to "nightly"
ifeq ($(VERSION),main)
VERSION := main-nightly
endif
LDFLAGS := $(LDFLAGS) -X "main.Version=$(GITEA_VERSION)" -X "main.Tags=$(TAGS)"
LINUX_ARCHS ?= linux/amd64,linux/386,linux/arm-5,linux/arm-6,linux/arm64,linux/riscv64
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,$(shell $(GO) list ./... | grep -v /vendor/))
MIGRATE_TEST_PACKAGES ?= $(shell $(GO) list code.gitea.io/gitea/models/migrations/...)
FRONTEND_SOURCES := $(shell find web_src/js web_src/css -type f)
FRONTEND_CONFIGS := vite.config.ts tailwind.config.ts
FRONTEND_DEST := public/assets/.vite/manifest.json
FRONTEND_DEST_ENTRIES := public/assets/js public/assets/css public/assets/fonts public/assets/.vite
FRONTEND_DEV_LOG_LEVEL ?= warn
BINDATA_DEST_WILDCARD := modules/migration/bindata.* modules/public/bindata.* modules/options/bindata.* modules/templates/bindata.*
GENERATED_GO_DEST := modules/charset/invisible_gen.go modules/charset/ambiguous_gen.go
SVG_DEST_DIR := public/assets/img/svg
AIR_TMP_DIR := .air
GO_LICENSE_FILE := assets/go-licenses.json
TAR_EXCLUDES := .git data indexers queues log node_modules $(EXECUTABLE) $(DIST) $(MAKE_EVIDENCE_DIR) $(AIR_TMP_DIR)
GO_DIRS := build cmd models modules routers services tests tools
WEB_DIRS := web_src/js web_src/css
ESLINT_FILES := web_src/js tools *.ts tests/e2e
STYLELINT_FILES := web_src/css web_src/js/components/*.vue
SPELLCHECK_FILES := $(GO_DIRS) $(WEB_DIRS) templates options/locale/locale_en-US.json .github $(filter-out CHANGELOG.md, $(wildcard *.go *.md *.yml *.yaml *.toml))
EDITORCONFIG_FILES := templates .github/workflows options/locale/locale_en-US.json
GO_SOURCES := $(wildcard *.go)
GO_SOURCES += $(shell find $(GO_DIRS) -type f -name "*.go")
GO_SOURCES += $(GENERATED_GO_DEST)
ESLINT_CONCURRENCY ?= 2
SWAGGER_SPEC := templates/swagger/v1_json.tmpl
SWAGGER_SPEC_INPUT := templates/swagger/v1_input.json
SWAGGER_EXCLUDE := code.gitea.io/sdk
OPENAPI3_SPEC := templates/swagger/v1_openapi3_json.tmpl
TEST_MYSQL_HOST ?= mysql:3306
TEST_MYSQL_DBNAME ?= testgitea
TEST_MYSQL_USERNAME ?= root
TEST_MYSQL_PASSWORD ?=
TEST_PGSQL_HOST ?= pgsql:5432
TEST_PGSQL_DBNAME ?= testgitea
TEST_PGSQL_USERNAME ?= postgres
TEST_PGSQL_PASSWORD ?= postgres
TEST_PGSQL_SCHEMA ?= gtestschema
TEST_MINIO_ENDPOINT ?= minio:9000
TEST_MSSQL_HOST ?= mssql:1433
TEST_MSSQL_DBNAME ?= testgitea
TEST_MSSQL_USERNAME ?= sa
TEST_MSSQL_PASSWORD ?= MwantsaSecurePassword1
# Include local Makefile
# Makefile.local is listed in .gitignore
ifneq ("$(wildcard Makefile.local)","")
include Makefile.local
endif
$(foreach v, $(filter TEST_%, $(.VARIABLES)), $(eval MAKEFILE_VARS+=$v=$($v)))
$(foreach v, $(filter GITEA_TEST_%, $(.VARIABLES)), $(eval MAKEFILE_VARS+=$v=$($v)))
export MAKEFILE_VARS
.PHONY: all
all: build
.PHONY: help
help: Makefile ## print Makefile help information.
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m[TARGETS] default target: build\033[0m\n\n\033[35mTargets:\033[0m\n"} /^[0-9A-Za-z._-]+:.*?##/ { printf " \033[36m%-45s\033[0m %s\n", $$1, $$2 }' Makefile #$(MAKEFILE_LIST)
@printf " \033[36m%-46s\033[0m %s\n" "test-e2e" "test end to end using playwright"
@printf " \033[36m%-46s\033[0m %s\n" "test-backend[#TestSpecificName]" "run unit test (sqlite only)"
@printf " \033[36m%-46s\033[0m %s\n" "test-integration[#TestSpecificName]" "run integration test for GITEA_TEST_DATABASE (sqlite, mysql, pgsql, mssql)"
.PHONY: clean-all
clean-all: clean ## delete backend, frontend and integration files
rm -rf $(FRONTEND_DEST_ENTRIES) node_modules
.PHONY: clean
clean: ## delete backend and integration files
rm -f $(EXECUTABLE) test-*.test tests/*.ini
rm -rf $(DIST) $(BINDATA_DEST_WILDCARD) man tests/integration/gitea-integration-*
clean:
go clean -i ./...
rm -rf $(EXECUTABLE) $(DIST) $(BINDATA)
.PHONY: fmt
fmt: ## format the Go and template code
$(GO) run $(GOLANGCI_LINT_PACKAGE) fmt
$(eval TEMPLATES := $(shell find templates -type f -name '*.tmpl'))
@# strip whitespace after '{{' or '(' and before '}}' or ')' unless there is only
@# whitespace before it
@$(SED_INPLACE) \
-e 's/{{[ ]\{1,\}/{{/g' -e '/^[ ]\{1,\}}}/! s/[ ]\{1,\}}}/}}/g' \
-e 's/([ ]\{1,\}/(/g' -e '/^[ ]\{1,\})/! s/[ ]\{1,\})/)/g' \
$(TEMPLATES)
fmt:
find . -name "*.go" -type f -not -path "./vendor/*" | xargs gofmt -s -w
.PHONY: fmt-check
fmt-check: fmt
@diff=$$(git diff --color=always $(GO_SOURCES) templates $(WEB_DIRS)); \
if [ -n "$$diff" ]; then \
echo "Please run 'make fmt' and commit the result:"; \
printf "%s" "$${diff}"; \
exit 1; \
.PHONY: vet
vet:
go vet $(PACKAGES)
.PHONY: generate
generate:
@hash go-bindata > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
go get -u github.com/jteeuwen/go-bindata/...; \
fi
go generate $(PACKAGES)
.PHONY: $(TAGS_EVIDENCE)
$(TAGS_EVIDENCE):
@mkdir -p $(MAKE_EVIDENCE_DIR)
@echo "$(TAGS)" > $(TAGS_EVIDENCE)
ifneq "$(TAGS)" "$(shell cat $(TAGS_EVIDENCE) 2>/dev/null)"
TAGS_PREREQ := $(TAGS_EVIDENCE)
endif
.PHONY: generate-swagger
generate-swagger: $(SWAGGER_SPEC) $(OPENAPI3_SPEC) ## generate the swagger spec from code comments
$(SWAGGER_SPEC): $(GO_SOURCES) $(SWAGGER_SPEC_INPUT)
$(GO) run $(SWAGGER_PACKAGE) generate spec --exclude "$(SWAGGER_EXCLUDE)" --input "$(SWAGGER_SPEC_INPUT)" --output './$(SWAGGER_SPEC)'
.PHONY: swagger-check
swagger-check: generate-swagger
@diff=$$(git diff --color=always '$(SWAGGER_SPEC)'); \
if [ -n "$$diff" ]; then \
echo "Please run 'make generate-swagger' and commit the result:"; \
printf "%s" "$${diff}"; \
exit 1; \
.PHONY: errcheck
errcheck:
@hash errcheck > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
go get -u github.com/kisielk/errcheck; \
fi
.PHONY: swagger-validate
swagger-validate: ## check if the swagger spec is valid
@# swagger "validate" requires that the "basePath" must start with a slash, but we are using Golang template "{{...}}"
@$(SED_INPLACE) -E -e 's|"basePath":( *)"(.*)"|"basePath":\1"/\2"|g' './$(SWAGGER_SPEC)' # add a prefix slash to basePath
@# FIXME: there are some warnings
$(GO) run $(SWAGGER_PACKAGE) validate './$(SWAGGER_SPEC)'
@$(SED_INPLACE) -E -e 's|"basePath":( *)"/(.*)"|"basePath":\1"\2"|g' './$(SWAGGER_SPEC)' # remove the prefix slash from basePath
.PHONY: generate-openapi3
generate-openapi3: $(OPENAPI3_SPEC) ## generate the OpenAPI 3.0 spec from the Swagger 2.0 spec
$(OPENAPI3_SPEC): $(SWAGGER_SPEC) build/generate-openapi.go $(wildcard build/openapi3gen/*.go)
$(GO) run build/generate-openapi.go
.PHONY: openapi3-check
openapi3-check: generate-openapi3
@diff=$$(git diff --color=always '$(OPENAPI3_SPEC)'); \
if [ -n "$$diff" ]; then \
echo "Please run 'make generate-openapi3' and commit the result:"; \
printf "%s" "$${diff}"; \
exit 1; \
fi
.PHONY: checks
checks: checks-frontend checks-backend ## run various consistency checks
.PHONY: checks-frontend
checks-frontend: lockfile-check svg-check ## check frontend files
.PHONY: checks-backend
checks-backend: tidy-check swagger-check openapi3-check fmt-check swagger-validate security-check ## check backend files
errcheck $(PACKAGES)
.PHONY: lint
lint: lint-frontend lint-backend lint-templates lint-swagger lint-spell lint-md lint-actions lint-json lint-yaml ## lint everything
.PHONY: lint-fix
lint-fix: lint-frontend-fix lint-backend-fix lint-spell-fix ## lint everything and fix issues
.PHONY: lint-frontend
lint-frontend: lint-js lint-css ## lint frontend files
.PHONY: lint-frontend-fix
lint-frontend-fix: lint-js-fix lint-css-fix ## lint frontend files and fix issues
.PHONY: lint-backend
lint-backend: lint-go lint-editorconfig ## lint backend files
.PHONY: lint-backend-fix
lint-backend-fix: lint-go-fix lint-editorconfig ## lint backend files and fix issues
.PHONY: lint-js
lint-js: node_modules ## lint js and ts files
pnpm exec eslint --color --max-warnings=0 --concurrency $(ESLINT_CONCURRENCY) $(ESLINT_FILES)
pnpm exec vue-tsc
.PHONY: lint-js-fix
lint-js-fix: node_modules ## lint js and ts files and fix issues
pnpm exec eslint --color --max-warnings=0 --concurrency $(ESLINT_CONCURRENCY) $(ESLINT_FILES) --fix
pnpm exec vue-tsc
.PHONY: lint-css
lint-css: node_modules ## lint css files
pnpm exec stylelint --color --max-warnings=0 $(STYLELINT_FILES)
.PHONY: lint-css-fix
lint-css-fix: node_modules ## lint css files and fix issues
pnpm exec stylelint --color --max-warnings=0 $(STYLELINT_FILES) --fix
.PHONY: lint-swagger
lint-swagger: node_modules ## lint swagger files
pnpm exec spectral lint -q -F hint $(SWAGGER_SPEC)
.PHONY: lint-md
lint-md: node_modules ## lint markdown files
pnpm exec markdownlint *.md
.PHONY: lint-md-fix
lint-md-fix: node_modules ## lint markdown files and fix issues
pnpm exec markdownlint --fix *.md
.PHONY: lint-pr-title
lint-pr-title: ## lint PR title against Conventional Commits (set PR_TITLE=...)
@node ./tools/lint-pr-title.js
.PHONY: lint-spell
lint-spell: ## lint spelling
@git ls-files $(SPELLCHECK_FILES) | xargs go run $(MISSPELL_PACKAGE) -dict assets/misspellings.csv -error
.PHONY: lint-spell-fix
lint-spell-fix: ## lint spelling and fix issues
@git ls-files $(SPELLCHECK_FILES) | xargs go run $(MISSPELL_PACKAGE) -dict assets/misspellings.csv -w
.PHONY: lint-go
lint-go: ## lint go files
$(GO) run $(GOLANGCI_LINT_PACKAGE) run
.PHONY: lint-go-fix
lint-go-fix: ## lint go files and fix issues
$(GO) run $(GOLANGCI_LINT_PACKAGE) run --fix
# workaround step for the lint-go-windows CI task because 'go run' can not
# have distinct GOOS/GOARCH for its build and run steps
.PHONY: lint-go-windows
lint-go-windows:
@GOOS= GOARCH= $(GO) install $(GOLANGCI_LINT_PACKAGE)
golangci-lint run
.PHONY: lint-editorconfig
lint-editorconfig:
@echo "Running editorconfig check..."
@$(GO) run $(EDITORCONFIG_CHECKER_PACKAGE) $(EDITORCONFIG_FILES)
.PHONY: lint-actions
lint-actions: ## lint action workflow files
$(GO) run $(ACTIONLINT_PACKAGE)
.PHONY: lint-templates
lint-templates: .venv node_modules ## lint template files
@node tools/lint-templates-svg.ts
@uv run --frozen djlint $(shell find templates -type f -iname '*.tmpl')
.PHONY: lint-yaml
lint-yaml: .venv ## lint yaml files
@uv run --frozen yamllint -s .
.PHONY: lint-json
lint-json: node_modules ## lint json files
pnpm exec eslint -c eslint.json.config.ts --color --max-warnings=0 --concurrency $(ESLINT_CONCURRENCY)
.PHONY: lint-json-fix
lint-json-fix: node_modules ## lint and fix json files
pnpm exec eslint -c eslint.json.config.ts --color --max-warnings=0 --concurrency $(ESLINT_CONCURRENCY) --fix
.PHONY: watch
watch: ## watch everything and continuously rebuild
@bash tools/watch.sh
.PHONY: watch-frontend
watch-frontend: node_modules ## start vite dev server for frontend
NODE_ENV=development pnpm exec vite --logLevel $(FRONTEND_DEV_LOG_LEVEL)
.PHONY: watch-backend
watch-backend: ## watch backend files and continuously rebuild
GITEA_RUN_MODE=dev $(GO) run $(AIR_PACKAGE) -c .air.toml
.PHONY: test-backend
test-backend: ## test backend files
@echo "Running go test with $(GOTEST_FLAGS) -tags '$(TAGS)'..."
@$(GO) test $(GOTEST_FLAGS) -tags='$(TAGS)' $(GO_TEST_PACKAGES)
.PHONY: test-frontend
test-frontend: node_modules ## test frontend files
pnpm exec vitest
.PHONY: test-check
test-check:
@echo "Running test-check...";
@diff=$$(git status -s); \
if [ -n "$$diff" ]; then \
echo "make test-backend has changed files in the source tree:"; \
printf "%s" "$${diff}"; \
echo "You should change the tests to create these files in a temporary directory."; \
echo "Do not simply add these files to .gitignore"; \
exit 1; \
lint:
@hash golint > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
go get -u github.com/golang/lint/golint; \
fi
for PKG in $(PACKAGES); do golint -set_exit_status $$PKG || exit 1; done;
.PHONY: test-backend\#%
test-backend\#%:
@echo "Running go test with -tags '$(TAGS)'..."
@$(GO) test $(GOTEST_FLAGS) -tags='$(TAGS)' -run $(subst .,/,$*) $(GO_TEST_PACKAGES)
.PHONY: integrations
integrations: TAGS=bindata sqlite
integrations: build
go test code.gitea.io/gitea/integrations
.PHONY: coverage
coverage:
grep '^\(mode: .*\)\|\(.*:[0-9]\+\.[0-9]\+,[0-9]\+\.[0-9]\+ [0-9]\+ [0-9]\+\)$$' coverage.out > coverage-bodged.out
grep '^\(mode: .*\)\|\(.*:[0-9]\+\.[0-9]\+,[0-9]\+\.[0-9]\+ [0-9]\+ [0-9]\+\)$$' integration.coverage.out > integration.coverage-bodged.out
$(GO) run tools/gocovmerge.go integration.coverage-bodged.out coverage-bodged.out > coverage.all
.PHONY: test
test:
for PKG in $(PACKAGES); do go test -cover -coverprofile $$GOPATH/src/$$PKG/coverage.out $$PKG || exit 1; done;
.PHONY: unit-test-coverage
unit-test-coverage:
@echo "Running unit-test-coverage $(GOTEST_FLAGS) -tags '$(TAGS)'..."
@$(GO) test $(GOTEST_FLAGS) -tags='$(TAGS)' -cover -coverprofile coverage.out $(GO_TEST_PACKAGES) && echo "\n==>\033[32m Ok\033[m\n" || exit 1
.PHONY: test-mysql
test-mysql:
@echo "Not integrated yet!"
.PHONY: tidy
tidy: ## run go mod tidy
$(eval MIN_GO_VERSION := $(shell grep -Eo '^go\s+[0-9]+\.[0-9.]+' go.mod | cut -d' ' -f2))
$(GO) mod tidy -compat=$(MIN_GO_VERSION)
@$(MAKE) --no-print-directory $(GO_LICENSE_FILE)
.PHONY: test-pgsql
test-pgsql:
@echo "Not integrated yet!"
vendor: go.mod go.sum
$(GO) mod vendor
@touch vendor
.PHONY: check
check: test
.PHONY: tidy-check
tidy-check: tidy
@diff=$$(git diff --color=always go.mod go.sum $(GO_LICENSE_FILE)); \
if [ -n "$$diff" ]; then \
echo "Please run 'make tidy' and commit the result:"; \
printf "%s" "$${diff}"; \
exit 1; \
fi
.PHONY: go-licenses
go-licenses: $(GO_LICENSE_FILE) ## regenerate go licenses
$(GO_LICENSE_FILE): go.mod go.sum
GO=$(GO) $(GO) run build/generate-go-licenses.go $(GO_LICENSE_FILE)
.PHONY: test-integration
test-integration:
@# Use a compiled binary: testlogger forwards gitea logs to t.Log, so `go test -v`
@# would flood output per passing test. testcache can't help these tests anyway —
@# they mutate the work directory, so cache inputs change between runs.
$(GO) test $(GOTEST_FLAGS) -tags '$(TAGS)' -c code.gitea.io/gitea/tests/integration -o ./test-integration-$(GITEA_TEST_DATABASE).test
./test-integration-$(GITEA_TEST_DATABASE).test
.PHONY: test-integration\#%
test-integration\#%:
$(GO) test $(GOTEST_FLAGS) -tags '$(TAGS)' -run $(subst .,/,$*) code.gitea.io/gitea/tests/integration
.PHONY: test-migration
test-migration: migrations.integration.test migrations.individual.test
.PHONY: migrations.integration.test
migrations.integration.test:
$(GO) test $(GOTEST_FLAGS) -tags '$(TAGS)' code.gitea.io/gitea/tests/integration/migration-test
.PHONY: migrations.individual.test
migrations.individual.test:
@# tests of multiple packages use the same database, don't run in parallel
$(GO) test $(GOTEST_FLAGS) -tags '$(TAGS)' -p 1 $(MIGRATE_TEST_PACKAGES)
.PHONY: migrations.individual.test\#%
migrations.individual.test\#%:
$(GO) test $(GOTEST_FLAGS) -tags '$(TAGS)' code.gitea.io/gitea/models/migrations/$*
.PHONY: playwright
playwright: deps-frontend
@# on GitHub Actions VMs, playwright's system deps are pre-installed
@pnpm exec playwright install $(if $(GITHUB_ACTIONS),,--with-deps) chromium firefox $(PLAYWRIGHT_FLAGS)
.PHONY: test-e2e
test-e2e: playwright frontend backend
@EXECUTABLE=$(EXECUTABLE) ./tools/test-e2e.sh $(GITEA_TEST_E2E_FLAGS)
.PHONY: install
install: $(wildcard *.go)
go install -v -tags '$(TAGS)' -ldflags '-s -w $(LDFLAGS)'
.PHONY: build
build: frontend backend ## build everything
build: $(EXECUTABLE)
.PHONY: frontend
frontend: $(FRONTEND_DEST) ## build frontend files
$(EXECUTABLE): $(SOURCES)
go build -i -v -tags '$(TAGS)' -ldflags '-s -w $(LDFLAGS)' -o $@
.PHONY: backend
backend: generate-backend $(EXECUTABLE) ## build backend files
# We generate the backend before the frontend in case we in future we want to generate things in the frontend from generated files in backend
.PHONY: generate
generate: generate-backend ## run "go generate"
.PHONY: generate-backend
generate-backend: $(TAGS_PREREQ) generate-go
.PHONY: generate-go
generate-go: $(TAGS_PREREQ)
@echo "Running go generate..."
@CC= GOOS= GOARCH= CGO_ENABLED=0 $(GO) generate -tags '$(TAGS)' ./...
.PHONY: security-check
security-check:
GOEXPERIMENT= go run $(GOVULNCHECK_PACKAGE) -show color ./... || true
$(EXECUTABLE): $(GO_SOURCES) $(TAGS_PREREQ)
ifneq ($(and $(STATIC),$(findstring pam,$(TAGS))),)
$(error pam support set via TAGS does not support static builds)
endif
CGO_ENABLED="$(CGO_ENABLED)" CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) build $(GOFLAGS) $(EXTRA_GOFLAGS) -tags '$(TAGS)' -ldflags '-s -w $(EXTLDFLAGS) $(LDFLAGS)' -o $@
.PHONY: docker
docker:
docker run -ti --rm -v $(CURDIR):/srv/app/src/code.gitea.io/gitea -w /srv/app/src/code.gitea.io/gitea -e TAGS="bindata $(TAGS)" webhippie/golang:edge make clean generate build
docker build -t gitea/gitea:latest .
.PHONY: release
release: frontend generate release-windows release-linux release-darwin release-freebsd release-copy release-compress vendor release-sources release-check
release: release-dirs release-windows release-linux release-darwin release-copy release-check
$(DIST_DIRS):
mkdir -p $(DIST_DIRS)
.PHONY: release-dirs
release-dirs:
mkdir -p $(DIST)/binaries $(DIST)/release
.PHONY: release-windows
release-windows: | $(DIST_DIRS)
CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -buildmode exe -dest $(DIST)/binaries -tags 'osusergo $(TAGS)' -ldflags '-s -w -linkmode external -extldflags "-static" $(LDFLAGS)' -targets 'windows/*' -out gitea-$(VERSION) .
ifeq (,$(findstring gogit,$(TAGS)))
CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -buildmode exe -dest $(DIST)/binaries -tags 'osusergo gogit $(TAGS)' -ldflags '-s -w -linkmode external -extldflags "-static" $(LDFLAGS)' -targets 'windows/*' -out gitea-$(VERSION)-gogit .
release-windows:
@hash xgo > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
go get -u github.com/karalabe/xgo; \
fi
xgo -dest $(DIST)/binaries -tags 'netgo $(TAGS)' -ldflags '-linkmode external -extldflags "-static" $(LDFLAGS)' -targets 'windows/*' -out gitea-$(VERSION) .
ifeq ($(CI),drone)
mv /build/* $(DIST)/binaries
endif
.PHONY: release-linux
release-linux: | $(DIST_DIRS)
CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '-s -w -linkmode external -extldflags "-static" $(LDFLAGS)' -targets '$(LINUX_ARCHS)' -out gitea-$(VERSION) .
release-linux:
@hash xgo > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
go get -u github.com/karalabe/xgo; \
fi
xgo -dest $(DIST)/binaries -tags 'netgo $(TAGS)' -ldflags '-linkmode external -extldflags "-static" $(LDFLAGS)' -targets 'linux/*' -out gitea-$(VERSION) .
ifeq ($(CI),drone)
mv /build/* $(DIST)/binaries
endif
.PHONY: release-darwin
release-darwin: | $(DIST_DIRS)
CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '-s -w $(LDFLAGS)' -targets 'darwin-10.12/amd64,darwin-10.12/arm64' -out gitea-$(VERSION) .
.PHONY: release-freebsd
release-freebsd: | $(DIST_DIRS)
CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '-s -w $(LDFLAGS)' -targets 'freebsd/amd64' -out gitea-$(VERSION) .
release-darwin:
@hash xgo > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
go get -u github.com/karalabe/xgo; \
fi
xgo -dest $(DIST)/binaries -tags 'netgo $(TAGS)' -ldflags '$(LDFLAGS)' -targets 'darwin/*' -out gitea-$(VERSION) .
ifeq ($(CI),drone)
mv /build/* $(DIST)/binaries
endif
.PHONY: release-copy
release-copy: | $(DIST_DIRS)
cd $(DIST); for file in `find . -type f -name "*"`; do cp $${file} ./release/; done;
release-copy:
$(foreach file,$(wildcard $(DIST)/binaries/$(EXECUTABLE)-*),cp $(file) $(DIST)/release/$(notdir $(file));)
.PHONY: release-check
release-check: | $(DIST_DIRS)
cd $(DIST)/release/; for file in `find . -type f -name "*"`; do echo "checksumming $${file}" && $(SHASUM) `echo $${file} | sed 's/^..//'` > $${file}.sha256; done;
release-check:
cd $(DIST)/release; $(foreach file,$(wildcard $(DIST)/release/$(EXECUTABLE)-*),sha256sum $(notdir $(file)) > $(notdir $(file)).sha256;)
.PHONY: release-compress
release-compress: | $(DIST_DIRS)
cd $(DIST)/release/; for file in `find . -type f -name "*"`; do echo "compressing $${file}" && $(GO) run $(GXZ_PACKAGE) -k -9 $${file}; done;
.PHONY: javascripts
javascripts: public/js/index.js
.PHONY: release-sources
release-sources: | $(DIST_DIRS)
echo $(VERSION) > $(STORED_VERSION_FILE)
# bsdtar needs a ^ to prevent matching subdirectories
$(eval EXCL := --exclude=$(shell tar --help | grep -q bsdtar && echo "^")./)
# use transform to a add a release-folder prefix; in bsdtar the transform parameter equivalent is -s
$(eval TRANSFORM := $(shell tar --help | grep -q bsdtar && echo "-s '/^./gitea-src-$(VERSION)/'" || echo "--transform 's|^./|gitea-src-$(VERSION)/|'"))
tar $(addprefix $(EXCL),$(TAR_EXCLUDES)) $(TRANSFORM) -czf $(DIST)/release/gitea-src-$(VERSION).tar.gz .
rm -f $(STORED_VERSION_FILE)
.IGNORE: public/js/index.js
public/js/index.js: $(JAVASCRIPTS)
cat $< >| $@
.PHONY: deps
deps: deps-frontend deps-backend deps-tools deps-py ## install dependencies
.PHONY: stylesheets
stylesheets: public/css/index.css
.PHONY: deps-py
deps-py: .venv ## install python dependencies
.IGNORE: public/css/index.css
public/css/index.css: $(STYLESHEETS)
lessc $< $@
.PHONY: deps-frontend
deps-frontend: node_modules ## install frontend dependencies
.PHONY: deps-backend
deps-backend: ## install backend dependencies
$(GO) mod download
.PHONY: deps-tools
deps-tools: ## install tool dependencies
$(GO) install $(AIR_PACKAGE) & \
$(GO) install $(EDITORCONFIG_CHECKER_PACKAGE) & \
$(GO) install $(GOLANGCI_LINT_PACKAGE) & \
$(GO) install $(GXZ_PACKAGE) & \
$(GO) install $(MISSPELL_PACKAGE) & \
$(GO) install $(SWAGGER_PACKAGE) & \
$(GO) install $(XGO_PACKAGE) & \
$(GO) install $(GOVULNCHECK_PACKAGE) & \
$(GO) install $(ACTIONLINT_PACKAGE) & \
wait
node_modules: pnpm-lock.yaml
pnpm install --frozen-lockfile
@touch node_modules
.venv: uv.lock
uv sync
@touch .venv
.PHONY: update
update: update-go update-js update-py ## update dependencies
.PHONY: update-go
update-go: ## update go dependencies
$(GO) get -u ./...
$(MAKE) tidy
.PHONY: update-js
update-js: node_modules ## update js dependencies
pnpm exec updates -u -f package.json
rm -rf node_modules pnpm-lock.yaml
pnpm install
@touch node_modules
$(MAKE) --no-print-directory nolyfill
.PHONY: nolyfill
nolyfill: node_modules ## apply nolyfill overrides to package.json and relock
pnpm exec nolyfill install
pnpm install
@touch node_modules
.PHONY: update-py
update-py: node_modules ## update py dependencies
pnpm exec updates -u -f pyproject.toml
rm -rf .venv uv.lock
uv sync
@touch .venv
.PHONY: vite
vite: $(FRONTEND_DEST) ## build vite files
$(FRONTEND_DEST): $(FRONTEND_SOURCES) $(FRONTEND_CONFIGS) pnpm-lock.yaml
@$(MAKE) -s node_modules
@rm -rf $(FRONTEND_DEST_ENTRIES)
@echo "Running vite build..."
@pnpm exec vite build
@touch $(FRONTEND_DEST)
.PHONY: svg
svg: node_modules ## build svg files
rm -rf $(SVG_DEST_DIR)
node tools/generate-svg.ts
.PHONY: svg-check
svg-check: svg
@git add $(SVG_DEST_DIR)
@diff=$$(git diff --color=always --cached $(SVG_DEST_DIR)); \
if [ -n "$$diff" ]; then \
echo "Please run 'make svg' and 'git add $(SVG_DEST_DIR)' and commit the result:"; \
printf "%s" "$${diff}"; \
exit 1; \
fi
.PHONY: lockfile-check
lockfile-check:
pnpm install --frozen-lockfile
@diff=$$(git diff --color=always pnpm-lock.yaml); \
if [ -n "$$diff" ]; then \
echo "pnpm-lock.yaml is inconsistent with package.json"; \
echo "Please run 'pnpm install --frozen-lockfile' and commit the result:"; \
printf "%s" "$${diff}"; \
exit 1; \
fi
.PHONY: generate-gitignore
generate-gitignore: ## update gitignore files
$(GO) run build/generate-gitignores.go
.PHONY: generate-images
generate-images: | node_modules ## generate images
cd tools && node generate-images.ts $(TAGS)
.PHONY: generate-manpage
generate-manpage: ## generate manpage
@[ -f gitea ] || make backend
@mkdir -p man/man1/ man/man5
@./gitea docs --man > man/man1/gitea.1
@gzip -9 man/man1/gitea.1 && echo man/man1/gitea.1.gz created
@#TODO A small script that formats config-cheat-sheet.en-us.md nicely for use as a config man page
# Disable parallel execution because it would break some targets that don't
# specify exact dependencies like 'backend' which does currently not depend
# on 'frontend' to enable Node.js-less builds from source tarballs.
.NOTPARALLEL:
.PHONY: assets
assets: javascripts stylesheets
-35
View File
@@ -1,35 +0,0 @@
{
"folders":
[
{
"name":"MokoGitea",
"path": ".",
"folder_exclude_patterns":
[
".git",
".claude/worktree"
],
"file_exclude_patterns":
[
"*.sublime-workspace"
],
},
{
"name":"Workspace",
"path": "E:\Documents\Workspace",
"folder_exclude_patterns":
[
".git",
".claude/worktree"
],
"file_exclude_patterns":
[
"*.sublime-workspace"
],
},
{
"name":"Scripts",
"path": "J:\Shared drives\Knowledgebase\Scripts",
},
],
}
+33 -29
View File
@@ -1,42 +1,46 @@
# MokoGitea
[简体中文](https://github.com/go-gitea/gitea/blob/master/README_ZH.md)
Custom Gitea fork with enhanced wiki system, DLID licensing, issue statuses, cascade merge, security scanning, org-level governance, org metadata, CI standardization, and project board API.
# Gitea - Git with a cup of tea
![Language](https://img.shields.io/badge/Go-00ADD8?style=flat-square&logo=go&logoColor=white) ![License](https://img.shields.io/badge/license-GPL--3.0--or--later-green?style=flat-square)
[![Build Status](https://drone.gitea.io/api/badges/go-gitea/gitea/status.svg)](https://drone.gitea.io/go-gitea/gitea)
[![Join the chat at https://gitter.im/go-gitea/gitea](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/go-gitea/gitea?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
[![](https://images.microbadger.com/badges/image/gitea/gitea.svg)](https://microbadger.com/images/gitea/gitea "Get your own image badge on microbadger.com")
[![Coverage Status](https://coverage.gitea.io/badges/go-gitea/gitea/coverage.svg)](https://coverage.gitea.io/go-gitea/gitea)
[![Go Report Card](https://goreportcard.com/badge/code.gitea.io/gitea)](https://goreportcard.com/report/code.gitea.io/gitea)
[![GoDoc](https://godoc.org/code.gitea.io/gitea?status.svg)](https://godoc.org/code.gitea.io/gitea)
[![Release](https://github-release-version.herokuapp.com/github/go-gitea/gitea/release.svg?style=flat)](https://github.com/go-gitea/gitea/releases/latest)
---
||||
|:-------------:|:-------:|:-------:|
|![Dashboard](https://i.imgur.com/3iEQsux.jpg)|![Repository](https://i.imgur.com/glqFnj8.jpg)|![Commits History](https://i.imgur.com/ad1FEpi.jpg)|
|![Profile](https://i.imgur.com/q81EcGa.jpg)|![Admin Dashboard](https://i.imgur.com/L2CQeN0.jpg)|![Diff](https://i.imgur.com/cNuvMum.jpg)|
|![Issues](https://i.imgur.com/xCYRqaF.jpg)|![Releases](https://i.imgur.com/ILpRBCe.jpg)|![Organization](https://i.imgur.com/0BHnrcL.jpg)|
||||
## Key Features
## Purpose
- **Wiki System** -- wikilinks, categories, backlinks, template transclusion, revision diffs, rename redirects, folder ACL, enhanced ToC, print view, ZIP export ([details](https://git.mokoconsulting.tech/MokoConsulting/.mokogitea/wiki/standards/Wiki-Features))
- **DLID Licensing** -- license management, entitlements, domain activations, ed25519-signed downloads
- **API Token Scope Editing** -- edit token scopes via API (PATCH) or web UI after creation
- **Issue Statuses** -- custom workflow statuses per org with required baseline protection, presets, cross-org migration
- **Cascade Merge** -- auto-create PRs to downstream branches after merge with configurable rules per repo
- **Security Scanning** -- secret detection (pre-receive blocking) + code analysis (SQL injection, XSS, command injection, path traversal, and more) with REST API for alerts, config, and on-demand scans
- **Default Org Teams** -- auto-create Developers, Reviewers, and CI/CD teams on org creation
- **Org Metadata** -- per-repo metadata API (public GET, admin PUT), platform detection for versioning
- **Branch Protection** -- delete allowlist for protected branches (per-user/team/deploy-key)
- **Org Governance** -- organization-wide rules that layer onto every repository: branch protection as a most-restrictive floor a repo cannot weaken, tag protection (team allowlist), push policy (branch/tag naming, mandatory secret-block, max file size, blocked paths), repository defaults (force-private, PR merge settings), and member email-domain allowlists
- **Project Board API** -- REST endpoints for project columns and cards
- **CI Infrastructure** -- reusable workflows, centralized ci-issue-reporter, standardized MOKOGITEA_TOKEN naming
- **Dev Deploy Gate** -- builds deploy to dev environment first, production checks dev health
The goal of this project is to make the easiest, fastest, and most painless way of setting up a self-hosted Git service. Using Go, this can be done with an independent binary distribution across **all platforms** which Go supports, including Linux, macOS, and Windows on x86, amd64, ARM and PowerPC architectures. Want to try it before doing anything else? Do it [with the online demo](https://try.gitea.io/)! This project has been [forked](https://blog.gitea.io/2016/12/welcome-to-gitea/) from [Gogs](https://gogs.io).
## Documentation
## Notes
- [Org Wiki](https://git.mokoconsulting.tech/MokoConsulting/.mokogitea/wiki/) -- standards, CLI reference, API docs
- [Wiki Features](https://git.mokoconsulting.tech/MokoConsulting/.mokogitea/wiki/standards/Wiki-Features) -- all 10 wiki enhancements
- [Licensing API](https://git.mokoconsulting.tech/MokoConsulting/.mokogitea/wiki/api/Licensing-API)
- [Repo Wiki](https://git.mokoconsulting.tech/MokoConsulting/MokoGitea-Fork/wiki/) -- feature docs, API reference, operations
1. **YOU MUST READ THE [CONTRIBUTORS GUIDE](CONTRIBUTING.md) BEFORE STARTING TO WORK ON A PULL REQUEST.**
2. If you found a vulnerability in the project, please write privately to **security@gitea.io**. Thanks!
3. If you're interested in using our APIs, we have experimental support with [documentation](https://godoc.org/code.gitea.io/sdk/gitea).
## Docs
For more information and instructions about how to install Gitea please look at our [documentation](https://docs.gitea.io/en-us/). If you cannot find some specific information, then head over to our [Gitter](https://gitter.im/go-gitea/gitea) channel to chat with us.
## Contributing
See the [org wiki](https://git.mokoconsulting.tech/MokoConsulting/.mokogitea/wiki/) for development guidelines, coding standards, and contribution instructions.
Fork -> Patch -> Push -> Pull Request
## Authors
* [Maintainers](https://github.com/orgs/go-gitea/people)
* [Contributors](https://github.com/go-gitea/gitea/graphs/contributors)
* [Translators](options/locale/TRANSLATORS)
## License
This project is licensed under the GNU General Public License v3.0 or later -- see the [LICENSE](LICENSE) file.
---
*[Moko Consulting](https://mokoconsulting.tech)*
This project is licensed under the MIT License. See the [LICENSE](https://github.com/go-gitea/gitea/blob/master/LICENSE) file for the full license text.
-201
View File
@@ -1,201 +0,0 @@
# Gitea
[![](https://github.com/go-gitea/gitea/actions/workflows/release-nightly.yml/badge.svg?branch=main)](https://github.com/go-gitea/gitea/actions/workflows/release-nightly.yml?query=branch%3Amain "Release Nightly")
[![](https://img.shields.io/discord/322538954119184384.svg?logo=discord&logoColor=white&label=Discord&color=5865F2)](https://discord.gg/Gitea "Join the Discord chat at https://discord.gg/Gitea")
[![](https://goreportcard.com/badge/code.gitea.io/gitea)](https://goreportcard.com/report/code.gitea.io/gitea "Go Report Card")
[![](https://pkg.go.dev/badge/code.gitea.io/gitea?status.svg)](https://pkg.go.dev/code.gitea.io/gitea "GoDoc")
[![](https://img.shields.io/github/release/go-gitea/gitea.svg)](https://github.com/go-gitea/gitea/releases/latest "GitHub release")
[![](https://www.codetriage.com/go-gitea/gitea/badges/users.svg)](https://www.codetriage.com/go-gitea/gitea "Help Contribute to Open Source")
[![](https://opencollective.com/gitea/tiers/backers/badge.svg?label=backers&color=brightgreen)](https://opencollective.com/gitea "Become a backer/sponsor of gitea")
[![](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT "License: MIT")
[![](https://badges.crowdin.net/gitea/localized.svg)](https://translate.gitea.com "Crowdin")
[English](./README.md) | [繁體中文](./README.zh-tw.md)
## 目的
这个项目的目标是提供最简单、最快速、最无痛的方式来设置自托管的 Git 服务。
由于 Gitea 是用 Go 语言编写的,它可以在 Go 支持的所有平台和架构上运行,包括 Linux、macOS 和 Windows 的 x86、amd64、ARM 和 PowerPC 架构。这个项目自 2016 年 11 月从 [Gogs](https://gogs.io) [分叉](https://blog.gitea.com/welcome-to-gitea/) 而来,但已经有了很多变化。
在线演示可以访问 [demo.gitea.com](https://demo.gitea.com)。
要访问免费的 Gitea 服务(有一定数量的仓库限制),可以访问 [gitea.com](https://gitea.com/user/login)。
要快速部署您自己的专用 Gitea 实例,可以在 [cloud.gitea.com](https://cloud.gitea.com) 开始免费试用。
## 文件
您可以在我们的官方 [文件网站](https://docs.gitea.com/) 上找到全面的文件。
它包括安装、管理、使用、开发、贡献指南等,帮助您快速入门并有效地探索所有功能。
如果您有任何建议或想要贡献,可以访问 [文件仓库](https://gitea.com/gitea/docs)
## 构建
从源代码树的根目录运行:
TAGS="bindata" make build
`build` 目标分为两个子目标:
- `make backend` 需要 [Go Stable](https://go.dev/dl/),所需版本在 [go.mod](/go.mod) 中定义。
- `make frontend` 需要 [Node.js LTS](https://nodejs.org/en/download/) 或更高版本以及 [pnpm](https://pnpm.io/installation)。
需要互联网连接来下载 go 和 npm 模块。从包含预构建前端文件的官方源代码压缩包构建时,不会触发 `frontend` 目标,因此可以在没有 Node.js 的情况下构建。
更多信息:https://docs.gitea.com/installation/install-from-source
## 使用
构建后,默认情况下会在源代码树的根目录生成一个名为 `gitea` 的二进制文件。要运行它,请使用:
./gitea web
> [!注意]
> 如果您对使用我们的 API 感兴趣,我们提供了实验性支持,并附有 [文件](https://docs.gitea.com/api)。
## 贡献
预期的工作流程是:Fork -> Patch -> Push -> Pull Request
> [!注意]
>
> 1. **在开始进行 Pull Request 之前,您必须阅读 [贡献者指南](CONTRIBUTING.md)。**
> 2. 如果您在项目中发现了漏洞,请私下写信给 **security@gitea.io**。谢谢!
## 翻译
[![Crowdin](https://badges.crowdin.net/gitea/localized.svg)](https://translate.gitea.com)
翻译通过 [Crowdin](https://translate.gitea.com) 进行。如果您想翻译成新的语言,请在 Crowdin 项目中请求管理员添加新语言。
您也可以创建一个 issue 来添加语言,或者在 discord 的 #translation 频道上询问。如果您需要上下文或发现一些翻译问题,可以在字符串上留言或在 Discord 上询问。对于一般的翻译问题,文档中有一个部分。目前有点空,但我们希望随着问题的出现而填充它。
更多信息请参阅 [文件](https://docs.gitea.com/contributing/localization)。
## 官方和第三方项目
我们提供了一个官方的 [go-sdk](https://gitea.com/gitea/go-sdk),一个名为 [tea](https://gitea.com/gitea/tea) 的 CLI 工具和一个 Gitea Action 的 [action runner](https://gitea.com/gitea/act_runner)。
我们在 [gitea/awesome-gitea](https://gitea.com/gitea/awesome-gitea) 维护了一个 Gitea 相关项目的列表,您可以在那里发现更多的第三方项目,包括 SDK、插件、主题等。
## 通讯
[![](https://img.shields.io/discord/322538954119184384.svg?logo=discord&logoColor=white&label=Discord&color=5865F2)](https://discord.gg/Gitea "Join the Discord chat at https://discord.gg/Gitea")
如果您有任何文件未涵盖的问题,可以在我们的 [Discord 服务器](https://discord.gg/Gitea) 上与我们联系,或者在 [discourse 论坛](https://forum.gitea.com/) 上创建帖子。
## 作者
- [维护者](https://github.com/orgs/go-gitea/people)
- [贡献者](https://github.com/go-gitea/gitea/graphs/contributors)
- [翻译者](options/locale/TRANSLATORS)
## 支持者
感谢所有支持者! 🙏 [[成为支持者](https://opencollective.com/gitea#backer)]
<a href="https://opencollective.com/gitea#backers" target="_blank"><img src="https://opencollective.com/gitea/backers.svg?width=890"></a>
## 赞助商
通过成为赞助商来支持这个项目。您的标志将显示在这里,并带有链接到您的网站。 [[成为赞助商](https://opencollective.com/gitea#sponsor)]
<a href="https://opencollective.com/gitea/sponsor/0/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/0/avatar.svg"></a>
<a href="https://opencollective.com/gitea/sponsor/1/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/1/avatar.svg"></a>
<a href="https://opencollective.com/gitea/sponsor/2/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/2/avatar.svg"></a>
<a href="https://opencollective.com/gitea/sponsor/3/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/3/avatar.svg"></a>
<a href="https://opencollective.com/gitea/sponsor/4/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/4/avatar.svg"></a>
<a href="https://opencollective.com/gitea/sponsor/5/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/5/avatar.svg"></a>
<a href="https://opencollective.com/gitea/sponsor/6/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/6/avatar.svg"></a>
<a href="https://opencollective.com/gitea/sponsor/7/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/7/avatar.svg"></a>
<a href="https://opencollective.com/gitea/sponsor/8/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/8/avatar.svg"></a>
<a href="https://opencollective.com/gitea/sponsor/9/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/9/avatar.svg"></a>
## 常见问题
**Gitea 怎么发音?**
Gitea 的发音是 [/ɡɪ’ti:/](https://youtu.be/EM71-2uDAoY),就像 "gi-tea" 一样,g 是硬音。
**为什么这个项目没有托管在 Gitea 实例上?**
我们正在 [努力](https://github.com/go-gitea/gitea/issues/1029)。
**在哪里可以找到安全补丁?**
在 [发布日志](https://github.com/go-gitea/gitea/releases) 或 [变更日志](https://github.com/go-gitea/gitea/blob/main/CHANGELOG.md) 中,搜索关键词 `SECURITY` 以找到安全补丁。
## 许可证
这个项目是根据 MIT 许可证授权的。
请参阅 [LICENSE](https://github.com/go-gitea/gitea/blob/main/LICENSE) 文件以获取完整的许可证文本。
## 进一步信息
<details>
<summary>寻找界面概述?查看这里!</summary>
### 登录/注册页面
![Login](https://dl.gitea.com/screenshots/login.png)
![Register](https://dl.gitea.com/screenshots/register.png)
### 用户仪表板
![Home](https://dl.gitea.com/screenshots/home.png)
![Issues](https://dl.gitea.com/screenshots/issues.png)
![Pull Requests](https://dl.gitea.com/screenshots/pull_requests.png)
![Milestones](https://dl.gitea.com/screenshots/milestones.png)
### 用户资料
![Profile](https://dl.gitea.com/screenshots/user_profile.png)
### 探索
![Repos](https://dl.gitea.com/screenshots/explore_repos.png)
![Users](https://dl.gitea.com/screenshots/explore_users.png)
![Orgs](https://dl.gitea.com/screenshots/explore_orgs.png)
### 仓库
![Home](https://dl.gitea.com/screenshots/repo_home.png)
![Commits](https://dl.gitea.com/screenshots/repo_commits.png)
![Branches](https://dl.gitea.com/screenshots/repo_branches.png)
![Labels](https://dl.gitea.com/screenshots/repo_labels.png)
![Milestones](https://dl.gitea.com/screenshots/repo_milestones.png)
![Releases](https://dl.gitea.com/screenshots/repo_releases.png)
![Tags](https://dl.gitea.com/screenshots/repo_tags.png)
#### 仓库问题
![List](https://dl.gitea.com/screenshots/repo_issues.png)
![Issue](https://dl.gitea.com/screenshots/repo_issue.png)
#### 仓库拉取请求
![List](https://dl.gitea.com/screenshots/repo_pull_requests.png)
![Pull Request](https://dl.gitea.com/screenshots/repo_pull_request.png)
![File](https://dl.gitea.com/screenshots/repo_pull_request_file.png)
![Commits](https://dl.gitea.com/screenshots/repo_pull_request_commits.png)
#### 仓库操作
![List](https://dl.gitea.com/screenshots/repo_actions.png)
![Details](https://dl.gitea.com/screenshots/repo_actions_run.png)
#### 仓库活动
![Activity](https://dl.gitea.com/screenshots/repo_activity.png)
![Contributors](https://dl.gitea.com/screenshots/repo_contributors.png)
![Code Frequency](https://dl.gitea.com/screenshots/repo_code_frequency.png)
![Recent Commits](https://dl.gitea.com/screenshots/repo_recent_commits.png)
### 组织
![Home](https://dl.gitea.com/screenshots/org_home.png)
</details>
-201
View File
@@ -1,201 +0,0 @@
# Gitea
[![](https://github.com/go-gitea/gitea/actions/workflows/release-nightly.yml/badge.svg?branch=main)](https://github.com/go-gitea/gitea/actions/workflows/release-nightly.yml?query=branch%3Amain "Release Nightly")
[![](https://img.shields.io/discord/322538954119184384.svg?logo=discord&logoColor=white&label=Discord&color=5865F2)](https://discord.gg/Gitea "Join the Discord chat at https://discord.gg/Gitea")
[![](https://goreportcard.com/badge/code.gitea.io/gitea)](https://goreportcard.com/report/code.gitea.io/gitea "Go Report Card")
[![](https://pkg.go.dev/badge/code.gitea.io/gitea?status.svg)](https://pkg.go.dev/code.gitea.io/gitea "GoDoc")
[![](https://img.shields.io/github/release/go-gitea/gitea.svg)](https://github.com/go-gitea/gitea/releases/latest "GitHub release")
[![](https://www.codetriage.com/go-gitea/gitea/badges/users.svg)](https://www.codetriage.com/go-gitea/gitea "Help Contribute to Open Source")
[![](https://opencollective.com/gitea/tiers/backers/badge.svg?label=backers&color=brightgreen)](https://opencollective.com/gitea "Become a backer/sponsor of gitea")
[![](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT "License: MIT")
[![](https://badges.crowdin.net/gitea/localized.svg)](https://translate.gitea.com "Crowdin")
[English](./README.md) | [简体中文](./README.zh-cn.md)
## 目的
這個項目的目標是提供最簡單、最快速、最無痛的方式來設置自託管的 Git 服務。
由於 Gitea 是用 Go 語言編寫的,它可以在 Go 支援的所有平台和架構上運行,包括 Linux、macOS 和 Windows 的 x86、amd64、ARM 和 PowerPC 架構。這個項目自 2016 年 11 月從 [Gogs](https://gogs.io) [分叉](https://blog.gitea.com/welcome-to-gitea/) 而來,但已經有了很多變化。
在線演示可以訪問 [demo.gitea.com](https://demo.gitea.com)。
要訪問免費的 Gitea 服務(有一定數量的倉庫限制),可以訪問 [gitea.com](https://gitea.com/user/login)。
要快速部署您自己的專用 Gitea 實例,可以在 [cloud.gitea.com](https://cloud.gitea.com) 開始免費試用。
## 文件
您可以在我們的官方 [文件網站](https://docs.gitea.com/) 上找到全面的文件。
它包括安裝、管理、使用、開發、貢獻指南等,幫助您快速入門並有效地探索所有功能。
如果您有任何建議或想要貢獻,可以訪問 [文件倉庫](https://gitea.com/gitea/docs)
## 構建
從源代碼樹的根目錄運行:
TAGS="bindata" make build
`build` 目標分為兩個子目標:
- `make backend` 需要 [Go Stable](https://go.dev/dl/),所需版本在 [go.mod](/go.mod) 中定義。
- `make frontend` 需要 [Node.js LTS](https://nodejs.org/en/download/) 或更高版本以及 [pnpm](https://pnpm.io/installation)。
需要互聯網連接來下載 go 和 npm 模塊。從包含預構建前端文件的官方源代碼壓縮包構建時,不會觸發 `frontend` 目標,因此可以在沒有 Node.js 的情況下構建。
更多信息:https://docs.gitea.com/installation/install-from-source
## 使用
構建後,默認情況下會在源代碼樹的根目錄生成一個名為 `gitea` 的二進制文件。要運行它,請使用:
./gitea web
> [!注意]
> 如果您對使用我們的 API 感興趣,我們提供了實驗性支援,並附有 [文件](https://docs.gitea.com/api)。
## 貢獻
預期的工作流程是:Fork -> Patch -> Push -> Pull Request
> [!注意]
>
> 1. **在開始進行 Pull Request 之前,您必須閱讀 [貢獻者指南](CONTRIBUTING.md)。**
> 2. 如果您在項目中發現了漏洞,請私下寫信給 **security@gitea.io**。謝謝!
## 翻譯
[![Crowdin](https://badges.crowdin.net/gitea/localized.svg)](https://translate.gitea.com)
翻譯通過 [Crowdin](https://translate.gitea.com) 進行。如果您想翻譯成新的語言,請在 Crowdin 項目中請求管理員添加新語言。
您也可以創建一個 issue 來添加語言,或者在 discord 的 #translation 頻道上詢問。如果您需要上下文或發現一些翻譯問題,可以在字符串上留言或在 Discord 上詢問。對於一般的翻譯問題,文檔中有一個部分。目前有點空,但我們希望隨著問題的出現而填充它。
更多信息請參閱 [文件](https://docs.gitea.com/contributing/localization)。
## 官方和第三方項目
我們提供了一個官方的 [go-sdk](https://gitea.com/gitea/go-sdk),一個名為 [tea](https://gitea.com/gitea/tea) 的 CLI 工具和一個 Gitea Action 的 [action runner](https://gitea.com/gitea/act_runner)。
我們在 [gitea/awesome-gitea](https://gitea.com/gitea/awesome-gitea) 維護了一個 Gitea 相關項目的列表,您可以在那裡發現更多的第三方項目,包括 SDK、插件、主題等。
## 通訊
[![](https://img.shields.io/discord/322538954119184384.svg?logo=discord&logoColor=white&label=Discord&color=5865F2)](https://discord.gg/Gitea "Join the Discord chat at https://discord.gg/Gitea")
如果您有任何文件未涵蓋的問題,可以在我們的 [Discord 服務器](https://discord.gg/Gitea) 上與我們聯繫,或者在 [discourse 論壇](https://forum.gitea.com/) 上創建帖子。
## 作者
- [維護者](https://github.com/orgs/go-gitea/people)
- [貢獻者](https://github.com/go-gitea/gitea/graphs/contributors)
- [翻譯者](options/locale/TRANSLATORS)
## 支持者
感謝所有支持者! 🙏 [[成為支持者](https://opencollective.com/gitea#backer)]
<a href="https://opencollective.com/gitea#backers" target="_blank"><img src="https://opencollective.com/gitea/backers.svg?width=890"></a>
## 贊助商
通過成為贊助商來支持這個項目。您的標誌將顯示在這裡,並帶有鏈接到您的網站。 [[成為贊助商](https://opencollective.com/gitea#sponsor)]
<a href="https://opencollective.com/gitea/sponsor/0/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/0/avatar.svg"></a>
<a href="https://opencollective.com/gitea/sponsor/1/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/1/avatar.svg"></a>
<a href="https://opencollective.com/gitea/sponsor/2/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/2/avatar.svg"></a>
<a href="https://opencollective.com/gitea/sponsor/3/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/3/avatar.svg"></a>
<a href="https://opencollective.com/gitea/sponsor/4/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/4/avatar.svg"></a>
<a href="https://opencollective.com/gitea/sponsor/5/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/5/avatar.svg"></a>
<a href="https://opencollective.com/gitea/sponsor/6/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/6/avatar.svg"></a>
<a href="https://opencollective.com/gitea/sponsor/7/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/7/avatar.svg"></a>
<a href="https://opencollective.com/gitea/sponsor/8/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/8/avatar.svg"></a>
<a href="https://opencollective.com/gitea/sponsor/9/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/9/avatar.svg"></a>
## 常見問題
**Gitea 怎麼發音?**
Gitea 的發音是 [/ɡɪ’ti:/](https://youtu.be/EM71-2uDAoY),就像 "gi-tea" 一樣,g 是硬音。
**為什麼這個項目沒有託管在 Gitea 實例上?**
我們正在 [努力](https://github.com/go-gitea/gitea/issues/1029)。
**在哪裡可以找到安全補丁?**
在 [發佈日誌](https://github.com/go-gitea/gitea/releases) 或 [變更日誌](https://github.com/go-gitea/gitea/blob/main/CHANGELOG.md) 中,搜索關鍵詞 `SECURITY` 以找到安全補丁。
## 許可證
這個項目是根據 MIT 許可證授權的。
請參閱 [LICENSE](https://github.com/go-gitea/gitea/blob/main/LICENSE) 文件以獲取完整的許可證文本。
## 進一步信息
<details>
<summary>尋找界面概述?查看這裡!</summary>
### 登錄/註冊頁面
![Login](https://dl.gitea.com/screenshots/login.png)
![Register](https://dl.gitea.com/screenshots/register.png)
### 用戶儀表板
![Home](https://dl.gitea.com/screenshots/home.png)
![Issues](https://dl.gitea.com/screenshots/issues.png)
![Pull Requests](https://dl.gitea.com/screenshots/pull_requests.png)
![Milestones](https://dl.gitea.com/screenshots/milestones.png)
### 用戶資料
![Profile](https://dl.gitea.com/screenshots/user_profile.png)
### 探索
![Repos](https://dl.gitea.com/screenshots/explore_repos.png)
![Users](https://dl.gitea.com/screenshots/explore_users.png)
![Orgs](https://dl.gitea.com/screenshots/explore_orgs.png)
### 倉庫
![Home](https://dl.gitea.com/screenshots/repo_home.png)
![Commits](https://dl.gitea.com/screenshots/repo_commits.png)
![Branches](https://dl.gitea.com/screenshots/repo_branches.png)
![Labels](https://dl.gitea.com/screenshots/repo_labels.png)
![Milestones](https://dl.gitea.com/screenshots/repo_milestones.png)
![Releases](https://dl.gitea.com/screenshots/repo_releases.png)
![Tags](https://dl.gitea.com/screenshots/repo_tags.png)
#### 倉庫問題
![List](https://dl.gitea.com/screenshots/repo_issues.png)
![Issue](https://dl.gitea.com/screenshots/repo_issue.png)
#### 倉庫拉取請求
![List](https://dl.gitea.com/screenshots/repo_pull_requests.png)
![Pull Request](https://dl.gitea.com/screenshots/repo_pull_request.png)
![File](https://dl.gitea.com/screenshots/repo_pull_request_file.png)
![Commits](https://dl.gitea.com/screenshots/repo_pull_request_commits.png)
#### 倉庫操作
![List](https://dl.gitea.com/screenshots/repo_actions.png)
![Details](https://dl.gitea.com/screenshots/repo_actions_run.png)
#### 倉庫活動
![Activity](https://dl.gitea.com/screenshots/repo_activity.png)
![Contributors](https://dl.gitea.com/screenshots/repo_contributors.png)
![Code Frequency](https://dl.gitea.com/screenshots/repo_code_frequency.png)
![Recent Commits](https://dl.gitea.com/screenshots/repo_recent_commits.png)
### 組織
![Home](https://dl.gitea.com/screenshots/org_home.png)
</details>
+48
View File
@@ -0,0 +1,48 @@
[English](https://github.com/go-gitea/gitea/blob/master/README.md)
# Gitea - Git with a cup of tea
[![Build Status](https://drone.gitea.io/api/badges/go-gitea/gitea/status.svg)](https://drone.gitea.io/go-gitea/gitea)
[![Join the chat at https://gitter.im/go-gitea/gitea](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/go-gitea/gitea?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
[![](https://images.microbadger.com/badges/image/gitea/gitea.svg)](https://microbadger.com/images/gitea/gitea "Get your own image badge on microbadger.com")
[![Coverage Status](https://coverage.gitea.io/badges/go-gitea/gitea/coverage.svg)](https://coverage.gitea.io/go-gitea/gitea)
[![Go Report Card](https://goreportcard.com/badge/code.gitea.io/gitea)](https://goreportcard.com/report/code.gitea.io/gitea)
[![GoDoc](https://godoc.org/code.gitea.io/gitea?status.svg)](https://godoc.org/code.gitea.io/gitea)
[![Release](https://github-release-version.herokuapp.com/github/go-gitea/gitea/release.svg?style=flat)](https://github.com/go-gitea/gitea/releases/latest)
||||
|:-------------:|:-------:|:-------:|
|![Dashboard](https://i.imgur.com/3iEQsux.jpg)|![Repository](https://i.imgur.com/glqFnj8.jpg)|![Commits History](https://i.imgur.com/ad1FEpi.jpg)|
|![Profile](https://i.imgur.com/q81EcGa.jpg)|![Admin Dashboard](https://i.imgur.com/L2CQeN0.jpg)|![Diff](https://i.imgur.com/cNuvMum.jpg)|
|![Issues](https://i.imgur.com/xCYRqaF.jpg)|![Releases](https://i.imgur.com/ILpRBCe.jpg)|![Organization](https://i.imgur.com/0BHnrcL.jpg)|
||||
## 目标
Gitea的首要目标是创建一个极易安装,运行非常快速,安装和使用体验良好的自建 Git 服务。我们采用Go作为后端语言,这使我们只要生成一个可执行程序即可。并且他还支持跨平台,支持 Linux, macOS 和 Windows 以及各种架构,除了x86amd64,还包括 ARM 和 PowerPC。
如果您想试用一下,请访问 [在线Demo](https://try.gitea.io/)
## 提示
1. **开始贡献代码之前请确保你已经看过了 [贡献者向导(英文)](CONTRIBUTING.md)**.
2. 所有的安全问题,请私下发送邮件给 **security@gitea.io**。谢谢!
3. 如果你要使用API,请参见 [API 文档](https://godoc.org/code.gitea.io/sdk/gitea).
## 文档
关于如何安装请访问我们的 [文档站](https://docs.gitea.io/zh-cn/),如果没有找到对应的文档,你也可以通过 [Gitter - 英文](https://gitter.im/go-gitea/gitea) 和 QQ群 328432459 来和我们交流。
## 贡献流程
Fork -> Patch -> Push -> Pull Request
## 作者
* [Maintainers](https://github.com/orgs/go-gitea/people)
* [Contributors](https://github.com/go-gitea/gitea/graphs/contributors)
* [Translators](options/locale/TRANSLATORS)
## 授权许可
本项目采用 MIT 开源授权许可证,完整的授权说明已放置在 [LICENSE](https://github.com/go-gitea/gitea/blob/master/LICENSE) 文件中。
-85
View File
@@ -1,85 +0,0 @@
# Reporting security issues
The Gitea maintainers take security seriously.
If you discover a security issue, please bring it to their attention right away!
Previous vulnerabilities are listed at https://about.gitea.com/security.
## Reporting a Vulnerability
Please **DO NOT** file a public issue, instead send your report privately to `security@gitea.io`.
## Protecting Security Information
Due to the sensitive nature of security information, you can use the below GPG public key to encrypt your mail body.
The PGP key is valid until July 4, 2026.
```
Key ID: 6FCD2D5B
Key Type: RSA
Expires: 7/4/2026
Key Size: 4096/4096
Fingerprint: 3DE0 3D1E 144A 7F06 9359 99DC AAFD 2381 6FCD 2D5B
```
UserID: Gitea Security <security@gitea.io>
```
-----BEGIN PGP PUBLIC KEY BLOCK-----
mQINBGK1Z/4BEADFMqXA9DeeChmSxUjF0Be5sq99ZUhgrZjcN/wOzz0wuCJZC0l8
4uC+d6mfv7JpJYlzYzOK97/x5UguKHkYNZ6mm1G9KHaXmoIBDLKDzfPdJopVNv2r
OajijaE0uMCnMjadlg5pbhMLRQG8a9J32yyaz7ZEAw72Ab31fvvcA53NkuqO4j2w
k7dtFQzhbNOYV0VffQT90WDZdalYHB1JHyEQ+70U9OjVD5ggNYSzX98Eu3Hjn7V7
kqFrcAxr5TE1elf0IXJcuBJtFzQSTUGlQldKOHtGTGgGjj9r/FFAE5ioBgVD05bV
rEEgIMM/GqYaG/nbNpWE6P3mEc2Mnn3pZaRJL0LuF26TLjnqEcMMDp5iIhLdFzXR
3tMdtKgQFu+Mtzs3ipwWARYgHyU09RJsI2HeBx7RmZO/Xqrec763Z7zdJ7SpCn0Z
q+pHZl24JYR0Kf3T/ZiOC0cGd2QJqpJtg5J6S/OqfX9NH6MsCczO8pUC1N/aHH2X
CTme2nF56izORqDWKoiICteL3GpYsCV9nyCidcCmoQsS+DKvE86YhIhVIVWGRY2F
lzpAjnN9/KLtQroutrm+Ft0mdjDiJUeFVl1cOHDhoyfCsQh62HumoyZoZvqzQd6e
AbN11nq6aViMe2Q3je1AbiBnRnQSHxt1Tc8X4IshO3MQK1Sk7oPI6LA5oQARAQAB
tCJHaXRlYSBTZWN1cml0eSA8c2VjdXJpdHlAZ2l0ZWEuaW8+iQJXBBMBCABBAhsD
BQsJCAcCAiICBhUKCQgLAgQWAgMBAh4HAheAFiEEPeA9HhRKfwaTWZncqv0jgW/N
LVsFAmhoHmkFCQeT6esACgkQqv0jgW/NLVuFLRAAmjBQSKRAgs2bFIEj7HLAbDp4
f+XkdH+GsT3jRPOZ9QZgmtM+TfoE4yNgIVfOl+s4RdjM/W4QzqZuPQ55hbEHd056
cJmm7B+6GsHFcdrPmh65sOCEIyh4+t45dUfeWpFsDPqm9j1UHXAJQIpB8vDEVAPH
t+3wLCk8GMPJs1o5tIyMmaO23ngvkwn8eG7KgY+rp2PzObrb5g7ppci0ILzILkrp
HVjZsEfUWRgSVF7LuU5ppqDKrlcqwUpQq6n3kGMZcLrCp6ACKP04TBmTfUxNwdL7
I0N7apI2Pbct9T1Gv/lYAUFWyU2c3gh/EBLbO6BukaLOFRQHrtNfdJV/YnMPlcXr
LUJjK9K4eAH9DsrZqrisz/LthsC2BaNIN3KRMTk5YTYgmIh8GXzSgihORmtDFELC
RroID3pTuS0zjXh+wpY9GuPTh7UW23p42Daxca4fAT4k5EclvDRUrL21xMopPMiL
HuNdELz4FVchRTy05PjzKVyjVInDNojE2KUxnjxZDzYJ6aT/g+coD5yfntYm8BEj
+ZzL0ndZES54hzKLpv7zwBQwFzam68clZYmDPILOPTflQDfpGEWmJK4undFU5obz
ZsQRz0R3ulspChATbZxO0d5LX2obLpKO9X3b5VoO1KF+R8Vjw1Y0KxrNZ6rIcfqH
Z50QVQKSe9dm08K0ON+5Ag0EYrVn/gEQALrFLQjCR3GjuHSindz0rd3Fnx/t7Sen
T+p07yCSSoSlmnJHCQmwh4vfg1blyz0zZ4vkIhtpHsEgc+ZAG+WQXSsJ2iRz+eSN
GwoOQl4XC3n+QWkc1ws+btr48+6UqXIQU+F8TPQyx/PIgi2nZXJB7f5+mjCqsk46
XvH4nTr4kJjuqMSR/++wvre2qNQRa/q/dTsK0OaN/mJsdX6Oi+aGNaQJUhIG7F+E
ZDMkn/O6xnwWNzy/+bpg43qH/Gk0eakOmz5NmQLRkV58SZLiJvuCUtkttf6CyhnX
03OcWaajv5W8qA39dBYQgDrrPbBWUnwfO3yMveqhwV4JjDoe8sPAyn1NwzakNYqP
RzsWyLrLS7R7J9s3FkZXhQw/QQcsaSMcGNQO047dm1P83N8JY5aEpiRo9zSWjoiw
qoExANj5lUTZPe8M50lI182FrcjAN7dClO3QI6pg7wy0erMxfFly3j8UQ91ysS9T
s+GsP9I3cmWWQcKYxWHtE8xTXnNCVPFZQj2nwhJzae8ypfOtulBRA3dUKWGKuDH/
axFENhUsT397aOU3qkP/od4a64JyNIEo4CTTSPVeWd7njsGqli2U3A4xL2CcyYvt
D/MWcMBGEoLSNTswwKdom4FaJpn5KThnK/T0bQcmJblJhoCtppXisbexZnCpuS0x
Zdlm2T14KJ3LABEBAAGJAjwEGAEIACYCGwwWIQQ94D0eFEp/BpNZmdyq/SOBb80t
WwUCaGgeJAUJB5PppgAKCRCq/SOBb80tW/NWEACB6Jrf0gWlk7e+hNCdnbM0ZVWU
f2sHNFfXxxsdhpcDgKbNHtkZb8nZgv8AX+5fTtUwMVa3vKcdw30xFiIM5N7cCIPV
vg/5z5BtfEaitnabEUG2iiVDIy8IHXIcK10rX+7BosA3QDl2PsiBHwyi5G13lRk8
zGTSNDuOalug33h5/lr2dPigamkq74Aoy29q8Rjad6GfWHipL2bFimgtY+Zdi0BH
NLk4EJXxj1SgVx5dtkQzWJReBA5M+FQ4QYQZBO+f4TDoOLmjui152uhkoLBQbGAa
WWJFTVxm0bG5MXloEL3gA8DfU7XDwuW/sHJC5pBko8RpQViooOhckMepZV3Y83DK
bwLYa3JmPgj2rEv4993dvrJbQhpGd082HOxOsllCs8pgNq1SnXpWYfcGTgGKC3ts
U8YZUUJUQ7mi2L8Tv3ix20c9EiGmA30JAmA8eZTC3cWup91ZkkVBFRml2czTXajd
RWZ6GbHV5503ueDQcB8yBVgF3CSixs67+dGSbD3p86OqGrjAcJzM5TFbNKcnGLdE
kGbZpNwAISy750lXzXKmyrh5RTCeTOQerbwCMBvHZO+HAevA/LXDTw2OAiSIQlP5
sYA4sFYLQ30OAkgJcmdp/pSgVj/erNtSN07ClrOpDb/uFpQymO6K2h0Pst3feNVK
9M2VbqL9C51z/wyHLg==
=SfZA
-----END PGP PUBLIC KEY BLOCK-----
```
Security reports are greatly appreciated and we will publicly thank you for it, although we keep your name confidential if you request it.
-11483
View File
File diff suppressed because it is too large Load Diff
-31
View File
@@ -1,31 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" id="main_outline" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
y="0px" viewBox="0 0 640 640" style="enable-background:new 0 0 640 640;" xml:space="preserve">
<g>
<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
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
c2.1,6.1,2,13.1-1,19.3l-61,126.9C423.6,484.9,408.4,490.3,395.9,484.2z"/>
<g>
<g>
<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
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
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
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
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
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
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
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
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
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"/>
<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
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
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
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
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
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
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
C343.2,346.5,335,363.3,326.8,380.1z"/>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.5 KiB

File diff suppressed because one or more lines are too long
-31
View File
@@ -1,31 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" id="main_outline" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
y="0px" viewBox="0 0 640 640" style="enable-background:new 0 0 640 640;" xml:space="preserve">
<g>
<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
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
c2.1,6.1,2,13.1-1,19.3l-61,126.9C423.6,484.9,408.4,490.3,395.9,484.2z"/>
<g>
<g>
<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
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
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
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
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
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
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
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
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
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"/>
<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
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
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
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
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
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
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
C343.2,346.5,335,363.3,326.8,380.1z"/>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.5 KiB

-21
View File
@@ -1,21 +0,0 @@
acounts,accounts
canidate,candidate
comfirm,confirm
converage,coverage
currrently,currently
delimeter,delimiter
differrent,different
exclusing,excluding
finshed,finished
formated,formatted
inderect,indirect
insuficient,insufficient
likly,likely
mergable,mergeable
overrided,overridden
priortized,prioritized
registeration,registration
reuqest,request
reviwer,reviewer
superceded,superseded
underlaying,underlying
1 acounts accounts
2 canidate candidate
3 comfirm confirm
4 converage coverage
5 currrently currently
6 delimeter delimiter
7 differrent different
8 exclusing excluding
9 finshed finished
10 formated formatted
11 inderect indirect
12 insuficient insufficient
13 likly likely
14 mergable mergeable
15 overrided overridden
16 priortized prioritized
17 registeration registration
18 reuqest request
19 reviwer reviewer
20 superceded superseded
21 underlaying underlying
-27
View File
@@ -1,27 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build ignore
package main
import (
"fmt"
"os"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/assetfs"
)
func main() {
if len(os.Args) != 3 {
fmt.Println("usage: ./generate-bindata {local-directory} {bindata-filename}")
os.Exit(1)
}
dir, filename := os.Args[1], os.Args[2]
fmt.Printf("generating bindata for %s to %s\n", dir, filename)
if err := assetfs.GenerateEmbedBindata(dir, filename); err != nil {
fmt.Printf("failed: %s\n", err.Error())
os.Exit(1)
}
}
-219
View File
@@ -1,219 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Copyright 2015 Kenneth Shaw
// SPDX-License-Identifier: MIT
//go:build ignore
package main
import (
"flag"
"fmt"
"go/format"
"io"
"log"
"net/http"
"os"
"regexp"
"sort"
"strconv"
"strings"
"unicode/utf8"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/json"
)
const (
gemojiURL = "https://raw.githubusercontent.com/rhysd/gemoji/537ff2d7e0496e9964824f7f73ec7ece88c9765a/db/emoji.json"
maxUnicodeVersion = 16
)
var flagOut = flag.String("o", "modules/emoji/emoji_data.go", "out")
// Gemoji is a set of emoji data.
type Gemoji []Emoji
// Emoji represents a single emoji and associated data.
type Emoji struct {
Emoji string `json:"emoji"`
Description string `json:"description,omitempty"`
Aliases []string `json:"aliases"`
UnicodeVersion string `json:"unicode_version,omitempty"`
SkinTones bool `json:"skin_tones,omitempty"`
}
// Don't include some fields in JSON
func (e Emoji) MarshalJSON() ([]byte, error) {
type emoji Emoji
x := emoji(e)
x.UnicodeVersion = ""
x.Description = ""
x.SkinTones = false
return json.Marshal(x)
}
func main() {
flag.Parse()
// generate data
buf, err := generate()
if err != nil {
log.Fatalf("generate err: %v", err)
}
// write
err = os.WriteFile(*flagOut, buf, 0o644)
if err != nil {
log.Fatalf("WriteFile err: %v", err)
}
}
var replacer = strings.NewReplacer(
"main.Gemoji", "Gemoji",
"main.Emoji", "\n",
"}}", "},\n}",
", Description:", ", ",
", Aliases:", ", ",
", UnicodeVersion:", ", ",
", SkinTones:", ", ",
)
var emojiRE = regexp.MustCompile(`\{Emoji:"([^"]*)"`)
func generate() ([]byte, error) {
// load gemoji data
res, err := http.Get(gemojiURL)
if err != nil {
return nil, err
}
defer res.Body.Close()
// read all
body, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
}
// unmarshal
var data Gemoji
err = json.Unmarshal(body, &data)
if err != nil {
return nil, err
}
skinTones := make(map[string]string)
skinTones["\U0001f3fb"] = "Light Skin Tone"
skinTones["\U0001f3fc"] = "Medium-Light Skin Tone"
skinTones["\U0001f3fd"] = "Medium Skin Tone"
skinTones["\U0001f3fe"] = "Medium-Dark Skin Tone"
skinTones["\U0001f3ff"] = "Dark Skin Tone"
var tmp Gemoji
// filter out emoji that require greater than max unicode version
for i := range data {
val, _ := strconv.ParseFloat(data[i].UnicodeVersion, 64)
if int(val) <= maxUnicodeVersion {
tmp = append(tmp, data[i])
}
}
data = tmp
sort.Slice(data, func(i, j int) bool {
return data[i].Aliases[0] < data[j].Aliases[0]
})
aliasMap := make(map[string]int, len(data))
for i, e := range data {
if e.Emoji == "" || len(e.Aliases) == 0 {
continue
}
for _, a := range e.Aliases {
if a == "" {
continue
}
aliasMap[a] = i
}
}
// gitea customizations
i, ok := aliasMap["tada"]
if ok {
data[i].Aliases = append(data[i].Aliases, "hooray")
}
i, ok = aliasMap["laughing"]
if ok {
data[i].Aliases = append(data[i].Aliases, "laugh")
}
// write a JSON file to use with tribute (write before adding skin tones since we can't support them there yet)
file, _ := json.MarshalIndent(data, "", " ")
_ = os.WriteFile("assets/emoji.json", append(file, '\n'), 0o644)
// Add skin tones to emoji that support it
var (
s []string
newEmoji string
newDescription string
newData Emoji
)
for i := range data {
if data[i].SkinTones {
for k, v := range skinTones {
s = strings.Split(data[i].Emoji, "")
if utf8.RuneCountInString(data[i].Emoji) == 1 {
s = append(s, k)
} else {
// insert into slice after first element because all emoji that support skin tones
// have that modifier placed at this spot
s = append(s, "")
copy(s[2:], s[1:])
s[1] = k
}
newEmoji = strings.Join(s, "")
newDescription = data[i].Description + ": " + v
newAlias := data[i].Aliases[0] + "_" + strings.ReplaceAll(v, " ", "_")
newData = Emoji{newEmoji, newDescription, []string{newAlias}, "12.0", false}
data = append(data, newData)
}
}
}
sort.Slice(data, func(i, j int) bool {
return data[i].Aliases[0] < data[j].Aliases[0]
})
// add header
str := replacer.Replace(fmt.Sprintf(hdr, gemojiURL, data))
// change the format of the unicode string
str = emojiRE.ReplaceAllStringFunc(str, func(s string) string {
var err error
s, err = strconv.Unquote(s[len("{Emoji:"):])
if err != nil {
panic(err)
}
return "{" + strconv.QuoteToASCII(s)
})
// format
return format.Source([]byte(str))
}
const hdr = `
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package emoji
// Code generated by build/generate-emoji.go. DO NOT EDIT.
// Sourced from %s
var GemojiData = %#v
`
-126
View File
@@ -1,126 +0,0 @@
//go:build ignore
package main
import (
"archive/tar"
"compress/gzip"
"flag"
"fmt"
"io"
"log"
"net/http"
"os"
"path"
"path/filepath"
"strings"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/util"
)
func main() {
var (
prefix = "gitea-gitignore"
url = "https://api.github.com/repos/github/gitignore/tarball"
githubApiToken = ""
githubUsername = ""
destination = ""
)
flag.StringVar(&destination, "dest", "options/gitignore/", "destination for the gitignores")
flag.StringVar(&githubUsername, "username", "", "github username")
flag.StringVar(&githubApiToken, "token", "", "github api token")
flag.Parse()
file, err := os.CreateTemp(os.TempDir(), prefix)
if err != nil {
log.Fatalf("Failed to create temp file. %s", err)
}
defer util.Remove(file.Name())
req, err := http.NewRequest("GET", url, nil)
if err != nil {
log.Fatalf("Failed to download archive. %s", err)
}
if len(githubApiToken) > 0 && len(githubUsername) > 0 {
req.SetBasicAuth(githubUsername, githubApiToken)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
log.Fatalf("Failed to download archive. %s", err)
}
defer resp.Body.Close()
if _, err := io.Copy(file, resp.Body); err != nil {
log.Fatalf("Failed to copy archive to file. %s", err)
}
if _, err := file.Seek(0, 0); err != nil {
log.Fatalf("Failed to reset seek on archive. %s", err)
}
gz, err := gzip.NewReader(file)
if err != nil {
log.Fatalf("Failed to gunzip the archive. %s", err)
}
tr := tar.NewReader(gz)
filesToCopy := make(map[string]string, 0)
for {
hdr, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
log.Fatalf("Failed to iterate archive. %s", err)
}
if filepath.Ext(hdr.Name) != ".gitignore" {
continue
}
if hdr.Typeflag == tar.TypeSymlink {
fmt.Printf("Found symlink %s -> %s\n", hdr.Name, hdr.Linkname)
filesToCopy[strings.TrimSuffix(filepath.Base(hdr.Name), ".gitignore")] = strings.TrimSuffix(filepath.Base(hdr.Linkname), ".gitignore")
continue
}
out, err := os.Create(path.Join(destination, strings.TrimSuffix(filepath.Base(hdr.Name), ".gitignore")))
if err != nil {
log.Fatalf("Failed to create new file. %s", err)
}
defer out.Close()
if _, err := io.Copy(out, tr); err != nil {
log.Fatalf("Failed to write new file. %s", err)
} else {
fmt.Printf("Written %s\n", out.Name())
}
}
for dst, src := range filesToCopy {
// Read all content of src to data
src = path.Join(destination, src)
data, err := os.ReadFile(src)
if err != nil {
log.Fatalf("Failed to read src file. %s", err)
}
// Write data to dst
dst = path.Join(destination, dst)
err = os.WriteFile(dst, data, 0o644)
if err != nil {
log.Fatalf("Failed to write new file. %s", err)
}
fmt.Printf("Written (copy of %s) %s\n", src, dst)
}
fmt.Println("Done")
}
-239
View File
@@ -1,239 +0,0 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build ignore
package main
import (
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"regexp"
"slices"
"sort"
"strings"
)
// regexp is based on go-license, excluding README and NOTICE
// https://github.com/google/go-licenses/blob/master/licenses/find.go
// also defined in vite.config.ts
var licenseRe = regexp.MustCompile(`^(?i)((UN)?LICEN(S|C)E|COPYING).*$`)
// primaryLicenseRe matches exact primary license filenames without suffixes.
// When a directory has both primary and variant files (e.g. LICENSE and
// LICENSE.docs), only the primary files are kept.
var primaryLicenseRe = regexp.MustCompile(`^(?i)(LICEN[SC]E|COPYING)$`)
// ignoredNames are LicenseEntry.Name values to exclude from the output.
var ignoredNames = map[string]bool{
"code.gitea.io/gitea": true,
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/options/license": true,
}
var excludedExt = map[string]bool{
".gitignore": true,
".go": true,
".mod": true,
".sum": true,
".toml": true,
".yaml": true,
".yml": true,
}
type ModuleInfo struct {
Path string
Dir string
PkgDirs []string // directories of packages imported from this module
}
type LicenseEntry struct {
Name string `json:"name"`
Path string `json:"path"`
LicenseText string `json:"licenseText"`
}
// getModules returns all dependency modules with their local directory paths
// and the package directories used from each module.
func getModules(goCmd string) []ModuleInfo {
cmd := exec.Command(goCmd, "list", "-deps", "-f",
"{{if .Module}}{{.Module.Path}}\t{{.Module.Dir}}\t{{.Dir}}{{end}}", "./...")
cmd.Stderr = os.Stderr
// Use GOOS=linux with CGO to ensure we capture all platform-specific
// dependencies, matching the CI environment.
cmd.Env = append(os.Environ(), "GOOS=linux", "GOARCH=amd64", "CGO_ENABLED=1")
output, err := cmd.Output()
if err != nil {
fmt.Fprintf(os.Stderr, "failed to run 'go list -deps': %v\n", err)
os.Exit(1)
}
var modules []ModuleInfo
seen := make(map[string]int) // module path -> index in modules
for _, line := range strings.Split(string(output), "\n") {
line = strings.TrimSpace(line)
if line == "" {
continue
}
parts := strings.Split(line, "\t")
if len(parts) != 3 {
continue
}
modPath, modDir, pkgDir := parts[0], parts[1], parts[2]
if idx, ok := seen[modPath]; ok {
modules[idx].PkgDirs = append(modules[idx].PkgDirs, pkgDir)
} else {
seen[modPath] = len(modules)
modules = append(modules, ModuleInfo{
Path: modPath,
Dir: modDir,
PkgDirs: []string{pkgDir},
})
}
}
return modules
}
// findLicenseFiles scans a module's root directory and its used package
// directories for license files. It also walks up from each package directory
// to the module root, scanning intermediate directories. Subdirectory licenses
// are only included if their text differs from the root license(s).
func findLicenseFiles(mod ModuleInfo) []LicenseEntry {
var entries []LicenseEntry
seenTexts := make(map[string]bool)
// First, collect root-level license files.
entries = append(entries, scanDirForLicenses(mod.Dir, mod.Path, "")...)
for _, e := range entries {
seenTexts[e.LicenseText] = true
}
// Then check each package directory and all intermediate parent directories
// up to the module root for license files with unique text.
seenDirs := map[string]bool{mod.Dir: true}
for _, pkgDir := range mod.PkgDirs {
for dir := pkgDir; dir != mod.Dir && strings.HasPrefix(dir, mod.Dir); dir = filepath.Dir(dir) {
if seenDirs[dir] {
continue
}
seenDirs[dir] = true
for _, e := range scanDirForLicenses(dir, mod.Path, mod.Dir) {
if !seenTexts[e.LicenseText] {
seenTexts[e.LicenseText] = true
entries = append(entries, e)
}
}
}
}
return entries
}
// scanDirForLicenses reads a single directory for license files and returns entries.
// If moduleRoot is non-empty, paths are made relative to it.
func scanDirForLicenses(dir, modulePath, moduleRoot string) []LicenseEntry {
dirEntries, err := os.ReadDir(dir)
if err != nil {
return nil
}
var entries []LicenseEntry
for _, entry := range dirEntries {
if entry.IsDir() {
continue
}
name := entry.Name()
if !licenseRe.MatchString(name) {
continue
}
if excludedExt[strings.ToLower(filepath.Ext(name))] {
continue
}
content, err := os.ReadFile(filepath.Join(dir, name))
if err != nil {
continue
}
entryName := modulePath
entryPath := modulePath + "/" + name
if moduleRoot != "" {
rel, _ := filepath.Rel(moduleRoot, dir)
if rel != "." {
relSlash := filepath.ToSlash(rel)
entryName = modulePath + "/" + relSlash
entryPath = modulePath + "/" + relSlash + "/" + name
}
}
entries = append(entries, LicenseEntry{
Name: entryName,
Path: entryPath,
LicenseText: string(content),
})
}
// When multiple license files exist, prefer primary files (e.g. LICENSE)
// over variants with suffixes (e.g. LICENSE.docs, LICENSE-2.0.txt).
// If no primary file exists, keep only the first variant.
if len(entries) > 1 {
var primary []LicenseEntry
for _, e := range entries {
fileName := e.Path[strings.LastIndex(e.Path, "/")+1:]
if primaryLicenseRe.MatchString(fileName) {
primary = append(primary, e)
}
}
if len(primary) > 0 {
return primary
}
return entries[:1]
}
return entries
}
func main() {
if len(os.Args) != 2 {
fmt.Println("usage: go run generate-go-licenses.go <out-json-file>")
os.Exit(1)
}
out := os.Args[1]
goCmd := "go"
if env := os.Getenv("GO"); env != "" {
goCmd = env
}
modules := getModules(goCmd)
var entries []LicenseEntry
for _, mod := range modules {
entries = append(entries, findLicenseFiles(mod)...)
}
entries = slices.DeleteFunc(entries, func(e LicenseEntry) bool {
return ignoredNames[e.Name]
})
sort.Slice(entries, func(i, j int) bool {
return entries[i].Path < entries[j].Path
})
jsonBytes, err := json.MarshalIndent(entries, "", " ")
if err != nil {
panic(err)
}
// Ensure file has a final newline
if jsonBytes[len(jsonBytes)-1] != '\n' {
jsonBytes = append(jsonBytes, '\n')
}
err = os.WriteFile(out, jsonBytes, 0o644)
if err != nil {
panic(err)
}
}
-102
View File
@@ -1,102 +0,0 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
// generate-openapi converts Gitea's Swagger 2.0 spec into an OpenAPI 3.0 spec.
//
// Gitea generates a Swagger 2.0 spec from code annotations (make generate-swagger).
// This tool converts it to OAS3 so that SDK generators and tools that require
// OAS3 (e.g. progenitor for Rust) can consume it directly. The conversion also
// deduplicates inline enum definitions into named schema components, producing
// cleaner SDK output with proper enum types instead of anonymous strings.
//
// Run: go run build/generate-openapi.go
// Output: templates/swagger/v1_openapi3_json.tmpl
//go:build ignore
package main
import (
"encoding/json"
"fmt"
"log"
"os"
"regexp"
"sort"
"strings"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/build/openapi3gen"
"github.com/getkin/kin-openapi/openapi3"
)
const (
swaggerSpecPath = "templates/swagger/v1_json.tmpl"
openapi3OutPath = "templates/swagger/v1_openapi3_json.tmpl"
appSubUrlVar = "{{.SwaggerAppSubUrl}}"
appVerVar = "{{.SwaggerAppVer}}"
appNameVar = "{{.SwaggerAppName}}"
appSubUrlPlaceholder = "GITEA_APP_SUB_URL_PLACEHOLDER"
appVerPlaceholder = "0.0.0-gitea-placeholder"
appNamePlaceholder = "GiteaAppNamePlaceholder"
)
var (
appSubUrlRe = regexp.MustCompile(regexp.QuoteMeta(appSubUrlVar))
appVerRe = regexp.MustCompile(regexp.QuoteMeta(appVerVar))
appNameRe = regexp.MustCompile(regexp.QuoteMeta(appNameVar))
enumScanDirs = []string{
"modules/structs",
"modules/commitstatus",
}
)
func main() {
astEnumMap, err := openapi3gen.ScanSwaggerEnumTypes(enumScanDirs)
if err != nil {
log.Fatalf("scanning swagger:enum annotations: %v", err)
}
names := make([]string, 0, len(astEnumMap))
for _, n := range astEnumMap {
names = append(names, n)
}
sort.Strings(names)
fmt.Fprintf(os.Stderr, "discovered %d swagger:enum types: %s\n", len(names), strings.Join(names, ", "))
data, err := os.ReadFile(swaggerSpecPath)
if err != nil {
log.Fatalf("reading swagger spec: %v", err)
}
cleaned := appSubUrlRe.ReplaceAll(data, []byte(appSubUrlPlaceholder))
cleaned = appVerRe.ReplaceAll(cleaned, []byte(appVerPlaceholder))
cleaned = appNameRe.ReplaceAll(cleaned, []byte(appNamePlaceholder))
oas3, err := openapi3gen.Convert(cleaned, astEnumMap)
if err != nil {
log.Fatalf("converting to openapi 3.0: %v", err)
}
oas3.Servers = openapi3.Servers{
{URL: appSubUrlPlaceholder + "/api/v1"},
}
out, err := json.MarshalIndent(oas3, "", " ")
if err != nil {
log.Fatalf("marshaling openapi 3.0: %v", err)
}
result := strings.ReplaceAll(string(out), appSubUrlPlaceholder, appSubUrlVar)
result = strings.ReplaceAll(result, appVerPlaceholder, appVerVar)
result = strings.ReplaceAll(result, appNamePlaceholder, appNameVar)
result = strings.TrimSpace(result)
if err := os.WriteFile(openapi3OutPath, []byte(result), 0o644); err != nil {
log.Fatalf("writing openapi 3.0 spec: %v", err)
}
fmt.Printf("Generated %s\n", openapi3OutPath)
}
-281
View File
@@ -1,281 +0,0 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package openapi3gen
import (
"fmt"
"regexp"
"strings"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/json"
"github.com/getkin/kin-openapi/openapi2"
"github.com/getkin/kin-openapi/openapi2conv"
"github.com/getkin/kin-openapi/openapi3"
)
// rxDeprecated matches "deprecated" as a word at the start of a description
// or preceded by whitespace/punctuation that indicates a leading marker (e.g.
// "Deprecated: true", "deprecated (use X instead)"). Rejects negated phrases
// like "not deprecated" or "previously deprecated, now supported".
var rxDeprecated = regexp.MustCompile(`(?i)(?:^|[\n.;])\s*deprecated\b`)
// Convert parses a Swagger 2.0 spec and returns an OAS3 spec, applying
// Gitea-specific post-processing: file-schema fixups, URI formats,
// deprecated flags, and shared-enum extraction.
//
// astEnumMap is a value-set-key → Go-type-name map (built by
// ScanSwaggerEnumTypes). If a shared enum in the spec has no entry in the
// map, Convert returns an error — no fallback naming.
func Convert(swaggerJSON []byte, astEnumMap map[string]string) (*openapi3.T, error) {
var swagger2 openapi2.T
if err := json.Unmarshal(swaggerJSON, &swagger2); err != nil {
return nil, fmt.Errorf("parsing swagger 2.0: %w", err)
}
oas3, err := openapi2conv.ToV3(&swagger2)
if err != nil {
return nil, fmt.Errorf("converting to openapi 3.0: %w", err)
}
fixFileSchemas(oas3)
addURIFormats(oas3)
addDeprecatedFlags(oas3)
if err := extractSharedEnums(oas3, astEnumMap); err != nil {
return nil, err
}
return oas3, nil
}
func fixFileSchemas(doc *openapi3.T) {
for _, pathItem := range doc.Paths.Map() {
for _, op := range []*openapi3.Operation{
pathItem.Get, pathItem.Post, pathItem.Put, pathItem.Patch,
pathItem.Delete, pathItem.Head, pathItem.Options, pathItem.Trace,
} {
if op == nil {
continue
}
for _, resp := range op.Responses.Map() {
if resp.Value == nil {
continue
}
for _, mediaType := range resp.Value.Content {
fixSchema(mediaType.Schema)
}
}
if op.RequestBody != nil && op.RequestBody.Value != nil {
for _, mediaType := range op.RequestBody.Value.Content {
fixSchema(mediaType.Schema)
}
}
}
}
}
// fixSchema rewrites any "type: file" schemas to the OAS3 equivalent
// (type: string, format: binary), recursing into Properties, Items, and
// AllOf/OneOf/AnyOf/Not branches. $ref nodes are skipped so shared schemas
// are rewritten exactly once when visited through their declaration.
func fixSchema(ref *openapi3.SchemaRef) {
if ref == nil || ref.Value == nil || ref.Ref != "" {
return
}
s := ref.Value
if s.Type.Is("file") {
s.Type = &openapi3.Types{"string"}
s.Format = "binary"
}
for _, p := range s.Properties {
fixSchema(p)
}
fixSchema(s.Items)
for _, sub := range s.AllOf {
fixSchema(sub)
}
for _, sub := range s.OneOf {
fixSchema(sub)
}
for _, sub := range s.AnyOf {
fixSchema(sub)
}
fixSchema(s.Not)
}
// addURIFormats sets format: uri on string properties whose names indicate
// they hold URLs. This information is lost in Swagger 2.0 but is valuable
// for code generators.
func addURIFormats(doc *openapi3.T) {
if doc.Components == nil {
return
}
for _, schemaRef := range doc.Components.Schemas {
if schemaRef.Value == nil {
continue
}
for propName, propRef := range schemaRef.Value.Properties {
if propRef == nil || propRef.Value == nil || propRef.Ref != "" {
continue
}
prop := propRef.Value
if !prop.Type.Is("string") || prop.Format != "" {
continue
}
if isURLProperty(propName) {
prop.Format = "uri"
}
}
}
}
func isURLProperty(name string) bool {
if strings.HasSuffix(name, "_url") {
return true
}
switch name {
case "url", "html_url", "clone_url":
return true
}
return false
}
// addDeprecatedFlags sets deprecated: true on schema properties whose
// description starts with a "deprecated" marker (e.g. "Deprecated: true"
// or "deprecated (use X instead)"). Does not match negated phrases.
func addDeprecatedFlags(doc *openapi3.T) {
if doc.Components == nil {
return
}
for _, schemaRef := range doc.Components.Schemas {
if schemaRef.Value == nil {
continue
}
for _, propRef := range schemaRef.Value.Properties {
if propRef == nil || propRef.Value == nil || propRef.Ref != "" {
continue
}
if rxDeprecated.MatchString(propRef.Value.Description) {
propRef.Value.Deprecated = true
}
}
}
}
type enumUsage struct {
schemaName string
propName string
propRef *openapi3.SchemaRef
inItems bool
}
// extractSharedEnums finds identical enum arrays used by multiple schema
// properties, creates a standalone named schema for each, and replaces
// the inline enums with $ref pointers.
//
// If the derived enum name collides with an existing component schema, or
// no // swagger:enum annotation matches the value set, generation aborts
// with an actionable error — there are no silent fallbacks.
func extractSharedEnums(doc *openapi3.T, astEnumMap map[string]string) error {
if doc.Components == nil {
return nil
}
enumGroups := map[string][]enumUsage{}
for schemaName, schemaRef := range doc.Components.Schemas {
if schemaRef.Value == nil {
continue
}
for propName, propRef := range schemaRef.Value.Properties {
if propRef == nil || propRef.Value == nil || propRef.Ref != "" {
continue
}
if len(propRef.Value.Enum) > 1 && propRef.Value.Type.Is("string") {
key := EnumKey(propRef.Value.Enum)
enumGroups[key] = append(enumGroups[key], enumUsage{schemaName, propName, propRef, false})
}
if propRef.Value.Type.Is("array") && propRef.Value.Items != nil &&
propRef.Value.Items.Value != nil && propRef.Value.Items.Ref == "" &&
len(propRef.Value.Items.Value.Enum) > 1 && propRef.Value.Items.Value.Type.Is("string") {
key := EnumKey(propRef.Value.Items.Value.Enum)
enumGroups[key] = append(enumGroups[key], enumUsage{schemaName, propName, propRef, true})
}
}
}
for key, usages := range enumGroups {
if len(usages) < 2 {
continue
}
enumName, err := deriveEnumName(key, usages, astEnumMap)
if err != nil {
return err
}
if _, exists := doc.Components.Schemas[enumName]; exists {
return fmt.Errorf("enum name collision: %s already exists as a component schema", enumName)
}
var enumValues []any
if usages[0].inItems {
enumValues = usages[0].propRef.Value.Items.Value.Enum
} else {
enumValues = usages[0].propRef.Value.Enum
}
doc.Components.Schemas[enumName] = &openapi3.SchemaRef{
Value: &openapi3.Schema{
Type: &openapi3.Types{"string"},
Enum: enumValues,
},
}
ref := "#/components/schemas/" + enumName
for _, usage := range usages {
if usage.inItems {
usage.propRef.Value.Items = &openapi3.SchemaRef{Ref: ref}
} else {
old := usage.propRef.Value
if old.Description == "" && !old.Deprecated && old.Format == "" {
usage.propRef.Ref = ref
usage.propRef.Value = nil
} else {
usage.propRef.Value = &openapi3.Schema{
AllOf: openapi3.SchemaRefs{
{Ref: ref},
},
Description: old.Description,
Deprecated: old.Deprecated,
Format: old.Format,
}
}
}
}
}
return nil
}
// deriveEnumName looks up a shared enum's Go type name from astEnumMap by
// value-set key. If no annotation matches, returns an error identifying the
// offending properties and the fix.
func deriveEnumName(key string, usages []enumUsage, astEnumMap map[string]string) (string, error) {
if name, ok := astEnumMap[key]; ok {
return name, nil
}
props := map[string]bool{}
for _, u := range usages {
props[fmt.Sprintf("%s.%s", u.schemaName, u.propName)] = true
}
propList := make([]string, 0, len(props))
for p := range props {
propList = append(propList, p)
}
return "", fmt.Errorf(
"no swagger:enum annotation matches value-set %q used by %d properties: %v; "+
"fix by adding a named string type with // swagger:enum to modules/structs or modules/commitstatus",
key, len(usages), propList,
)
}
-170
View File
@@ -1,170 +0,0 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package openapi3gen
import (
"strings"
"testing"
"github.com/getkin/kin-openapi/openapi3"
)
func TestDeriveEnumName_hit(t *testing.T) {
key := EnumKey([]any{"red", "green", "blue"})
astMap := map[string]string{key: "Color"}
usages := []enumUsage{{schemaName: "Paint", propName: "color"}}
got, err := deriveEnumName(key, usages, astMap)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got != "Color" {
t.Fatalf("got %q, want %q", got, "Color")
}
}
func TestDeriveEnumName_miss(t *testing.T) {
key := EnumKey([]any{"x", "y"})
usages := []enumUsage{{schemaName: "Thing", propName: "kind"}}
_, err := deriveEnumName(key, usages, map[string]string{})
if err == nil {
t.Fatal("expected miss error, got nil")
}
msg := err.Error()
if !strings.Contains(msg, "Thing.kind") {
t.Fatalf("error %q should list the missing usage", msg)
}
if !strings.Contains(msg, "swagger:enum") {
t.Fatalf("error %q should hint at the fix", msg)
}
}
func TestExtractSharedEnums_usesASTMap(t *testing.T) {
doc := &openapi3.T{
Components: &openapi3.Components{
Schemas: openapi3.Schemas{
"A": {Value: &openapi3.Schema{
Type: &openapi3.Types{"object"},
Properties: openapi3.Schemas{
"color": {Value: &openapi3.Schema{
Type: &openapi3.Types{"string"},
Enum: []any{"red", "green", "blue"},
}},
},
}},
"B": {Value: &openapi3.Schema{
Type: &openapi3.Types{"object"},
Properties: openapi3.Schemas{
"color": {Value: &openapi3.Schema{
Type: &openapi3.Types{"string"},
Enum: []any{"red", "green", "blue"},
}},
},
}},
},
},
}
astMap := map[string]string{EnumKey([]any{"red", "green", "blue"}): "Color"}
if err := extractSharedEnums(doc, astMap); err != nil {
t.Fatalf("extractSharedEnums: %v", err)
}
if _, ok := doc.Components.Schemas["Color"]; !ok {
t.Fatalf("expected Color schema to be extracted")
}
}
func TestFixFileSchemas_recursesIntoNested(t *testing.T) {
fileType := func() *openapi3.SchemaRef {
return &openapi3.SchemaRef{Value: &openapi3.Schema{Type: &openapi3.Types{"file"}}}
}
doc := &openapi3.T{
Paths: openapi3.NewPaths(),
}
doc.Paths.Set("/upload", &openapi3.PathItem{
Post: &openapi3.Operation{
RequestBody: &openapi3.RequestBodyRef{
Value: &openapi3.RequestBody{
Content: openapi3.Content{
"multipart/form-data": {
Schema: &openapi3.SchemaRef{Value: &openapi3.Schema{
Type: &openapi3.Types{"object"},
Properties: openapi3.Schemas{
"attachment": fileType(),
"items": {Value: &openapi3.Schema{
Type: &openapi3.Types{"array"},
Items: fileType(),
}},
"alt": {Value: &openapi3.Schema{
AllOf: openapi3.SchemaRefs{fileType()},
}},
"one": {Value: &openapi3.Schema{
OneOf: openapi3.SchemaRefs{fileType()},
}},
"any": {Value: &openapi3.Schema{
AnyOf: openapi3.SchemaRefs{fileType()},
}},
"not": {Value: &openapi3.Schema{
Not: fileType(),
}},
},
}},
},
},
},
},
Responses: openapi3.NewResponses(),
},
})
fixFileSchemas(doc)
props := doc.Paths.Value("/upload").Post.RequestBody.Value.Content["multipart/form-data"].Schema.Value.Properties
if !props["attachment"].Value.Type.Is("string") || props["attachment"].Value.Format != "binary" {
t.Errorf("nested property not fixed: %+v", props["attachment"].Value)
}
if !props["items"].Value.Items.Value.Type.Is("string") || props["items"].Value.Items.Value.Format != "binary" {
t.Errorf("array items not fixed: %+v", props["items"].Value.Items.Value)
}
if !props["alt"].Value.AllOf[0].Value.Type.Is("string") || props["alt"].Value.AllOf[0].Value.Format != "binary" {
t.Errorf("allOf branch not fixed: %+v", props["alt"].Value.AllOf[0].Value)
}
if !props["one"].Value.OneOf[0].Value.Type.Is("string") || props["one"].Value.OneOf[0].Value.Format != "binary" {
t.Errorf("oneOf branch not fixed: %+v", props["one"].Value.OneOf[0].Value)
}
if !props["any"].Value.AnyOf[0].Value.Type.Is("string") || props["any"].Value.AnyOf[0].Value.Format != "binary" {
t.Errorf("anyOf branch not fixed: %+v", props["any"].Value.AnyOf[0].Value)
}
if !props["not"].Value.Not.Value.Type.Is("string") || props["not"].Value.Not.Value.Format != "binary" {
t.Errorf("not branch not fixed: %+v", props["not"].Value.Not.Value)
}
}
func TestExtractSharedEnums_missReturnsError(t *testing.T) {
doc := &openapi3.T{
Components: &openapi3.Components{
Schemas: openapi3.Schemas{
"A": {Value: &openapi3.Schema{
Type: &openapi3.Types{"object"},
Properties: openapi3.Schemas{
"color": {Value: &openapi3.Schema{
Type: &openapi3.Types{"string"},
Enum: []any{"red", "green"},
}},
},
}},
"B": {Value: &openapi3.Schema{
Type: &openapi3.Types{"object"},
Properties: openapi3.Schemas{
"color": {Value: &openapi3.Schema{
Type: &openapi3.Types{"string"},
Enum: []any{"red", "green"},
}},
},
}},
},
},
}
if err := extractSharedEnums(doc, map[string]string{}); err == nil {
t.Fatal("expected miss error")
}
}
-188
View File
@@ -1,188 +0,0 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
// Package openapi3gen converts Gitea's Swagger 2.0 spec to an OpenAPI 3.0
// spec. It discovers Go enum type names by scanning swagger:enum annotations
// in the source tree, then names extracted shared-enum schemas accordingly.
package openapi3gen
import (
"fmt"
"go/ast"
"go/parser"
"go/token"
"os"
"path/filepath"
"regexp"
"sort"
"strconv"
"strings"
)
// EnumKey returns a canonical key for a set of enum values: values are
// stringified, sorted, and joined with "|". Used to match enum value sets
// across spec properties and scanned Go type declarations.
func EnumKey(values []any) string {
strs := make([]string, len(values))
for i, v := range values {
strs[i] = fmt.Sprintf("%v", v)
}
sort.Strings(strs)
return strings.Join(strs, "|")
}
var rxSwaggerEnum = regexp.MustCompile(`swagger:enum\s+(\w+)`)
// ScanSwaggerEnumTypes walks .go files under each dir and returns a map from
// a canonical value-set key (see EnumKey) to the Go type name declared with
// // swagger:enum TypeName.
//
// Returns an error on parse failure, on an annotation for a type whose
// constants can't be extracted, or on value-set collisions between two
// different enum types.
func ScanSwaggerEnumTypes(dirs []string) (map[string]string, error) {
fset := token.NewFileSet()
parsed := []*ast.File{}
for _, dir := range dirs {
entries, err := os.ReadDir(dir)
if err != nil {
return nil, fmt.Errorf("reading %s: %w", dir, err)
}
for _, entry := range entries {
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".go") {
continue
}
if strings.HasSuffix(entry.Name(), "_test.go") {
continue
}
path := filepath.Join(dir, entry.Name())
file, err := parser.ParseFile(fset, path, nil, parser.ParseComments)
if err != nil {
return nil, fmt.Errorf("%s: %w", path, err)
}
parsed = append(parsed, file)
}
}
enumTypes := map[string]string{} // typeName → "" (presence marker)
enumValues := map[string][]any{} // typeName → values
// Pass 1: collect every // swagger:enum TypeName declaration.
for _, file := range parsed {
for _, decl := range file.Decls {
gd, ok := decl.(*ast.GenDecl)
if !ok || gd.Tok != token.TYPE {
continue
}
if err := collectEnumType(gd, enumTypes); err != nil {
return nil, fmt.Errorf("%s: %w", fset.Position(gd.Pos()).Filename, err)
}
}
}
// Pass 2: collect const values; now every annotated type is visible.
for _, file := range parsed {
for _, decl := range file.Decls {
gd, ok := decl.(*ast.GenDecl)
if !ok || gd.Tok != token.CONST {
continue
}
collectEnumValues(gd, enumTypes, enumValues)
}
}
result := map[string]string{}
for typeName := range enumTypes {
values, ok := enumValues[typeName]
if !ok || len(values) == 0 {
return nil, fmt.Errorf("swagger:enum %s has no const block with typed string values", typeName)
}
key := EnumKey(values)
if existing, ok := result[key]; ok && existing != typeName {
return nil, fmt.Errorf("swagger:enum value-set collision: %s and %s both use %q", existing, typeName, key)
}
result[key] = typeName
}
return result, nil
}
// collectEnumType scans a `type` GenDecl for // swagger:enum annotations,
// handling both the lone form (`// swagger:enum Foo\n type Foo string`)
// where the comment group is attached to the GenDecl, and the grouped form:
//
// type (
// // swagger:enum Foo
// Foo string
// )
//
// where the comment group is attached to each TypeSpec. Caveat: Go's parser
// only attaches a CommentGroup when it is immediately adjacent to the decl.
// A blank line (not a `//` continuation line) between the comment and the
// declaration drops the Doc, so annotations MUST sit directly above their
// type. All current annotated files obey this — the rule is noted here so
// a future edit that inserts a blank line fails fast rather than silently.
func collectEnumType(gd *ast.GenDecl, enumTypes map[string]string) error {
if err := registerEnumAnnotation(gd.Doc, gd.Specs, enumTypes); err != nil {
return err
}
for _, spec := range gd.Specs {
ts, ok := spec.(*ast.TypeSpec)
if !ok || ts.Doc == nil {
continue
}
if err := registerEnumAnnotation(ts.Doc, []ast.Spec{ts}, enumTypes); err != nil {
return err
}
}
return nil
}
func registerEnumAnnotation(doc *ast.CommentGroup, specs []ast.Spec, enumTypes map[string]string) error {
if doc == nil {
return nil
}
matches := rxSwaggerEnum.FindStringSubmatch(doc.Text())
if len(matches) < 2 {
return nil
}
annotated := matches[1]
for _, spec := range specs {
ts, ok := spec.(*ast.TypeSpec)
if !ok {
continue
}
if ts.Name.Name == annotated {
enumTypes[annotated] = ""
return nil
}
}
return fmt.Errorf("swagger:enum %s: no type declaration with that name in the same decl group; check for a typo", annotated)
}
func collectEnumValues(gd *ast.GenDecl, enumTypes map[string]string, enumValues map[string][]any) {
for _, spec := range gd.Specs {
vs, ok := spec.(*ast.ValueSpec)
if !ok || vs.Type == nil {
continue
}
ident, ok := vs.Type.(*ast.Ident)
if !ok {
continue
}
if _, isEnum := enumTypes[ident.Name]; !isEnum {
continue
}
for _, val := range vs.Values {
lit, ok := val.(*ast.BasicLit)
if !ok || lit.Kind != token.STRING {
continue
}
unquoted, err := strconv.Unquote(lit.Value)
if err != nil {
continue
}
enumValues[ident.Name] = append(enumValues[ident.Name], unquoted)
}
}
}
-239
View File
@@ -1,239 +0,0 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package openapi3gen
import (
"os"
"path/filepath"
"strings"
"testing"
)
func TestEnumKey_sortsAndJoins(t *testing.T) {
key := EnumKey([]any{"b", "a", "c"})
if key != "a|b|c" {
t.Fatalf("EnumKey = %q, want %q", key, "a|b|c")
}
}
func TestEnumKey_handlesNonStringValues(t *testing.T) {
key := EnumKey([]any{2, 1, 3})
if key != "1|2|3" {
t.Fatalf("EnumKey = %q, want %q", key, "1|2|3")
}
}
func TestScanSwaggerEnumTypes_basic(t *testing.T) {
dir := t.TempDir()
src := `package fixture
// Color is a primary color.
// swagger:enum Color
type Color string
const (
ColorRed Color = "red"
ColorGreen Color = "green"
ColorBlue Color = "blue"
)
`
if err := os.WriteFile(filepath.Join(dir, "color.go"), []byte(src), 0o644); err != nil {
t.Fatal(err)
}
got, err := ScanSwaggerEnumTypes([]string{dir})
if err != nil {
t.Fatalf("ScanSwaggerEnumTypes: %v", err)
}
wantKey := EnumKey([]any{"red", "green", "blue"})
if got[wantKey] != "Color" {
t.Fatalf("map[%q] = %q, want %q", wantKey, got[wantKey], "Color")
}
}
func TestScanSwaggerEnumTypes_orphanAnnotation(t *testing.T) {
dir := t.TempDir()
src := `package fixture
// swagger:enum Sttype
type StateType string
const (
StateOpen StateType = "open"
)
`
if err := os.WriteFile(filepath.Join(dir, "typo.go"), []byte(src), 0o644); err != nil {
t.Fatal(err)
}
_, err := ScanSwaggerEnumTypes([]string{dir})
if err == nil {
t.Fatal("expected error for annotation referencing a non-matching type name")
}
if !strings.Contains(err.Error(), "Sttype") {
t.Fatalf("error %q should mention the typo'd name Sttype", err.Error())
}
}
func TestScanSwaggerEnumTypes_collision(t *testing.T) {
dir := t.TempDir()
src := `package fixture
// swagger:enum Alpha
type Alpha string
const (
AlphaX Alpha = "x"
AlphaY Alpha = "y"
)
// swagger:enum Beta
type Beta string
const (
BetaX Beta = "x"
BetaY Beta = "y"
)
`
if err := os.WriteFile(filepath.Join(dir, "dup.go"), []byte(src), 0o644); err != nil {
t.Fatal(err)
}
_, err := ScanSwaggerEnumTypes([]string{dir})
if err == nil {
t.Fatal("expected collision error, got nil")
}
msg := err.Error()
if !strings.Contains(msg, "Alpha") || !strings.Contains(msg, "Beta") {
t.Fatalf("error %q should mention both Alpha and Beta", msg)
}
}
func TestScanSwaggerEnumTypes_parseFailure(t *testing.T) {
dir := t.TempDir()
if err := os.WriteFile(filepath.Join(dir, "bad.go"), []byte("package fixture\nfunc Foo() {"), 0o644); err != nil {
t.Fatal(err)
}
_, err := ScanSwaggerEnumTypes([]string{dir})
if err == nil {
t.Fatal("expected parse error, got nil")
}
}
func TestScanSwaggerEnumTypes_annotationWithoutConsts(t *testing.T) {
dir := t.TempDir()
src := `package fixture
// swagger:enum Lonely
type Lonely string
`
if err := os.WriteFile(filepath.Join(dir, "lonely.go"), []byte(src), 0o644); err != nil {
t.Fatal(err)
}
_, err := ScanSwaggerEnumTypes([]string{dir})
if err == nil {
t.Fatal("expected error for annotation without consts")
}
if !strings.Contains(err.Error(), "Lonely") {
t.Fatalf("error %q should mention Lonely", err.Error())
}
}
func TestScanSwaggerEnumTypes_constsAndTypeInDifferentFiles(t *testing.T) {
dir := t.TempDir()
// Name ordering: `a_consts.go` < `b_type.go`, so readdir returns consts first.
// Old single-pass scanner would miss the values; two-pass must not.
constsSrc := `package fixture
const (
HueA Hue = "a"
HueB Hue = "b"
)
`
typeSrc := `package fixture
// swagger:enum Hue
type Hue string
`
if err := os.WriteFile(filepath.Join(dir, "a_consts.go"), []byte(constsSrc), 0o644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, "b_type.go"), []byte(typeSrc), 0o644); err != nil {
t.Fatal(err)
}
got, err := ScanSwaggerEnumTypes([]string{dir})
if err != nil {
t.Fatalf("ScanSwaggerEnumTypes: %v", err)
}
wantKey := EnumKey([]any{"a", "b"})
if got[wantKey] != "Hue" {
t.Fatalf("map[%q] = %q, want %q", wantKey, got[wantKey], "Hue")
}
}
func TestScanSwaggerEnumTypes_constsBeforeType(t *testing.T) {
dir := t.TempDir()
src := `package fixture
const (
ShadeDark Shade = "dark"
ShadeLight Shade = "light"
)
// swagger:enum Shade
type Shade string
`
if err := os.WriteFile(filepath.Join(dir, "shade.go"), []byte(src), 0o644); err != nil {
t.Fatal(err)
}
got, err := ScanSwaggerEnumTypes([]string{dir})
if err != nil {
t.Fatalf("ScanSwaggerEnumTypes: %v", err)
}
wantKey := EnumKey([]any{"dark", "light"})
if got[wantKey] != "Shade" {
t.Fatalf("map[%q] = %q, want %q", wantKey, got[wantKey], "Shade")
}
}
func TestScanSwaggerEnumTypes_groupedTypeDecl(t *testing.T) {
dir := t.TempDir()
src := `package fixture
type (
// swagger:enum Color
Color string
// swagger:enum Shade
Shade string
)
const (
ColorRed Color = "red"
ColorBlue Color = "blue"
)
const (
ShadeDark Shade = "dark"
ShadeLight Shade = "light"
)
`
if err := os.WriteFile(filepath.Join(dir, "grouped.go"), []byte(src), 0o644); err != nil {
t.Fatal(err)
}
got, err := ScanSwaggerEnumTypes([]string{dir})
if err != nil {
t.Fatalf("ScanSwaggerEnumTypes: %v", err)
}
colorKey := EnumKey([]any{"red", "blue"})
shadeKey := EnumKey([]any{"dark", "light"})
if got[colorKey] != "Color" {
t.Fatalf("Color: map[%q] = %q, want %q", colorKey, got[colorKey], "Color")
}
if got[shadeKey] != "Shade" {
t.Fatalf("Shade: map[%q] = %q, want %q", shadeKey, got[shadeKey], "Shade")
}
}
-24
View File
@@ -1,24 +0,0 @@
#!/bin/sh
set -e
if [ ! -f ./build/test-env-check.sh ]; then
echo "${0} can only be executed in gitea source root directory"
exit 1
fi
echo "check uid ..."
# the uid of gitea defined in "https://gitea.com/gitea/test-env" is 1000
gitea_uid=$(id -u gitea)
if [ "$gitea_uid" != "1000" ]; then
echo "The uid of linux user 'gitea' is expected to be 1000, but it is $gitea_uid"
exit 1
fi
cur_uid=$(id -u)
if [ "$cur_uid" != "0" -a "$cur_uid" != "$gitea_uid" ]; then
echo "The uid of current linux user is expected to be 0 or $gitea_uid, but it is $cur_uid"
exit 1
fi
-11
View File
@@ -1,11 +0,0 @@
#!/bin/sh
set -e
if [ ! -f ./build/test-env-prepare.sh ]; then
echo "${0} can only be executed in gitea source root directory"
exit 1
fi
echo "change the owner of files to gitea ..."
chown -R gitea:gitea .
-22
View File
@@ -1,22 +0,0 @@
#!/bin/sh
# this script runs in alpine image which only has `sh` shell
if [ ! -f ./options/locale/locale_en-US.json ]; then
echo "please run this script in the root directory of the project"
exit 1
fi
mv ./options/locale/locale_en-US.json ./options/
# Remove translation under 25% of en_us
baselines=$(cat "./options/locale_en-US.json" | wc -l)
baselines=$((baselines / 4))
for filename in ./options/locale/*.json; do
lines=$(cat "$filename" | wc -l)
if [ "$lines" -lt "$baselines" ]; then
echo "Removing $filename: $lines/$baselines"
rm "$filename"
fi
done
mv ./options/locale_en-US.json ./options/locale/
-54
View File
@@ -1,54 +0,0 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package cmd
import (
"context"
"fmt"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/private"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
"github.com/urfave/cli/v3"
)
func newActionsCommand() *cli.Command {
return &cli.Command{
Name: "actions",
Usage: "Manage Gitea Actions",
Commands: []*cli.Command{
newActionsGenerateRunnerTokenCommand(),
},
}
}
func newActionsGenerateRunnerTokenCommand() *cli.Command {
return &cli.Command{
Name: "generate-runner-token",
Usage: "Generate a new token for a runner to use to register with the server",
Action: runGenerateActionsRunnerToken,
Aliases: []string{"grt"},
Flags: []cli.Flag{
&cli.StringFlag{
Name: "scope",
Aliases: []string{"s"},
Value: "",
Usage: "{owner}[/{repo}] - leave empty for a global runner",
},
},
}
}
func runGenerateActionsRunnerToken(ctx context.Context, c *cli.Command) error {
setting.MustInstalled()
scope := c.String("scope")
respText, extra := private.GenerateActionsRunnerToken(ctx, scope)
if extra.HasError() {
return handleCliResponseExtra(extra)
}
_, _ = fmt.Printf("%s\n", respText.Text)
return nil
}
+64 -143
View File
@@ -1,174 +1,95 @@
// Copyright 2016 The Gogs Authors. All rights reserved.
// Copyright 2016 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package cmd
import (
"context"
"fmt"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/git"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/gitrepo"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
repo_module "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/repository"
"github.com/urfave/cli"
"github.com/urfave/cli/v3"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/setting"
)
func newAdminCommand() *cli.Command {
return &cli.Command{
var (
// CmdAdmin represents the available admin sub-command.
CmdAdmin = cli.Command{
Name: "admin",
Usage: "Perform common administrative operations",
Commands: []*cli.Command{
newUserCommand(),
newRepoSyncReleasesCommand(),
newRegenerateCommand(),
newAuthCommand(),
newSendMailCommand(),
Usage: "Perform admin operations on command line",
Description: `Allow using internal logic of Gitea without hacking into the source code
to make automatic initialization process more smoothly`,
Subcommands: []cli.Command{
subcmdCreateUser,
},
}
}
func newRepoSyncReleasesCommand() *cli.Command {
return &cli.Command{
Name: "repo-sync-releases",
Usage: "Synchronize repository releases with tags",
Action: runRepoSyncReleases,
}
}
func newRegenerateCommand() *cli.Command {
return &cli.Command{
Name: "regenerate",
Usage: "Regenerate specific files",
Commands: []*cli.Command{
newRegenerateHooksCommand(),
newRegenerateKeysCommand(),
},
}
}
func newAuthCommand() *cli.Command {
return &cli.Command{
Name: "auth",
Usage: "Modify external auth providers",
Commands: []*cli.Command{
microcmdAuthAddOauth(),
microcmdAuthUpdateOauth(),
microcmdAuthAddLdapBindDn(),
microcmdAuthUpdateLdapBindDn(),
microcmdAuthAddLdapSimpleAuth(),
microcmdAuthUpdateLdapSimpleAuth(),
microcmdAuthAddSMTP(),
microcmdAuthUpdateSMTP(),
newAuthListCommand(),
newAuthDeleteCommand(),
},
}
}
func newSendMailCommand() *cli.Command {
return &cli.Command{
Name: "sendmail",
Usage: "Send a message to all users",
Action: runSendMail,
subcmdCreateUser = cli.Command{
Name: "create-user",
Usage: "Create a new user in database",
Action: runCreateUser,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "title",
Usage: "a title of a message",
Required: true,
},
&cli.StringFlag{
Name: "content",
Usage: "a content of a message",
cli.StringFlag{
Name: "name",
Value: "",
Usage: "Username",
},
&cli.BoolFlag{
Name: "force",
Aliases: []string{"f"},
Usage: "A flag to bypass a confirmation step",
cli.StringFlag{
Name: "password",
Value: "",
Usage: "User password",
},
cli.StringFlag{
Name: "email",
Value: "",
Usage: "User email address",
},
cli.BoolFlag{
Name: "admin",
Usage: "User is an admin",
},
cli.StringFlag{
Name: "config, c",
Value: "custom/conf/app.ini",
Usage: "Custom configuration file path",
},
},
}
}
)
func idFlag() *cli.Int64Flag {
return &cli.Int64Flag{
Name: "id",
Usage: "ID of authentication source",
}
}
func runRepoSyncReleases(ctx context.Context, _ *cli.Command) error {
if err := initDB(ctx); err != nil {
return err
func runCreateUser(c *cli.Context) error {
if !c.IsSet("name") {
return fmt.Errorf("Username is not specified")
} else if !c.IsSet("password") {
return fmt.Errorf("Password is not specified")
} else if !c.IsSet("email") {
return fmt.Errorf("Email is not specified")
}
if err := git.InitSimple(); err != nil {
return err
if c.IsSet("config") {
setting.CustomConf = c.String("config")
}
log.Trace("Synchronizing repository releases (this may take a while)")
for page := 1; ; page++ {
repos, count, err := repo_model.SearchRepositoryByName(ctx, repo_model.SearchRepoOptions{
ListOptions: db.ListOptions{
PageSize: repo_model.RepositoryListDefaultPageSize,
Page: page,
},
Private: true,
})
if err != nil {
return fmt.Errorf("SearchRepositoryByName: %w", err)
}
if len(repos) == 0 {
break
}
log.Trace("Processing next %d repos of %d", len(repos), count)
for _, repo := range repos {
log.Trace("Synchronizing repo %s with path %s", repo.FullName(), repo.RelativePath())
gitRepo, err := gitrepo.OpenRepository(ctx, repo)
if err != nil {
log.Warn("OpenRepository: %v", err)
continue
}
setting.NewContext()
models.LoadConfigs()
oldnum, err := getReleaseCount(ctx, repo.ID)
if err != nil {
log.Warn(" GetReleaseCountByRepoID: %v", err)
}
log.Trace(" currentNumReleases is %d, running SyncReleasesWithTags", oldnum)
if _, err = repo_module.SyncReleasesWithTags(ctx, repo, gitRepo); err != nil {
log.Warn(" SyncReleasesWithTags: %v", err)
gitRepo.Close()
continue
}
count, err = getReleaseCount(ctx, repo.ID)
if err != nil {
log.Warn(" GetReleaseCountByRepoID: %v", err)
gitRepo.Close()
continue
}
log.Trace("repo %s releases synchronized to tags: from %d to %d",
repo.FullName(), oldnum, count)
gitRepo.Close()
}
setting.NewXORMLogService(false)
if err := models.SetEngine(); err != nil {
return fmt.Errorf("models.SetEngine: %v", err)
}
if err := models.CreateUser(&models.User{
Name: c.String("name"),
Email: c.String("email"),
Passwd: c.String("password"),
IsActive: true,
IsAdmin: c.Bool("admin"),
}); err != nil {
return fmt.Errorf("CreateUser: %v", err)
}
fmt.Printf("New user '%s' has been successfully created!\n", c.String("name"))
return nil
}
func getReleaseCount(ctx context.Context, id int64) (int64, error) {
return db.Count[repo_model.Release](
ctx,
repo_model.FindReleasesOptions{
RepoID: id,
IncludeTags: true,
},
)
}
-109
View File
@@ -1,109 +0,0 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package cmd
import (
"context"
"errors"
"fmt"
"os"
"text/tabwriter"
auth_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/auth"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
auth_service "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/auth"
"github.com/urfave/cli/v3"
)
func newAuthDeleteCommand() *cli.Command {
return &cli.Command{
Name: "delete",
Usage: "Delete specific auth source",
Flags: []cli.Flag{idFlag()},
Action: runDeleteAuth,
}
}
func newAuthListCommand() *cli.Command {
return &cli.Command{
Name: "list",
Usage: "List auth sources",
Action: runListAuth,
Flags: []cli.Flag{
&cli.IntFlag{
Name: "min-width",
Usage: "Minimal cell width including any padding for the formatted table",
Value: 0,
},
&cli.IntFlag{
Name: "tab-width",
Usage: "width of tab characters in formatted table (equivalent number of spaces)",
Value: 8,
},
&cli.IntFlag{
Name: "padding",
Usage: "padding added to a cell before computing its width",
Value: 1,
},
&cli.StringFlag{
Name: "pad-char",
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)`,
Value: "\t",
},
&cli.BoolFlag{
Name: "vertical-bars",
Usage: "Set to true to print vertical bars between columns",
},
},
}
}
func runListAuth(ctx context.Context, c *cli.Command) error {
if err := initDB(ctx); err != nil {
return err
}
authSources, err := db.Find[auth_model.Source](ctx, auth_model.FindSourcesOptions{})
if err != nil {
return err
}
flags := tabwriter.AlignRight
if c.Bool("vertical-bars") {
flags |= tabwriter.Debug
}
padChar := byte('\t')
if len(c.String("pad-char")) > 0 {
padChar = c.String("pad-char")[0]
}
// loop through each source and print
w := tabwriter.NewWriter(os.Stdout, c.Int("min-width"), c.Int("tab-width"), c.Int("padding"), padChar, flags)
fmt.Fprintf(w, "ID\tName\tType\tEnabled\n")
for _, source := range authSources {
fmt.Fprintf(w, "%d\t%s\t%s\t%t\n", source.ID, source.Name, source.Type.String(), source.IsActive)
}
w.Flush()
return nil
}
func runDeleteAuth(ctx context.Context, c *cli.Command) error {
if !c.IsSet("id") {
return errors.New("--id flag is missing")
}
if err := initDB(ctx); err != nil {
return err
}
source, err := auth_model.GetSourceByID(ctx, c.Int64("id"))
if err != nil {
return err
}
return auth_service.DeleteSource(ctx, source)
}
-462
View File
@@ -1,462 +0,0 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package cmd
import (
"context"
"fmt"
"strings"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/auth"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/util"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/auth/source/ldap"
"github.com/urfave/cli/v3"
)
type (
authService struct {
initDB func(ctx context.Context) error
createAuthSource func(context.Context, *auth.Source) error
updateAuthSource func(context.Context, *auth.Source) error
getAuthSourceByID func(ctx context.Context, id int64) (*auth.Source, error)
}
)
func commonLdapCLIFlags() []cli.Flag {
return []cli.Flag{
&cli.StringFlag{
Name: "name",
Usage: "Authentication name.",
},
&cli.BoolFlag{
Name: "not-active",
Usage: "Deactivate the authentication source.",
},
&cli.BoolFlag{
Name: "active",
Usage: "Activate the authentication source.",
},
&cli.StringFlag{
Name: "security-protocol",
Usage: "Security protocol name.",
},
&cli.BoolFlag{
Name: "skip-tls-verify",
Usage: "Disable TLS verification.",
},
&cli.StringFlag{
Name: "host",
Usage: "The address where the LDAP server can be reached.",
},
&cli.IntFlag{
Name: "port",
Usage: "The port to use when connecting to the LDAP server.",
},
&cli.StringFlag{
Name: "user-search-base",
Usage: "The LDAP base at which user accounts will be searched for.",
},
&cli.StringFlag{
Name: "user-filter",
Usage: "An LDAP filter declaring how to find the user record that is attempting to authenticate.",
},
&cli.StringFlag{
Name: "admin-filter",
Usage: "An LDAP filter specifying if a user should be given administrator privileges.",
},
&cli.StringFlag{
Name: "restricted-filter",
Usage: "An LDAP filter specifying if a user should be given restricted status.",
},
&cli.BoolFlag{
Name: "allow-deactivate-all",
Usage: "Allow empty search results to deactivate all users.",
},
&cli.StringFlag{
Name: "username-attribute",
Usage: "The attribute of the users LDAP record containing the user name.",
},
&cli.StringFlag{
Name: "firstname-attribute",
Usage: "The attribute of the users LDAP record containing the users first name.",
},
&cli.StringFlag{
Name: "surname-attribute",
Usage: "The attribute of the users LDAP record containing the users surname.",
},
&cli.StringFlag{
Name: "email-attribute",
Usage: "The attribute of the users LDAP record containing the users email address.",
},
&cli.StringFlag{
Name: "public-ssh-key-attribute",
Usage: "The attribute of the users LDAP record containing the users public ssh key.",
},
&cli.BoolFlag{
Name: "ssh-keys-are-verified",
Usage: "Set to true to automatically flag SSH keys in LDAP as verified.",
},
&cli.BoolFlag{
Name: "skip-local-2fa",
Usage: "Set to true to skip local 2fa for users authenticated by this source",
},
&cli.StringFlag{
Name: "avatar-attribute",
Usage: "The attribute of the users LDAP record containing the users avatar.",
},
}
}
func ldapBindDnCLIFlags() []cli.Flag {
return append(commonLdapCLIFlags(),
&cli.StringFlag{
Name: "bind-dn",
Usage: "The DN to bind to the LDAP server with when searching for the user.",
},
&cli.StringFlag{
Name: "bind-password",
Usage: "The password for the Bind DN, if any.",
},
&cli.BoolFlag{
Name: "attributes-in-bind",
Usage: "Fetch attributes in bind DN context.",
},
&cli.BoolFlag{
Name: "synchronize-users",
Usage: "Enable user synchronization.",
},
&cli.BoolFlag{
Name: "disable-synchronize-users",
Usage: "Disable user synchronization.",
},
&cli.UintFlag{
Name: "page-size",
Usage: "Search page size.",
},
&cli.BoolFlag{
Name: "enable-groups",
Usage: "Enable LDAP groups",
},
&cli.StringFlag{
Name: "group-search-base-dn",
Usage: "The LDAP base DN at which group accounts will be searched for",
},
&cli.StringFlag{
Name: "group-member-attribute",
Usage: "Group attribute containing list of users",
},
&cli.StringFlag{
Name: "group-user-attribute",
Usage: "User attribute listed in group",
},
&cli.StringFlag{
Name: "group-filter",
Usage: "Verify group membership in LDAP",
},
&cli.StringFlag{
Name: "group-team-map",
Usage: "Map LDAP groups to Organization teams",
},
&cli.BoolFlag{
Name: "group-team-map-removal",
Usage: "Remove users from synchronized teams if user does not belong to corresponding LDAP group",
})
}
func ldapSimpleAuthCLIFlags() []cli.Flag {
return append(commonLdapCLIFlags(),
&cli.StringFlag{
Name: "user-dn",
Usage: "The user's DN.",
})
}
func microcmdAuthAddLdapBindDn() *cli.Command {
return &cli.Command{
Name: "add-ldap",
Usage: "Add new LDAP (via Bind DN) authentication source",
Action: func(ctx context.Context, cmd *cli.Command) error {
return newAuthService().addLdapBindDn(ctx, cmd)
},
Flags: ldapBindDnCLIFlags(),
}
}
func microcmdAuthUpdateLdapBindDn() *cli.Command {
return &cli.Command{
Name: "update-ldap",
Usage: "Update existing LDAP (via Bind DN) authentication source",
Action: func(ctx context.Context, cmd *cli.Command) error {
return newAuthService().updateLdapBindDn(ctx, cmd)
},
Flags: append([]cli.Flag{idFlag()}, ldapBindDnCLIFlags()...),
}
}
func microcmdAuthAddLdapSimpleAuth() *cli.Command {
return &cli.Command{
Name: "add-ldap-simple",
Usage: "Add new LDAP (simple auth) authentication source",
Action: func(ctx context.Context, cmd *cli.Command) error {
return newAuthService().addLdapSimpleAuth(ctx, cmd)
},
Flags: ldapSimpleAuthCLIFlags(),
}
}
func microcmdAuthUpdateLdapSimpleAuth() *cli.Command {
return &cli.Command{
Name: "update-ldap-simple",
Usage: "Update existing LDAP (simple auth) authentication source",
Action: func(ctx context.Context, cmd *cli.Command) error {
return newAuthService().updateLdapSimpleAuth(ctx, cmd)
},
Flags: append([]cli.Flag{idFlag()}, ldapSimpleAuthCLIFlags()...),
}
}
// newAuthService creates a service with default functions.
func newAuthService() *authService {
return &authService{
initDB: initDB,
createAuthSource: auth.CreateSource,
updateAuthSource: auth.UpdateSource,
getAuthSourceByID: auth.GetSourceByID,
}
}
// parseAuthSourceLdap assigns values on authSource according to command line flags.
func parseAuthSourceLdap(c *cli.Command, authSource *auth.Source) {
if c.IsSet("name") {
authSource.Name = c.String("name")
}
if c.IsSet("not-active") {
authSource.IsActive = !c.Bool("not-active")
}
if c.IsSet("active") {
authSource.IsActive = c.Bool("active")
}
if c.IsSet("synchronize-users") {
authSource.IsSyncEnabled = c.Bool("synchronize-users")
}
if c.IsSet("disable-synchronize-users") {
authSource.IsSyncEnabled = !c.Bool("disable-synchronize-users")
}
authSource.TwoFactorPolicy = util.Iif(c.Bool("skip-local-2fa"), "skip", "")
}
// parseLdapConfig assigns values on config according to command line flags.
func parseLdapConfig(c *cli.Command, config *ldap.Source) error {
if c.IsSet("name") {
config.Name = c.String("name")
}
if c.IsSet("host") {
config.Host = c.String("host")
}
if c.IsSet("port") {
config.Port = c.Int("port")
}
if c.IsSet("security-protocol") {
p, ok := findLdapSecurityProtocolByName(c.String("security-protocol"))
if !ok {
return fmt.Errorf("unknown security protocol name: %s", c.String("security-protocol"))
}
config.SecurityProtocol = p
}
if c.IsSet("skip-tls-verify") {
config.SkipVerify = c.Bool("skip-tls-verify")
}
if c.IsSet("bind-dn") {
config.BindDN = c.String("bind-dn")
}
if c.IsSet("user-dn") {
config.UserDN = c.String("user-dn")
}
if c.IsSet("bind-password") {
config.BindPassword = c.String("bind-password")
}
if c.IsSet("user-search-base") {
config.UserBase = c.String("user-search-base")
}
if c.IsSet("username-attribute") {
config.AttributeUsername = c.String("username-attribute")
}
if c.IsSet("firstname-attribute") {
config.AttributeName = c.String("firstname-attribute")
}
if c.IsSet("surname-attribute") {
config.AttributeSurname = c.String("surname-attribute")
}
if c.IsSet("email-attribute") {
config.AttributeMail = c.String("email-attribute")
}
if c.IsSet("attributes-in-bind") {
config.AttributesInBind = c.Bool("attributes-in-bind")
}
if c.IsSet("public-ssh-key-attribute") {
config.AttributeSSHPublicKey = c.String("public-ssh-key-attribute")
}
if c.IsSet("ssh-keys-are-verified") {
config.SSHKeysAreVerified = c.Bool("ssh-keys-are-verified")
}
if c.IsSet("avatar-attribute") {
config.AttributeAvatar = c.String("avatar-attribute")
}
if c.IsSet("page-size") {
config.SearchPageSize = uint32(c.Uint("page-size"))
}
if c.IsSet("user-filter") {
config.Filter = c.String("user-filter")
}
if c.IsSet("admin-filter") {
config.AdminFilter = c.String("admin-filter")
}
if c.IsSet("restricted-filter") {
config.RestrictedFilter = c.String("restricted-filter")
}
if c.IsSet("allow-deactivate-all") {
config.AllowDeactivateAll = c.Bool("allow-deactivate-all")
}
if c.IsSet("enable-groups") {
config.GroupsEnabled = c.Bool("enable-groups")
}
if c.IsSet("group-search-base-dn") {
config.GroupDN = c.String("group-search-base-dn")
}
if c.IsSet("group-member-attribute") {
config.GroupMemberUID = c.String("group-member-attribute")
}
if c.IsSet("group-user-attribute") {
config.UserUID = c.String("group-user-attribute")
}
if c.IsSet("group-filter") {
config.GroupFilter = c.String("group-filter")
}
if c.IsSet("group-team-map") {
config.GroupTeamMap = c.String("group-team-map")
}
if c.IsSet("group-team-map-removal") {
config.GroupTeamMapRemoval = c.Bool("group-team-map-removal")
}
return nil
}
// findLdapSecurityProtocolByName finds security protocol by its name ignoring case.
// It returns the value of the security protocol and if it was found.
func findLdapSecurityProtocolByName(name string) (ldap.SecurityProtocol, bool) {
for i, n := range ldap.SecurityProtocolNames {
if strings.EqualFold(name, n) {
return i, true
}
}
return 0, false
}
// getAuthSource gets the login source by its id defined in the command line flags.
// It returns an error if the id is not set, does not match any source or if the source is not of expected type.
func (a *authService) getAuthSource(ctx context.Context, c *cli.Command, authType auth.Type) (*auth.Source, error) {
if err := argsSet(c, "id"); err != nil {
return nil, err
}
authSource, err := a.getAuthSourceByID(ctx, c.Int64("id"))
if err != nil {
return nil, err
}
if authSource.Type != authType {
return nil, fmt.Errorf("invalid authentication type. expected: %s, actual: %s", authType.String(), authSource.Type.String())
}
return authSource, nil
}
// addLdapBindDn adds a new LDAP via Bind DN authentication source.
func (a *authService) addLdapBindDn(ctx context.Context, c *cli.Command) error {
if err := argsSet(c, "name", "security-protocol", "host", "port", "user-search-base", "user-filter", "email-attribute"); err != nil {
return err
}
if err := a.initDB(ctx); err != nil {
return err
}
authSource := &auth.Source{
Type: auth.LDAP,
IsActive: true, // active by default
Cfg: &ldap.Source{
Enabled: true, // always true
},
}
parseAuthSourceLdap(c, authSource)
if err := parseLdapConfig(c, authSource.Cfg.(*ldap.Source)); err != nil {
return err
}
return a.createAuthSource(ctx, authSource)
}
// updateLdapBindDn updates a new LDAP via Bind DN authentication source.
func (a *authService) updateLdapBindDn(ctx context.Context, c *cli.Command) error {
if err := a.initDB(ctx); err != nil {
return err
}
authSource, err := a.getAuthSource(ctx, c, auth.LDAP)
if err != nil {
return err
}
parseAuthSourceLdap(c, authSource)
if err := parseLdapConfig(c, authSource.Cfg.(*ldap.Source)); err != nil {
return err
}
return a.updateAuthSource(ctx, authSource)
}
// addLdapSimpleAuth adds a new LDAP (simple auth) authentication source.
func (a *authService) addLdapSimpleAuth(ctx context.Context, c *cli.Command) error {
if err := argsSet(c, "name", "security-protocol", "host", "port", "user-dn", "user-filter", "email-attribute"); err != nil {
return err
}
if err := a.initDB(ctx); err != nil {
return err
}
authSource := &auth.Source{
Type: auth.DLDAP,
IsActive: true, // active by default
Cfg: &ldap.Source{
Enabled: true, // always true
},
}
parseAuthSourceLdap(c, authSource)
if err := parseLdapConfig(c, authSource.Cfg.(*ldap.Source)); err != nil {
return err
}
return a.createAuthSource(ctx, authSource)
}
// updateLdapSimpleAuth updates a new LDAP (simple auth) authentication source.
func (a *authService) updateLdapSimpleAuth(ctx context.Context, c *cli.Command) error {
if err := a.initDB(ctx); err != nil {
return err
}
authSource, err := a.getAuthSource(ctx, c, auth.DLDAP)
if err != nil {
return err
}
parseAuthSourceLdap(c, authSource)
if err := parseLdapConfig(c, authSource.Cfg.(*ldap.Source)); err != nil {
return err
}
return a.updateAuthSource(ctx, authSource)
}
File diff suppressed because it is too large Load Diff
-322
View File
@@ -1,322 +0,0 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package cmd
import (
"context"
"errors"
"fmt"
"net/url"
auth_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/auth"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/util"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/auth/source/oauth2"
"github.com/urfave/cli/v3"
)
func oauthCLIFlags() []cli.Flag {
return []cli.Flag{
&cli.StringFlag{
Name: "name",
Value: "",
Usage: "Application Name",
},
&cli.StringFlag{
Name: "provider",
Value: "",
Usage: "OAuth2 Provider",
},
&cli.StringFlag{
Name: "key",
Value: "",
Usage: "Client ID (Key)",
},
&cli.StringFlag{
Name: "secret",
Value: "",
Usage: "Client Secret",
},
&cli.StringFlag{
Name: "auto-discover-url",
Value: "",
Usage: "OpenID Connect Auto Discovery URL (only required when using OpenID Connect as provider)",
},
&cli.StringFlag{
Name: "use-custom-urls",
Value: "false",
Usage: "Use custom URLs for GitLab/GitHub OAuth endpoints",
},
&cli.StringFlag{
Name: "custom-tenant-id",
Value: "",
Usage: "Use custom Tenant ID for OAuth endpoints",
},
&cli.StringFlag{
Name: "custom-auth-url",
Value: "",
Usage: "Use a custom Authorization URL (option for GitLab/GitHub)",
},
&cli.StringFlag{
Name: "custom-token-url",
Value: "",
Usage: "Use a custom Token URL (option for GitLab/GitHub)",
},
&cli.StringFlag{
Name: "custom-profile-url",
Value: "",
Usage: "Use a custom Profile URL (option for GitLab/GitHub)",
},
&cli.StringFlag{
Name: "custom-email-url",
Value: "",
Usage: "Use a custom Email URL (option for GitHub)",
},
&cli.StringFlag{
Name: "icon-url",
Value: "",
Usage: "Custom icon URL for OAuth2 login source",
},
&cli.BoolFlag{
Name: "skip-local-2fa",
Usage: "Set to true to skip local 2fa for users authenticated by this source",
},
&cli.StringSliceFlag{
Name: "scopes",
Value: nil,
Usage: "Scopes to request when to authenticate against this OAuth2 source",
},
&cli.StringFlag{
Name: "ssh-public-key-claim-name",
Usage: "Claim name that provides SSH public keys",
},
&cli.StringFlag{
Name: "full-name-claim-name",
Usage: "Claim name that provides user's full name",
},
&cli.StringFlag{
Name: "required-claim-name",
Value: "",
Usage: "Claim name that has to be set to allow users to login with this source",
},
&cli.StringFlag{
Name: "required-claim-value",
Value: "",
Usage: "Claim value that has to be set to allow users to login with this source",
},
&cli.StringFlag{
Name: "group-claim-name",
Value: "",
Usage: "Claim name providing group names for this source",
},
&cli.StringFlag{
Name: "admin-group",
Value: "",
Usage: "Group Claim value for administrator users",
},
&cli.StringFlag{
Name: "restricted-group",
Value: "",
Usage: "Group Claim value for restricted users",
},
&cli.StringFlag{
Name: "group-team-map",
Value: "",
Usage: "JSON mapping between groups and org teams",
},
&cli.BoolFlag{
Name: "group-team-map-removal",
Usage: "Activate automatic team membership removal depending on groups",
},
}
}
func microcmdAuthAddOauth() *cli.Command {
return &cli.Command{
Name: "add-oauth",
Usage: "Add new Oauth authentication source",
Action: func(ctx context.Context, cmd *cli.Command) error {
return newAuthService().runAddOauth(ctx, cmd)
},
Flags: oauthCLIFlags(),
}
}
func microcmdAuthUpdateOauth() *cli.Command {
return &cli.Command{
Name: "update-oauth",
Usage: "Update existing Oauth authentication source",
Action: func(ctx context.Context, cmd *cli.Command) error {
return newAuthService().runUpdateOauth(ctx, cmd)
},
Flags: append(oauthCLIFlags()[:1], append([]cli.Flag{&cli.Int64Flag{
Name: "id",
Usage: "ID of authentication source",
}}, oauthCLIFlags()[1:]...)...),
}
}
func parseOAuth2Config(c *cli.Command) *oauth2.Source {
var customURLMapping *oauth2.CustomURLMapping
if c.IsSet("use-custom-urls") {
customURLMapping = &oauth2.CustomURLMapping{
TokenURL: c.String("custom-token-url"),
AuthURL: c.String("custom-auth-url"),
ProfileURL: c.String("custom-profile-url"),
EmailURL: c.String("custom-email-url"),
Tenant: c.String("custom-tenant-id"),
}
} else {
customURLMapping = nil
}
return &oauth2.Source{
Provider: c.String("provider"),
ClientID: c.String("key"),
ClientSecret: c.String("secret"),
OpenIDConnectAutoDiscoveryURL: c.String("auto-discover-url"),
CustomURLMapping: customURLMapping,
IconURL: c.String("icon-url"),
Scopes: c.StringSlice("scopes"),
RequiredClaimName: c.String("required-claim-name"),
RequiredClaimValue: c.String("required-claim-value"),
GroupClaimName: c.String("group-claim-name"),
AdminGroup: c.String("admin-group"),
RestrictedGroup: c.String("restricted-group"),
GroupTeamMap: c.String("group-team-map"),
GroupTeamMapRemoval: c.Bool("group-team-map-removal"),
SSHPublicKeyClaimName: c.String("ssh-public-key-claim-name"),
FullNameClaimName: c.String("full-name-claim-name"),
}
}
func (a *authService) runAddOauth(ctx context.Context, c *cli.Command) error {
if err := a.initDB(ctx); err != nil {
return err
}
config := parseOAuth2Config(c)
if config.Provider == "openidConnect" {
discoveryURL, err := url.Parse(config.OpenIDConnectAutoDiscoveryURL)
if err != nil || (discoveryURL.Scheme != "http" && discoveryURL.Scheme != "https") {
return fmt.Errorf("invalid Auto Discovery URL: %s (this must be a valid URL starting with http:// or https://)", config.OpenIDConnectAutoDiscoveryURL)
}
}
return a.createAuthSource(ctx, &auth_model.Source{
Type: auth_model.OAuth2,
Name: c.String("name"),
IsActive: true,
Cfg: config,
TwoFactorPolicy: util.Iif(c.Bool("skip-local-2fa"), "skip", ""),
})
}
func (a *authService) runUpdateOauth(ctx context.Context, c *cli.Command) error {
if !c.IsSet("id") {
return errors.New("--id flag is missing")
}
if err := a.initDB(ctx); err != nil {
return err
}
source, err := a.getAuthSourceByID(ctx, c.Int64("id"))
if err != nil {
return err
}
oAuth2Config := source.Cfg.(*oauth2.Source)
if c.IsSet("name") {
source.Name = c.String("name")
}
if c.IsSet("provider") {
oAuth2Config.Provider = c.String("provider")
}
if c.IsSet("key") {
oAuth2Config.ClientID = c.String("key")
}
if c.IsSet("secret") {
oAuth2Config.ClientSecret = c.String("secret")
}
if c.IsSet("auto-discover-url") {
oAuth2Config.OpenIDConnectAutoDiscoveryURL = c.String("auto-discover-url")
}
if c.IsSet("icon-url") {
oAuth2Config.IconURL = c.String("icon-url")
}
if c.IsSet("scopes") {
oAuth2Config.Scopes = c.StringSlice("scopes")
}
if c.IsSet("required-claim-name") {
oAuth2Config.RequiredClaimName = c.String("required-claim-name")
}
if c.IsSet("required-claim-value") {
oAuth2Config.RequiredClaimValue = c.String("required-claim-value")
}
if c.IsSet("group-claim-name") {
oAuth2Config.GroupClaimName = c.String("group-claim-name")
}
if c.IsSet("admin-group") {
oAuth2Config.AdminGroup = c.String("admin-group")
}
if c.IsSet("restricted-group") {
oAuth2Config.RestrictedGroup = c.String("restricted-group")
}
if c.IsSet("group-team-map") {
oAuth2Config.GroupTeamMap = c.String("group-team-map")
}
if c.IsSet("group-team-map-removal") {
oAuth2Config.GroupTeamMapRemoval = c.Bool("group-team-map-removal")
}
if c.IsSet("ssh-public-key-claim-name") {
oAuth2Config.SSHPublicKeyClaimName = c.String("ssh-public-key-claim-name")
}
if c.IsSet("full-name-claim-name") {
oAuth2Config.FullNameClaimName = c.String("full-name-claim-name")
}
// update custom URL mapping
customURLMapping := &oauth2.CustomURLMapping{}
if oAuth2Config.CustomURLMapping != nil {
customURLMapping.TokenURL = oAuth2Config.CustomURLMapping.TokenURL
customURLMapping.AuthURL = oAuth2Config.CustomURLMapping.AuthURL
customURLMapping.ProfileURL = oAuth2Config.CustomURLMapping.ProfileURL
customURLMapping.EmailURL = oAuth2Config.CustomURLMapping.EmailURL
customURLMapping.Tenant = oAuth2Config.CustomURLMapping.Tenant
}
if c.IsSet("use-custom-urls") && c.IsSet("custom-token-url") {
customURLMapping.TokenURL = c.String("custom-token-url")
}
if c.IsSet("use-custom-urls") && c.IsSet("custom-auth-url") {
customURLMapping.AuthURL = c.String("custom-auth-url")
}
if c.IsSet("use-custom-urls") && c.IsSet("custom-profile-url") {
customURLMapping.ProfileURL = c.String("custom-profile-url")
}
if c.IsSet("use-custom-urls") && c.IsSet("custom-email-url") {
customURLMapping.EmailURL = c.String("custom-email-url")
}
if c.IsSet("use-custom-urls") && c.IsSet("custom-tenant-id") {
customURLMapping.Tenant = c.String("custom-tenant-id")
}
oAuth2Config.CustomURLMapping = customURLMapping
source.Cfg = oAuth2Config
source.TwoFactorPolicy = util.Iif(c.Bool("skip-local-2fa"), "skip", "")
return a.updateAuthSource(ctx, source)
}
-343
View File
@@ -1,343 +0,0 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package cmd
import (
"context"
"testing"
auth_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/auth"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/auth/source/oauth2"
"github.com/stretchr/testify/assert"
"github.com/urfave/cli/v3"
)
func TestAddOauth(t *testing.T) {
testCases := []struct {
name string
args []string
source *auth_model.Source
errMsg string
}{
{
name: "valid config",
args: []string{
"--name", "test",
"--provider", "github",
"--key", "some_key",
"--secret", "some_secret",
},
source: &auth_model.Source{
Type: auth_model.OAuth2,
Name: "test",
IsActive: true,
Cfg: &oauth2.Source{
Scopes: []string{},
Provider: "github",
ClientID: "some_key",
ClientSecret: "some_secret",
},
TwoFactorPolicy: "",
},
},
{
name: "valid config with openid connect",
args: []string{
"--name", "test",
"--provider", "openidConnect",
"--key", "some_key",
"--secret", "some_secret",
"--auto-discover-url", "https://example.com",
},
source: &auth_model.Source{
Type: auth_model.OAuth2,
Name: "test",
IsActive: true,
Cfg: &oauth2.Source{
Scopes: []string{},
Provider: "openidConnect",
ClientID: "some_key",
ClientSecret: "some_secret",
OpenIDConnectAutoDiscoveryURL: "https://example.com",
},
TwoFactorPolicy: "",
},
},
{
name: "valid config with options",
args: []string{
"--name", "test",
"--provider", "gitlab",
"--key", "some_key",
"--secret", "some_secret",
"--use-custom-urls", "true",
"--custom-token-url", "https://example.com/token",
"--custom-auth-url", "https://example.com/auth",
"--custom-profile-url", "https://example.com/profile",
"--custom-email-url", "https://example.com/email",
"--custom-tenant-id", "some_tenant",
"--icon-url", "https://example.com/icon",
"--scopes", "scope1,scope2",
"--skip-local-2fa", "true",
"--required-claim-name", "claim_name",
"--required-claim-value", "claim_value",
"--group-claim-name", "group_name",
"--admin-group", "admin",
"--restricted-group", "restricted",
"--group-team-map", `{"group1": [1,2]}`,
"--group-team-map-removal=true",
"--ssh-public-key-claim-name", "attr_ssh_pub_key",
"--full-name-claim-name", "attr_full_name",
},
source: &auth_model.Source{
Type: auth_model.OAuth2,
Name: "test",
IsActive: true,
Cfg: &oauth2.Source{
Provider: "gitlab",
ClientID: "some_key",
ClientSecret: "some_secret",
CustomURLMapping: &oauth2.CustomURLMapping{
TokenURL: "https://example.com/token",
AuthURL: "https://example.com/auth",
ProfileURL: "https://example.com/profile",
EmailURL: "https://example.com/email",
Tenant: "some_tenant",
},
IconURL: "https://example.com/icon",
Scopes: []string{"scope1", "scope2"},
RequiredClaimName: "claim_name",
RequiredClaimValue: "claim_value",
GroupClaimName: "group_name",
AdminGroup: "admin",
RestrictedGroup: "restricted",
GroupTeamMap: `{"group1": [1,2]}`,
GroupTeamMapRemoval: true,
SSHPublicKeyClaimName: "attr_ssh_pub_key",
FullNameClaimName: "attr_full_name",
},
TwoFactorPolicy: "skip",
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
var createdSource *auth_model.Source
a := &authService{
initDB: func(ctx context.Context) error {
return nil
},
createAuthSource: func(ctx context.Context, source *auth_model.Source) error {
createdSource = source
return nil
},
}
app := &cli.Command{
Flags: microcmdAuthAddOauth().Flags,
Action: a.runAddOauth,
}
args := []string{"oauth-test"}
args = append(args, tc.args...)
err := app.Run(t.Context(), args)
if tc.errMsg != "" {
assert.EqualError(t, err, tc.errMsg)
} else {
assert.NoError(t, err)
assert.Equal(t, tc.source, createdSource)
}
})
}
}
func TestUpdateOauth(t *testing.T) {
testCases := []struct {
name string
args []string
id int64
existingAuthSource *auth_model.Source
authSource *auth_model.Source
errMsg string
}{
{
name: "missing id",
args: []string{
"--name", "test",
},
errMsg: "--id flag is missing",
},
{
name: "valid config",
id: 1,
existingAuthSource: &auth_model.Source{
ID: 1,
Type: auth_model.OAuth2,
Name: "old name",
IsActive: true,
Cfg: &oauth2.Source{
Provider: "github",
ClientID: "old_key",
ClientSecret: "old_secret",
},
TwoFactorPolicy: "",
},
args: []string{
"--id", "1",
"--name", "test",
"--provider", "gitlab",
"--key", "new_key",
"--secret", "new_secret",
},
authSource: &auth_model.Source{
ID: 1,
Type: auth_model.OAuth2,
Name: "test",
IsActive: true,
Cfg: &oauth2.Source{
Provider: "gitlab",
ClientID: "new_key",
ClientSecret: "new_secret",
CustomURLMapping: &oauth2.CustomURLMapping{},
},
TwoFactorPolicy: "",
},
},
{
name: "valid config with options",
id: 1,
existingAuthSource: &auth_model.Source{
ID: 1,
Type: auth_model.OAuth2,
Name: "old name",
IsActive: true,
Cfg: &oauth2.Source{
Provider: "gitlab",
ClientID: "old_key",
ClientSecret: "old_secret",
CustomURLMapping: &oauth2.CustomURLMapping{
TokenURL: "https://old.example.com/token",
AuthURL: "https://old.example.com/auth",
ProfileURL: "https://old.example.com/profile",
EmailURL: "https://old.example.com/email",
Tenant: "old_tenant",
},
IconURL: "https://old.example.com/icon",
Scopes: []string{"old_scope1", "old_scope2"},
RequiredClaimName: "old_claim_name",
RequiredClaimValue: "old_claim_value",
GroupClaimName: "old_group_name",
AdminGroup: "old_admin",
RestrictedGroup: "old_restricted",
GroupTeamMap: `{"old_group1": [1,2]}`,
GroupTeamMapRemoval: true,
SSHPublicKeyClaimName: "old_ssh_pub_key",
FullNameClaimName: "old_full_name",
},
TwoFactorPolicy: "",
},
args: []string{
"--id", "1",
"--name", "test",
"--provider", "github",
"--key", "new_key",
"--secret", "new_secret",
"--use-custom-urls", "true",
"--custom-token-url", "https://example.com/token",
"--custom-auth-url", "https://example.com/auth",
"--custom-profile-url", "https://example.com/profile",
"--custom-email-url", "https://example.com/email",
"--custom-tenant-id", "new_tenant",
"--icon-url", "https://example.com/icon",
"--scopes", "scope1,scope2",
"--skip-local-2fa=true",
"--required-claim-name", "claim_name",
"--required-claim-value", "claim_value",
"--group-claim-name", "group_name",
"--admin-group", "admin",
"--restricted-group", "restricted",
"--group-team-map", `{"group1": [1,2]}`,
"--group-team-map-removal=false",
"--ssh-public-key-claim-name", "new_ssh_pub_key",
"--full-name-claim-name", "new_full_name",
},
authSource: &auth_model.Source{
ID: 1,
Type: auth_model.OAuth2,
Name: "test",
IsActive: true,
Cfg: &oauth2.Source{
Provider: "github",
ClientID: "new_key",
ClientSecret: "new_secret",
CustomURLMapping: &oauth2.CustomURLMapping{
TokenURL: "https://example.com/token",
AuthURL: "https://example.com/auth",
ProfileURL: "https://example.com/profile",
EmailURL: "https://example.com/email",
Tenant: "new_tenant",
},
IconURL: "https://example.com/icon",
Scopes: []string{"scope1", "scope2"},
RequiredClaimName: "claim_name",
RequiredClaimValue: "claim_value",
GroupClaimName: "group_name",
AdminGroup: "admin",
RestrictedGroup: "restricted",
GroupTeamMap: `{"group1": [1,2]}`,
GroupTeamMapRemoval: false,
SSHPublicKeyClaimName: "new_ssh_pub_key",
FullNameClaimName: "new_full_name",
},
TwoFactorPolicy: "skip",
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
a := &authService{
initDB: func(ctx context.Context) error {
return nil
},
getAuthSourceByID: func(ctx context.Context, id int64) (*auth_model.Source, error) {
return &auth_model.Source{
ID: 1,
Type: auth_model.OAuth2,
Name: "test",
IsActive: true,
Cfg: &oauth2.Source{
CustomURLMapping: &oauth2.CustomURLMapping{},
},
TwoFactorPolicy: "skip",
}, nil
},
updateAuthSource: func(ctx context.Context, source *auth_model.Source) error {
assert.Equal(t, tc.authSource, source)
return nil
},
}
app := &cli.Command{
Flags: microcmdAuthUpdateOauth().Flags,
Action: a.runUpdateOauth,
}
args := []string{"oauth-test"}
args = append(args, tc.args...)
err := app.Run(t.Context(), args)
if tc.errMsg != "" {
assert.EqualError(t, err, tc.errMsg)
} else {
assert.NoError(t, err)
}
})
}
}
-200
View File
@@ -1,200 +0,0 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package cmd
import (
"context"
"errors"
"strings"
auth_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/auth"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/util"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/auth/source/smtp"
"github.com/urfave/cli/v3"
)
func smtpCLIFlags() []cli.Flag {
return []cli.Flag{
&cli.StringFlag{
Name: "name",
Value: "",
Usage: "Application Name",
},
&cli.StringFlag{
Name: "auth-type",
Value: "PLAIN",
Usage: "SMTP Authentication Type (PLAIN/LOGIN/CRAM-MD5) default PLAIN",
},
&cli.StringFlag{
Name: "host",
Value: "",
Usage: "SMTP Host",
},
&cli.IntFlag{
Name: "port",
Usage: "SMTP Port",
},
&cli.BoolFlag{
Name: "force-smtps",
Usage: "SMTPS is always used on port 465. Set this to force SMTPS on other ports.",
},
&cli.BoolFlag{
Name: "skip-verify",
Usage: "Skip TLS verify.",
},
&cli.StringFlag{
Name: "helo-hostname",
Value: "",
Usage: "Hostname sent with HELO. Leave blank to send current hostname",
},
&cli.BoolFlag{
Name: "disable-helo",
Usage: "Disable SMTP helo.",
},
&cli.StringFlag{
Name: "allowed-domains",
Value: "",
Usage: "Leave empty to allow all domains. Separate multiple domains with a comma (',')",
},
&cli.BoolFlag{
Name: "skip-local-2fa",
Usage: "Skip 2FA to log on.",
},
&cli.BoolFlag{
Name: "active",
Usage: "This Authentication Source is Activated.",
Value: true,
},
}
}
func microcmdAuthUpdateSMTP() *cli.Command {
return &cli.Command{
Name: "update-smtp",
Usage: "Update existing SMTP authentication source",
Action: func(ctx context.Context, cmd *cli.Command) error {
return newAuthService().runUpdateSMTP(ctx, cmd)
},
Flags: append(smtpCLIFlags()[:1], append([]cli.Flag{&cli.Int64Flag{
Name: "id",
Usage: "ID of authentication source",
}}, smtpCLIFlags()[1:]...)...),
}
}
func microcmdAuthAddSMTP() *cli.Command {
return &cli.Command{
Name: "add-smtp",
Usage: "Add new SMTP authentication source",
Action: func(ctx context.Context, cmd *cli.Command) error {
return newAuthService().runAddSMTP(ctx, cmd)
},
Flags: smtpCLIFlags(),
}
}
func parseSMTPConfig(c *cli.Command, conf *smtp.Source) error {
if c.IsSet("auth-type") {
conf.Auth = c.String("auth-type")
validAuthTypes := []string{"PLAIN", "LOGIN", "CRAM-MD5"}
if !util.SliceContainsString(validAuthTypes, strings.ToUpper(c.String("auth-type"))) {
return errors.New("Auth must be one of PLAIN/LOGIN/CRAM-MD5")
}
conf.Auth = c.String("auth-type")
}
if c.IsSet("host") {
conf.Host = c.String("host")
}
if c.IsSet("port") {
conf.Port = c.Int("port")
}
if c.IsSet("allowed-domains") {
conf.AllowedDomains = c.String("allowed-domains")
}
if c.IsSet("force-smtps") {
conf.ForceSMTPS = c.Bool("force-smtps")
}
if c.IsSet("skip-verify") {
conf.SkipVerify = c.Bool("skip-verify")
}
if c.IsSet("helo-hostname") {
conf.HeloHostname = c.String("helo-hostname")
}
if c.IsSet("disable-helo") {
conf.DisableHelo = c.Bool("disable-helo")
}
return nil
}
func (a *authService) runAddSMTP(ctx context.Context, c *cli.Command) error {
if err := a.initDB(ctx); err != nil {
return err
}
if !c.IsSet("name") || len(c.String("name")) == 0 {
return errors.New("name must be set")
}
if !c.IsSet("host") || len(c.String("host")) == 0 {
return errors.New("host must be set")
}
if !c.IsSet("port") {
return errors.New("port must be set")
}
active := true
if c.IsSet("active") {
active = c.Bool("active")
}
var smtpConfig smtp.Source
if err := parseSMTPConfig(c, &smtpConfig); err != nil {
return err
}
// If not set default to PLAIN
if len(smtpConfig.Auth) == 0 {
smtpConfig.Auth = "PLAIN"
}
return a.createAuthSource(ctx, &auth_model.Source{
Type: auth_model.SMTP,
Name: c.String("name"),
IsActive: active,
Cfg: &smtpConfig,
TwoFactorPolicy: util.Iif(c.Bool("skip-local-2fa"), "skip", ""),
})
}
func (a *authService) runUpdateSMTP(ctx context.Context, c *cli.Command) error {
if !c.IsSet("id") {
return errors.New("--id flag is missing")
}
if err := a.initDB(ctx); err != nil {
return err
}
source, err := a.getAuthSourceByID(ctx, c.Int64("id"))
if err != nil {
return err
}
smtpConfig := source.Cfg.(*smtp.Source)
if err := parseSMTPConfig(c, smtpConfig); err != nil {
return err
}
if c.IsSet("name") {
source.Name = c.String("name")
}
if c.IsSet("active") {
source.IsActive = c.Bool("active")
}
source.Cfg = smtpConfig
source.TwoFactorPolicy = util.Iif(c.Bool("skip-local-2fa"), "skip", "")
return a.updateAuthSource(ctx, source)
}
-271
View File
@@ -1,271 +0,0 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package cmd
import (
"context"
"testing"
auth_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/auth"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/auth/source/smtp"
"github.com/stretchr/testify/assert"
"github.com/urfave/cli/v3"
)
func TestAddSMTP(t *testing.T) {
testCases := []struct {
name string
args []string
source *auth_model.Source
errMsg string
}{
{
name: "missing name",
args: []string{
"--host", "localhost",
"--port", "25",
},
errMsg: "name must be set",
},
{
name: "missing host",
args: []string{
"--name", "test",
"--port", "25",
},
errMsg: "host must be set",
},
{
name: "missing port",
args: []string{
"--name", "test",
"--host", "localhost",
},
errMsg: "port must be set",
},
{
name: "valid config",
args: []string{
"--name", "test",
"--host", "localhost",
"--port", "25",
},
source: &auth_model.Source{
Type: auth_model.SMTP,
Name: "test",
IsActive: true,
Cfg: &smtp.Source{
Auth: "PLAIN",
Host: "localhost",
Port: 25,
},
TwoFactorPolicy: "",
},
},
{
name: "valid config with options",
args: []string{
"--name", "test",
"--host", "localhost",
"--port", "25",
"--auth-type", "LOGIN",
"--force-smtps",
"--skip-verify",
"--helo-hostname", "example.com",
"--disable-helo=true",
"--allowed-domains", "example.com,example.org",
"--skip-local-2fa",
"--active=false",
},
source: &auth_model.Source{
Type: auth_model.SMTP,
Name: "test",
IsActive: false,
Cfg: &smtp.Source{
Auth: "LOGIN",
Host: "localhost",
Port: 25,
ForceSMTPS: true,
SkipVerify: true,
HeloHostname: "example.com",
DisableHelo: true,
AllowedDomains: "example.com,example.org",
},
TwoFactorPolicy: "skip",
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
a := &authService{
initDB: func(ctx context.Context) error {
return nil
},
createAuthSource: func(ctx context.Context, source *auth_model.Source) error {
assert.Equal(t, tc.source, source)
return nil
},
}
cmd := &cli.Command{
Flags: microcmdAuthAddSMTP().Flags,
Action: a.runAddSMTP,
}
args := []string{"smtp-test"}
args = append(args, tc.args...)
t.Log(args)
err := cmd.Run(t.Context(), args)
if tc.errMsg != "" {
assert.EqualError(t, err, tc.errMsg)
} else {
assert.NoError(t, err)
}
})
}
}
func TestUpdateSMTP(t *testing.T) {
testCases := []struct {
name string
args []string
existingAuthSource *auth_model.Source
authSource *auth_model.Source
errMsg string
}{
{
name: "missing id",
args: []string{
"--name", "test",
"--host", "localhost",
"--port", "25",
},
errMsg: "--id flag is missing",
},
{
name: "valid config",
existingAuthSource: &auth_model.Source{
ID: 1,
Type: auth_model.SMTP,
Name: "old name",
IsActive: true,
Cfg: &smtp.Source{
Auth: "PLAIN",
Host: "old host",
Port: 26,
},
},
args: []string{
"--id", "1",
"--name", "test",
"--host", "localhost",
"--port", "25",
},
authSource: &auth_model.Source{
ID: 1,
Type: auth_model.SMTP,
Name: "test",
IsActive: true,
Cfg: &smtp.Source{
Auth: "PLAIN",
Host: "localhost",
Port: 25,
},
},
},
{
name: "valid config with options",
existingAuthSource: &auth_model.Source{
ID: 1,
Type: auth_model.SMTP,
Name: "old name",
IsActive: true,
Cfg: &smtp.Source{
Auth: "PLAIN",
Host: "old host",
Port: 26,
HeloHostname: "old.example.com",
AllowedDomains: "old.example.com",
},
TwoFactorPolicy: "",
},
args: []string{
"--id", "1",
"--name", "test",
"--host", "localhost",
"--port", "25",
"--auth-type", "LOGIN",
"--force-smtps",
"--skip-verify",
"--helo-hostname", "example.com",
"--disable-helo",
"--allowed-domains", "example.com,example.org",
"--skip-local-2fa",
"--active=false",
},
authSource: &auth_model.Source{
ID: 1,
Type: auth_model.SMTP,
Name: "test",
IsActive: false,
Cfg: &smtp.Source{
Auth: "LOGIN",
Host: "localhost",
Port: 25,
ForceSMTPS: true,
SkipVerify: true,
HeloHostname: "example.com",
DisableHelo: true,
AllowedDomains: "example.com,example.org",
},
TwoFactorPolicy: "skip",
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
a := &authService{
initDB: func(ctx context.Context) error {
return nil
},
getAuthSourceByID: func(ctx context.Context, id int64) (*auth_model.Source, error) {
return &auth_model.Source{
ID: 1,
Type: auth_model.SMTP,
Name: "test",
IsActive: true,
Cfg: &smtp.Source{
Auth: "PLAIN",
},
}, nil
},
updateAuthSource: func(ctx context.Context, source *auth_model.Source) error {
assert.Equal(t, tc.authSource, source)
return nil
},
}
app := &cli.Command{
Flags: microcmdAuthUpdateSMTP().Flags,
Action: a.runUpdateSMTP,
}
args := []string{"smtp-tests"}
args = append(args, tc.args...)
err := app.Run(t.Context(), args)
if tc.errMsg != "" {
assert.EqualError(t, err, tc.errMsg)
} else {
assert.NoError(t, err)
}
})
}
}
-44
View File
@@ -1,44 +0,0 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package cmd
import (
"context"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/graceful"
asymkey_service "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/asymkey"
repo_service "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/repository"
"github.com/urfave/cli/v3"
)
func newRegenerateHooksCommand() *cli.Command {
return &cli.Command{
Name: "hooks",
Usage: "Regenerate git-hooks",
Action: runRegenerateHooks,
}
}
func newRegenerateKeysCommand() *cli.Command {
return &cli.Command{
Name: "keys",
Usage: "Regenerate authorized_keys file",
Action: runRegenerateKeys,
}
}
func runRegenerateHooks(ctx context.Context, _ *cli.Command) error {
if err := initDB(ctx); err != nil {
return err
}
return repo_service.SyncRepositoryHooks(graceful.GetManager().ShutdownContext())
}
func runRegenerateKeys(ctx context.Context, _ *cli.Command) error {
if err := initDB(ctx); err != nil {
return err
}
return asymkey_service.RewriteAllPublicKeys(ctx)
}
-23
View File
@@ -1,23 +0,0 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package cmd
import (
"github.com/urfave/cli/v3"
)
func newUserCommand() *cli.Command {
return &cli.Command{
Name: "user",
Usage: "Modify users",
Commands: []*cli.Command{
microcmdUserCreate(),
newUserListCommand(),
microcmdUserChangePassword(),
microcmdUserDelete(),
newUserGenerateAccessTokenCommand(),
microcmdUserMustChangePassword(),
},
}
}
-78
View File
@@ -1,78 +0,0 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package cmd
import (
"context"
"errors"
"fmt"
user_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/user"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/auth/password"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/optional"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
user_service "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/user"
"github.com/urfave/cli/v3"
)
func microcmdUserChangePassword() *cli.Command {
return &cli.Command{
Name: "change-password",
Usage: "Change a user's password",
Action: runChangePassword,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "username",
Aliases: []string{"u"},
Usage: "The user to change password for",
Required: true,
},
&cli.StringFlag{
Name: "password",
Aliases: []string{"p"},
Usage: "New password to set for user",
Required: true,
},
&cli.BoolFlag{
Name: "must-change-password",
Usage: "User must change password (can be disabled by --must-change-password=false)",
Value: true,
},
},
}
}
func runChangePassword(ctx context.Context, c *cli.Command) error {
if !setting.IsInTesting {
if err := initDB(ctx); err != nil {
return err
}
}
user, err := user_model.GetUserByName(ctx, c.String("username"))
if err != nil {
return err
}
opts := &user_service.UpdateAuthOptions{
Password: optional.Some(c.String("password")),
MustChangePassword: optional.Some(c.Bool("must-change-password")),
}
if err := user_service.UpdateAuth(ctx, user, opts); err != nil {
switch {
case errors.Is(err, password.ErrMinLength):
return fmt.Errorf("password is not long enough, needs to be at least %d characters", setting.MinPasswordLength)
case errors.Is(err, password.ErrComplexity):
return errors.New("password does not meet complexity requirements")
case errors.Is(err, password.ErrIsPwned):
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")
default:
return err
}
}
fmt.Printf("%s's password has been successfully updated!\n", user.Name)
return nil
}
-94
View File
@@ -1,94 +0,0 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package cmd
import (
"io"
"testing"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/unittest"
user_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/user"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestChangePasswordCommand(t *testing.T) {
ctx := t.Context()
defer func() {
require.NoError(t, db.TruncateBeans(t.Context(), &user_model.User{}))
}()
t.Run("change password successfully", func(t *testing.T) {
// defer func() {
// require.NoError(t, db.TruncateBeans(t.Context(), &user_model.User{}))
// }()
// Prepare test user
unittest.AssertNotExistsBean(t, &user_model.User{LowerName: "testuser"})
err := microcmdUserCreate().Run(ctx, []string{"create", "--username", "testuser", "--email", "testuser@gitea.local", "--random-password"})
require.NoError(t, err)
// load test user
userBase := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
// Change the password
err = microcmdUserChangePassword().Run(ctx, []string{"change-password", "--username", "testuser", "--password", "newpassword"})
require.NoError(t, err)
// Verify the password has been changed
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
assert.NotEqual(t, userBase.Passwd, user.Passwd)
assert.NotEqual(t, userBase.Salt, user.Salt)
// Additional check for must-change-password flag
require.NoError(t, microcmdUserChangePassword().Run(ctx, []string{"change-password", "--username", "testuser", "--password", "anotherpassword", "--must-change-password=false"}))
user = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
assert.False(t, user.MustChangePassword)
require.NoError(t, microcmdUserChangePassword().Run(ctx, []string{"change-password", "--username", "testuser", "--password", "yetanotherpassword", "--must-change-password"}))
user = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
assert.True(t, user.MustChangePassword)
})
t.Run("failure cases", func(t *testing.T) {
testCases := []struct {
name string
args []string
expectedErr string
}{
{
name: "user does not exist",
args: []string{"change-password", "--username", "nonexistentuser", "--password", "newpassword"},
expectedErr: "user does not exist",
},
{
name: "missing username",
args: []string{"change-password", "--password", "newpassword"},
expectedErr: `"username" not set`,
},
{
name: "missing password",
args: []string{"change-password", "--username", "testuser"},
expectedErr: `"password" not set`,
},
{
name: "too short password",
args: []string{"change-password", "--username", "testuser", "--password", "1"},
expectedErr: "password is not long enough",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
cmd := microcmdUserChangePassword()
cmd.Writer, cmd.ErrWriter = io.Discard, io.Discard
err := cmd.Run(ctx, tc.args)
require.Error(t, err)
require.Contains(t, err.Error(), tc.expectedErr)
})
}
})
}
-241
View File
@@ -1,241 +0,0 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package cmd
import (
"context"
"errors"
"fmt"
"strings"
auth_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/auth"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
user_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/user"
pwd "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/auth/password"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/optional"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
"github.com/urfave/cli/v3"
)
func microcmdUserCreate() *cli.Command {
return &cli.Command{
Name: "create",
Usage: "Create a new user in database",
Action: runCreateUser,
MutuallyExclusiveFlags: []cli.MutuallyExclusiveFlags{
{
Flags: [][]cli.Flag{
{
&cli.StringFlag{
Name: "name",
Usage: "Username. DEPRECATED: use username instead",
},
&cli.StringFlag{
Name: "username",
Usage: "Username",
},
},
},
Required: true,
},
},
Flags: []cli.Flag{
&cli.StringFlag{
Name: "user-type",
Usage: "Set user's type: individual or bot",
Value: "individual",
},
&cli.StringFlag{
Name: "password",
Usage: "User password",
},
&cli.StringFlag{
Name: "email",
Usage: "User email address",
Required: true,
},
&cli.BoolFlag{
Name: "admin",
Usage: "User is an admin",
},
&cli.BoolFlag{
Name: "random-password",
Usage: "Generate a random password for the user",
},
&cli.BoolFlag{
Name: "must-change-password",
Usage: "User must change password after initial login, defaults to true for all users except the first one (can be disabled by --must-change-password=false)",
HideDefault: true,
},
&cli.IntFlag{
Name: "random-password-length",
Usage: "Length of the random password to be generated",
Value: 12,
},
&cli.BoolFlag{
Name: "access-token",
Usage: "Generate access token for the user",
},
&cli.StringFlag{
Name: "access-token-name",
Usage: `Name of the generated access token`,
Value: "gitea-admin",
},
&cli.StringFlag{
Name: "access-token-scopes",
Usage: `Scopes of the generated access token, comma separated. Examples: "all", "public-only,read:issue", "write:repository,write:user"`,
Value: "all",
},
&cli.BoolFlag{
Name: "restricted",
Usage: "Make a restricted user account",
},
&cli.StringFlag{
Name: "fullname",
Usage: `The full, human-readable name of the user`,
},
},
}
}
func runCreateUser(ctx context.Context, c *cli.Command) error {
// this command highly depends on the many setting options (create org, visibility, etc.), so it must have a full setting load first
// duplicate setting loading should be safe at the moment, but it should be refactored & improved in the future.
setting.LoadSettings()
userTypes := map[string]user_model.UserType{
"individual": user_model.UserTypeIndividual,
"bot": user_model.UserTypeBot,
}
userType, ok := userTypes[c.String("user-type")]
if !ok {
return fmt.Errorf("invalid user type: %s", c.String("user-type"))
}
if userType != user_model.UserTypeIndividual {
// Some other commands like "change-password" also only support individual users.
// It needs to clarify the "password" behavior for bot users in the future.
// At the moment, we do not allow setting password for bot users.
if c.IsSet("password") || c.IsSet("random-password") {
return errors.New("password can only be set for individual users")
}
}
if c.IsSet("password") && c.IsSet("random-password") {
return errors.New("cannot set both -random-password and -password flags")
}
var username string
if c.IsSet("username") {
username = c.String("username")
} else {
username = c.String("name")
_, _ = fmt.Fprintf(c.ErrWriter, "--name flag is deprecated. Use --username instead.\n")
}
if !setting.IsInTesting {
// FIXME: need to refactor the "initDB" related code later
// it doesn't make sense to call it in (almost) every command action function
if err := initDB(ctx); err != nil {
return err
}
}
var password string
if c.IsSet("password") {
password = c.String("password")
} else if c.IsSet("random-password") {
var err error
password, err = pwd.Generate(c.Int("random-password-length"))
if err != nil {
return err
}
// codeql[disable-next-line=go/clear-text-logging]
fmt.Printf("generated random password is '%s'\n", password)
} else if userType == user_model.UserTypeIndividual {
return errors.New("must set either password or random-password flag")
}
isAdmin := c.Bool("admin")
mustChangePassword := true // always default to true
if c.IsSet("must-change-password") {
if userType != user_model.UserTypeIndividual {
return errors.New("must-change-password flag can only be set for individual users")
}
// if the flag is set, use the value provided by the user
mustChangePassword = c.Bool("must-change-password")
} else if userType == user_model.UserTypeIndividual {
// check whether there are users in the database
hasUserRecord, err := db.IsTableNotEmpty(&user_model.User{})
if err != nil {
return fmt.Errorf("IsTableNotEmpty: %w", err)
}
if !hasUserRecord {
// if this is the first one being created, don't force to change password (keep the old behavior)
mustChangePassword = false
}
}
restricted := optional.None[bool]()
if c.IsSet("restricted") {
restricted = optional.Some(c.Bool("restricted"))
}
// default user visibility in app.ini
visibility := setting.Service.DefaultUserVisibilityMode
u := &user_model.User{
Name: username,
Email: c.String("email"),
IsAdmin: isAdmin,
Type: userType,
Passwd: password,
MustChangePassword: mustChangePassword,
Visibility: visibility,
FullName: c.String("fullname"),
}
overwriteDefault := &user_model.CreateUserOverwriteOptions{
IsActive: optional.Some(true),
IsRestricted: restricted,
}
var accessTokenName string
var accessTokenScope auth_model.AccessTokenScope
if c.IsSet("access-token") {
accessTokenName = strings.TrimSpace(c.String("access-token-name"))
if accessTokenName == "" {
return errors.New("access-token-name cannot be empty")
}
var err error
accessTokenScope, err = auth_model.AccessTokenScope(c.String("access-token-scopes")).Normalize()
if err != nil {
return fmt.Errorf("invalid access token scope provided: %w", err)
}
if !accessTokenScope.HasPermissionScope() {
return errors.New("access token does not have any permission")
}
} else if c.IsSet("access-token-name") || c.IsSet("access-token-scopes") {
return errors.New("access-token-name and access-token-scopes flags are only valid when access-token flag is set")
}
// arguments should be prepared before creating the user & access token, in case there is anything wrong
// create the user
if err := user_model.CreateUser(ctx, u, &user_model.Meta{}, overwriteDefault); err != nil {
return fmt.Errorf("CreateUser: %w", err)
}
fmt.Printf("New user '%s' has been successfully created!\n", username)
// create the access token
if accessTokenScope != "" {
t := &auth_model.AccessToken{Name: accessTokenName, UID: u.ID, Scope: accessTokenScope}
if err := auth_model.NewAccessToken(ctx, t); err != nil {
return err
}
fmt.Printf("Access token was successfully created... %s\n", t.Token)
}
return nil
}
-134
View File
@@ -1,134 +0,0 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package cmd
import (
"fmt"
"strings"
"testing"
auth_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/auth"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/unittest"
user_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/user"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAdminUserCreate(t *testing.T) {
reset := func() {
require.NoError(t, db.TruncateBeans(t.Context(), &user_model.User{}))
require.NoError(t, db.TruncateBeans(t.Context(), &user_model.EmailAddress{}))
require.NoError(t, db.TruncateBeans(t.Context(), &auth_model.AccessToken{}))
}
t.Run("MustChangePassword", func(t *testing.T) {
type check struct {
IsAdmin bool
MustChangePassword bool
}
createCheck := func(name, args string) check {
require.NoError(t, microcmdUserCreate().Run(t.Context(), strings.Fields(fmt.Sprintf("create --username %s --email %s@gitea.local %s --password foobar", name, name, args))))
u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: name})
return check{IsAdmin: u.IsAdmin, MustChangePassword: u.MustChangePassword}
}
reset()
assert.Equal(t, check{IsAdmin: false, MustChangePassword: false}, createCheck("u", ""), "first non-admin user doesn't need to change password")
reset()
assert.Equal(t, check{IsAdmin: true, MustChangePassword: false}, createCheck("u", "--admin"), "first admin user doesn't need to change password")
reset()
assert.Equal(t, check{IsAdmin: true, MustChangePassword: true}, createCheck("u", "--admin --must-change-password"))
assert.Equal(t, check{IsAdmin: true, MustChangePassword: true}, createCheck("u2", "--admin"))
assert.Equal(t, check{IsAdmin: true, MustChangePassword: false}, createCheck("u3", "--admin --must-change-password=false"))
assert.Equal(t, check{IsAdmin: false, MustChangePassword: true}, createCheck("u4", ""))
assert.Equal(t, check{IsAdmin: false, MustChangePassword: false}, createCheck("u5", "--must-change-password=false"))
})
createUser := func(name string, args ...string) error {
return microcmdUserCreate().Run(t.Context(), append([]string{"create", "--username", name, "--email", name + "@gitea.local"}, args...))
}
t.Run("UserType", func(t *testing.T) {
reset()
assert.ErrorContains(t, createUser("u", "--user-type", "invalid"), "invalid user type")
assert.ErrorContains(t, createUser("u", "--user-type", "bot", "--password", "123"), "can only be set for individual users")
assert.ErrorContains(t, createUser("u", "--user-type", "bot", "--must-change-password"), "can only be set for individual users")
assert.NoError(t, createUser("u", "--user-type", "bot"))
u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "u"})
assert.Equal(t, user_model.UserTypeBot, u.Type)
assert.Empty(t, u.Passwd)
})
t.Run("AccessToken", func(t *testing.T) {
// no generated access token
reset()
assert.NoError(t, createUser("u", "--random-password"))
assert.Equal(t, 1, unittest.GetCount(t, &user_model.User{}))
assert.Equal(t, 0, unittest.GetCount(t, &auth_model.AccessToken{}))
// using "--access-token" only means "all" access
reset()
assert.NoError(t, createUser("u", "--random-password", "--access-token"))
assert.Equal(t, 1, unittest.GetCount(t, &user_model.User{}))
assert.Equal(t, 1, unittest.GetCount(t, &auth_model.AccessToken{}))
accessToken := unittest.AssertExistsAndLoadBean(t, &auth_model.AccessToken{Name: "gitea-admin"})
hasScopes, err := accessToken.Scope.HasScope(auth_model.AccessTokenScopeWriteAdmin, auth_model.AccessTokenScopeWriteRepository)
assert.NoError(t, err)
assert.True(t, hasScopes)
// using "--access-token" with name & scopes
reset()
assert.NoError(t, createUser("u", "--random-password", "--access-token", "--access-token-name", "new-token-name", "--access-token-scopes", "read:issue,read:user"))
assert.Equal(t, 1, unittest.GetCount(t, &user_model.User{}))
assert.Equal(t, 1, unittest.GetCount(t, &auth_model.AccessToken{}))
accessToken = unittest.AssertExistsAndLoadBean(t, &auth_model.AccessToken{Name: "new-token-name"})
hasScopes, err = accessToken.Scope.HasScope(auth_model.AccessTokenScopeReadIssue, auth_model.AccessTokenScopeReadUser)
assert.NoError(t, err)
assert.True(t, hasScopes)
hasScopes, err = accessToken.Scope.HasScope(auth_model.AccessTokenScopeWriteAdmin, auth_model.AccessTokenScopeWriteRepository)
assert.NoError(t, err)
assert.False(t, hasScopes)
// using "--access-token-name" without "--access-token"
reset()
err = createUser("u", "--random-password", "--access-token-name", "new-token-name")
assert.Equal(t, 0, unittest.GetCount(t, &user_model.User{}))
assert.Equal(t, 0, unittest.GetCount(t, &auth_model.AccessToken{}))
assert.ErrorContains(t, err, "access-token-name and access-token-scopes flags are only valid when access-token flag is set")
// using "--access-token-scopes" without "--access-token"
reset()
err = createUser("u", "--random-password", "--access-token-scopes", "read:issue")
assert.Equal(t, 0, unittest.GetCount(t, &user_model.User{}))
assert.Equal(t, 0, unittest.GetCount(t, &auth_model.AccessToken{}))
assert.ErrorContains(t, err, "access-token-name and access-token-scopes flags are only valid when access-token flag is set")
// empty permission
reset()
err = createUser("u", "--random-password", "--access-token", "--access-token-scopes", "public-only")
assert.Equal(t, 0, unittest.GetCount(t, &user_model.User{}))
assert.Equal(t, 0, unittest.GetCount(t, &auth_model.AccessToken{}))
assert.ErrorContains(t, err, "access token does not have any permission")
})
t.Run("UserFields", func(t *testing.T) {
reset()
assert.NoError(t, createUser("u-FullNameWithSpace", "--random-password", "--fullname", "First O'Middle Last"))
unittest.AssertExistsAndLoadBean(t, &user_model.User{
Name: "u-FullNameWithSpace",
LowerName: "u-fullnamewithspace",
FullName: "First O'Middle Last",
Email: "u-FullNameWithSpace@gitea.local",
})
assert.NoError(t, createUser("u-FullNameEmpty", "--random-password", "--fullname", ""))
u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "u-fullnameempty"})
assert.Empty(t, u.FullName)
})
}
-84
View File
@@ -1,84 +0,0 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package cmd
import (
"context"
"errors"
"fmt"
"strings"
user_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/user"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/storage"
user_service "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/user"
"github.com/urfave/cli/v3"
)
func microcmdUserDelete() *cli.Command {
return &cli.Command{
Name: "delete",
Usage: "Delete specific user by id, name or email",
Flags: []cli.Flag{
&cli.Int64Flag{
Name: "id",
Usage: "ID of user of the user to delete",
},
&cli.StringFlag{
Name: "username",
Aliases: []string{"u"},
Usage: "Username of the user to delete",
},
&cli.StringFlag{
Name: "email",
Aliases: []string{"e"},
Usage: "Email of the user to delete",
},
&cli.BoolFlag{
Name: "purge",
Usage: "Purge user, all their repositories, organizations and comments",
},
},
Action: runDeleteUser,
}
}
func runDeleteUser(ctx context.Context, c *cli.Command) error {
if !c.IsSet("id") && !c.IsSet("username") && !c.IsSet("email") {
return errors.New("You must provide the id, username or email of a user to delete")
}
if !setting.IsInTesting {
if err := initDB(ctx); err != nil {
return err
}
}
if err := storage.Init(); err != nil {
return err
}
var err error
var user *user_model.User
if c.IsSet("email") {
user, err = user_model.GetUserByEmail(ctx, c.String("email"))
} else if c.IsSet("username") {
user, err = user_model.GetUserByName(ctx, c.String("username"))
} else {
user, err = user_model.GetUserByID(ctx, c.Int64("id"))
}
if err != nil {
return err
}
if c.IsSet("username") && user.LowerName != strings.ToLower(strings.TrimSpace(c.String("username"))) {
return fmt.Errorf("the user %s who has email %s does not match the provided username %s", user.Name, c.String("email"), c.String("username"))
}
if c.IsSet("id") && user.ID != c.Int64("id") {
return fmt.Errorf("the user %s does not match the provided id %d", user.Name, c.Int64("id"))
}
return user_service.DeleteUser(ctx, user, c.Bool("purge"))
}
-111
View File
@@ -1,111 +0,0 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package cmd
import (
"strconv"
"strings"
"testing"
auth_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/auth"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/unittest"
user_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/user"
"github.com/stretchr/testify/require"
)
func TestAdminUserDelete(t *testing.T) {
ctx := t.Context()
defer func() {
require.NoError(t, db.TruncateBeans(t.Context(), &user_model.User{}))
require.NoError(t, db.TruncateBeans(t.Context(), &user_model.EmailAddress{}))
require.NoError(t, db.TruncateBeans(t.Context(), &auth_model.AccessToken{}))
}()
setupTestUser := func(t *testing.T) {
unittest.AssertNotExistsBean(t, &user_model.User{LowerName: "testuser"})
err := microcmdUserCreate().Run(t.Context(), []string{"create", "--username", "testuser", "--email", "testuser@gitea.local", "--random-password"})
require.NoError(t, err)
}
t.Run("delete user by id", func(t *testing.T) {
setupTestUser(t)
u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
err := microcmdUserDelete().Run(ctx, []string{"delete-test", "--id", strconv.FormatInt(u.ID, 10)})
require.NoError(t, err)
unittest.AssertNotExistsBean(t, &user_model.User{LowerName: "testuser"})
})
t.Run("delete user by username", func(t *testing.T) {
setupTestUser(t)
err := microcmdUserDelete().Run(ctx, []string{"delete-test", "--username", "testuser"})
require.NoError(t, err)
unittest.AssertNotExistsBean(t, &user_model.User{LowerName: "testuser"})
})
t.Run("delete user by email", func(t *testing.T) {
setupTestUser(t)
err := microcmdUserDelete().Run(ctx, []string{"delete-test", "--email", "testuser@gitea.local"})
require.NoError(t, err)
unittest.AssertNotExistsBean(t, &user_model.User{LowerName: "testuser"})
})
t.Run("delete user by all 3 attributes", func(t *testing.T) {
setupTestUser(t)
u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
err := microcmdUserDelete().Run(ctx, []string{"delete", "--id", strconv.FormatInt(u.ID, 10), "--username", "testuser", "--email", "testuser@gitea.local"})
require.NoError(t, err)
unittest.AssertNotExistsBean(t, &user_model.User{LowerName: "testuser"})
})
}
func TestAdminUserDeleteFailure(t *testing.T) {
testCases := []struct {
name string
args []string
expectedErr string
}{
{
name: "no user to delete",
args: []string{"delete", "--username", "nonexistentuser"},
expectedErr: "user does not exist",
},
{
name: "user exists but provided username does not match",
args: []string{"delete", "--email", "testuser@gitea.local", "--username", "wrongusername"},
expectedErr: "the user testuser who has email testuser@gitea.local does not match the provided username wrongusername",
},
{
name: "user exists but provided id does not match",
args: []string{"delete", "--username", "testuser", "--id", "999"},
expectedErr: "the user testuser does not match the provided id 999",
},
{
name: "no required flags are provided",
args: []string{"delete"},
expectedErr: "You must provide the id, username or email of a user to delete",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
ctx := t.Context()
if strings.Contains(tc.name, "user exists") {
unittest.AssertNotExistsBean(t, &user_model.User{LowerName: "testuser"})
err := microcmdUserCreate().Run(t.Context(), []string{"create", "--username", "testuser", "--email", "testuser@gitea.local", "--random-password"})
require.NoError(t, err)
}
err := microcmdUserDelete().Run(ctx, tc.args)
require.Error(t, err)
require.Contains(t, err.Error(), tc.expectedErr)
})
require.NoError(t, db.TruncateBeans(t.Context(), &user_model.User{}))
require.NoError(t, db.TruncateBeans(t.Context(), &user_model.EmailAddress{}))
require.NoError(t, db.TruncateBeans(t.Context(), &auth_model.AccessToken{}))
}
}
-97
View File
@@ -1,97 +0,0 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package cmd
import (
"context"
"errors"
"fmt"
auth_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/auth"
user_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/user"
"github.com/urfave/cli/v3"
)
func newUserGenerateAccessTokenCommand() *cli.Command {
return &cli.Command{
Name: "generate-access-token",
Usage: "Generate an access token for a specific user",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "username",
Aliases: []string{"u"},
Usage: "Username",
},
&cli.StringFlag{
Name: "token-name",
Aliases: []string{"t"},
Usage: "Token name",
Value: "gitea-admin",
},
&cli.BoolFlag{
Name: "raw",
Usage: "Display only the token value",
},
&cli.StringFlag{
Name: "scopes",
Value: "all",
Usage: `Comma separated list of scopes to apply to access token, examples: "all", "public-only,read:issue", "write:repository,write:user"`,
},
},
Action: runGenerateAccessToken,
}
}
func runGenerateAccessToken(ctx context.Context, c *cli.Command) error {
if !c.IsSet("username") {
return errors.New("you must provide a username to generate a token for")
}
if err := initDB(ctx); err != nil {
return err
}
user, err := user_model.GetUserByName(ctx, c.String("username"))
if err != nil {
return err
}
// construct token with name and user so we can make sure it is unique
t := &auth_model.AccessToken{
Name: c.String("token-name"),
UID: user.ID,
}
exist, err := auth_model.AccessTokenByNameExists(ctx, t)
if err != nil {
return err
}
if exist {
return errors.New("access token name has been used already")
}
// make sure the scopes are valid
accessTokenScope, err := auth_model.AccessTokenScope(c.String("scopes")).Normalize()
if err != nil {
return fmt.Errorf("invalid access token scope provided: %w", err)
}
if !accessTokenScope.HasPermissionScope() {
return errors.New("access token does not have any permission")
}
t.Scope = accessTokenScope
// create the token
if err := auth_model.NewAccessToken(ctx, t); err != nil {
return err
}
if c.Bool("raw") {
fmt.Printf("%s\n", t.Token)
} else {
fmt.Printf("Access token was successfully created: %s\n", t.Token)
}
return nil
}
-60
View File
@@ -1,60 +0,0 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package cmd
import (
"context"
"fmt"
"os"
"text/tabwriter"
user_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/user"
"github.com/urfave/cli/v3"
)
func newUserListCommand() *cli.Command {
return &cli.Command{
Name: "list",
Usage: "List users",
Action: runListUsers,
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "admin",
Usage: "List only admin users",
},
},
}
}
func runListUsers(ctx context.Context, c *cli.Command) error {
if err := initDB(ctx); err != nil {
return err
}
users, err := user_model.GetAllUsers(ctx)
if err != nil {
return err
}
w := tabwriter.NewWriter(os.Stdout, 5, 0, 1, ' ', 0)
if c.IsSet("admin") {
fmt.Fprintf(w, "ID\tUsername\tEmail\tIsActive\n")
for _, u := range users {
if u.IsAdmin {
fmt.Fprintf(w, "%d\t%s\t%s\t%t\n", u.ID, u.Name, u.Email, u.IsActive)
}
}
} else {
twofa := user_model.UserList(users).GetTwoFaStatus(ctx)
fmt.Fprintf(w, "ID\tUsername\tEmail\tIsActive\tIsAdmin\t2FA\n")
for _, u := range users {
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])
}
}
w.Flush()
return nil
}
-64
View File
@@ -1,64 +0,0 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package cmd
import (
"context"
"errors"
"fmt"
user_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/user"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
"github.com/urfave/cli/v3"
)
func microcmdUserMustChangePassword() *cli.Command {
return &cli.Command{
Name: "must-change-password",
Usage: "Set the must change password flag for the provided users or all users",
Action: runMustChangePassword,
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "all",
Aliases: []string{"A"},
Usage: "All users must change password, except those explicitly excluded with --exclude",
},
&cli.StringSliceFlag{
Name: "exclude",
Aliases: []string{"e"},
Usage: "Do not change the must-change-password flag for these users",
},
&cli.BoolFlag{
Name: "unset",
Usage: "Instead of setting the must-change-password flag, unset it",
},
},
}
}
func runMustChangePassword(ctx context.Context, c *cli.Command) error {
if c.NArg() == 0 && !c.IsSet("all") {
return errors.New("either usernames or --all must be provided")
}
mustChangePassword := !c.Bool("unset")
all := c.Bool("all")
exclude := c.StringSlice("exclude")
if !setting.IsInTesting {
if err := initDB(ctx); err != nil {
return err
}
}
n, err := user_model.SetMustChangePassword(ctx, all, mustChangePassword, c.Args().Slice(), exclude)
if err != nil {
return err
}
// codeql[disable-next-line=go/clear-text-logging]
fmt.Printf("Updated %d users setting MustChangePassword to %t\n", n, mustChangePassword)
return nil
}
@@ -1,78 +0,0 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package cmd
import (
"testing"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/unittest"
user_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/user"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestMustChangePassword(t *testing.T) {
defer func() {
require.NoError(t, db.TruncateBeans(t.Context(), &user_model.User{}))
}()
err := microcmdUserCreate().Run(t.Context(), []string{"create", "--username", "testuser", "--email", "testuser@gitea.local", "--random-password"})
require.NoError(t, err)
err = microcmdUserCreate().Run(t.Context(), []string{"create", "--username", "testuserexclude", "--email", "testuserexclude@gitea.local", "--random-password"})
require.NoError(t, err)
// Reset password change flag
err = microcmdUserMustChangePassword().Run(t.Context(), []string{"change-test", "--all", "--unset"})
require.NoError(t, err)
testUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
assert.False(t, testUser.MustChangePassword)
testUserExclude := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuserexclude"})
assert.False(t, testUserExclude.MustChangePassword)
// Make all users change password
err = microcmdUserMustChangePassword().Run(t.Context(), []string{"change-test", "--all"})
require.NoError(t, err)
testUser = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
assert.True(t, testUser.MustChangePassword)
testUserExclude = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuserexclude"})
assert.True(t, testUserExclude.MustChangePassword)
// Reset password change flag but exclude all tested users
err = microcmdUserMustChangePassword().Run(t.Context(), []string{"change-test", "--all", "--unset", "--exclude", "testuser,testuserexclude"})
require.NoError(t, err)
testUser = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
assert.True(t, testUser.MustChangePassword)
testUserExclude = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuserexclude"})
assert.True(t, testUserExclude.MustChangePassword)
// Reset password change flag by listing multiple users
err = microcmdUserMustChangePassword().Run(t.Context(), []string{"change-test", "--unset", "testuser", "testuserexclude"})
require.NoError(t, err)
testUser = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
assert.False(t, testUser.MustChangePassword)
testUserExclude = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuserexclude"})
assert.False(t, testUserExclude.MustChangePassword)
// Exclude a user from all user
err = microcmdUserMustChangePassword().Run(t.Context(), []string{"change-test", "--all", "--exclude", "testuserexclude"})
require.NoError(t, err)
testUser = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
assert.True(t, testUser.MustChangePassword)
testUserExclude = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuserexclude"})
assert.False(t, testUserExclude.MustChangePassword)
// Unset a flag for single user
err = microcmdUserMustChangePassword().Run(t.Context(), []string{"change-test", "--unset", "testuser"})
require.NoError(t, err)
testUser = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
assert.False(t, testUser.MustChangePassword)
testUserExclude = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuserexclude"})
assert.False(t, testUserExclude.MustChangePassword)
}
+74 -94
View File
@@ -1,12 +1,12 @@
// Copyright 2009 The Go Authors. All rights reserved.
// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2016 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package cmd
import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
@@ -14,7 +14,6 @@ import (
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"fmt"
"log"
"math/big"
"net"
@@ -22,62 +21,50 @@ import (
"strings"
"time"
"github.com/urfave/cli/v3"
"github.com/urfave/cli"
)
// cmdCert represents the available cert sub-command.
func cmdCert() *cli.Command {
return &cli.Command{
Name: "cert",
Usage: "Generate self-signed certificate",
Description: `Generate a self-signed X.509 certificate for a TLS server.
// CmdCert represents the available cert sub-command.
var CmdCert = cli.Command{
Name: "cert",
Usage: "Generate self-signed certificate",
Description: `Generate a self-signed X.509 certificate for a TLS server.
Outputs to 'cert.pem' and 'key.pem' and will overwrite existing files.`,
Action: runCert,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "host",
Usage: "Comma-separated hostnames and IPs to generate a certificate for",
Required: true,
},
&cli.StringFlag{
Name: "ecdsa-curve",
Value: "",
Usage: "ECDSA curve to use to generate a key. Valid values are P224, P256, P384, P521",
},
&cli.IntFlag{
Name: "rsa-bits",
Value: 3072,
Usage: "Size of RSA key to generate. Ignored if --ecdsa-curve is set",
},
&cli.StringFlag{
Name: "start-date",
Value: "",
Usage: "Creation date formatted as Jan 1 15:04:05 2011",
},
&cli.DurationFlag{
Name: "duration",
Value: 365 * 24 * time.Hour,
Usage: "Duration that certificate is valid for",
},
&cli.BoolFlag{
Name: "ca",
Usage: "whether this cert should be its own Certificate Authority",
},
&cli.StringFlag{
Name: "out",
Value: "cert.pem",
Usage: "Path to the file where there certificate will be saved",
},
&cli.StringFlag{
Name: "keyout",
Value: "key.pem",
Usage: "Path to the file where there certificate key will be saved",
},
Action: runCert,
Flags: []cli.Flag{
cli.StringFlag{
Name: "host",
Value: "",
Usage: "Comma-separated hostnames and IPs to generate a certificate for",
},
}
cli.StringFlag{
Name: "ecdsa-curve",
Value: "",
Usage: "ECDSA curve to use to generate a key. Valid values are P224, P256, P384, P521",
},
cli.IntFlag{
Name: "rsa-bits",
Value: 2048,
Usage: "Size of RSA key to generate. Ignored if --ecdsa-curve is set",
},
cli.StringFlag{
Name: "start-date",
Value: "",
Usage: "Creation date formatted as Jan 1 15:04:05 2011",
},
cli.DurationFlag{
Name: "duration",
Value: 365 * 24 * time.Hour,
Usage: "Duration that certificate is valid for",
},
cli.BoolFlag{
Name: "ca",
Usage: "whether this cert should be its own Certificate Authority",
},
},
}
func publicKey(priv any) any {
func publicKey(priv interface{}) interface{} {
switch k := priv.(type) {
case *rsa.PrivateKey:
return &k.PublicKey
@@ -88,7 +75,7 @@ func publicKey(priv any) any {
}
}
func pemBlockForKey(priv any) *pem.Block {
func pemBlockForKey(priv interface{}) *pem.Block {
switch k := priv.(type) {
case *rsa.PrivateKey:
return &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(k)}
@@ -103,12 +90,16 @@ func pemBlockForKey(priv any) *pem.Block {
}
}
func runCert(_ context.Context, c *cli.Command) error {
var priv any
func runCert(ctx *cli.Context) error {
if len(ctx.String("host")) == 0 {
log.Fatal("Missing required --host parameter")
}
var priv interface{}
var err error
switch c.String("ecdsa-curve") {
switch ctx.String("ecdsa-curve") {
case "":
priv, err = rsa.GenerateKey(rand.Reader, c.Int("rsa-bits"))
priv, err = rsa.GenerateKey(rand.Reader, ctx.Int("rsa-bits"))
case "P224":
priv, err = ecdsa.GenerateKey(elliptic.P224(), rand.Reader)
case "P256":
@@ -118,28 +109,28 @@ func runCert(_ context.Context, c *cli.Command) error {
case "P521":
priv, err = ecdsa.GenerateKey(elliptic.P521(), rand.Reader)
default:
err = fmt.Errorf("unrecognized elliptic curve: %q", c.String("ecdsa-curve"))
log.Fatalf("Unrecognized elliptic curve: %q", ctx.String("ecdsa-curve"))
}
if err != nil {
return fmt.Errorf("failed to generate private key: %w", err)
log.Fatalf("Failed to generate private key: %v", err)
}
var notBefore time.Time
if startDate := c.String("start-date"); startDate != "" {
notBefore, err = time.Parse("Jan 2 15:04:05 2006", startDate)
if err != nil {
return fmt.Errorf("failed to parse creation date %w", err)
}
} else {
if len(ctx.String("start-date")) == 0 {
notBefore = time.Now()
} else {
notBefore, err = time.Parse("Jan 2 15:04:05 2006", ctx.String("start-date"))
if err != nil {
log.Fatalf("Failed to parse creation date: %v", err)
}
}
notAfter := notBefore.Add(c.Duration("duration"))
notAfter := notBefore.Add(ctx.Duration("duration"))
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
if err != nil {
return fmt.Errorf("failed to generate serial number: %w", err)
log.Fatalf("Failed to generate serial number: %v", err)
}
template := x509.Certificate{
@@ -156,8 +147,8 @@ func runCert(_ context.Context, c *cli.Command) error {
BasicConstraintsValid: true,
}
hosts := strings.SplitSeq(c.String("host"), ",")
for h := range hosts {
hosts := strings.Split(ctx.String("host"), ",")
for _, h := range hosts {
if ip := net.ParseIP(h); ip != nil {
template.IPAddresses = append(template.IPAddresses, ip)
} else {
@@ -165,42 +156,31 @@ func runCert(_ context.Context, c *cli.Command) error {
}
}
if c.Bool("ca") {
if ctx.Bool("ca") {
template.IsCA = true
template.KeyUsage |= x509.KeyUsageCertSign
}
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, publicKey(priv), priv)
if err != nil {
return fmt.Errorf("failed to create certificate: %w", err)
log.Fatalf("Failed to create certificate: %v", err)
}
certOut, err := os.Create(c.String("out"))
certOut, err := os.Create("cert.pem")
if err != nil {
return fmt.Errorf("failed to open %s for writing: %w", c.String("keyout"), err)
log.Fatalf("Failed to open cert.pem for writing: %v", err)
}
err = pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
if err != nil {
return fmt.Errorf("failed to encode certificate: %w", err)
}
err = certOut.Close()
if err != nil {
return fmt.Errorf("failed to write cert: %w", err)
}
fmt.Fprintf(c.Writer, "Written cert to %s\n", c.String("out"))
pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
certOut.Close()
log.Println("Written cert.pem")
keyOut, err := os.OpenFile(c.String("keyout"), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600)
keyOut, err := os.OpenFile("key.pem", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
return fmt.Errorf("failed to open %s for writing: %w", c.String("keyout"), err)
log.Fatalf("Failed to open key.pem for writing: %v", err)
}
err = pem.Encode(keyOut, pemBlockForKey(priv))
if err != nil {
return fmt.Errorf("failed to encode key: %w", err)
}
err = keyOut.Close()
if err != nil {
return fmt.Errorf("failed to write key: %w", err)
}
fmt.Fprintf(c.Writer, "Written key to %s\n", c.String("keyout"))
pem.Encode(keyOut, pemBlockForKey(priv))
keyOut.Close()
log.Println("Written key.pem")
return nil
}
-125
View File
@@ -1,125 +0,0 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package cmd
import (
"io"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestCertCommand(t *testing.T) {
cases := []struct {
name string
args []string
}{
{
name: "RSA cert generation",
args: []string{
"cert-test",
"--host", "localhost",
"--rsa-bits", "2048",
"--duration", "1h",
"--start-date", "Jan 1 00:00:00 2024",
},
},
{
name: "ECDSA cert generation",
args: []string{
"cert-test",
"--host", "localhost",
"--ecdsa-curve", "P256",
"--duration", "1h",
"--start-date", "Jan 1 00:00:00 2024",
},
},
{
name: "mixed host, certificate authority",
args: []string{
"cert-test",
"--host", "localhost,127.0.0.1",
"--duration", "1h",
"--start-date", "Jan 1 00:00:00 2024",
},
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
app := cmdCert()
tempDir := t.TempDir()
certFile := filepath.Join(tempDir, "cert.pem")
keyFile := filepath.Join(tempDir, "key.pem")
err := app.Run(t.Context(), append(c.args, "--out", certFile, "--keyout", keyFile))
require.NoError(t, err)
assert.FileExists(t, certFile)
assert.FileExists(t, keyFile)
})
}
}
func TestCertCommandFailures(t *testing.T) {
cases := []struct {
name string
args []string
errMsg string
}{
{
name: "Start Date Parsing failure",
args: []string{
"cert-test",
"--host", "localhost",
"--start-date", "invalid-date",
},
errMsg: "parsing time",
},
{
name: "Unknown curve",
args: []string{
"cert-test",
"--host", "localhost",
"--ecdsa-curve", "invalid-curve",
},
errMsg: "unrecognized elliptic curve",
},
{
name: "Key generation failure",
args: []string{
"cert-test",
"--host", "localhost",
"--rsa-bits", "invalid-bits",
},
},
{
name: "Missing parameters",
args: []string{
"cert-test",
},
errMsg: `"host" not set`,
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
app := cmdCert()
app.Writer, app.ErrWriter = io.Discard, io.Discard
tempDir := t.TempDir()
certFile := filepath.Join(tempDir, "cert.pem")
keyFile := filepath.Join(tempDir, "key.pem")
err := app.Run(t.Context(), append(c.args, "--out", certFile, "--keyout", keyFile))
require.Error(t, err)
if c.errMsg != "" {
assert.ErrorContains(t, err, c.errMsg)
}
assert.NoFileExists(t, certFile)
assert.NoFileExists(t, keyFile)
})
}
}
-237
View File
@@ -1,237 +0,0 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
// Tests here reload the config system multiple times with uncontrollable details.
// So they must be in a separate package, to avoid affecting other tests
package cmdtest
import (
"context"
"errors"
"fmt"
"io"
"path/filepath"
"strings"
"testing"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/cmd"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/unittest"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/test"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/util"
"github.com/stretchr/testify/assert"
"github.com/urfave/cli/v3"
)
func TestMain(m *testing.M) {
unittest.MainTest(m)
}
func makePathOutput(workPath, customPath, customConf string) string {
return fmt.Sprintf("WorkPath=%s\nCustomPath=%s\nCustomConf=%s", workPath, customPath, customConf)
}
func newTestApp(testCmd cli.Command) *cli.Command {
app := cmd.NewMainApp(cmd.AppVersion{})
testCmd.Name = util.IfZero(testCmd.Name, "test-cmd")
cmd.PrepareSubcommandWithGlobalFlags(&testCmd)
app.Commands = append(app.Commands, &testCmd)
app.DefaultCommand = testCmd.Name
return app
}
type runResult struct {
Stdout string
Stderr string
ExitCode int
}
func runTestApp(app *cli.Command, args ...string) (runResult, error) {
outBuf := new(strings.Builder)
errBuf := new(strings.Builder)
app.Writer = outBuf
app.ErrWriter = errBuf
exitCode := -1
defer test.MockVariableValue(&cli.ErrWriter, app.ErrWriter)()
defer test.MockVariableValue(&cli.OsExiter, func(code int) {
if exitCode == -1 {
exitCode = code // save the exit code once and then reset the writer (to simulate the exit)
app.Writer, app.ErrWriter, cli.ErrWriter = io.Discard, io.Discard, io.Discard
}
})()
err := cmd.RunMainApp(app, args...)
return runResult{outBuf.String(), errBuf.String(), exitCode}, err
}
func TestCliCmd(t *testing.T) {
defaultWorkPath := filepath.FromSlash("/tmp/mocked-work-path")
defaultCustomPath := filepath.Join(defaultWorkPath, "custom")
defaultCustomConf := filepath.Join(defaultCustomPath, "conf/app.ini")
defer setting.MockBuiltinPaths(defaultWorkPath, "", "")()
cli.CommandHelpTemplate = "(command help template)"
cli.RootCommandHelpTemplate = "(app help template)"
cli.SubcommandHelpTemplate = "(subcommand help template)"
cases := []struct {
env map[string]string
cmd string
exp string
}{
// help commands
{
cmd: "./gitea -h",
exp: "DEFAULT CONFIGURATION:",
},
{
cmd: "./gitea help",
exp: "DEFAULT CONFIGURATION:",
},
{
cmd: "./gitea -c /dev/null -h",
exp: "ConfigFile: /dev/null",
},
{
cmd: "./gitea -c /dev/null help",
exp: "ConfigFile: /dev/null",
},
{
cmd: "./gitea help -c /dev/null",
exp: "ConfigFile: /dev/null",
},
{
cmd: "./gitea -c /dev/null test-cmd -h",
exp: "ConfigFile: /dev/null",
},
{
cmd: "./gitea test-cmd -c /dev/null -h",
exp: "ConfigFile: /dev/null",
},
{
cmd: "./gitea test-cmd -h -c /dev/null",
exp: "ConfigFile: /dev/null",
},
{
cmd: "./gitea -c /dev/null test-cmd help",
exp: "ConfigFile: /dev/null",
},
{
cmd: "./gitea test-cmd -c /dev/null help",
exp: "ConfigFile: /dev/null",
},
{
cmd: "./gitea test-cmd help -c /dev/null",
exp: "ConfigFile: /dev/null",
},
// parse paths
{
cmd: "./gitea test-cmd",
exp: makePathOutput(defaultWorkPath, defaultCustomPath, defaultCustomConf),
},
{
cmd: "./gitea -c /tmp/app.ini test-cmd",
exp: makePathOutput(defaultWorkPath, defaultCustomPath, "/tmp/app.ini"),
},
{
cmd: "./gitea test-cmd -c /tmp/app.ini",
exp: makePathOutput(defaultWorkPath, defaultCustomPath, "/tmp/app.ini"),
},
{
env: map[string]string{"GITEA_WORK_DIR": "/tmp"},
cmd: "./gitea test-cmd",
exp: makePathOutput("/tmp", "/tmp/custom", "/tmp/custom/conf/app.ini"),
},
{
env: map[string]string{"GITEA_WORK_DIR": "/tmp"},
cmd: "./gitea test-cmd --work-path /tmp/other",
exp: makePathOutput("/tmp/other", "/tmp/other/custom", "/tmp/other/custom/conf/app.ini"),
},
{
env: map[string]string{"GITEA_WORK_DIR": "/tmp"},
cmd: "./gitea test-cmd --config /tmp/app-other.ini",
exp: makePathOutput("/tmp", "/tmp/custom", "/tmp/app-other.ini"),
},
}
for _, c := range cases {
t.Run(c.cmd, func(t *testing.T) {
app := newTestApp(cli.Command{
Action: func(ctx context.Context, cmd *cli.Command) error {
_, _ = fmt.Fprint(cmd.Root().Writer, makePathOutput(setting.AppWorkPath, setting.CustomPath, setting.CustomConf))
return nil
},
})
for k, v := range c.env {
t.Setenv(k, v)
}
args := strings.Split(c.cmd, " ") // for test only, "split" is good enough
r, err := runTestApp(app, args...)
assert.NoError(t, err, c.cmd)
assert.NotEmpty(t, c.exp, c.cmd)
if !assert.Contains(t, r.Stdout, c.exp, c.cmd) {
t.Log("Full output:\n" + r.Stdout)
t.Log("Expected:\n" + c.exp)
}
})
}
}
func TestCliCmdError(t *testing.T) {
app := newTestApp(cli.Command{Action: func(ctx context.Context, cmd *cli.Command) error { return errors.New("normal error") }})
r, err := runTestApp(app, "./gitea", "test-cmd")
assert.Error(t, err)
assert.Equal(t, 1, r.ExitCode)
assert.Empty(t, r.Stdout)
assert.Equal(t, "Command error: normal error\n", r.Stderr)
app = newTestApp(cli.Command{Action: func(ctx context.Context, cmd *cli.Command) error { return cli.Exit("exit error", 2) }})
r, err = runTestApp(app, "./gitea", "test-cmd")
assert.Error(t, err)
assert.Equal(t, 2, r.ExitCode)
assert.Empty(t, r.Stdout)
assert.Equal(t, "exit error\n", r.Stderr)
app = newTestApp(cli.Command{Action: func(ctx context.Context, cmd *cli.Command) error { return nil }})
r, err = runTestApp(app, "./gitea", "test-cmd", "--no-such")
assert.Error(t, err)
assert.Equal(t, 1, r.ExitCode)
assert.Empty(t, r.Stdout)
assert.Equal(t, "Incorrect Usage: flag provided but not defined: -no-such\n\n", r.Stderr)
app = newTestApp(cli.Command{Action: func(ctx context.Context, cmd *cli.Command) error { return nil }})
r, err = runTestApp(app, "./gitea", "test-cmd")
assert.NoError(t, err)
assert.Equal(t, -1, r.ExitCode) // the cli.OsExiter is not called
assert.Empty(t, r.Stdout)
assert.Empty(t, r.Stderr)
}
func TestCliCmdBefore(t *testing.T) {
ctxNew := context.WithValue(context.Background(), any("key"), "value")
configValues := map[string]string{}
setting.CustomConf = "/tmp/any.ini"
var actionCtx context.Context
app := newTestApp(cli.Command{
Before: func(context.Context, *cli.Command) (context.Context, error) {
configValues["before"] = setting.CustomConf
return ctxNew, nil
},
Action: func(ctx context.Context, cmd *cli.Command) error {
configValues["action"] = setting.CustomConf
actionCtx = ctx
return nil
},
})
_, err := runTestApp(app, "./gitea", "--config", "/dev/null", "test-cmd")
assert.NoError(t, err)
assert.Equal(t, ctxNew, actionCtx)
assert.Equal(t, "/tmp/any.ini", configValues["before"], "BeforeFunc must be called before preparing config")
assert.Equal(t, "/dev/null", configValues["action"])
}
-156
View File
@@ -1,156 +0,0 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package cmd
import (
"context"
"errors"
"fmt"
"os"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
"github.com/urfave/cli/v3"
)
func cmdConfig() *cli.Command {
subcmdConfigEditIni := &cli.Command{
Name: "edit-ini",
Usage: "Load an existing INI file, apply environment variables, keep specified keys, and output to a new INI file.",
Description: `
Help users to edit the Gitea configuration INI file.
# Keep Specified Keys
If you need to re-create the configuration file with only a subset of keys,
you can provide an INI template file for the kept keys and use the "--config-keep-keys" flag.
For example, if a helm chart needs to reset the settings and only keep SECRET_KEY,
it can use a template file (only keys take effect, values are ignored):
[security]
SECRET_KEY=
$ ./gitea config edit-ini --config app-old.ini --config-keep-keys app-keys.ini --out app-new.ini
# Map Environment Variables to INI Configuration
Environment variables of the form "GITEA__section_name__KEY_NAME"
will be mapped to the ini section "[section_name]" and the key
"KEY_NAME" with the value as provided.
Environment variables of the form "GITEA__section_name__KEY_NAME__FILE"
will be mapped to the ini section "[section_name]" and the key
"KEY_NAME" with the value loaded from the specified file.
Environment variable keys can only contain characters "0-9A-Z_",
if a section or key name contains dot ".", it needs to be escaped as _0x2E_.
For example, to apply this config:
[git.config]
foo.bar=val
$ export GITEA__git_0x2E_config__foo_0x2E_bar=val
# Put All Together
$ ./gitea config edit-ini --config app.ini --config-keep-keys app-keys.ini --apply-env {--in-place|--out app-new.ini}
`,
Flags: []cli.Flag{
// "--config" flag is provided by global flags, and this flag is also used by "environment-to-ini" script wrapper
// "--in-place" is also used by "environment-to-ini" script wrapper for its old behavior: always overwrite the existing config file
&cli.BoolFlag{
Name: "in-place",
Usage: "Output to the same config file as input. This flag will be ignored if --out is set.",
},
&cli.StringFlag{
Name: "config-keep-keys",
Usage: "An INI template file containing keys for keeping. Only the keys defined in the INI template will be kept from old config. If not set, all keys will be kept.",
},
&cli.BoolFlag{
Name: "apply-env",
Usage: "Apply all GITEA__* variables from the environment to the config.",
},
&cli.StringFlag{
Name: "out",
Usage: "Destination config file to write to.",
},
},
Action: runConfigEditIni,
}
return &cli.Command{
Name: "config",
Usage: "Manage Gitea configuration",
Commands: []*cli.Command{
subcmdConfigEditIni,
},
}
}
func runConfigEditIni(_ context.Context, c *cli.Command) error {
// the config system may change the environment variables, so get a copy first, to be used later
env := append([]string{}, os.Environ()...)
// don't use the guessed setting.CustomConf, instead, require the user to provide --config explicitly
if !c.IsSet("config") {
return errors.New("flag is required but not set: --config")
}
configFileIn := c.String("config")
cfgIn, err := setting.NewConfigProviderFromFile(configFileIn)
if err != nil {
return fmt.Errorf("failed to load config file %q: %v", configFileIn, err)
}
// determine output config file: use "--out" flag or use "--in-place" flag to overwrite input file
inPlace := c.Bool("in-place")
configFileOut := c.String("out")
if configFileOut == "" {
if !inPlace {
return errors.New("either --in-place or --out must be specified")
}
configFileOut = configFileIn // in-place edit
}
needWriteOut := configFileOut != configFileIn
cfgOut := cfgIn
configKeepKeys := c.String("config-keep-keys")
if configKeepKeys != "" {
needWriteOut = true
cfgOut, err = setting.NewConfigProviderFromFile(configKeepKeys)
if err != nil {
return fmt.Errorf("failed to load config-keep-keys template file %q: %v", configKeepKeys, err)
}
for _, secOut := range cfgOut.Sections() {
for _, keyOut := range secOut.Keys() {
secIn := cfgIn.Section(secOut.Name())
keyIn := setting.ConfigSectionKey(secIn, keyOut.Name())
if keyIn != nil {
keyOut.SetValue(keyIn.String())
} else {
secOut.DeleteKey(keyOut.Name())
}
}
if len(secOut.Keys()) == 0 {
cfgOut.DeleteSection(secOut.Name())
}
}
}
if c.Bool("apply-env") {
if setting.EnvironmentToConfig(cfgOut, env) {
needWriteOut = true
}
}
if needWriteOut {
err = cfgOut.SaveTo(configFileOut)
if err != nil {
return err
}
}
return nil
}
-85
View File
@@ -1,85 +0,0 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package cmd
import (
"os"
"testing"
"github.com/stretchr/testify/require"
)
func TestConfigEdit(t *testing.T) {
tmpDir := t.TempDir()
configOld := tmpDir + "/app-old.ini"
configTemplate := tmpDir + "/app-template.ini"
_ = os.WriteFile(configOld, []byte(`
[sec]
k1=v1
k2=v2
`), os.ModePerm)
_ = os.WriteFile(configTemplate, []byte(`
[sec]
k1=in-template
[sec2]
k3=v3
`), os.ModePerm)
t.Setenv("GITEA__EnV__KeY", "val")
t.Run("OutputToNewWithEnv", func(t *testing.T) {
configNew := tmpDir + "/app-new.ini"
err := NewMainApp(AppVersion{}).Run(t.Context(), []string{
"./gitea", "--config", configOld,
"config", "edit-ini",
"--apply-env",
"--config-keep-keys", configTemplate,
"--out", configNew,
})
require.NoError(t, err)
// "k1" old value is kept because its key is in the template
// "k2" is removed because it isn't in the template
// "k3" isn't in new config because it isn't in the old config
// [env] is applied from environment variable
data, _ := os.ReadFile(configNew)
require.Equal(t, `[sec]
k1 = v1
[env]
KeY = val
`, string(data))
})
t.Run("OutputToExisting(environment-to-ini)", func(t *testing.T) {
// the legacy "environment-to-ini" (now a wrapper script) behavior:
// if no "--out", then "--in-place" must be used to overwrite the existing "--config" file
err := NewMainApp(AppVersion{}).Run(t.Context(), []string{
"./gitea", "config", "edit-ini",
"--apply-env",
"--config", configOld,
})
require.ErrorContains(t, err, "either --in-place or --out must be specified")
// simulate the "environment-to-ini" behavior with "--in-place"
err = NewMainApp(AppVersion{}).Run(t.Context(), []string{
"./gitea", "config", "edit-ini",
"--in-place",
"--apply-env",
"--config", configOld,
})
require.NoError(t, err)
data, _ := os.ReadFile(configOld)
require.Equal(t, `[sec]
k1 = v1
k2 = v2
[env]
KeY = val
`, string(data))
})
}
-68
View File
@@ -1,68 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package cmd
import (
"context"
"fmt"
"os"
"strings"
cli_docs "github.com/urfave/cli-docs/v3"
"github.com/urfave/cli/v3"
)
func newDocsCommand() *cli.Command {
return &cli.Command{
Name: "docs",
Usage: "Output CLI documentation",
Description: "A command to output Gitea's CLI documentation, optionally to a file.",
Action: runDocs,
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "man",
Usage: "Output man pages instead",
},
&cli.StringFlag{
Name: "output",
Aliases: []string{"o"},
Usage: "Path to output to instead of stdout (will overwrite if exists)",
},
},
}
}
func runDocs(_ context.Context, cmd *cli.Command) error {
docs, err := cli_docs.ToMarkdown(cmd.Root())
if cmd.Bool("man") {
docs, err = cli_docs.ToMan(cmd.Root())
}
if err != nil {
return err
}
if !cmd.Bool("man") {
// Clean up markdown. The following bug was fixed in v2, but is present in v1.
// It affects markdown output (even though the issue is referring to man pages)
// https://github.com/urfave/cli/issues/1040
firstHashtagIndex := strings.Index(docs, "#")
if firstHashtagIndex > 0 {
docs = docs[firstHashtagIndex:]
}
}
out := os.Stdout
if cmd.String("output") != "" {
fi, err := os.Create(cmd.String("output"))
if err != nil {
return err
}
defer fi.Close()
out = fi
}
_, err = fmt.Fprintln(out, docs)
return err
}
-219
View File
@@ -1,219 +0,0 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package cmd
import (
"context"
"fmt"
golog "log"
"os"
"path/filepath"
"strings"
"text/tabwriter"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/migrations"
migrate_base "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/migrations/base"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/container"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/doctor"
"github.com/urfave/cli/v3"
"xorm.io/xorm"
)
func newDoctorCommand() *cli.Command {
return &cli.Command{
Name: "doctor",
Usage: "Diagnose and optionally fix problems, convert or re-create database tables",
Description: "A command to diagnose problems with the current Gitea instance according to the given configuration. Some problems can optionally be fixed by modifying the database or data storage.",
Commands: []*cli.Command{
newDoctorCheckCommand(),
newRecreateTableCommand(),
newDoctorConvertCommand(),
},
}
}
func newDoctorCheckCommand() *cli.Command {
return &cli.Command{
Name: "check",
Usage: "Diagnose and optionally fix problems",
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.",
Action: runDoctorCheck,
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "list",
Usage: "List the available checks",
},
&cli.BoolFlag{
Name: "default",
Usage: "Run the default checks (if neither --run or --all is set, this is the default behaviour)",
},
&cli.StringSliceFlag{
Name: "run",
Usage: "Run the provided checks - (if --default is set, the default checks will also run)",
},
&cli.BoolFlag{
Name: "all",
Usage: "Run all the available checks",
},
&cli.BoolFlag{
Name: "fix",
Usage: "Automatically fix what we can",
},
&cli.StringFlag{
Name: "log-file",
Usage: `Name of the log file (no verbose log output by default). Set to "-" to output to stdout`,
},
&cli.BoolFlag{
Name: "color",
Aliases: []string{"H"},
Usage: "Use color for outputted information",
},
},
}
}
func newRecreateTableCommand() *cli.Command {
return &cli.Command{
Name: "recreate-table",
Usage: "Recreate tables from XORM definitions and copy the data.",
ArgsUsage: "[TABLE]... : (TABLEs to recreate - leave blank for all)",
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "debug",
Usage: "Print SQL commands sent",
},
},
Description: `The database definitions Gitea uses change across versions, sometimes changing default values and leaving old unused columns.
This command will cause Xorm to recreate tables, copying over the data and deleting the old table.
You should back-up your database before doing this and ensure that your database is up-to-date first.`,
Action: runRecreateTable,
}
}
func runRecreateTable(ctx context.Context, cmd *cli.Command) error {
// Redirect the default golog to here
golog.SetFlags(0)
golog.SetPrefix("")
golog.SetOutput(log.LoggerToWriter(log.GetLogger(log.DEFAULT).Info))
debug := cmd.Bool("debug")
setting.MustInstalled()
setting.LoadDBSetting()
if debug {
setting.InitSQLLoggersForCli(log.DEBUG)
} else {
setting.InitSQLLoggersForCli(log.INFO)
}
setting.Database.LogSQL = debug
if err := db.InitEngine(ctx); err != nil {
fmt.Println(err)
fmt.Println("Check if you are using the right config file. You can use a --config directive to specify one.")
return nil
}
args := cmd.Args()
names := make([]string, 0, cmd.NArg())
for i := 0; i < cmd.NArg(); i++ {
names = append(names, args.Get(i))
}
beans, err := db.NamesToBean(names...)
if err != nil {
return err
}
recreateTables := migrate_base.RecreateTables(beans...)
return db.InitEngineWithMigration(context.Background(), func(ctx context.Context, x *xorm.Engine) error {
if err := migrations.EnsureUpToDate(ctx, x); err != nil {
return err
}
return recreateTables(x)
})
}
func setupDoctorDefaultLogger(cmd *cli.Command, colorize bool) {
// Silence the default loggers
setupConsoleLogger(log.FATAL, log.CanColorStderr, os.Stderr)
logFile := cmd.String("log-file")
switch logFile {
case "":
return // if no doctor log-file is set, do not show any log from default logger
case "-":
setupConsoleLogger(log.TRACE, colorize, os.Stdout)
default:
logFile, _ = filepath.Abs(logFile)
writeMode := log.WriterMode{Level: log.TRACE, WriterOption: log.WriterFileOption{FileName: logFile}}
writer, err := log.NewEventWriter("console-to-file", "file", writeMode)
if err != nil {
log.FallbackErrorf("unable to create file log writer: %v", err)
return
}
log.GetManager().GetLogger(log.DEFAULT).ReplaceAllWriters(writer)
}
}
func runDoctorCheck(ctx context.Context, cmd *cli.Command) error {
colorize := log.CanColorStdout
if cmd.IsSet("color") {
colorize = cmd.Bool("color")
}
setupDoctorDefaultLogger(cmd, colorize)
// Finally redirect the default golang's log to here
golog.SetFlags(0)
golog.SetPrefix("")
golog.SetOutput(log.LoggerToWriter(log.GetLogger(log.DEFAULT).Info))
if cmd.IsSet("list") {
w := tabwriter.NewWriter(os.Stdout, 0, 8, 1, '\t', 0)
_, _ = w.Write([]byte("Default\tName\tTitle\n"))
doctor.SortChecks(doctor.Checks)
for _, check := range doctor.Checks {
if check.IsDefault {
_, _ = w.Write([]byte{'*'})
}
_, _ = w.Write([]byte{'\t'})
_, _ = w.Write([]byte(check.Name))
_, _ = w.Write([]byte{'\t'})
_, _ = w.Write([]byte(check.Title))
_, _ = w.Write([]byte{'\n'})
}
return w.Flush()
}
var checks []*doctor.Check
if cmd.Bool("all") {
checks = make([]*doctor.Check, len(doctor.Checks))
copy(checks, doctor.Checks)
} else if cmd.IsSet("run") {
addDefault := cmd.Bool("default")
runNamesSet := container.SetOf(cmd.StringSlice("run")...)
for _, check := range doctor.Checks {
if (addDefault && check.IsDefault) || runNamesSet.Contains(check.Name) {
checks = append(checks, check)
runNamesSet.Remove(check.Name)
}
}
if len(runNamesSet) > 0 {
return fmt.Errorf("unknown checks: %q", strings.Join(runNamesSet.Values(), ","))
}
} else {
for _, check := range doctor.Checks {
if check.IsDefault {
checks = append(checks, check)
}
}
}
return doctor.RunChecks(ctx, colorize, cmd.Bool("fix"), checks)
}
-55
View File
@@ -1,55 +0,0 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package cmd
import (
"context"
"fmt"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
"github.com/urfave/cli/v3"
)
func newDoctorConvertCommand() *cli.Command {
return &cli.Command{
Name: "convert",
Usage: "Convert the database",
Description: "A command to convert an existing MySQL database from utf8 to utf8mb4 or MSSQL database from varchar to nvarchar",
Action: runDoctorConvert,
}
}
func runDoctorConvert(ctx context.Context, cmd *cli.Command) error {
if err := initDB(ctx); err != nil {
return err
}
log.Info("AppPath: %s", setting.AppPath)
log.Info("AppWorkPath: %s", setting.AppWorkPath)
log.Info("Custom path: %s", setting.CustomPath)
log.Info("Log path: %s", setting.Log.RootPath)
log.Info("Configuration file: %s", setting.CustomConf)
switch {
case setting.Database.Type.IsMySQL():
if err := db.ConvertDatabaseTable(); err != nil {
log.Fatal("Failed to convert database & table: %v", err)
return err
}
fmt.Println("Converted successfully, please confirm your database's character set is now utf8mb4")
case setting.Database.Type.IsMSSQL():
if err := db.ConvertVarcharToNVarchar(); err != nil {
log.Fatal("Failed to convert database from varchar to nvarchar: %v", err)
return err
}
fmt.Println("Converted successfully, please confirm your database's all columns character is NVARCHAR now")
default:
fmt.Println("This command can only be used with a MySQL or MSSQL database")
}
return nil
}
-34
View File
@@ -1,34 +0,0 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package cmd
import (
"context"
"testing"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/doctor"
"github.com/stretchr/testify/assert"
"github.com/urfave/cli/v3"
)
func TestDoctorRun(t *testing.T) {
doctor.Register(&doctor.Check{
Title: "Test Check",
Name: "test-check",
Run: func(ctx context.Context, logger log.Logger, autofix bool) error { return nil },
SkipDatabaseInitialization: true,
})
app := &cli.Command{
Commands: []*cli.Command{newDoctorCheckCommand()},
}
err := app.Run(t.Context(), []string{"./gitea", "check", "--run", "test-check"})
assert.NoError(t, err)
err = app.Run(t.Context(), []string{"./gitea", "check", "--run", "no-such"})
assert.ErrorContains(t, err, `unknown checks: "no-such"`)
err = app.Run(t.Context(), []string{"./gitea", "check", "--run", "test-check,no-such"})
assert.ErrorContains(t, err, `unknown checks: "no-such"`)
}
+153 -290
View File
@@ -1,328 +1,191 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2016 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package cmd
import (
"context"
"fmt"
"io/ioutil"
"log"
"os"
"path"
"path/filepath"
"strings"
"time"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/dump"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/json"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/storage"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/util"
"gitea.com/go-chi/session"
"github.com/urfave/cli/v3"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/setting"
"github.com/Unknwon/cae/zip"
"github.com/Unknwon/com"
"github.com/urfave/cli"
)
func newDumpCommand() *cli.Command {
return &cli.Command{
Name: "dump",
Usage: "Dump Gitea files and database",
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`,
Action: runDump,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "file",
Aliases: []string{"f"},
Usage: `Name of the dump file which will be created, default to "gitea-dump-{time}.zip". Supply '-' for stdout. See type for available types.`,
},
&cli.BoolFlag{
Name: "verbose",
Aliases: []string{"V"},
Usage: "Show process details",
},
&cli.BoolFlag{
Name: "quiet",
Aliases: []string{"q"},
Usage: "Only display warnings and errors",
},
&cli.StringFlag{
Name: "tempdir",
Aliases: []string{"t"},
Value: os.TempDir(),
Usage: "Temporary dir path",
},
&cli.StringFlag{
Name: "database",
Aliases: []string{"d"},
Usage: "Specify the database SQL syntax: sqlite3, mysql, mssql, postgres",
},
&cli.BoolFlag{
Name: "skip-repository",
Aliases: []string{"R"},
Usage: "Skip the repository dumping",
},
&cli.BoolFlag{
Name: "skip-log",
Aliases: []string{"L"},
Usage: "Skip the log dumping",
},
&cli.BoolFlag{
Name: "skip-custom-dir",
Usage: "Skip custom directory",
},
&cli.BoolFlag{
Name: "skip-lfs-data",
Usage: "Skip LFS data",
},
&cli.BoolFlag{
Name: "skip-attachment-data",
Usage: "Skip attachment data",
},
&cli.BoolFlag{
Name: "skip-package-data",
Usage: "Skip package data",
},
&cli.BoolFlag{
Name: "skip-index",
Usage: "Skip bleve index data",
},
&cli.BoolFlag{
Name: "skip-db",
Usage: "Skip database",
},
&cli.StringFlag{
Name: "type",
Usage: `Dump output format, default to "zip", supported types: ` + strings.Join(dump.SupportedOutputTypes, ", "),
},
// CmdDump represents the available dump sub-command.
var CmdDump = cli.Command{
Name: "dump",
Usage: "Dump Gitea files and database",
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`,
Action: runDump,
Flags: []cli.Flag{
cli.StringFlag{
Name: "config, c",
Value: "custom/conf/app.ini",
Usage: "Custom configuration file path",
},
}
cli.BoolFlag{
Name: "verbose, v",
Usage: "Show process details",
},
cli.StringFlag{
Name: "tempdir, t",
Value: os.TempDir(),
Usage: "Temporary dir path",
},
cli.StringFlag{
Name: "database, d",
Usage: "Specify the database SQL syntax",
},
},
}
func fatal(format string, args ...any) {
log.Fatal(format, args...)
}
func runDump(ctx context.Context, cmd *cli.Command) error {
setting.MustInstalled()
quite := cmd.Bool("quiet")
verbose := cmd.Bool("verbose")
if verbose && quite {
fatal("Option --quiet and --verbose cannot both be set")
func runDump(ctx *cli.Context) error {
if ctx.IsSet("config") {
setting.CustomConf = ctx.String("config")
}
setting.NewContext()
setting.NewServices() // cannot access session settings otherwise
models.LoadConfigs()
// outFileName is either "-" or a file name (will be made absolute)
outFileName, outType := dump.PrepareFileNameAndType(cmd.String("file"), cmd.String("type"))
if outType == "" {
fatal("Invalid output type")
}
outFile := os.Stdout
if outFileName != "-" {
var err error
if outFileName, err = filepath.Abs(outFileName); err != nil {
fatal("Unable to get absolute path of dump file: %v", err)
}
if exist, _ := util.IsExist(outFileName); exist {
fatal("Dump file %q exists", outFileName)
}
if outFile, err = os.Create(outFileName); err != nil {
fatal("Unable to create dump file %q: %v", outFileName, err)
}
defer outFile.Close()
}
setupConsoleLogger(util.Iif(quite, log.WARN, log.INFO), log.CanColorStderr, os.Stderr)
setting.DisableLoggerInit()
setting.LoadSettings() // cannot access session settings otherwise
err := db.InitEngine(ctx)
err := models.SetEngine()
if err != nil {
return err
}
if err = storage.Init(); err != nil {
return err
tmpDir := ctx.String("tempdir")
if _, err := os.Stat(tmpDir); os.IsNotExist(err) {
log.Fatalf("Path does not exist: %s", tmpDir)
}
dumper, err := dump.NewDumper(ctx, outType, outFile)
TmpWorkDir, err := ioutil.TempDir(tmpDir, "gitea-dump-")
if err != nil {
log.Fatalf("Failed to create tmp work directory: %v", err)
}
log.Printf("Creating tmp work dir: %s", TmpWorkDir)
reposDump := path.Join(TmpWorkDir, "gitea-repo.zip")
dbDump := path.Join(TmpWorkDir, "gitea-db.sql")
log.Printf("Dumping local repositories...%s", setting.RepoRootPath)
zip.Verbose = ctx.Bool("verbose")
if err := zip.PackTo(setting.RepoRootPath, reposDump, true); err != nil {
log.Fatalf("Failed to dump local repositories: %v", err)
}
targetDBType := ctx.String("database")
if len(targetDBType) > 0 && targetDBType != models.DbCfg.Type {
log.Printf("Dumping database %s => %s...", models.DbCfg.Type, targetDBType)
} else {
log.Printf("Dumping database...")
}
if err := models.DumpDatabase(dbDump, targetDBType); err != nil {
log.Fatalf("Failed to dump database: %v", err)
}
fileName := fmt.Sprintf("gitea-dump-%d.zip", time.Now().Unix())
log.Printf("Packing dump files...")
z, err := zip.Create(fileName)
if err != nil {
log.Fatalf("Failed to create %s: %v", fileName, err)
}
if err := z.AddFile("gitea-repo.zip", reposDump); err != nil {
log.Fatalf("Failed to include gitea-repo.zip: %v", err)
}
if err := z.AddFile("gitea-db.sql", dbDump); err != nil {
log.Fatalf("Failed to include gitea-db.sql: %v", err)
}
customDir, err := os.Stat(setting.CustomPath)
if err == nil && customDir.IsDir() {
if err := z.AddDir("custom", setting.CustomPath); err != nil {
log.Fatalf("Failed to include custom: %v", err)
}
} else {
log.Printf("Custom dir %s doesn't exist, skipped", setting.CustomPath)
}
if com.IsExist(setting.AppDataPath) {
log.Printf("Packing data directory...%s", setting.AppDataPath)
var sessionAbsPath string
if setting.SessionConfig.Provider == "file" {
if len(setting.SessionConfig.ProviderConfig) == 0 {
setting.SessionConfig.ProviderConfig = "data/sessions"
}
sessionAbsPath, _ = filepath.Abs(setting.SessionConfig.ProviderConfig)
}
if err := zipAddDirectoryExclude(z, "data", setting.AppDataPath, sessionAbsPath); err != nil {
log.Fatalf("Failed to include data directory: %v", err)
}
}
if err := z.AddDir("log", setting.LogRootPath); err != nil {
log.Fatalf("Failed to include log: %v", err)
}
if err = z.Close(); err != nil {
_ = os.Remove(fileName)
log.Fatalf("Failed to save %s: %v", fileName, err)
}
if err := os.Chmod(fileName, 0600); err != nil {
log.Printf("Can't change file access permissions mask to 0600: %v", err)
}
log.Printf("Removing tmp work dir: %s", TmpWorkDir)
if err := os.RemoveAll(TmpWorkDir); err != nil {
log.Fatalf("Failed to remove %s: %v", TmpWorkDir, err)
}
log.Printf("Finish dumping in file %s", fileName)
return nil
}
// zipAddDirectoryExclude zips absPath to specified zipPath inside z excluding excludeAbsPath
func zipAddDirectoryExclude(zip *zip.ZipArchive, zipPath, absPath string, excludeAbsPath string) error {
absPath, err := filepath.Abs(absPath)
if err != nil {
fatal("Failed to create archive %q: %v", outFile, err)
return err
}
dumper.Verbose = verbose
dumper.GlobalExcludeAbsPath(outFileName)
defer func() {
if err := dumper.Close(); err != nil {
fatal("Failed to save archive %q: %v", outFileName, err)
}
}()
if cmd.IsSet("skip-repository") && cmd.Bool("skip-repository") {
log.Info("Skip dumping local repositories")
} else {
log.Info("Dumping local repositories... %s", setting.RepoRootPath)
if err := dumper.AddRecursiveExclude("repos", setting.RepoRootPath, nil); err != nil {
fatal("Failed to include repositories: %v", err)
}
if cmd.IsSet("skip-lfs-data") && cmd.Bool("skip-lfs-data") {
log.Info("Skip dumping LFS data")
} else if !setting.LFS.StartServer {
log.Info("LFS isn't enabled. Skip dumping LFS data")
} else if err := storage.LFS.IterateObjects("", func(objPath string, object storage.Object) error {
info, err := object.Stat()
if err != nil {
return err
}
return dumper.AddFileByReader(object, info, path.Join("data", "lfs", objPath))
}); err != nil {
fatal("Failed to dump LFS objects: %v", err)
}
dir, err := os.Open(absPath)
if err != nil {
return err
}
defer dir.Close()
if cmd.Bool("skip-db") {
// Ensure that we don't dump the database file that may reside in setting.AppDataPath or elsewhere.
dumper.GlobalExcludeAbsPath(setting.Database.Path)
log.Info("Skipping database")
} else {
tmpDir := cmd.String("tempdir")
if _, err := os.Stat(tmpDir); os.IsNotExist(err) {
fatal("Path does not exist: %s", tmpDir)
}
zip.AddEmptyDir(zipPath)
dbDump, err := os.CreateTemp(tmpDir, "gitea-db.sql")
if err != nil {
fatal("Failed to create tmp file: %v", err)
}
defer func() {
_ = dbDump.Close()
if err := util.Remove(dbDump.Name()); err != nil {
log.Warn("Unable to remove temporary file: %s: Error: %v", dbDump.Name(), err)
}
}()
targetDBType := setting.DatabaseType(cmd.String("database"))
if targetDBType != "" && targetDBType != setting.Database.Type {
log.Info("Dumping database %s => %s...", setting.Database.Type, targetDBType)
} else {
log.Info("Dumping database...")
}
if err := db.DumpDatabase(dbDump.Name(), targetDBType); err != nil {
fatal("Failed to dump database: %v", err)
}
if err = dumper.AddFileByPath("gitea-db.sql", dbDump.Name()); err != nil {
fatal("Failed to include gitea-db.sql: %v", err)
}
files, err := dir.Readdir(0)
if err != nil {
return err
}
log.Info("Adding custom configuration file from %s", setting.CustomConf)
if err = dumper.AddFileByPath("app.ini", setting.CustomConf); err != nil {
fatal("Failed to include specified app.ini: %v", err)
}
if cmd.IsSet("skip-custom-dir") && cmd.Bool("skip-custom-dir") {
log.Info("Skipping custom directory")
} else {
customDir, err := os.Stat(setting.CustomPath)
if err == nil && customDir.IsDir() {
if is, _ := dump.IsSubdir(setting.AppDataPath, setting.CustomPath); !is {
if err := dumper.AddRecursiveExclude("custom", setting.CustomPath, nil); err != nil {
fatal("Failed to include custom: %v", err)
for _, file := range files {
currentAbsPath := path.Join(absPath, file.Name())
currentZipPath := path.Join(zipPath, file.Name())
if file.IsDir() {
if currentAbsPath != excludeAbsPath {
if err = zipAddDirectoryExclude(zip, currentZipPath, currentAbsPath, excludeAbsPath); err != nil {
return err
}
} else {
log.Info("Custom dir %s is inside data dir %s, skipped", setting.CustomPath, setting.AppDataPath)
}
} else {
log.Info("Custom dir %s doesn't exist, skipped", setting.CustomPath)
}
}
isExist, err := util.IsExist(setting.AppDataPath)
if err != nil {
log.Error("Unable to check if %s exists. Error: %v", setting.AppDataPath, err)
}
if isExist {
log.Info("Packing data directory...%s", setting.AppDataPath)
var excludes []string
if setting.SessionConfig.OriginalProvider == "file" {
var opts session.Options
if err = json.Unmarshal([]byte(setting.SessionConfig.ProviderConfig), &opts); err != nil {
if err = zip.AddFile(currentZipPath, currentAbsPath); err != nil {
return err
}
excludes = append(excludes, opts.ProviderConfig)
}
if cmd.IsSet("skip-index") && cmd.Bool("skip-index") {
excludes = append(excludes, setting.Indexer.RepoPath)
excludes = append(excludes, setting.Indexer.IssuePath)
}
excludes = append(excludes, setting.RepoRootPath)
excludes = append(excludes, setting.LFS.Storage.Path)
excludes = append(excludes, setting.Attachment.Storage.Path)
excludes = append(excludes, setting.Packages.Storage.Path)
excludes = append(excludes, setting.RepoArchive.Storage.Path)
excludes = append(excludes, setting.Log.RootPath)
if err := dumper.AddRecursiveExclude("data", setting.AppDataPath, excludes); err != nil {
fatal("Failed to include data directory: %v", err)
}
}
if cmd.IsSet("skip-attachment-data") && cmd.Bool("skip-attachment-data") {
log.Info("Skip dumping attachment data")
} else if err := storage.Attachments.IterateObjects("", func(objPath string, object storage.Object) error {
info, err := object.Stat()
if err != nil {
return err
}
return dumper.AddFileByReader(object, info, path.Join("data", "attachments", objPath))
}); err != nil {
fatal("Failed to dump attachments: %v", err)
}
if cmd.IsSet("skip-package-data") && cmd.Bool("skip-package-data") {
log.Info("Skip dumping package data")
} else if !setting.Packages.Enabled {
log.Info("Packages isn't enabled. Skip dumping package data")
} else if err := storage.Packages.IterateObjects("", func(objPath string, object storage.Object) error {
info, err := object.Stat()
if err != nil {
return err
}
return dumper.AddFileByReader(object, info, path.Join("data", "packages", objPath))
}); err != nil {
fatal("Failed to dump packages: %v", err)
}
// Doesn't check if LogRootPath exists before processing --skip-log intentionally,
// ensuring that it's clear the dump is skipped whether the directory's initialized
// yet or not.
if cmd.IsSet("skip-log") && cmd.Bool("skip-log") {
log.Info("Skip dumping log files")
} else {
isExist, err := util.IsExist(setting.Log.RootPath)
if err != nil {
log.Error("Unable to check if %s exists. Error: %v", setting.Log.RootPath, err)
}
if isExist {
if err := dumper.AddRecursiveExclude("log", setting.Log.RootPath, nil); err != nil {
fatal("Failed to include log: %v", err)
}
}
}
if outFileName == "-" {
log.Info("Finish dumping to stdout")
} else {
if err = os.Chmod(outFileName, 0o600); err != nil {
log.Info("Can't change file access permissions mask to 0600: %v", err)
}
log.Info("Finish dumping in file %s", outFileName)
}
return nil
}
-195
View File
@@ -1,195 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package cmd
import (
"context"
"errors"
"fmt"
"os"
"strings"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/git"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
base "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/migration"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/util"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/convert"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/migrations"
"github.com/urfave/cli/v3"
)
func newDumpRepositoryCommand() *cli.Command {
return &cli.Command{
Name: "dump-repo",
Usage: "Dump the repository from git/github/gitea/gitlab",
Description: "This is a command for dumping the repository data.",
Action: runDumpRepository,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "git_service",
Value: "",
Usage: "Git service, git, github, gitea, gitlab. If clone_addr could be recognized, this could be ignored.",
},
&cli.StringFlag{
Name: "repo_dir",
Aliases: []string{"r"},
Value: "./data",
Usage: "Repository dir path to store the data",
},
&cli.StringFlag{
Name: "clone_addr",
Value: "",
Usage: "The URL will be clone, currently could be a git/github/gitea/gitlab http/https URL",
},
&cli.StringFlag{
Name: "auth_username",
Value: "",
Usage: "The username to visit the clone_addr",
},
&cli.StringFlag{
Name: "auth_password",
Value: "",
Usage: "The password to visit the clone_addr",
},
&cli.StringFlag{
Name: "auth_token",
Value: "",
Usage: "The personal token to visit the clone_addr",
},
&cli.StringFlag{
Name: "owner_name",
Value: "",
Usage: "The data will be stored on a directory with owner name if not empty",
},
&cli.StringFlag{
Name: "repo_name",
Value: "",
Usage: "The data will be stored on a directory with repository name if not empty",
},
&cli.StringFlag{
Name: "units",
Value: "",
Usage: `Which items will be migrated, one or more units should be separated as comma.
wiki, issues, labels, releases, release_assets, milestones, pull_requests, comments are allowed. Empty means all units.`,
},
},
}
}
func runDumpRepository(ctx context.Context, cmd *cli.Command) error {
setupConsoleLogger(log.INFO, log.CanColorStderr, os.Stderr)
setting.DisableLoggerInit()
setting.LoadSettings() // cannot access skip_tls_verify settings otherwise
if err := initDB(ctx); err != nil {
return err
}
// migrations.GiteaLocalUploader depends on git module
if err := git.InitSimple(); err != nil {
return err
}
log.Info("AppPath: %s", setting.AppPath)
log.Info("AppWorkPath: %s", setting.AppWorkPath)
log.Info("Custom path: %s", setting.CustomPath)
log.Info("Log path: %s", setting.Log.RootPath)
log.Info("Configuration file: %s", setting.CustomConf)
var (
serviceType structs.GitServiceType
cloneAddr = cmd.String("clone_addr")
serviceStr = cmd.String("git_service")
)
if strings.HasPrefix(strings.ToLower(cloneAddr), "https://github.com/") {
serviceStr = "github"
} else if strings.HasPrefix(strings.ToLower(cloneAddr), "https://gitlab.com/") {
serviceStr = "gitlab"
} else if strings.HasPrefix(strings.ToLower(cloneAddr), "https://gitea.com/") {
serviceStr = "gitea"
}
if serviceStr == "" {
return errors.New("git_service missed or clone_addr cannot be recognized")
}
serviceType = convert.ToGitServiceType(serviceStr)
opts := base.MigrateOptions{
GitServiceType: serviceType,
CloneAddr: cloneAddr,
AuthUsername: cmd.String("auth_username"),
AuthPassword: cmd.String("auth_password"),
AuthToken: cmd.String("auth_token"),
RepoName: cmd.String("repo_name"),
}
if len(cmd.String("units")) == 0 {
opts.Wiki = true
opts.Issues = true
opts.Milestones = true
opts.Labels = true
opts.Releases = true
opts.Comments = true
opts.PullRequests = true
opts.ReleaseAssets = true
} else {
units := strings.SplitSeq(cmd.String("units"), ",")
for unit := range units {
switch strings.ToLower(strings.TrimSpace(unit)) {
case "":
continue
case "wiki":
opts.Wiki = true
case "issues":
opts.Issues = true
case "milestones":
opts.Milestones = true
case "labels":
opts.Labels = true
case "releases":
opts.Releases = true
case "release_assets":
opts.ReleaseAssets = true
case "comments":
opts.Comments = true
case "pull_requests":
opts.PullRequests = true
default:
return errors.New("invalid unit: " + unit)
}
}
}
// the repo_dir will be removed if error occurs in DumpRepository
// make sure the directory doesn't exist or is empty, prevent from deleting user files
repoDir := cmd.String("repo_dir")
if exists, err := util.IsExist(repoDir); err != nil {
return fmt.Errorf("unable to stat repo_dir %q: %w", repoDir, err)
} else if exists {
if isDir, _ := util.IsDir(repoDir); !isDir {
return fmt.Errorf("repo_dir %q already exists but it's not a directory", repoDir)
}
if dir, _ := os.ReadDir(repoDir); len(dir) > 0 {
return fmt.Errorf("repo_dir %q is not empty", repoDir)
}
}
if err := migrations.DumpRepository(
ctx,
repoDir,
cmd.String("owner_name"),
opts,
); err != nil {
log.Fatal("Failed to dump repository: %v", err)
return err
}
log.Trace("Dump finished!!!")
return nil
}
-314
View File
@@ -1,314 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package cmd
import (
"context"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/assetfs"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/glob"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/options"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/public"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/templates"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/util"
"github.com/urfave/cli/v3"
)
var matchedAssetFiles []assetFile
func newEmbeddedCommand() *cli.Command {
return &cli.Command{
Name: "embedded",
Usage: "Extract embedded resources",
Description: "A command for extracting embedded resources, like templates and images",
Commands: []*cli.Command{
newEmbeddedListCommand(),
newEmbeddedViewCommand(),
newEmbeddedExtractCommand(),
},
}
}
func newEmbeddedListCommand() *cli.Command {
return &cli.Command{
Name: "list",
Usage: "List files matching the given pattern",
Action: runList,
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "include-vendored",
Aliases: []string{"vendor"},
Usage: "Include files under public/vendor as well",
},
},
}
}
func newEmbeddedViewCommand() *cli.Command {
return &cli.Command{
Name: "view",
Usage: "View a file matching the given pattern",
Action: runView,
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "include-vendored",
Aliases: []string{"vendor"},
Usage: "Include files under public/vendor as well",
},
},
}
}
func newEmbeddedExtractCommand() *cli.Command {
return &cli.Command{
Name: "extract",
Usage: "Extract resources",
Action: runExtract,
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "include-vendored",
Aliases: []string{"vendor"},
Usage: "Include files under public/vendor as well",
},
&cli.BoolFlag{
Name: "overwrite",
Usage: "Overwrite files if they already exist",
},
&cli.BoolFlag{
Name: "rename",
Usage: "Rename files as {name}.bak if they already exist (overwrites previous .bak)",
},
&cli.BoolFlag{
Name: "custom",
Usage: "Extract to the 'custom' directory as per app.ini",
},
&cli.StringFlag{
Name: "destination",
Aliases: []string{"dest-dir"},
Usage: "Extract to the specified directory",
},
},
}
}
type assetFile struct {
fs *assetfs.LayeredFS
name string
path string
}
func initEmbeddedExtractor(c *cli.Command) error {
setupConsoleLogger(log.ERROR, log.CanColorStderr, os.Stderr)
patterns, err := compileCollectPatterns(c.Args().Slice())
if err != nil {
return err
}
collectAssetFilesByPattern(c, patterns, "options", options.BuiltinAssets())
collectAssetFilesByPattern(c, patterns, "public", public.BuiltinAssets())
collectAssetFilesByPattern(c, patterns, "templates", templates.BuiltinAssets())
return nil
}
func runList(_ context.Context, c *cli.Command) error {
if err := runListDo(c); err != nil {
_, _ = fmt.Fprintf(os.Stderr, "%v\n", err)
return err
}
return nil
}
func runView(_ context.Context, c *cli.Command) error {
if err := runViewDo(c); err != nil {
_, _ = fmt.Fprintf(os.Stderr, "%v\n", err)
return err
}
return nil
}
func runExtract(_ context.Context, c *cli.Command) error {
if err := runExtractDo(c); err != nil {
_, _ = fmt.Fprintf(os.Stderr, "%v\n", err)
return err
}
return nil
}
func runListDo(c *cli.Command) error {
if err := initEmbeddedExtractor(c); err != nil {
return err
}
for _, a := range matchedAssetFiles {
fmt.Println(a.path)
}
return nil
}
func runViewDo(c *cli.Command) error {
if err := initEmbeddedExtractor(c); err != nil {
return err
}
if len(matchedAssetFiles) == 0 {
return errors.New("no files matched the given pattern")
} else if len(matchedAssetFiles) > 1 {
return errors.New("too many files matched the given pattern, try to be more specific")
}
data, err := matchedAssetFiles[0].fs.ReadFile(matchedAssetFiles[0].name)
if err != nil {
return fmt.Errorf("%s: %w", matchedAssetFiles[0].path, err)
}
if _, err = os.Stdout.Write(data); err != nil {
return fmt.Errorf("%s: %w", matchedAssetFiles[0].path, err)
}
return nil
}
func runExtractDo(c *cli.Command) error {
if err := initEmbeddedExtractor(c); err != nil {
return err
}
if c.NArg() == 0 {
return errors.New("a list of pattern of files to extract is mandatory (e.g. '**' for all)")
}
destdir := "."
if c.IsSet("destination") {
destdir = c.String("destination")
} else if c.Bool("custom") {
destdir = setting.CustomPath
fmt.Println("Using app.ini at", setting.CustomConf)
}
fi, err := os.Stat(destdir)
if errors.Is(err, os.ErrNotExist) {
// In case Windows users attempt to provide a forward-slash path
wdestdir := filepath.FromSlash(destdir)
if wfi, werr := os.Stat(wdestdir); werr == nil {
destdir = wdestdir
fi = wfi
err = nil
}
}
if err != nil {
return fmt.Errorf("%s: %s", destdir, err)
} else if !fi.IsDir() {
return fmt.Errorf("destination %q is not a directory", destdir)
}
fmt.Printf("Extracting to %s:\n", destdir)
overwrite := c.Bool("overwrite")
rename := c.Bool("rename")
for _, a := range matchedAssetFiles {
if err := extractAsset(destdir, a, overwrite, rename); err != nil {
// Non-fatal error
_, _ = fmt.Fprintf(os.Stderr, "%s: %v\n", a.path, err)
}
}
return nil
}
func extractAsset(d string, a assetFile, overwrite, rename bool) error {
dest := filepath.Join(d, filepath.FromSlash(a.path))
dir := filepath.Dir(dest)
data, err := a.fs.ReadFile(a.name)
if err != nil {
return fmt.Errorf("%s: %w", a.path, err)
}
if err := os.MkdirAll(dir, os.ModePerm); err != nil {
return fmt.Errorf("%s: %w", dir, err)
}
perms := os.ModePerm & 0o666
fi, err := os.Lstat(dest)
if err != nil {
if !errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("%s: %w", dest, err)
}
} else if !overwrite && !rename {
fmt.Printf("%s already exists; skipped.\n", dest)
return nil
} else if !fi.Mode().IsRegular() {
return fmt.Errorf("%s already exists, but it's not a regular file", dest)
} else if rename {
if err := util.Rename(dest, dest+".bak"); err != nil {
return fmt.Errorf("error creating backup for %s: %w", dest, err)
}
// Attempt to respect file permissions mask (even if user:group will be set anew)
perms = fi.Mode()
}
file, err := os.OpenFile(dest, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, perms)
if err != nil {
return fmt.Errorf("%s: %w", dest, err)
}
defer file.Close()
if _, err = file.Write(data); err != nil {
return fmt.Errorf("%s: %w", dest, err)
}
fmt.Println(dest)
return nil
}
func collectAssetFilesByPattern(c *cli.Command, globs []glob.Glob, path string, layer *assetfs.Layer) {
fs := assetfs.Layered(layer)
files, err := fs.ListAllFiles(".", true)
if err != nil {
log.Error("Error listing files in %q: %v", path, err)
return
}
for _, name := range files {
if path == "public" &&
strings.HasPrefix(name, "vendor/") &&
!c.Bool("include-vendored") {
continue
}
matchName := path + "/" + name
for _, g := range globs {
if g.Match(matchName) {
matchedAssetFiles = append(matchedAssetFiles, assetFile{fs: fs, name: name, path: path + "/" + name})
break
}
}
}
}
func compileCollectPatterns(args []string) (_ []glob.Glob, err error) {
if len(args) == 0 {
args = []string{"**"}
}
pat := make([]glob.Glob, len(args))
for i := range args {
if pat[i], err = glob.Compile(args[i], '/'); err != nil {
return nil, fmt.Errorf("invalid glob patterh %q: %w", args[i], err)
}
}
return pat, nil
}
-105
View File
@@ -1,105 +0,0 @@
// Copyright 2016 The Gogs Authors. All rights reserved.
// Copyright 2016 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package cmd
import (
"context"
"fmt"
"os"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/generate"
"github.com/mattn/go-isatty"
"github.com/urfave/cli/v3"
)
func newGenerateCommand() *cli.Command {
return &cli.Command{
Name: "generate",
Usage: "Generate Gitea's secrets/keys/tokens",
Commands: []*cli.Command{
newGenerateSecretCommand(),
},
}
}
func newGenerateSecretCommand() *cli.Command {
return &cli.Command{
Name: "secret",
Usage: "Generate a secret token",
Commands: []*cli.Command{
newGenerateInternalTokenCommand(),
newGenerateLfsJWTSecretCommand(),
newGenerateSecretKeyCommand(),
},
}
}
func newGenerateInternalTokenCommand() *cli.Command {
return &cli.Command{
Name: "INTERNAL_TOKEN",
Usage: "Generate a new INTERNAL_TOKEN",
Action: runGenerateInternalToken,
}
}
func newGenerateLfsJWTSecretCommand() *cli.Command {
return &cli.Command{
Name: "JWT_SECRET",
Aliases: []string{"LFS_JWT_SECRET"},
Usage: "Generate a new JWT_SECRET",
Action: runGenerateLfsJwtSecret,
}
}
func newGenerateSecretKeyCommand() *cli.Command {
return &cli.Command{
Name: "SECRET_KEY",
Usage: "Generate a new SECRET_KEY",
Action: runGenerateSecretKey,
}
}
func runGenerateInternalToken(_ context.Context, c *cli.Command) error {
internalToken, err := generate.NewInternalToken()
if err != nil {
return err
}
fmt.Printf("%s", internalToken)
if isatty.IsTerminal(os.Stdout.Fd()) {
fmt.Printf("\n")
}
return nil
}
func runGenerateLfsJwtSecret(_ context.Context, c *cli.Command) error {
_, jwtSecretBase64 := generate.NewJwtSecretWithBase64()
fmt.Printf("%s", jwtSecretBase64)
if isatty.IsTerminal(os.Stdout.Fd()) {
fmt.Printf("\n")
}
return nil
}
func runGenerateSecretKey(_ context.Context, c *cli.Command) error {
secretKey, err := generate.NewSecretKey()
if err != nil {
return err
}
// codeql[disable-next-line=go/clear-text-logging]
fmt.Printf("%s", secretKey)
if isatty.IsTerminal(os.Stdout.Fd()) {
fmt.Printf("\n")
}
return nil
}
-150
View File
@@ -1,150 +0,0 @@
// Copyright 2018 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
// Package cmd provides subcommands to the gitea binary - such as "web" or
// "admin".
package cmd
import (
"context"
"errors"
"fmt"
"io"
"os"
"os/signal"
"strings"
"syscall"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
"github.com/urfave/cli/v3"
)
// argsSet checks that all the required arguments are set. args is a list of
// arguments that must be set in the passed Context.
func argsSet(c *cli.Command, args ...string) error {
for _, a := range args {
if !c.IsSet(a) {
return errors.New(a + " is not set")
}
if c.Value(a) == nil {
return errors.New(a + " is required")
}
}
return nil
}
// confirm waits for user input which confirms an action
func confirm() (bool, error) {
var response string
_, err := fmt.Scanln(&response)
if err != nil {
return false, err
}
switch strings.ToLower(response) {
case "y", "yes":
return true, nil
case "n", "no":
return false, nil
default:
return false, errors.New(response + " isn't a correct confirmation string")
}
}
func initDB(ctx context.Context) error {
setting.MustInstalled()
setting.LoadDBSetting()
setting.InitSQLLoggersForCli(log.INFO)
if setting.Database.Type == "" {
log.Fatal(`Database settings are missing from the configuration file: %q.
Ensure you are running in the correct environment or set the correct configuration file with -c.
If this is the intended configuration file complete the [database] section.`, setting.CustomConf)
}
if err := db.InitEngine(ctx); err != nil {
return fmt.Errorf("unable to initialize the database using the configuration in %q. Error: %w", setting.CustomConf, err)
}
return nil
}
func installSignals() (context.Context, context.CancelFunc) {
ctx, cancel := context.WithCancel(context.Background())
go func() {
// install notify
signalChannel := make(chan os.Signal, 1)
signal.Notify(
signalChannel,
syscall.SIGINT,
syscall.SIGTERM,
)
select {
case <-signalChannel:
case <-ctx.Done():
}
cancel()
signal.Reset()
}()
return ctx, cancel
}
func setupConsoleLogger(level log.Level, colorize bool, out io.Writer) {
if out != os.Stdout && out != os.Stderr {
panic("setupConsoleLogger can only be used with os.Stdout or os.Stderr")
}
writeMode := log.WriterMode{
Level: level,
Colorize: colorize,
WriterOption: log.WriterConsoleOption{Stderr: out == os.Stderr},
}
writer := log.NewEventWriterConsole("console-default", writeMode)
log.GetManager().GetLogger(log.DEFAULT).ReplaceAllWriters(writer)
}
func globalBool(c *cli.Command, name string) bool {
for _, ctx := range c.Lineage() {
if ctx.Bool(name) {
return true
}
}
return false
}
// PrepareConsoleLoggerLevel by default, use INFO level for console logger, but some sub-commands (for git/ssh protocol) shouldn't output any log to stdout.
// Any log appears in git stdout pipe will break the git protocol, eg: client can't push and hangs forever.
func PrepareConsoleLoggerLevel(defaultLevel log.Level) func(context.Context, *cli.Command) (context.Context, error) {
return func(ctx context.Context, c *cli.Command) (context.Context, error) {
if setting.InstallLock {
// During config loading, there might also be logs (for example: deprecation warnings).
// It must make sure that console logger is set up before config is loaded.
log.Error("Config is loaded before console logger is setup, it will cause bugs. Please fix it. CustomConf=%s", setting.CustomConf)
return nil, errors.New("console logger must be setup before config is loaded")
}
level := defaultLevel
if globalBool(c, "quiet") {
level = log.FATAL
}
if globalBool(c, "debug") || globalBool(c, "verbose") {
level = log.TRACE
}
log.SetupStderrLogger(log.DEFAULT, "console-stderr", level)
return ctx, nil
}
}
func isValidDefaultSubCommand(cmd *cli.Command) (string, bool) {
// Dirty patch for urfave/cli's strange design.
// "./gitea bad-cmd" should not start the web server.
rootArgs := cmd.Root().Args().Slice()
if len(rootArgs) != 0 && rootArgs[0] != cmd.Name {
return rootArgs[0], false
}
return "", true
}
+187 -785
View File
File diff suppressed because it is too large Load Diff

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