diff --git a/.air.toml b/.air.toml index 069a889243..71d7623dfb 100644 --- a/.air.toml +++ b/.air.toml @@ -4,7 +4,9 @@ tmp_dir = ".air" [build] cmd = "make backend" bin = "gitea" +delay = 1000 include_ext = ["go", "tmpl"] -exclude_dir = ["modules/git/tests", "services/gitdiff/testdata", "modules/avatar/testdata", "models/fixtures", "models/migrations/fixtures", "modules/migration/file_format_testdata", "modules/avatar/identicon/testdata"] +include_file = ["main.go"] include_dir = ["cmd", "models", "modules", "options", "routers", "services"] +exclude_dir = ["modules/git/tests", "services/gitdiff/testdata", "modules/avatar/testdata", "models/fixtures", "models/migrations/fixtures", "modules/migration/file_format_testdata", "modules/avatar/identicon/testdata"] exclude_regex = ["_test.go$", "_gen.go$"] diff --git a/.drone.yml b/.drone.yml index 5f1167d286..5096ce781f 100644 --- a/.drone.yml +++ b/.drone.yml @@ -59,7 +59,7 @@ steps: - name: lint-backend-windows image: gitea/test_env:linux-1.20-amd64 # https://gitea.com/gitea/test-env commands: - - make golangci-lint-windows vet + - make lint-go-windows lint-go-vet environment: GOPROXY: https://goproxy.io # proxy.golang.org is blocked in China, this proxy is not GOSUMDB: sum.golang.org @@ -756,11 +756,11 @@ steps: # TODO: We should probably build all dependencies into a test image - name: test-e2e - image: mcr.microsoft.com/playwright:v1.32.1-focal + image: mcr.microsoft.com/playwright:v1.32.3-focal commands: - - curl -sLO https://go.dev/dl/go1.20.linux-amd64.tar.gz && tar -C /usr/local -xzf go1.20.linux-amd64.tar.gz + - apt-get -qq update && apt-get -qqy install jq build-essential + - curl -fsSL "https://go.dev/dl/$(curl -s 'https://go.dev/dl/?mode=json' | jq -r '.[].version' | sort -Vr | head -1).linux-amd64.tar.gz" | tar -xz -C /usr/local - groupadd --gid 1001 gitea && useradd -m --gid 1001 --uid 1001 gitea - - apt-get -qq update && apt-get -qqy install build-essential - ./build/test-env-prepare.sh - su gitea bash -c "export PATH=$PATH:/usr/local/go/bin && timeout -s ABRT 40m make test-e2e-sqlite" environment: @@ -773,109 +773,6 @@ steps: - name: deps path: /go ---- -kind: pipeline -name: update_translations - -platform: - os: linux - arch: arm64 - -trigger: - branch: - - main - event: - - cron - cron: - - update_translations - -steps: - - name: download - image: jonasfranz/crowdin - pull: always - settings: - download: true - export_dir: options/locale/ - ignore_branch: true - project_identifier: gitea - environment: - CROWDIN_KEY: - from_secret: crowdin_key - - - name: update - image: alpine:3.17 - pull: always - commands: - - ./build/update-locales.sh - - - name: push - image: appleboy/drone-git-push - pull: always - settings: - author_email: "teabot@gitea.io" - author_name: GiteaBot - branch: main - commit: true - commit_message: "[skip ci] Updated translations via Crowdin" - remote: "git@github.com:go-gitea/gitea.git" - environment: - DRONE_COMMIT_AUTHOR_EMAIL: "teabot@gitea.io" - DRONE_COMMIT_AUTHOR: GiteaBot - GIT_PUSH_SSH_KEY: - from_secret: git_push_ssh_key - - - name: upload_translations - image: jonasfranz/crowdin - pull: always - settings: - files: - locale_en-US.ini: options/locale/locale_en-US.ini - ignore_branch: true - project_identifier: gitea - environment: - CROWDIN_KEY: - from_secret: crowdin_key - ---- -kind: pipeline -type: docker -name: update_gitignore_and_licenses - -platform: - os: linux - arch: arm64 - -trigger: - branch: - - main - event: - - cron - cron: - - update_gitignore_and_licenses - -steps: - - name: download - image: gitea/test_env:linux-1.20-amd64 - pull: always - commands: - - timeout -s ABRT 40m make generate-license generate-gitignore - - - name: push - image: appleboy/drone-git-push - pull: always - settings: - author_email: "teabot@gitea.io" - author_name: "GiteaBot" - branch: main - commit: true - commit_message: "[skip ci] Updated licenses and gitignores" - remote: "git@github.com:go-gitea/gitea.git" - environment: - DRONE_COMMIT_AUTHOR_EMAIL: "teabot@gitea.io" - DRONE_COMMIT_AUTHOR: "GiteaBot" - GIT_PUSH_SSH_KEY: - from_secret: git_push_ssh_key - --- kind: pipeline type: docker @@ -1369,7 +1266,7 @@ steps: pull: always settings: auto_tag: false - tags: dev-linux-amd64 + tags: nightly-linux-amd64 repo: gitea/gitea build_args: - GOPROXY=https://goproxy.io @@ -1391,7 +1288,7 @@ steps: settings: dockerfile: Dockerfile.rootless auto_tag: false - tags: dev-linux-amd64-rootless + tags: nightly-linux-amd64-rootless repo: gitea/gitea build_args: - GOPROXY=https://goproxy.io @@ -1711,7 +1608,7 @@ steps: pull: always settings: auto_tag: false - tags: dev-linux-arm64 + tags: nightly-linux-arm64 repo: gitea/gitea build_args: - GOPROXY=https://goproxy.io @@ -1733,7 +1630,7 @@ steps: settings: dockerfile: Dockerfile.rootless auto_tag: false - tags: dev-linux-arm64-rootless + tags: nightly-linux-arm64-rootless repo: gitea/gitea build_args: - GOPROXY=https://goproxy.io diff --git a/.eslintrc.yaml b/.eslintrc.yaml index 81b3a10f30..f87a4149ac 100644 --- a/.eslintrc.yaml +++ b/.eslintrc.yaml @@ -12,6 +12,7 @@ plugins: - eslint-plugin-unicorn - eslint-plugin-import - eslint-plugin-jquery + - eslint-plugin-no-jquery - eslint-plugin-sonarjs - eslint-plugin-custom-elements @@ -65,7 +66,7 @@ rules: curly: [0] custom-elements/expose-class-on-global: [0] custom-elements/extends-correct-class: [2] - custom-elements/file-name-matches-element: [0] + custom-elements/file-name-matches-element: [2] custom-elements/no-constructor: [2] custom-elements/no-customized-built-in-elements: [2] custom-elements/no-dom-traversal-in-attributechangedcallback: [2] @@ -270,6 +271,99 @@ rules: no-invalid-this: [0] no-irregular-whitespace: [2] no-iterator: [2] + no-jquery/no-ajax-events: [2] + no-jquery/no-ajax: [0] + no-jquery/no-and-self: [2] + no-jquery/no-animate-toggle: [2] + no-jquery/no-animate: [2] + no-jquery/no-append-html: [0] + no-jquery/no-attr: [0] + no-jquery/no-bind: [2] + no-jquery/no-box-model: [2] + no-jquery/no-browser: [2] + no-jquery/no-camel-case: [2] + no-jquery/no-class-state: [0] + no-jquery/no-class: [0] + no-jquery/no-clone: [2] + no-jquery/no-closest: [0] + no-jquery/no-constructor-attributes: [2] + no-jquery/no-contains: [2] + no-jquery/no-context-prop: [2] + no-jquery/no-css: [0] + no-jquery/no-data: [0] + no-jquery/no-deferred: [2] + no-jquery/no-delegate: [2] + no-jquery/no-each-collection: [0] + no-jquery/no-each-util: [0] + no-jquery/no-each: [0] + no-jquery/no-error-shorthand: [2] + no-jquery/no-error: [2] + no-jquery/no-escape-selector: [2] + no-jquery/no-event-shorthand: [2] + no-jquery/no-extend: [2] + no-jquery/no-fade: [2] + no-jquery/no-filter: [0] + no-jquery/no-find-collection: [0] + no-jquery/no-find-util: [2] + no-jquery/no-find: [0] + no-jquery/no-fx-interval: [2] + no-jquery/no-global-eval: [2] + no-jquery/no-global-selector: [0] + no-jquery/no-grep: [2] + no-jquery/no-has: [2] + no-jquery/no-hold-ready: [2] + no-jquery/no-html: [0] + no-jquery/no-in-array: [2] + no-jquery/no-is-array: [2] + no-jquery/no-is-empty-object: [2] + no-jquery/no-is-function: [2] + no-jquery/no-is-numeric: [2] + no-jquery/no-is-plain-object: [2] + no-jquery/no-is-window: [2] + no-jquery/no-is: [0] + no-jquery/no-jquery-constructor: [0] + no-jquery/no-live: [2] + no-jquery/no-load-shorthand: [2] + no-jquery/no-load: [2] + no-jquery/no-map-collection: [0] + no-jquery/no-map-util: [2] + no-jquery/no-map: [0] + no-jquery/no-merge: [2] + no-jquery/no-node-name: [2] + no-jquery/no-noop: [2] + no-jquery/no-now: [2] + no-jquery/no-on-ready: [2] + no-jquery/no-other-methods: [0] + no-jquery/no-other-utils: [2] + no-jquery/no-param: [2] + no-jquery/no-parent: [0] + no-jquery/no-parents: [0] + no-jquery/no-parse-html-literal: [0] + no-jquery/no-parse-html: [2] + no-jquery/no-parse-json: [2] + no-jquery/no-parse-xml: [2] + no-jquery/no-prop: [0] + no-jquery/no-proxy: [2] + no-jquery/no-ready-shorthand: [2] + no-jquery/no-ready: [2] + no-jquery/no-selector-prop: [2] + no-jquery/no-serialize: [2] + no-jquery/no-size: [2] + no-jquery/no-sizzle: [0] + no-jquery/no-slide: [2] + no-jquery/no-sub: [2] + no-jquery/no-support: [2] + no-jquery/no-text: [0] + no-jquery/no-trigger: [0] + no-jquery/no-trim: [2] + no-jquery/no-type: [2] + no-jquery/no-unique: [2] + no-jquery/no-unload-shorthand: [2] + no-jquery/no-val: [0] + no-jquery/no-visibility: [2] + no-jquery/no-when: [2] + no-jquery/no-wrap: [2] + no-jquery/variable-pattern: [0] no-label-var: [2] no-labels: [0] # handled by no-restricted-syntax no-lone-blocks: [2] @@ -500,7 +594,7 @@ rules: unicorn/prefer-default-parameters: [0] unicorn/prefer-event-key: [2] unicorn/prefer-event-target: [2] - unicorn/prefer-export-from: [2] + unicorn/prefer-export-from: [2, {ignoreUsedVariables: true}] unicorn/prefer-includes: [2] unicorn/prefer-json-parse-buffer: [0] unicorn/prefer-logical-operator-over-ternary: [2] diff --git a/.gitattributes b/.gitattributes index 34482b9e8c..16167a2cd6 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,7 +1,9 @@ * text=auto eol=lf *.tmpl linguist-language=Handlebars /assets/*.json linguist-generated +/public/img/svg/*.svg linguist-generated /public/vendor/** -text -eol linguist-vendored +/templates/swagger/v1_json.tmpl linguist-generated /vendor/** -text -eol linguist-vendored /web_src/fomantic/build/** linguist-generated /web_src/js/vendor/** -text -eol linguist-vendored diff --git a/.github/workflows/cron-licenses.yml b/.github/workflows/cron-licenses.yml new file mode 100644 index 0000000000..116b8dd095 --- /dev/null +++ b/.github/workflows/cron-licenses.yml @@ -0,0 +1,28 @@ +on: + schedule: + # weekly on Monday at 0:07 UTC + - cron: "7 0 * * 1" + +name: Update licenses and gitignores + +jobs: + cron: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - uses: actions/setup-go@v3 + with: + go-version: '>=1.20.1' + - name: update licenses and gitignores + run: timeout -s ABRT 40m make generate-license generate-gitignore + - name: push translations to repo + uses: appleboy/git-push-action@v0.0.2 + with: + author_email: "teabot@gitea.io" + author_name: GiteaBot + branch: main + commit: true + commit_message: "[skip ci] Updated licenses and gitignores" + remote: "git@github.com:go-gitea/gitea.git" + ssh_key: ${{ secrets.DEPLOY_KEY }} diff --git a/.github/workflows/cron-translations.yml b/.github/workflows/cron-translations.yml new file mode 100644 index 0000000000..31262fd3dd --- /dev/null +++ b/.github/workflows/cron-translations.yml @@ -0,0 +1,47 @@ +on: + schedule: + - cron: "7 0 * * *" # every day at 0:07 UTC + +name: Pull translations from Crowdin + +jobs: + crowdin_pull: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: download from crowdin + uses: docker://jonasfranz/crowdin + env: + CROWDIN_KEY: ${{ secrets.CROWDIN_KEY }} + PLUGIN_DOWNLOAD: true + PLUGIN_EXPORT_DIR: options/locale/ + PLUGIN_IGNORE_BRANCH: true + PLUGIN_PROJECT_IDENTIFIER: gitea + - name: update locales + run: ./build/update-locales.sh + - name: push translations to repo + uses: appleboy/git-push-action@v0.0.2 + with: + author_email: "teabot@gitea.io" + author_name: GiteaBot + branch: main + commit: true + commit_message: "[skip ci] Updated translations via Crowdin" + remote: "git@github.com:go-gitea/gitea.git" + ssh_key: ${{ secrets.DEPLOY_KEY }} + crowdin_push: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: push translations to crowdin + uses: docker://jonasfranz/crowdin + env: + CROWDIN_KEY: ${{ secrets.CROWDIN_KEY }} + PLUGIN_UPLOAD: true + PLUGIN_IGNORE_BRANCH: true + PLUGIN_PROJECT_IDENTIFIER: gitea + PLUGIN_FILES: | + locale_en-US.ini: options/locale/locale_en-US.ini + PLUGIN_BRANCH: main diff --git a/.gitignore b/.gitignore index 1ce2a87611..ad141c30cd 100644 --- a/.gitignore +++ b/.gitignore @@ -71,6 +71,7 @@ cpu.out /tests/e2e/test-artifacts /tests/e2e/test-snapshots /tests/*.ini +/tests/**/*.git/**/*.sample /node_modules /yarn.lock /yarn-error.log diff --git a/.golangci.yml b/.golangci.yml index 263149f773..342fe97837 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -5,6 +5,7 @@ linters: - depguard - dupl - errcheck + - forbidigo - gocritic # - gocyclo # The cyclomatic complexety of a lot of functions is too high, we should refactor those another time. - gofmt @@ -22,7 +23,7 @@ linters: - unconvert - unused # - varcheck # deprecated - https://github.com/golangci/golangci-lint/issues/1841 - # - wastedassign # disabled - https://github.com/golangci/golangci-lint/issues/2649 + - wastedassign enable-all: false disable-all: true fast: false @@ -105,73 +106,25 @@ issues: - errcheck - dupl - gosec + - path: cmd + linters: + - forbidigo - linters: - dupl text: "webhook" - linters: - gocritic text: "`ID' should not be capitalized" - - path: modules/templates/helper.go - linters: - - gocritic - linters: - unused - deadcode text: "swagger" - - path: contrib/pr/checkout.go - linters: - - errcheck - - path: models/issue.go - linters: - - errcheck - - path: models/migrations/ - linters: - - errcheck - - path: modules/log/ - linters: - - errcheck - - path: routers/api/v1/repo/issue_subscription.go - linters: - - dupl - - path: routers/repo/view.go - linters: - - dupl - - path: models/migrations/ - linters: - - unused - linters: - staticcheck text: "argument x is overwritten before first use" - - path: modules/httplib/httplib.go - linters: - - staticcheck - # Enabling this would require refactoring the methods and how they are called. - - path: models/issue_comment_list.go - linters: - - dupl - - path: models/update.go - linters: - - unused - - path: cmd/dump.go - linters: - - dupl - - path: services/webhook/webhook.go - linters: - - structcheck - text: "commentFormatting: put a space between `//` and comment text" linters: - gocritic - text: "exitAfterDefer:" linters: - gocritic - - path: modules/graceful/manager_windows.go - linters: - - staticcheck - text: "svc.IsAnInteractiveSession is deprecated: Use IsWindowsService instead." - - path: models/user/openid.go - linters: - - golint - - path: models/user/badge.go - linters: - - revive - text: "exported: type name will be used as user.UserBadge by other packages, and that stutters; consider calling this Badge" diff --git a/MAINTAINERS b/MAINTAINERS index f920f71f63..ef416639b0 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -41,7 +41,7 @@ Patrick Schratz (@pat-s) Janis Estelmann (@KN4CK3R) Steven Kriegler (@justusbunsi) Jimmy Praet (@jpraet) -Leon Hofmeister (@delvh) +Leon Hofmeister (@delvh) Wim (@42wim) Jason Song (@wolfogre) Yarden Shoham (@yardenshoham) @@ -49,3 +49,4 @@ Yu Tian (@Zettat123) Eddie Yang <576951401@qq.com> (@yp05327) Dong Ge (@sillyguodong) Xinyi Gong (@HesterG) +wxiaoguang (@wxiaoguang) diff --git a/Makefile b/Makefile index 59cc27ee8a..5192d2c5a6 100644 --- a/Makefile +++ b/Makefile @@ -25,16 +25,15 @@ COMMA := , XGO_VERSION := go-1.20.x -AIR_PACKAGE ?= github.com/cosmtrek/air@v1.40.4 -EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/cmd/editorconfig-checker@2.6.0 -ERRCHECK_PACKAGE ?= github.com/kisielk/errcheck@v1.6.2 -GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.4.0 -GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/cmd/golangci-lint@v1.51.2 -GXZ_PAGAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.10 +AIR_PACKAGE ?= github.com/cosmtrek/air@v1.43.0 +EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/cmd/editorconfig-checker@2.7.0 +GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.5.0 +GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/cmd/golangci-lint@v1.52.2 +GXZ_PAGAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.11 MISSPELL_PACKAGE ?= github.com/client9/misspell/cmd/misspell@v0.3.4 SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.30.4 XGO_PACKAGE ?= src.techknowlogick.com/xgo@latest -GO_LICENSES_PACKAGE ?= github.com/google/go-licenses@v1.5.0 +GO_LICENSES_PACKAGE ?= github.com/google/go-licenses@v1.6.0 GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@latest DOCKER_IMAGE ?= gitea/gitea @@ -97,6 +96,11 @@ else endif endif +# if version = "main" then update version to "nightly" +ifeq ($(VERSION),main) + VERSION := main-nightly +endif + LDFLAGS := $(LDFLAGS) -X "main.MakeVersion=$(MAKE_VERSION)" -X "main.Version=$(GITEA_VERSION)" -X "main.Tags=$(TAGS)" LINUX_ARCHS ?= linux/amd64,linux/386,linux/arm-5,linux/arm-6,linux/arm64 @@ -194,9 +198,20 @@ help: @echo " - deps-backend install backend dependencies" @echo " - deps-tools install tool dependencies" @echo " - lint lint everything" + @echo " - lint-fix lint everything and fix issues" @echo " - lint-frontend lint frontend files" + @echo " - lint-frontend-fix lint frontend files and fix issues" @echo " - lint-backend lint backend files" + @echo " - lint-backend-fix lint backend files and fix issues" + @echo " - lint-go lint go files" + @echo " - lint-go-fix lint go files and fix issues" + @echo " - lint-go-vet lint go files with vet" + @echo " - lint-js lint js files" + @echo " - lint-js-fix lint js files and fix issues" + @echo " - lint-css lint css files" + @echo " - lint-css-fix lint css files and fix issues" @echo " - lint-md lint markdown files" + @echo " - lint-swagger lint swagger files" @echo " - checks run various consistency checks" @echo " - checks-frontend check frontend files" @echo " - checks-backend check backend files" @@ -214,9 +229,7 @@ help: @echo " - generate-manpage generate manpage" @echo " - generate-swagger generate the swagger spec from code comments" @echo " - swagger-validate check if the swagger spec is valid" - @echo " - golangci-lint run golangci-lint linter" @echo " - go-licenses regenerate go licenses" - @echo " - vet examines Go source code and reports suspicious constructs" @echo " - tidy run go mod tidy" @echo " - test[\#TestSpecificName] run unit test" @echo " - test-sqlite[\#TestSpecificName] run integration test for sqlite" @@ -270,8 +283,12 @@ clean: fmt: GOFUMPT_PACKAGE=$(GOFUMPT_PACKAGE) $(GO) run build/code-batch-process.go gitea-fmt -w '{file-list}' $(eval TEMPLATES := $(shell find templates -type f -name '*.tmpl')) - @# strip whitespace after '{{' and before `}}` unless there is only whitespace before it - @$(SED_INPLACE) -e 's/{{[ ]\{1,\}/{{/g' -e '/^[ ]\{1,\}}}/! s/[ ]\{1,\}}}/}}/g' $(TEMPLATES) + @# 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) .PHONY: fmt-check fmt-check: fmt @@ -286,12 +303,6 @@ fmt-check: fmt misspell-check: go run $(MISSPELL_PACKAGE) -error $(GO_DIRS) $(WEB_DIRS) -.PHONY: vet -vet: - @echo "Running go vet..." - @GOOS= GOARCH= $(GO) build code.gitea.io/gitea-vet - @$(GO) vet -vettool=gitea-vet $(GO_PACKAGES) - .PHONY: $(TAGS_EVIDENCE) $(TAGS_EVIDENCE): @mkdir -p $(MAKE_EVIDENCE_DIR) @@ -324,11 +335,6 @@ swagger-validate: $(GO) run $(SWAGGER_PACKAGE) validate './$(SWAGGER_SPEC)' $(SED_INPLACE) '$(SWAGGER_SPEC_S_TMPL)' './$(SWAGGER_SPEC)' -.PHONY: errcheck -errcheck: - @echo "Running errcheck..." - $(GO) run $(ERRCHECK_PACKAGE) $(GO_PACKAGES) - .PHONY: checks checks: checks-frontend checks-backend @@ -341,18 +347,69 @@ checks-backend: tidy-check swagger-check fmt-check misspell-check swagger-valida .PHONY: lint lint: lint-frontend lint-backend +.PHONY: lint-fix +lint-fix: lint-frontend-fix lint-backend-fix + .PHONY: lint-frontend -lint-frontend: node_modules lint-md +lint-frontend: lint-js lint-css lint-md lint-swagger + +.PHONY: lint-frontend-fix +lint-frontend-fix: lint-js-fix lint-css-fix lint-md lint-swagger + +.PHONY: lint-backend +lint-backend: lint-go lint-go-vet lint-editorconfig + +.PHONY: lint-backend-fix +lint-backend-fix: lint-go-fix lint-go-vet lint-editorconfig + +.PHONY: lint-js +lint-js: node_modules npx eslint --color --max-warnings=0 --ext js,vue web_src/js build *.config.js docs/assets/js tests/e2e + +.PHONY: lint-js-fix +lint-js-fix: node_modules + npx eslint --color --max-warnings=0 --ext js,vue web_src/js build *.config.js docs/assets/js tests/e2e --fix + +.PHONY: lint-css +lint-css: node_modules npx stylelint --color --max-warnings=0 web_src/css + +.PHONY: lint-css-fix +lint-css-fix: node_modules + npx stylelint --color --max-warnings=0 web_src/css --fix + +.PHONY: lint-swagger +lint-swagger: node_modules npx spectral lint -q -F hint $(SWAGGER_SPEC) .PHONY: lint-md lint-md: node_modules npx markdownlint docs *.md -.PHONY: lint-backend -lint-backend: golangci-lint vet editorconfig-checker +.PHONY: lint-go +lint-go: + $(GO) run $(GOLANGCI_LINT_PACKAGE) run + +.PHONY: lint-go-fix +lint-go-fix: + $(GO) run $(GOLANGCI_LINT_PACKAGE) run --fix + +# workaround step for the lint-backend-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-go-vet +lint-go-vet: + @echo "Running go vet..." + @GOOS= GOARCH= $(GO) build code.gitea.io/gitea-vet + @$(GO) vet -vettool=gitea-vet $(GO_PACKAGES) + +.PHONY: lint-editorconfig +lint-editorconfig: + $(GO) run $(EDITORCONFIG_CHECKER_PACKAGE) templates .PHONY: watch watch: @@ -843,7 +900,6 @@ deps-backend: deps-tools: $(GO) install $(AIR_PACKAGE) $(GO) install $(EDITORCONFIG_CHECKER_PACKAGE) - $(GO) install $(ERRCHECK_PACKAGE) $(GO) install $(GOFUMPT_PACKAGE) $(GO) install $(GOLANGCI_LINT_PACKAGE) $(GO) install $(GXZ_PAGAGE) @@ -942,21 +998,6 @@ generate-manpage: @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 -.PHONY: golangci-lint -golangci-lint: - $(GO) run $(GOLANGCI_LINT_PACKAGE) run - -# workaround step for the lint-backend-windows CI task because 'go run' can not -# have distinct GOOS/GOARCH for its build and run steps -.PHONY: golangci-lint-windows -golangci-lint-windows: - @GOOS= GOARCH= $(GO) install $(GOLANGCI_LINT_PACKAGE) - golangci-lint run - -.PHONY: editorconfig-checker -editorconfig-checker: - $(GO) run $(EDITORCONFIG_CHECKER_PACKAGE) templates - .PHONY: docker docker: docker build --disable-content-trust=false -t $(DOCKER_REF) . diff --git a/assets/go-licenses.json b/assets/go-licenses.json index 3e7db196f7..1b6ead5df6 100644 --- a/assets/go-licenses.json +++ b/assets/go-licenses.json @@ -1004,6 +1004,11 @@ "path": "golang.org/x/crypto/LICENSE", "licenseText": "Copyright (c) 2009 The Go Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" }, + { + "name": "golang.org/x/image", + "path": "golang.org/x/image/LICENSE", + "licenseText": "Copyright (c) 2009 The Go Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" + }, { "name": "golang.org/x/mod/semver", "path": "golang.org/x/mod/semver/LICENSE", diff --git a/cmd/migrate_storage_test.go b/cmd/migrate_storage_test.go index aae366c0cf..e6658b6e65 100644 --- a/cmd/migrate_storage_test.go +++ b/cmd/migrate_storage_test.go @@ -67,7 +67,7 @@ func TestMigratePackages(t *testing.T) { entries, err := os.ReadDir(p) assert.NoError(t, err) - assert.EqualValues(t, 2, len(entries)) + assert.Len(t, entries, 2) assert.EqualValues(t, "01", entries[0].Name()) assert.EqualValues(t, "tmp", entries[1].Name()) } diff --git a/cmd/web.go b/cmd/web.go index 11620c6b00..e451cf7dfa 100644 --- a/cmd/web.go +++ b/cmd/web.go @@ -24,7 +24,6 @@ import ( "github.com/felixge/fgprof" "github.com/urfave/cli" - ini "gopkg.in/ini.v1" ) // PIDFile could be set from build tag @@ -223,9 +222,10 @@ func setPort(port string) error { defaultLocalURL += ":" + setting.HTTPPort + "/" // Save LOCAL_ROOT_URL if port changed - setting.CreateOrAppendToCustomConf("server.LOCAL_ROOT_URL", func(cfg *ini.File) { - cfg.Section("server").Key("LOCAL_ROOT_URL").SetValue(defaultLocalURL) - }) + setting.CfgProvider.Section("server").Key("LOCAL_ROOT_URL").SetValue(defaultLocalURL) + if err := setting.CfgProvider.Save(); err != nil { + return fmt.Errorf("Failed to save config file: %v", err) + } } return nil } diff --git a/contrib/backport/backport.go b/contrib/backport/backport.go index aa665ac2e1..358f4ed3c8 100644 --- a/contrib/backport/backport.go +++ b/contrib/backport/backport.go @@ -1,6 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT +//nolint:forbidigo package main import ( diff --git a/contrib/fixtures/fixture_generation.go b/contrib/fixtures/fixture_generation.go index 210e0aae06..06c0354efa 100644 --- a/contrib/fixtures/fixture_generation.go +++ b/contrib/fixtures/fixture_generation.go @@ -1,6 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT +//nolint:forbidigo package main import ( diff --git a/contrib/upgrade.sh b/contrib/upgrade.sh index 3a98c277d6..4b166a02a0 100755 --- a/contrib/upgrade.sh +++ b/contrib/upgrade.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # This is an update script for gitea installed via the binary distribution -# from dl.gitea.io on linux as systemd service. It performs a backup and updates +# from dl.gitea.com on linux as systemd service. It performs a backup and updates # Gitea in place. # NOTE: This adds the GPG Signing Key of the Gitea maintainers to the keyring. # Depends on: bash, curl, xz, sha256sum. optionally jq, gpg @@ -10,6 +10,15 @@ # upgrade.sh 1.15.10 # giteahome=/opt/gitea giteaconf=$giteahome/app.ini upgrade.sh +# Check if gitea service is running +if ! pidof gitea &> /dev/null; then + echo "Error: gitea is not running." + exit 1 +fi + +# Continue with rest of the script if gitea is running +echo "Gitea is running. Continuing with rest of script..." + # apply variables from environment : "${giteabin:="/usr/local/bin/gitea"}" : "${giteahome:="/var/lib/gitea"}" @@ -69,7 +78,7 @@ require curl xz sha256sum "$sudocmd" # select version to install if [[ -z "${giteaversion:-}" ]]; then require jq - giteaversion=$(curl --connect-timeout 10 -sL https://dl.gitea.io/gitea/version.json | jq -r .latest.version) + giteaversion=$(curl --connect-timeout 10 -sL https://dl.gitea.com/gitea/version.json | jq -r .latest.version) echo "Latest available version is $giteaversion" fi @@ -91,7 +100,7 @@ cd "$giteahome" # needed for gitea dump later # download new binary binname="gitea-${giteaversion}-${arch}" -binurl="https://dl.gitea.io/gitea/${giteaversion}/${binname}.xz" +binurl="https://dl.gitea.com/gitea/${giteaversion}/${binname}.xz" echo "Downloading $binurl..." curl --connect-timeout 10 --silent --show-error --fail --location -O "$binurl{,.sha256,.asc}" diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index f9f207522c..144691c00c 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -1203,10 +1203,9 @@ ROUTER = console ;; Number of line of codes shown for a code comment ;CODE_COMMENT_LINES = 4 ;; -;; Value of `theme-color` meta tag, used by Android >= 5.0 -;; An invalid color like "none" or "disable" will have the default style -;; More info: https://developers.google.com/web/updates/2014/11/Support-for-theme-color-in-Chrome-39-for-Android -;THEME_COLOR_META_TAG = `#6cc644` +;; Value of `theme-color` meta tag, used by some mobile browers for chrome and +;; out-of-viewport areas. Default is unset which uses body color. +;THEME_COLOR_META_TAG = ;; ;; Max size of files to be displayed (default is 8MiB) ;MAX_DISPLAY_FILE_SIZE = 8388608 @@ -2340,7 +2339,6 @@ ROUTER = console ;[other] ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;SHOW_FOOTER_BRANDING = false ;; Show version information about Gitea and Go in the footer ;SHOW_FOOTER_VERSION = true ;; Show template execution time in the footer diff --git a/docker/manifest.rootless.tmpl b/docker/manifest.rootless.tmpl index ef48dd1684..87e6e9dfc2 100644 --- a/docker/manifest.rootless.tmpl +++ b/docker/manifest.rootless.tmpl @@ -1,4 +1,4 @@ -image: gitea/gitea:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}{{#if (hasPrefix "refs/heads/release/v" build.ref)}}{{trimPrefix "refs/heads/release/v" build.ref}}-{{/if}}dev{{/if}}-rootless +image: gitea/gitea:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}{{#if (hasPrefix "refs/heads/release/v" build.ref)}}{{trimPrefix "refs/heads/release/v" build.ref}}-{{/if}}nightly{{/if}}-rootless {{#if build.tags}} {{#unless (contains "-rc" build.tag)}} tags: @@ -10,12 +10,12 @@ tags: {{/if}} manifests: - - image: gitea/gitea:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}{{#if (hasPrefix "refs/heads/release/v" build.ref)}}{{trimPrefix "refs/heads/release/v" build.ref}}-{{/if}}dev{{/if}}-linux-amd64-rootless + image: gitea/gitea:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}{{#if (hasPrefix "refs/heads/release/v" build.ref)}}{{trimPrefix "refs/heads/release/v" build.ref}}-{{/if}}nightly{{/if}}-linux-amd64-rootless platform: architecture: amd64 os: linux - - image: gitea/gitea:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}{{#if (hasPrefix "refs/heads/release/v" build.ref)}}{{trimPrefix "refs/heads/release/v" build.ref}}-{{/if}}dev{{/if}}-linux-arm64-rootless + image: gitea/gitea:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}{{#if (hasPrefix "refs/heads/release/v" build.ref)}}{{trimPrefix "refs/heads/release/v" build.ref}}-{{/if}}nightly{{/if}}-linux-arm64-rootless platform: architecture: arm64 os: linux diff --git a/docker/manifest.tmpl b/docker/manifest.tmpl index 018d87e979..18847064f6 100644 --- a/docker/manifest.tmpl +++ b/docker/manifest.tmpl @@ -1,4 +1,4 @@ -image: gitea/gitea:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}{{#if (hasPrefix "refs/heads/release/v" build.ref)}}{{trimPrefix "refs/heads/release/v" build.ref}}-{{/if}}dev{{/if}} +image: gitea/gitea:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}{{#if (hasPrefix "refs/heads/release/v" build.ref)}}{{trimPrefix "refs/heads/release/v" build.ref}}-{{/if}}nightly{{/if}} {{#if build.tags}} {{#unless (contains "-rc" build.tag)}} tags: @@ -10,12 +10,12 @@ tags: {{/if}} manifests: - - image: gitea/gitea:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}{{#if (hasPrefix "refs/heads/release/v" build.ref)}}{{trimPrefix "refs/heads/release/v" build.ref}}-{{/if}}dev{{/if}}-linux-amd64 + image: gitea/gitea:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}{{#if (hasPrefix "refs/heads/release/v" build.ref)}}{{trimPrefix "refs/heads/release/v" build.ref}}-{{/if}}nightly{{/if}}-linux-amd64 platform: architecture: amd64 os: linux - - image: gitea/gitea:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}{{#if (hasPrefix "refs/heads/release/v" build.ref)}}{{trimPrefix "refs/heads/release/v" build.ref}}-{{/if}}dev{{/if}}-linux-arm64 + image: gitea/gitea:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}{{#if (hasPrefix "refs/heads/release/v" build.ref)}}{{trimPrefix "refs/heads/release/v" build.ref}}-{{/if}}nightly{{/if}}-linux-arm64 platform: architecture: arm64 os: linux diff --git a/docs/content/doc/administration/config-cheat-sheet.en-us.md b/docs/content/doc/administration/config-cheat-sheet.en-us.md index f26e7eaa08..74110ea443 100644 --- a/docs/content/doc/administration/config-cheat-sheet.en-us.md +++ b/docs/content/doc/administration/config-cheat-sheet.en-us.md @@ -220,7 +220,7 @@ The following configuration set `Content-Type: application/vnd.android.package-a - `SHOW_USER_EMAIL`: **true**: Whether the email of the user should be shown in the Explore Users page. - `THEMES`: **auto,gitea,arc-green**: All available themes. Allow users select personalized themes. regardless of the value of `DEFAULT_THEME`. -- `THEME_COLOR_META_TAG`: **#6cc644**: Value of `theme-color` meta tag, used by Android >= 5.0. An invalid color like "none" or "disable" will have the default style. More info: https://developers.google.com/web/updates/2014/11/Support-for-theme-color-in-Chrome-39-for-Android +- `THEME_COLOR_META_TAG`: **\**: Value of `theme-color` meta tag, used by some mobile browers for chrome and out-of-viewport areas. Default is unset which uses body color. - `MAX_DISPLAY_FILE_SIZE`: **8388608**: Max size of files to be displayed (default is 8MiB) - `REACTIONS`: All available reactions users can choose on issues/prs and comments Values can be emoji alias (:smile:) or a unicode emoji. @@ -1391,7 +1391,6 @@ although Github don't support this form. ## Other (`other`) -- `SHOW_FOOTER_BRANDING`: **false**: Show Gitea branding in the footer. - `SHOW_FOOTER_VERSION`: **true**: Show Gitea and Go version information in the footer. - `SHOW_FOOTER_TEMPLATE_LOAD_TIME`: **true**: Show time of template execution in the footer. - `ENABLE_SITEMAP`: **true**: Generate sitemap. diff --git a/docs/content/doc/administration/config-cheat-sheet.zh-cn.md b/docs/content/doc/administration/config-cheat-sheet.zh-cn.md index 83e212f32b..485d106234 100644 --- a/docs/content/doc/administration/config-cheat-sheet.zh-cn.md +++ b/docs/content/doc/administration/config-cheat-sheet.zh-cn.md @@ -483,5 +483,4 @@ PROXY_HOSTS = *.github.com ## Other (`other`) -- `SHOW_FOOTER_BRANDING`: 为真则在页面底部显示Gitea的字样。 - `SHOW_FOOTER_VERSION`: 为真则在页面底部显示Gitea的版本。 diff --git a/docs/content/doc/contributing/guidelines-frontend.en-us.md b/docs/content/doc/contributing/guidelines-frontend.en-us.md index 36c88739ec..bde7d31510 100644 --- a/docs/content/doc/contributing/guidelines-frontend.en-us.md +++ b/docs/content/doc/contributing/guidelines-frontend.en-us.md @@ -49,6 +49,7 @@ We recommend [Google HTML/CSS Style Guide](https://google.github.io/styleguide/h 9. Avoid unnecessary `!important` in CSS, add comments to explain why it's necessary if it can't be avoided. 10. Avoid mixing different events in one event listener, prefer to use individual event listeners for every event. 11. Custom event names are recommended to use `ce-` prefix. +12. Gitea's tailwind-style CSS classes use `gt-` prefix (`gt-relative`), while Gitea's own private framework-level CSS classes use `g-` prefix (`g-modal-confirm`). ### Accessibility / ARIA diff --git a/go.mod b/go.mod index 944f6d2c91..a71b136cb1 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module code.gitea.io/gitea go 1.19 require ( - code.gitea.io/actions-proto-go v0.2.0 + code.gitea.io/actions-proto-go v0.2.1 code.gitea.io/gitea-vet v0.2.2 code.gitea.io/sdk/gitea v0.15.1 codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570 @@ -104,10 +104,11 @@ require ( github.com/yuin/goldmark-highlighting/v2 v2.0.0-20220924101305-151362477c87 github.com/yuin/goldmark-meta v1.1.0 golang.org/x/crypto v0.7.0 + golang.org/x/image v0.7.0 golang.org/x/net v0.8.0 golang.org/x/oauth2 v0.6.0 golang.org/x/sys v0.6.0 - golang.org/x/text v0.8.0 + golang.org/x/text v0.9.0 golang.org/x/tools v0.6.0 google.golang.org/grpc v1.53.0 google.golang.org/protobuf v1.28.1 @@ -290,7 +291,7 @@ replace github.com/shurcooL/vfsgen => github.com/lunny/vfsgen v0.0.0-20220105142 replace github.com/blevesearch/zapx/v15 v15.3.6 => github.com/zeripath/zapx/v15 v15.3.6-alignment-fix -replace github.com/nektos/act => gitea.com/gitea/act v0.243.3-0.20230407083103-5c4a96bcb797 +replace github.com/nektos/act => gitea.com/gitea/act v0.243.4 exclude github.com/gofrs/uuid v3.2.0+incompatible diff --git a/go.sum b/go.sum index df57c65918..2a3cc5967e 100644 --- a/go.sum +++ b/go.sum @@ -40,8 +40,8 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= -code.gitea.io/actions-proto-go v0.2.0 h1:nYh9nhhfk67YA4wVNLsCzd//RCvXnljwXClJ33+HPVk= -code.gitea.io/actions-proto-go v0.2.0/go.mod h1:00ys5QDo1iHN1tHNvvddAcy2W/g+425hQya1cCSvq9A= +code.gitea.io/actions-proto-go v0.2.1 h1:ToMN/8thz2q10TuCq8dL2d8mI+/pWpJcHCvG+TELwa0= +code.gitea.io/actions-proto-go v0.2.1/go.mod h1:00ys5QDo1iHN1tHNvvddAcy2W/g+425hQya1cCSvq9A= code.gitea.io/gitea-vet v0.2.1/go.mod h1:zcNbT/aJEmivCAhfmkHOlT645KNOf9W2KnkLgFjGGfE= code.gitea.io/gitea-vet v0.2.2 h1:TEOV/Glf38iGmKzKP0EB++Z5OSL4zGg3RrAvlwaMuvk= code.gitea.io/gitea-vet v0.2.2/go.mod h1:zcNbT/aJEmivCAhfmkHOlT645KNOf9W2KnkLgFjGGfE= @@ -52,8 +52,8 @@ codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570/go.mod h1:IIAjsi dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078 h1:cliQ4HHsCo6xi2oWZYKWW4bly/Ory9FuTpFPRxj/mAg= git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078/go.mod h1:g/V2Hjas6Z1UHUp4yIx6bATpNzJ7DYtD0FG3+xARWxs= -gitea.com/gitea/act v0.243.3-0.20230407083103-5c4a96bcb797 h1:QdQ+pHxpbIi2qsgZbYVxqYV7trfW3P5M/T4AfpoM7iw= -gitea.com/gitea/act v0.243.3-0.20230407083103-5c4a96bcb797/go.mod h1:mabw6AZAiDgxGlK83orWLrNERSPvgBJzEUS3S7u2bHI= +gitea.com/gitea/act v0.243.4 h1:MuBHBLCJfpa6mzwwvs4xqQynrSP2RRzpHpWfTV16PmI= +gitea.com/gitea/act v0.243.4/go.mod h1:mabw6AZAiDgxGlK83orWLrNERSPvgBJzEUS3S7u2bHI= gitea.com/go-chi/binding v0.0.0-20221013104517-b29891619681 h1:MMSPgnVULVwV9kEBgvyEUhC9v/uviZ55hPJEMjpbNR4= gitea.com/go-chi/binding v0.0.0-20221013104517-b29891619681/go.mod h1:77TZu701zMXWJFvB8gvTbQ92zQ3DQq/H7l5wAEjQRKc= gitea.com/go-chi/cache v0.0.0-20210110083709-82c4c9ce2d5e/go.mod h1:k2V/gPDEtXGjjMGuBJiapffAXTv76H4snSmlJRLUhH0= @@ -1331,6 +1331,8 @@ golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EH golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.7.0 h1:gzS29xtG1J5ybQlv0PuyfE3nmc6R4qB73m6LUUmvFuw= +golang.org/x/image v0.7.0/go.mod h1:nd/q4ef1AKKYl/4kft7g+6UyGbdiqWqTP1ZAbRoV7Rg= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -1415,6 +1417,7 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.0.0-20220906165146-f3363e06e74c/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -1533,6 +1536,7 @@ golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= @@ -1540,6 +1544,7 @@ golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9sn golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1551,8 +1556,9 @@ golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= -golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= diff --git a/models/actions/task_output.go b/models/actions/task_output.go new file mode 100644 index 0000000000..5291a783d6 --- /dev/null +++ b/models/actions/task_output.go @@ -0,0 +1,51 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + "context" + + "code.gitea.io/gitea/models/db" +) + +// ActionTaskOutput represents an output of ActionTask. +// So the outputs are bound to a task, that means when a completed job has been rerun, +// the outputs of the job will be reset because the task is new. +// It's by design, to avoid the outputs of the old task to be mixed with the new task. +type ActionTaskOutput struct { + ID int64 + TaskID int64 `xorm:"INDEX UNIQUE(task_id_output_key)"` + OutputKey string `xorm:"VARCHAR(255) UNIQUE(task_id_output_key)"` + OutputValue string `xorm:"MEDIUMTEXT"` +} + +// FindTaskOutputByTaskID returns the outputs of the task. +func FindTaskOutputByTaskID(ctx context.Context, taskID int64) ([]*ActionTaskOutput, error) { + var outputs []*ActionTaskOutput + return outputs, db.GetEngine(ctx).Where("task_id=?", taskID).Find(&outputs) +} + +// FindTaskOutputKeyByTaskID returns the keys of the outputs of the task. +func FindTaskOutputKeyByTaskID(ctx context.Context, taskID int64) ([]string, error) { + var keys []string + return keys, db.GetEngine(ctx).Table(ActionTaskOutput{}).Where("task_id=?", taskID).Cols("output_key").Find(&keys) +} + +// InsertTaskOutputIfNotExist inserts a new task output if it does not exist. +func InsertTaskOutputIfNotExist(ctx context.Context, taskID int64, key, value string) error { + return db.WithTx(ctx, func(ctx context.Context) error { + sess := db.GetEngine(ctx) + if exist, err := sess.Exist(&ActionTaskOutput{TaskID: taskID, OutputKey: key}); err != nil { + return err + } else if exist { + return nil + } + _, err := sess.Insert(&ActionTaskOutput{ + TaskID: taskID, + OutputKey: key, + OutputValue: value, + }) + return err + }) +} diff --git a/models/db/iterate_test.go b/models/db/iterate_test.go index 6bcf740c23..5362f34075 100644 --- a/models/db/iterate_test.go +++ b/models/db/iterate_test.go @@ -19,13 +19,16 @@ func TestIterate(t *testing.T) { xe := unittest.GetXORMEngine() assert.NoError(t, xe.Sync(&repo_model.RepoUnit{})) - var repoCnt int - err := db.Iterate(db.DefaultContext, nil, func(ctx context.Context, repo *repo_model.RepoUnit) error { - repoCnt++ + cnt, err := db.GetEngine(db.DefaultContext).Count(&repo_model.RepoUnit{}) + assert.NoError(t, err) + + var repoUnitCnt int + err = db.Iterate(db.DefaultContext, nil, func(ctx context.Context, repo *repo_model.RepoUnit) error { + repoUnitCnt++ return nil }) assert.NoError(t, err) - assert.EqualValues(t, 84, repoCnt) + assert.EqualValues(t, cnt, repoUnitCnt) err = db.Iterate(db.DefaultContext, nil, func(ctx context.Context, repoUnit *repo_model.RepoUnit) error { reopUnit2 := repo_model.RepoUnit{ID: repoUnit.ID} diff --git a/models/db/list_test.go b/models/db/list_test.go index 1295692cec..b923dc9c22 100644 --- a/models/db/list_test.go +++ b/models/db/list_test.go @@ -31,15 +31,20 @@ func TestFind(t *testing.T) { xe := unittest.GetXORMEngine() assert.NoError(t, xe.Sync(&repo_model.RepoUnit{})) + var repoUnitCount int + _, err := db.GetEngine(db.DefaultContext).SQL("SELECT COUNT(*) FROM repo_unit").Get(&repoUnitCount) + assert.NoError(t, err) + assert.NotEmpty(t, repoUnitCount) + opts := mockListOptions{} var repoUnits []repo_model.RepoUnit - err := db.Find(db.DefaultContext, &opts, &repoUnits) + err = db.Find(db.DefaultContext, &opts, &repoUnits) assert.NoError(t, err) - assert.EqualValues(t, 84, len(repoUnits)) + assert.Len(t, repoUnits, repoUnitCount) cnt, err := db.Count(db.DefaultContext, &opts, new(repo_model.RepoUnit)) assert.NoError(t, err) - assert.EqualValues(t, 84, cnt) + assert.EqualValues(t, repoUnitCount, cnt) repoUnits = make([]repo_model.RepoUnit, 0, 10) newCnt, err := db.FindAndCount(db.DefaultContext, &opts, &repoUnits) diff --git a/models/dbfs/dbfs_test.go b/models/dbfs/dbfs_test.go index 30aa6463c5..300758c623 100644 --- a/models/dbfs/dbfs_test.go +++ b/models/dbfs/dbfs_test.go @@ -12,8 +12,6 @@ import ( "code.gitea.io/gitea/models/db" "github.com/stretchr/testify/assert" - - _ "github.com/mattn/go-sqlite3" ) func changeDefaultFileBlockSize(n int64) (restore func()) { diff --git a/models/fixtures/release.yml b/models/fixtures/release.yml index 1703f959d2..6d09401ebc 100644 --- a/models/fixtures/release.yml +++ b/models/fixtures/release.yml @@ -66,3 +66,45 @@ is_prerelease: true is_tag: false created_unix: 946684800 + +- id: 6 + repo_id: 57 + publisher_id: 2 + tag_name: "v1.0" + lower_tag_name: "v1.0" + target: "main" + title: "v1.0" + sha1: "a8a700e8c644c783ba2c6e742bb81bf91e244bff" + num_commits: 3 + is_draft: false + is_prerelease: false + is_tag: false + created_unix: 946684801 + +- id: 7 + repo_id: 57 + publisher_id: 2 + tag_name: "v1.1" + lower_tag_name: "v1.1" + target: "main" + title: "v1.1" + sha1: "cef06e48f2642cd0dc9597b4bea09f4b3f74aad6" + num_commits: 5 + is_draft: false + is_prerelease: false + is_tag: false + created_unix: 946684802 + +- id: 8 + repo_id: 57 + publisher_id: 2 + tag_name: "v2.0" + lower_tag_name: "v2.0" + target: "main" + title: "v2.0" + sha1: "7197b56fdc75b453f47c9110938cb46a303579fd" + num_commits: 6 + is_draft: false + is_prerelease: false + is_tag: false + created_unix: 946684803 diff --git a/models/fixtures/repo_unit.yml b/models/fixtures/repo_unit.yml index ef0b7c1a94..5bb974a7d7 100644 --- a/models/fixtures/repo_unit.yml +++ b/models/fixtures/repo_unit.yml @@ -1,3 +1,4 @@ +# See models/unit/unit.go for the meaning of the type - id: 1 repo_id: 1 @@ -575,3 +576,34 @@ repo_id: 56 type: 1 created_unix: 946684810 +- + id: 85 + repo_id: 57 + type: 1 + created_unix: 946684810 +- + id: 86 + repo_id: 57 + type: 2 + created_unix: 946684810 +- + id: 87 + repo_id: 57 + type: 3 + created_unix: 946684810 +- + id: 88 + repo_id: 57 + type: 4 + created_unix: 946684810 +- + id: 89 + repo_id: 57 + type: 5 + created_unix: 946684810 + +- + id: 90 + repo_id: 52 + type: 1 + created_unix: 946684810 diff --git a/models/fixtures/repository.yml b/models/fixtures/repository.yml index 4c888e47ed..ef7730780f 100644 --- a/models/fixtures/repository.yml +++ b/models/fixtures/repository.yml @@ -1,3 +1,4 @@ +# don't forget to add fixtures in repo_unit.yml - id: 1 owner_id: 2 @@ -1559,6 +1560,7 @@ owner_name: user30 lower_name: empty name: empty + default_branch: master num_watches: 0 num_stars: 0 num_forks: 0 @@ -1647,3 +1649,16 @@ is_private: true status: 0 num_issues: 0 + +- + id: 57 + owner_id: 2 + owner_name: user2 + lower_name: repo-release + name: repo-release + default_branch: main + is_empty: false + is_archived: false + is_private: false + status: 0 + num_issues: 0 diff --git a/models/fixtures/user.yml b/models/fixtures/user.yml index a4a85f6b1a..eba33a7c36 100644 --- a/models/fixtures/user.yml +++ b/models/fixtures/user.yml @@ -66,7 +66,7 @@ num_followers: 2 num_following: 1 num_stars: 2 - num_repos: 12 + num_repos: 13 num_teams: 0 num_members: 0 visibility: 0 @@ -1091,7 +1091,7 @@ max_repo_creation: -1 is_active: true is_admin: false - is_restricted: true + is_restricted: false allow_git_hook: false allow_import_local: false allow_create_organization: true diff --git a/models/git/branches_test.go b/models/git/branches_test.go index b7df7f243e..5d18d9525e 100644 --- a/models/git/branches_test.go +++ b/models/git/branches_test.go @@ -88,12 +88,12 @@ func TestFindRenamedBranch(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) branch, exist, err := git_model.FindRenamedBranch(db.DefaultContext, 1, "dev") assert.NoError(t, err) - assert.Equal(t, true, exist) + assert.True(t, exist) assert.Equal(t, "master", branch.To) _, exist, err = git_model.FindRenamedBranch(db.DefaultContext, 1, "unknow") assert.NoError(t, err) - assert.Equal(t, false, exist) + assert.False(t, exist) } func TestRenameBranch(t *testing.T) { @@ -115,7 +115,7 @@ func TestRenameBranch(t *testing.T) { return nil })) - assert.Equal(t, true, _isDefault) + assert.True(t, _isDefault) repo1 = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) assert.Equal(t, "main", repo1.DefaultBranch) diff --git a/models/issues/comment.go b/models/issues/comment.go index a47dc7c151..c7e815c8b6 100644 --- a/models/issues/comment.go +++ b/models/issues/comment.go @@ -52,84 +52,61 @@ func (err ErrCommentNotExist) Unwrap() error { // CommentType defines whether a comment is just a simple comment, an action (like close) or a reference. type CommentType int -// define unknown comment type -const ( - CommentTypeUnknown CommentType = -1 -) +// CommentTypeUndefined is used to search for comments of any type +const CommentTypeUndefined CommentType = -1 -// Enumerate all the comment types const ( - // 0 Plain comment, can be associated with a commit (CommitID > 0) and a line (LineNum > 0) - CommentTypeComment CommentType = iota - CommentTypeReopen // 1 - CommentTypeClose // 2 + CommentTypeComment CommentType = iota // 0 Plain comment, can be associated with a commit (CommitID > 0) and a line (LineNum > 0) + + CommentTypeReopen // 1 + CommentTypeClose // 2 + + CommentTypeIssueRef // 3 References. + CommentTypeCommitRef // 4 Reference from a commit (not part of a pull request) + CommentTypeCommentRef // 5 Reference from a comment + CommentTypePullRef // 6 Reference from a pull request + + CommentTypeLabel // 7 Labels changed + CommentTypeMilestone // 8 Milestone changed + CommentTypeAssignees // 9 Assignees changed + CommentTypeChangeTitle // 10 Change Title + CommentTypeDeleteBranch // 11 Delete Branch + + CommentTypeStartTracking // 12 Start a stopwatch for time tracking + CommentTypeStopTracking // 13 Stop a stopwatch for time tracking + CommentTypeAddTimeManual // 14 Add time manual for time tracking + CommentTypeCancelTracking // 15 Cancel a stopwatch for time tracking + CommentTypeAddedDeadline // 16 Added a due date + CommentTypeModifiedDeadline // 17 Modified the due date + CommentTypeRemovedDeadline // 18 Removed a due date + + CommentTypeAddDependency // 19 Dependency added + CommentTypeRemoveDependency // 20 Dependency removed + + CommentTypeCode // 21 Comment a line of code + CommentTypeReview // 22 Reviews a pull request by giving general feedback + + CommentTypeLock // 23 Lock an issue, giving only collaborators access + CommentTypeUnlock // 24 Unlocks a previously locked issue + + CommentTypeChangeTargetBranch // 25 Change pull request's target branch + + CommentTypeDeleteTimeManual // 26 Delete time manual for time tracking + + CommentTypeReviewRequest // 27 add or remove Request from one + CommentTypeMergePull // 28 merge pull request + CommentTypePullRequestPush // 29 push to PR head branch + + CommentTypeProject // 30 Project changed + CommentTypeProjectBoard // 31 Project board changed + + CommentTypeDismissReview // 32 Dismiss Review + + CommentTypeChangeIssueRef // 33 Change issue ref + + CommentTypePRScheduledToAutoMerge // 34 pr was scheduled to auto merge when checks succeed + CommentTypePRUnScheduledToAutoMerge // 35 pr was un scheduled to auto merge when checks succeed - // 3 References. - CommentTypeIssueRef - // 4 Reference from a commit (not part of a pull request) - CommentTypeCommitRef - // 5 Reference from a comment - CommentTypeCommentRef - // 6 Reference from a pull request - CommentTypePullRef - // 7 Labels changed - CommentTypeLabel - // 8 Milestone changed - CommentTypeMilestone - // 9 Assignees changed - CommentTypeAssignees - // 10 Change Title - CommentTypeChangeTitle - // 11 Delete Branch - CommentTypeDeleteBranch - // 12 Start a stopwatch for time tracking - CommentTypeStartTracking - // 13 Stop a stopwatch for time tracking - CommentTypeStopTracking - // 14 Add time manual for time tracking - CommentTypeAddTimeManual - // 15 Cancel a stopwatch for time tracking - CommentTypeCancelTracking - // 16 Added a due date - CommentTypeAddedDeadline - // 17 Modified the due date - CommentTypeModifiedDeadline - // 18 Removed a due date - CommentTypeRemovedDeadline - // 19 Dependency added - CommentTypeAddDependency - // 20 Dependency removed - CommentTypeRemoveDependency - // 21 Comment a line of code - CommentTypeCode - // 22 Reviews a pull request by giving general feedback - CommentTypeReview - // 23 Lock an issue, giving only collaborators access - CommentTypeLock - // 24 Unlocks a previously locked issue - CommentTypeUnlock - // 25 Change pull request's target branch - CommentTypeChangeTargetBranch - // 26 Delete time manual for time tracking - CommentTypeDeleteTimeManual - // 27 add or remove Request from one - CommentTypeReviewRequest - // 28 merge pull request - CommentTypeMergePull - // 29 push to PR head branch - CommentTypePullRequestPush - // 30 Project changed - CommentTypeProject - // 31 Project board changed - CommentTypeProjectBoard - // 32 Dismiss Review - CommentTypeDismissReview - // 33 Change issue ref - CommentTypeChangeIssueRef - // 34 pr was scheduled to auto merge when checks succeed - CommentTypePRScheduledToAutoMerge - // 35 pr was un scheduled to auto merge when checks succeed - CommentTypePRUnScheduledToAutoMerge ) var commentStrings = []string{ @@ -181,7 +158,23 @@ func AsCommentType(typeName string) CommentType { return CommentType(index) } } - return CommentTypeUnknown + return CommentTypeUndefined +} + +func (t CommentType) HasContentSupport() bool { + switch t { + case CommentTypeComment, CommentTypeCode, CommentTypeReview: + return true + } + return false +} + +func (t CommentType) HasAttachmentSupport() bool { + switch t { + case CommentTypeComment, CommentTypeCode, CommentTypeReview: + return true + } + return false } // RoleDescriptor defines comment tag type @@ -1039,7 +1032,7 @@ func (opts *FindCommentsOptions) ToConds() builder.Cond { if opts.Before > 0 { cond = cond.And(builder.Lte{"comment.updated_unix": opts.Before}) } - if opts.Type != CommentTypeUnknown { + if opts.Type != CommentTypeUndefined { cond = cond.And(builder.Eq{"comment.type": opts.Type}) } if opts.Line != 0 { diff --git a/models/issues/comment_list.go b/models/issues/comment_list.go index 0411d44531..477337443d 100644 --- a/models/issues/comment_list.go +++ b/models/issues/comment_list.go @@ -56,7 +56,7 @@ func (comments CommentList) getLabelIDs() []int64 { return ids.Values() } -func (comments CommentList) loadLabels(ctx context.Context) error { //nolint +func (comments CommentList) loadLabels(ctx context.Context) error { if len(comments) == 0 { return nil } @@ -415,7 +415,7 @@ func (comments CommentList) getReviewIDs() []int64 { return ids.Values() } -func (comments CommentList) loadReviews(ctx context.Context) error { //nolint +func (comments CommentList) loadReviews(ctx context.Context) error { if len(comments) == 0 { return nil } @@ -453,6 +453,14 @@ func (comments CommentList) loadReviews(ctx context.Context) error { //nolint for _, comment := range comments { comment.Review = reviews[comment.ReviewID] + + // If the comment dismisses a review, we need to load the reviewer to show whose review has been dismissed. + // Otherwise, the reviewer is the poster of the comment, so we don't need to load it. + if comment.Type == CommentTypeDismissReview { + if err := comment.Review.LoadReviewer(ctx); err != nil { + return err + } + } } return nil } diff --git a/models/issues/comment_test.go b/models/issues/comment_test.go index f1232729f1..43bad1660f 100644 --- a/models/issues/comment_test.go +++ b/models/issues/comment_test.go @@ -64,8 +64,9 @@ func TestFetchCodeComments(t *testing.T) { } func TestAsCommentType(t *testing.T) { - assert.Equal(t, issues_model.CommentTypeUnknown, issues_model.AsCommentType("")) - assert.Equal(t, issues_model.CommentTypeUnknown, issues_model.AsCommentType("nonsense")) + assert.Equal(t, issues_model.CommentType(0), issues_model.CommentTypeComment) + assert.Equal(t, issues_model.CommentTypeUndefined, issues_model.AsCommentType("")) + assert.Equal(t, issues_model.CommentTypeUndefined, issues_model.AsCommentType("nonsense")) assert.Equal(t, issues_model.CommentTypeComment, issues_model.AsCommentType("comment")) assert.Equal(t, issues_model.CommentTypePRUnScheduledToAutoMerge, issues_model.AsCommentType("pull_cancel_scheduled_merge")) } diff --git a/models/issues/issue.go b/models/issues/issue.go index 5836030476..4f8e9e161d 100644 --- a/models/issues/issue.go +++ b/models/issues/issue.go @@ -269,7 +269,7 @@ func (issue *Issue) LoadPullRequest(ctx context.Context) (err error) { } func (issue *Issue) loadComments(ctx context.Context) (err error) { - return issue.loadCommentsByType(ctx, CommentTypeUnknown) + return issue.loadCommentsByType(ctx, CommentTypeUndefined) } // LoadDiscussComments loads discuss comments diff --git a/models/issues/pull_test.go b/models/issues/pull_test.go index bcd33329eb..8d99b2667c 100644 --- a/models/issues/pull_test.go +++ b/models/issues/pull_test.go @@ -109,11 +109,11 @@ func TestHasUnmergedPullRequestsByHeadInfo(t *testing.T) { exist, err := issues_model.HasUnmergedPullRequestsByHeadInfo(db.DefaultContext, 1, "branch2") assert.NoError(t, err) - assert.Equal(t, true, exist) + assert.True(t, exist) exist, err = issues_model.HasUnmergedPullRequestsByHeadInfo(db.DefaultContext, 1, "not_exist_branch") assert.NoError(t, err) - assert.Equal(t, false, exist) + assert.False(t, exist) } func TestGetUnmergedPullRequestsByHeadInfo(t *testing.T) { diff --git a/models/issues/tracked_time_test.go b/models/issues/tracked_time_test.go index becfd79d41..99b977cca5 100644 --- a/models/issues/tracked_time_test.go +++ b/models/issues/tracked_time_test.go @@ -35,7 +35,7 @@ func TestAddTime(t *testing.T) { assert.Equal(t, int64(3661), tt.Time) comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{Type: issues_model.CommentTypeAddTimeManual, PosterID: 3, IssueID: 1}) - assert.Equal(t, comment.Content, "1 hour 1 minute") + assert.Equal(t, "1 hour 1 minute", comment.Content) } func TestGetTrackedTimes(t *testing.T) { diff --git a/models/migrations/base/testlogger.go b/models/migrations/base/testlogger.go index 7cbf4602be..80e672952a 100644 --- a/models/migrations/base/testlogger.go +++ b/models/migrations/base/testlogger.go @@ -165,11 +165,6 @@ func (log *TestLogger) Init(config string) error { return nil } -// Content returns the content accumulated in the content provider -func (log *TestLogger) Content() (string, error) { - return "", fmt.Errorf("not supported") -} - // Flush when log should be flushed func (log *TestLogger) Flush() { } diff --git a/models/migrations/base/tests.go b/models/migrations/base/tests.go index 2f1b246648..14f374f1a7 100644 --- a/models/migrations/base/tests.go +++ b/models/migrations/base/tests.go @@ -1,6 +1,7 @@ // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT +//nolint:forbidigo package base import ( diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index 4e25aaaa0f..5320a96bd8 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -485,6 +485,8 @@ var migrations = []Migration{ NewMigration("Fix incorrect admin team unit access mode", v1_20.FixIncorrectAdminTeamUnitAccessMode), // v253 -> v254 NewMigration("Fix ExternalTracker and ExternalWiki accessMode in owner and admin team", v1_20.FixExternalTrackerAndExternalWikiAccessModeInOwnerAndAdminTeam), + // v254 -> v255 + NewMigration("Add ActionTaskOutput table", v1_20.AddActionTaskOutputTable), // to modify later NewMigration("Add size limit on repository", v1_20.AddSizeLimitOnRepo), } diff --git a/models/migrations/v1_11/v112.go b/models/migrations/v1_11/v112.go index 3478ffe449..ff1f972204 100644 --- a/models/migrations/v1_11/v112.go +++ b/models/migrations/v1_11/v112.go @@ -31,7 +31,7 @@ func RemoveAttachmentMissedRepo(x *xorm.Engine) error { for i := 0; i < len(attachments); i++ { uuid := attachments[i].UUID if err = util.RemoveAll(filepath.Join(setting.Attachment.Path, uuid[0:1], uuid[1:2], uuid)); err != nil { - fmt.Printf("Error: %v", err) + fmt.Printf("Error: %v", err) //nolint:forbidigo } } diff --git a/models/migrations/v1_13/v140.go b/models/migrations/v1_13/v140.go index 4d87b955fd..30c1bc07d2 100644 --- a/models/migrations/v1_13/v140.go +++ b/models/migrations/v1_13/v140.go @@ -22,10 +22,10 @@ func FixLanguageStatsToSaveSize(x *xorm.Engine) error { type RepoIndexerType int const ( - // RepoIndexerTypeCode code indexer - RepoIndexerTypeCode RepoIndexerType = iota // 0 - // RepoIndexerTypeStats repository stats indexer - RepoIndexerTypeStats // 1 + // RepoIndexerTypeCode code indexer - 0 + RepoIndexerTypeCode RepoIndexerType = iota //nolint:unused + // RepoIndexerTypeStats repository stats indexer - 1 + RepoIndexerTypeStats ) // RepoIndexerStatus see models/repo_indexer.go diff --git a/models/migrations/v1_14/v157.go b/models/migrations/v1_14/v157.go index b9d8225af8..7187278d29 100644 --- a/models/migrations/v1_14/v157.go +++ b/models/migrations/v1_14/v157.go @@ -8,13 +8,13 @@ import ( ) func FixRepoTopics(x *xorm.Engine) error { - type Topic struct { + type Topic struct { //nolint:unused ID int64 `xorm:"pk autoincr"` Name string `xorm:"UNIQUE VARCHAR(25)"` RepoCount int } - type RepoTopic struct { + type RepoTopic struct { //nolint:unused RepoID int64 `xorm:"pk"` TopicID int64 `xorm:"pk"` } diff --git a/models/migrations/v1_14/v165.go b/models/migrations/v1_14/v165.go index a784832e1c..926350cdf7 100644 --- a/models/migrations/v1_14/v165.go +++ b/models/migrations/v1_14/v165.go @@ -16,7 +16,7 @@ func ConvertHookTaskTypeToVarcharAndTrim(x *xorm.Engine) error { return nil } - type HookTask struct { + type HookTask struct { //nolint:unused Typ string `xorm:"VARCHAR(16) index"` } @@ -42,7 +42,7 @@ func ConvertHookTaskTypeToVarcharAndTrim(x *xorm.Engine) error { return err } - type Webhook struct { + type Webhook struct { //nolint:unused Type string `xorm:"VARCHAR(16) index"` } diff --git a/models/migrations/v1_20/v254.go b/models/migrations/v1_20/v254.go new file mode 100644 index 0000000000..1e26979a5b --- /dev/null +++ b/models/migrations/v1_20/v254.go @@ -0,0 +1,18 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_20 //nolint + +import ( + "xorm.io/xorm" +) + +func AddActionTaskOutputTable(x *xorm.Engine) error { + type ActionTaskOutput struct { + ID int64 + TaskID int64 `xorm:"INDEX UNIQUE(task_id_output_key)"` + OutputKey string `xorm:"VARCHAR(255) UNIQUE(task_id_output_key)"` + OutputValue string `xorm:"MEDIUMTEXT"` + } + return x.Sync(new(ActionTaskOutput)) +} diff --git a/models/org_team.go b/models/org_team.go index be3b63b52e..be0c859a4b 100644 --- a/models/org_team.go +++ b/models/org_team.go @@ -414,10 +414,17 @@ func DeleteTeam(t *organization.Team) error { &organization.TeamUser{OrgID: t.OrgID, TeamID: t.ID}, &organization.TeamUnit{TeamID: t.ID}, &organization.TeamInvite{TeamID: t.ID}, + &issues_model.Review{Type: issues_model.ReviewTypeRequest, ReviewerTeamID: t.ID}, // batch delete the binding relationship between team and PR (request review from team) ); err != nil { return err } + for _, tm := range t.Members { + if err := removeInvalidOrgUser(ctx, tm.ID, t.OrgID); err != nil { + return err + } + } + // Update organization number of teams. if _, err := db.Exec(ctx, "UPDATE `user` SET num_teams=num_teams-1 WHERE id=?", t.OrgID); err != nil { return err @@ -567,16 +574,19 @@ func removeTeamMember(ctx context.Context, team *organization.Team, userID int64 } } + return removeInvalidOrgUser(ctx, userID, team.OrgID) +} + +func removeInvalidOrgUser(ctx context.Context, userID, orgID int64) error { // Check if the user is a member of any team in the organization. - if count, err := e.Count(&organization.TeamUser{ + if count, err := db.GetEngine(ctx).Count(&organization.TeamUser{ UID: userID, - OrgID: team.OrgID, + OrgID: orgID, }); err != nil { return err } else if count == 0 { - return removeOrgUser(ctx, team.OrgID, userID) + return removeOrgUser(ctx, orgID, userID) } - return nil } diff --git a/models/organization/org.go b/models/organization/org.go index 53b020d704..30b76fb1a0 100644 --- a/models/organization/org.go +++ b/models/organization/org.go @@ -342,11 +342,15 @@ func CreateOrganization(org *Organization, owner *user_model.User) (err error) { // insert units for team units := make([]TeamUnit, 0, len(unit.AllRepoUnitTypes)) for _, tp := range unit.AllRepoUnitTypes { + up := perm.AccessModeOwner + if tp == unit.TypeExternalTracker || tp == unit.TypeExternalWiki { + up = perm.AccessModeRead + } units = append(units, TeamUnit{ OrgID: org.ID, TeamID: t.ID, Type: tp, - AccessMode: perm.AccessModeOwner, + AccessMode: up, }) } diff --git a/models/repo/repo.go b/models/repo/repo.go index acd18f019d..d63c0449b6 100644 --- a/models/repo/repo.go +++ b/models/repo/repo.go @@ -226,6 +226,12 @@ func (repo *Repository) IsBroken() bool { return repo.Status == RepositoryBroken } +// MarkAsBrokenEmpty marks the repo as broken and empty +func (repo *Repository) MarkAsBrokenEmpty() { + repo.Status = RepositoryBroken + repo.IsEmpty = true +} + // AfterLoad is invoked from XORM after setting the values of all fields of this object. func (repo *Repository) AfterLoad() { repo.NumOpenIssues = repo.NumIssues - repo.NumClosedIssues @@ -769,7 +775,7 @@ func IsRepositoryExist(ctx context.Context, u *user_model.User, repoName string) return false, err } isDir, err := util.IsDir(RepoPath(u.Name, repoName)) - return has && isDir, err + return has || isDir, err } // GetTemplateRepo populates repo.TemplateRepo for a generated repository and diff --git a/models/repo/repo_list_test.go b/models/repo/repo_list_test.go index d87d70c189..573281ea0b 100644 --- a/models/repo/repo_list_test.go +++ b/models/repo/repo_list_test.go @@ -235,12 +235,12 @@ func TestSearchRepository(t *testing.T) { { name: "AllPublic/PublicRepositoriesOfUserIncludingCollaborative", opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, AllPublic: true, Template: util.OptionalBoolFalse}, - count: 29, + count: 30, }, { name: "AllPublic/PublicAndPrivateRepositoriesOfUserIncludingCollaborative", opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Private: true, AllPublic: true, AllLimited: true, Template: util.OptionalBoolFalse}, - count: 34, + count: 35, }, { name: "AllPublic/PublicAndPrivateRepositoriesOfUserIncludingCollaborativeByName", @@ -255,7 +255,7 @@ func TestSearchRepository(t *testing.T) { { name: "AllPublic/PublicRepositoriesOfOrganization", opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 17, AllPublic: true, Collaborate: util.OptionalBoolFalse, Template: util.OptionalBoolFalse}, - count: 29, + count: 30, }, { name: "AllTemplates", diff --git a/models/unittest/consistency.go b/models/unittest/consistency.go index 17ff75c089..41798c6253 100644 --- a/models/unittest/consistency.go +++ b/models/unittest/consistency.go @@ -152,7 +152,7 @@ func init() { Query() assert.NoError(t, err) - assert.EqualValues(t, label.int("NumIssues"), len(issueLabels), "Unexpected number of issue for label id: %d", label.int("ID")) + assert.Len(t, issueLabels, label.int("NumIssues"), "Unexpected number of issue for label id: %d", label.int("ID")) issueIDs := make([]int, len(issueLabels)) for i, issueLabel := range issueLabels { diff --git a/models/unittest/fixtures.go b/models/unittest/fixtures.go index 545452a159..c653ce1e38 100644 --- a/models/unittest/fixtures.go +++ b/models/unittest/fixtures.go @@ -1,6 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT +//nolint:forbidigo package unittest import ( @@ -17,7 +18,7 @@ import ( "xorm.io/xorm/schemas" ) -var fixtures *testfixtures.Loader +var fixturesLoader *testfixtures.Loader // GetXORMEngine gets the XORM engine func GetXORMEngine(engine ...*xorm.Engine) (x *xorm.Engine) { @@ -30,11 +31,11 @@ func GetXORMEngine(engine ...*xorm.Engine) (x *xorm.Engine) { // InitFixtures initialize test fixtures for a test database func InitFixtures(opts FixturesOptions, engine ...*xorm.Engine) (err error) { e := GetXORMEngine(engine...) - var testfiles func(*testfixtures.Loader) error + var fixtureOptionFiles func(*testfixtures.Loader) error if opts.Dir != "" { - testfiles = testfixtures.Directory(opts.Dir) + fixtureOptionFiles = testfixtures.Directory(opts.Dir) } else { - testfiles = testfixtures.Files(opts.Files...) + fixtureOptionFiles = testfixtures.Files(opts.Files...) } dialect := "unknown" switch e.Dialect().URI().DBType { @@ -54,14 +55,14 @@ func InitFixtures(opts FixturesOptions, engine ...*xorm.Engine) (err error) { testfixtures.Database(e.DB().DB), testfixtures.Dialect(dialect), testfixtures.DangerousSkipTestDatabaseCheck(), - testfiles, + fixtureOptionFiles, } if e.Dialect().URI().DBType == schemas.POSTGRES { loaderOptions = append(loaderOptions, testfixtures.SkipResetSequences()) } - fixtures, err = testfixtures.New(loaderOptions...) + fixturesLoader, err = testfixtures.New(loaderOptions...) if err != nil { return err } @@ -78,11 +79,9 @@ func InitFixtures(opts FixturesOptions, engine ...*xorm.Engine) (err error) { func LoadFixtures(engine ...*xorm.Engine) error { e := GetXORMEngine(engine...) var err error - // Database transaction conflicts could occur and result in ROLLBACK - // As a simple workaround, we just retry 20 times. - for i := 0; i < 20; i++ { - err = fixtures.Load() - if err == nil { + // (doubt) database transaction conflicts could occur and result in ROLLBACK? just try for a few times. + for i := 0; i < 5; i++ { + if err = fixturesLoader.Load(); err == nil { break } time.Sleep(200 * time.Millisecond) diff --git a/models/user/badge.go b/models/user/badge.go index 948b0b1065..ee52b44cf5 100644 --- a/models/user/badge.go +++ b/models/user/badge.go @@ -17,7 +17,7 @@ type Badge struct { } // UserBadge represents a user badge -type UserBadge struct { +type UserBadge struct { //nolint:revive ID int64 `xorm:"pk autoincr"` BadgeID int64 UserID int64 `xorm:"INDEX"` diff --git a/models/user/user.go b/models/user/user.go index 5f152780bf..053d6680cd 100644 --- a/models/user/user.go +++ b/models/user/user.go @@ -62,7 +62,7 @@ const ( EmailNotificationsOnMention = "onmention" // EmailNotificationsDisabled indicates that the user would not like to be notified via email. EmailNotificationsDisabled = "disabled" - // EmailNotificationsEnabled indicates that the user would like to receive all email notifications and your own + // EmailNotificationsAndYourOwn indicates that the user would like to receive all email notifications and your own EmailNotificationsAndYourOwn = "andyourown" ) diff --git a/models/user/user_test.go b/models/user/user_test.go index 8e78fee6b3..cbfcd15463 100644 --- a/models/user/user_test.go +++ b/models/user/user_test.go @@ -5,6 +5,7 @@ package user_test import ( "context" + "fmt" "math/rand" "strings" "testing" @@ -35,10 +36,10 @@ func TestGetUserEmailsByNames(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) // ignore none active user email - assert.Equal(t, []string{"user8@example.com"}, user_model.GetUserEmailsByNames(db.DefaultContext, []string{"user8", "user9"})) - assert.Equal(t, []string{"user8@example.com", "user5@example.com"}, user_model.GetUserEmailsByNames(db.DefaultContext, []string{"user8", "user5"})) + assert.ElementsMatch(t, []string{"user8@example.com"}, user_model.GetUserEmailsByNames(db.DefaultContext, []string{"user8", "user9"})) + assert.ElementsMatch(t, []string{"user8@example.com", "user5@example.com"}, user_model.GetUserEmailsByNames(db.DefaultContext, []string{"user8", "user5"})) - assert.Equal(t, []string{"user8@example.com"}, user_model.GetUserEmailsByNames(db.DefaultContext, []string{"user8", "user7"})) + assert.ElementsMatch(t, []string{"user8@example.com"}, user_model.GetUserEmailsByNames(db.DefaultContext, []string{"user8", "user7"})) } func TestCanCreateOrganization(t *testing.T) { @@ -64,9 +65,10 @@ func TestSearchUsers(t *testing.T) { testSuccess := func(opts *user_model.SearchUserOptions, expectedUserOrOrgIDs []int64) { users, _, err := user_model.SearchUsers(opts) assert.NoError(t, err) - if assert.Len(t, users, len(expectedUserOrOrgIDs), opts) { + cassText := fmt.Sprintf("ids: %v, opts: %v", expectedUserOrOrgIDs, opts) + if assert.Len(t, users, len(expectedUserOrOrgIDs), "case: %s", cassText) { for i, expectedID := range expectedUserOrOrgIDs { - assert.EqualValues(t, expectedID, users[i].ID) + assert.EqualValues(t, expectedID, users[i].ID, "case: %s", cassText) } } } @@ -118,7 +120,7 @@ func TestSearchUsers(t *testing.T) { []int64{1}) testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsRestricted: util.OptionalBoolTrue}, - []int64{29, 30}) + []int64{29}) testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsProhibitLogin: util.OptionalBoolTrue}, []int64{30}) diff --git a/modules/actions/workflows.go b/modules/actions/workflows.go index f37f4f2878..d560b7718f 100644 --- a/modules/actions/workflows.go +++ b/modules/actions/workflows.go @@ -29,6 +29,14 @@ func init() { } } +func IsWorkflow(path string) bool { + if (!strings.HasSuffix(path, ".yaml")) && (!strings.HasSuffix(path, ".yml")) { + return false + } + + return strings.HasPrefix(path, ".gitea/workflows") || strings.HasPrefix(path, ".github/workflows") +} + func ListWorkflows(commit *git.Commit) (git.Entries, error) { tree, err := commit.SubTree(".gitea/workflows") if _, ok := err.(git.ErrNotExist); ok { diff --git a/modules/avatar/avatar.go b/modules/avatar/avatar.go index 9ee926b059..c166f14404 100644 --- a/modules/avatar/avatar.go +++ b/modules/avatar/avatar.go @@ -18,6 +18,8 @@ import ( "github.com/nfnt/resize" "github.com/oliamb/cutter" + + _ "golang.org/x/image/webp" // for processing webp images ) // AvatarSize returns avatar's size diff --git a/modules/charset/escape_stream.go b/modules/charset/escape_stream.go index 89bf36edce..03d4cfc0c1 100644 --- a/modules/charset/escape_stream.go +++ b/modules/charset/escape_stream.go @@ -47,7 +47,9 @@ func (e *escapeStreamer) EscapeStatus() *EscapeStatus { // Text tells the next streamer there is a text func (e *escapeStreamer) Text(data string) error { sb := &strings.Builder{} - pos, until, next := 0, 0, 0 + var until int + var next int + pos := 0 if len(data) > len(UTF8BOM) && data[:len(UTF8BOM)] == string(UTF8BOM) { _, _ = sb.WriteString(data[:len(UTF8BOM)]) pos = len(UTF8BOM) diff --git a/modules/context/auth.go b/modules/context/auth.go deleted file mode 100644 index 5e071b4fab..0000000000 --- a/modules/context/auth.go +++ /dev/null @@ -1,194 +0,0 @@ -// Copyright 2014 The Gogs Authors. All rights reserved. -// Copyright 2019 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package context - -import ( - "net/http" - "strings" - - "code.gitea.io/gitea/models/auth" - "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/web/middleware" -) - -// ToggleOptions contains required or check options -type ToggleOptions struct { - SignInRequired bool - SignOutRequired bool - AdminRequired bool - DisableCSRF bool -} - -// Toggle returns toggle options as middleware -func Toggle(options *ToggleOptions) func(ctx *Context) { - return func(ctx *Context) { - // Check prohibit login users. - if ctx.IsSigned { - if !ctx.Doer.IsActive && setting.Service.RegisterEmailConfirm { - ctx.Data["Title"] = ctx.Tr("auth.active_your_account") - ctx.HTML(http.StatusOK, "user/auth/activate") - return - } - if !ctx.Doer.IsActive || ctx.Doer.ProhibitLogin { - log.Info("Failed authentication attempt for %s from %s", ctx.Doer.Name, ctx.RemoteAddr()) - ctx.Data["Title"] = ctx.Tr("auth.prohibit_login") - ctx.HTML(http.StatusOK, "user/auth/prohibit_login") - return - } - - if ctx.Doer.MustChangePassword { - if ctx.Req.URL.Path != "/user/settings/change_password" { - if strings.HasPrefix(ctx.Req.UserAgent(), "git") { - ctx.Error(http.StatusUnauthorized, ctx.Tr("auth.must_change_password")) - return - } - ctx.Data["Title"] = ctx.Tr("auth.must_change_password") - ctx.Data["ChangePasscodeLink"] = setting.AppSubURL + "/user/change_password" - if ctx.Req.URL.Path != "/user/events" { - middleware.SetRedirectToCookie(ctx.Resp, setting.AppSubURL+ctx.Req.URL.RequestURI()) - } - ctx.Redirect(setting.AppSubURL + "/user/settings/change_password") - return - } - } else if ctx.Req.URL.Path == "/user/settings/change_password" { - // make sure that the form cannot be accessed by users who don't need this - ctx.Redirect(setting.AppSubURL + "/") - return - } - } - - // Redirect to dashboard if user tries to visit any non-login page. - if options.SignOutRequired && ctx.IsSigned && ctx.Req.URL.RequestURI() != "/" { - ctx.Redirect(setting.AppSubURL + "/") - return - } - - if !options.SignOutRequired && !options.DisableCSRF && ctx.Req.Method == "POST" { - ctx.Csrf.Validate(ctx) - if ctx.Written() { - return - } - } - - if options.SignInRequired { - if !ctx.IsSigned { - if ctx.Req.URL.Path != "/user/events" { - middleware.SetRedirectToCookie(ctx.Resp, setting.AppSubURL+ctx.Req.URL.RequestURI()) - } - ctx.Redirect(setting.AppSubURL + "/user/login") - return - } else if !ctx.Doer.IsActive && setting.Service.RegisterEmailConfirm { - ctx.Data["Title"] = ctx.Tr("auth.active_your_account") - ctx.HTML(http.StatusOK, "user/auth/activate") - return - } - } - - // Redirect to log in page if auto-signin info is provided and has not signed in. - if !options.SignOutRequired && !ctx.IsSigned && - len(ctx.GetSiteCookie(setting.CookieUserName)) > 0 { - if ctx.Req.URL.Path != "/user/events" { - middleware.SetRedirectToCookie(ctx.Resp, setting.AppSubURL+ctx.Req.URL.RequestURI()) - } - ctx.Redirect(setting.AppSubURL + "/user/login") - return - } - - if options.AdminRequired { - if !ctx.Doer.IsAdmin { - ctx.Error(http.StatusForbidden) - return - } - ctx.Data["PageIsAdmin"] = true - } - } -} - -// ToggleAPI returns toggle options as middleware -func ToggleAPI(options *ToggleOptions) func(ctx *APIContext) { - return func(ctx *APIContext) { - // Check prohibit login users. - if ctx.IsSigned { - if !ctx.Doer.IsActive && setting.Service.RegisterEmailConfirm { - ctx.Data["Title"] = ctx.Tr("auth.active_your_account") - ctx.JSON(http.StatusForbidden, map[string]string{ - "message": "This account is not activated.", - }) - return - } - if !ctx.Doer.IsActive || ctx.Doer.ProhibitLogin { - log.Info("Failed authentication attempt for %s from %s", ctx.Doer.Name, ctx.RemoteAddr()) - ctx.Data["Title"] = ctx.Tr("auth.prohibit_login") - ctx.JSON(http.StatusForbidden, map[string]string{ - "message": "This account is prohibited from signing in, please contact your site administrator.", - }) - return - } - - if ctx.Doer.MustChangePassword { - ctx.JSON(http.StatusForbidden, map[string]string{ - "message": "You must change your password. Change it at: " + setting.AppURL + "/user/change_password", - }) - return - } - } - - // Redirect to dashboard if user tries to visit any non-login page. - if options.SignOutRequired && ctx.IsSigned && ctx.Req.URL.RequestURI() != "/" { - ctx.Redirect(setting.AppSubURL + "/") - return - } - - if options.SignInRequired { - if !ctx.IsSigned { - // Restrict API calls with error message. - ctx.JSON(http.StatusForbidden, map[string]string{ - "message": "Only signed in user is allowed to call APIs.", - }) - return - } else if !ctx.Doer.IsActive && setting.Service.RegisterEmailConfirm { - ctx.Data["Title"] = ctx.Tr("auth.active_your_account") - ctx.HTML(http.StatusOK, "user/auth/activate") - return - } - if ctx.IsSigned && ctx.IsBasicAuth { - if skip, ok := ctx.Data["SkipLocalTwoFA"]; ok && skip.(bool) { - return // Skip 2FA - } - twofa, err := auth.GetTwoFactorByUID(ctx.Doer.ID) - if err != nil { - if auth.IsErrTwoFactorNotEnrolled(err) { - return // No 2FA enrollment for this user - } - ctx.InternalServerError(err) - return - } - otpHeader := ctx.Req.Header.Get("X-Gitea-OTP") - ok, err := twofa.ValidateTOTP(otpHeader) - if err != nil { - ctx.InternalServerError(err) - return - } - if !ok { - ctx.JSON(http.StatusForbidden, map[string]string{ - "message": "Only signed in user is allowed to call APIs.", - }) - return - } - } - } - - if options.AdminRequired { - if !ctx.Doer.IsAdmin { - ctx.JSON(http.StatusForbidden, map[string]string{ - "message": "You have no permission to request for this.", - }) - return - } - ctx.Data["PageIsAdmin"] = true - } - } -} diff --git a/modules/context/context.go b/modules/context/context.go index 2507cc10c0..f262c7cce7 100644 --- a/modules/context/context.go +++ b/modules/context/context.go @@ -47,7 +47,7 @@ const CookieNameFlash = "gitea_flash" // Render represents a template render type Render interface { - TemplateLookup(tmpl string) (*template.Template, error) + TemplateLookup(tmpl string) (templates.TemplateExecutor, error) HTML(w io.Writer, status int, name string, data interface{}) error } @@ -301,7 +301,7 @@ func (ctx *Context) serverErrorInternal(logMsg string, logErr error) { // it's safe to show internal error to admin users, and it helps if !setting.IsProd || (ctx.Doer != nil && ctx.Doer.IsAdmin) { - ctx.Data["ErrorMsg"] = logErr + ctx.Data["ErrorMsg"] = fmt.Sprintf("%s, %s", logMsg, logErr) } } @@ -741,8 +741,7 @@ func Contexter(ctx context.Context) func(next http.Handler) http.Handler { ctx.Data["ShowRegistrationButton"] = setting.Service.ShowRegistrationButton ctx.Data["ShowMilestonesDashboardPage"] = setting.Service.ShowMilestonesDashboardPage - ctx.Data["ShowFooterBranding"] = setting.ShowFooterBranding - ctx.Data["ShowFooterVersion"] = setting.ShowFooterVersion + ctx.Data["ShowFooterVersion"] = setting.Other.ShowFooterVersion ctx.Data["EnableSwagger"] = setting.API.EnableSwagger ctx.Data["EnableOpenIDSignIn"] = setting.Service.EnableOpenIDSignIn diff --git a/modules/context/repo.go b/modules/context/repo.go index 8b4d0c1bf4..6b811054c2 100644 --- a/modules/context/repo.go +++ b/modules/context/repo.go @@ -184,6 +184,9 @@ func (r *Repository) CanCreateIssueDependencies(user *user_model.User, isPull bo // GetCommitsCount returns cached commit count for current view func (r *Repository) GetCommitsCount() (int64, error) { + if r.Commit == nil { + return 0, nil + } var contextName string if r.IsViewBranch { contextName = r.BranchName @@ -449,7 +452,7 @@ func RepoAssignment(ctx *Context) (cancel context.CancelFunc) { userName := ctx.Params(":username") repoName := ctx.Params(":reponame") repoName = strings.TrimSuffix(repoName, ".git") - if setting.EnableFeed { + if setting.Other.EnableFeed { repoName = strings.TrimSuffix(repoName, ".rss") repoName = strings.TrimSuffix(repoName, ".atom") } @@ -537,7 +540,7 @@ func RepoAssignment(ctx *Context) (cancel context.CancelFunc) { ctx.Data["RepoLink"] = ctx.Repo.RepoLink ctx.Data["RepoRelPath"] = ctx.Repo.Owner.Name + "/" + ctx.Repo.Repository.Name - if setting.EnableFeed { + if setting.Other.EnableFeed { ctx.Data["EnableFeed"] = true ctx.Data["FeedURL"] = ctx.Repo.RepoLink } @@ -642,8 +645,7 @@ func RepoAssignment(ctx *Context) (cancel context.CancelFunc) { if err != nil { if strings.Contains(err.Error(), "repository does not exist") || strings.Contains(err.Error(), "no such file or directory") { log.Error("Repository %-v has a broken repository on the file system: %s Error: %v", ctx.Repo.Repository, ctx.Repo.Repository.RepoPath(), err) - ctx.Repo.Repository.Status = repo_model.RepositoryBroken - ctx.Repo.Repository.IsEmpty = true + ctx.Repo.Repository.MarkAsBrokenEmpty() ctx.Data["BranchName"] = ctx.Repo.Repository.DefaultBranch // Only allow access to base of repo or settings if !isHomeOrSettings { @@ -689,7 +691,7 @@ func RepoAssignment(ctx *Context) (cancel context.CancelFunc) { ctx.Data["BranchesCount"] = len(brs) // If not branch selected, try default one. - // If default branch doesn't exists, fall back to some other branch. + // If default branch doesn't exist, fall back to some other branch. if len(ctx.Repo.BranchName) == 0 { if len(ctx.Repo.Repository.DefaultBranch) > 0 && gitRepo.IsBranchExist(ctx.Repo.Repository.DefaultBranch) { ctx.Repo.BranchName = ctx.Repo.Repository.DefaultBranch @@ -878,6 +880,10 @@ func RepoRefByType(refType RepoRefType, ignoreNotExistErr ...bool) func(*Context return func(ctx *Context) (cancel context.CancelFunc) { // Empty repository does not have reference information. if ctx.Repo.Repository.IsEmpty { + // assume the user is viewing the (non-existent) default branch + ctx.Repo.IsViewBranch = true + ctx.Repo.BranchName = ctx.Repo.Repository.DefaultBranch + ctx.Data["TreePath"] = "" return } @@ -907,27 +913,30 @@ func RepoRefByType(refType RepoRefType, ignoreNotExistErr ...bool) func(*Context refName = ctx.Repo.Repository.DefaultBranch if !ctx.Repo.GitRepo.IsBranchExist(refName) { brs, _, err := ctx.Repo.GitRepo.GetBranchNames(0, 0) - if err != nil { - ctx.ServerError("GetBranches", err) - return + if err == nil && len(brs) != 0 { + refName = brs[0] } else if len(brs) == 0 { - err = fmt.Errorf("No branches in non-empty repository %s", - ctx.Repo.GitRepo.Path) - ctx.ServerError("GetBranches", err) - return + log.Error("No branches in non-empty repository %s", ctx.Repo.GitRepo.Path) + ctx.Repo.Repository.MarkAsBrokenEmpty() + } else { + log.Error("GetBranches error: %v", err) + ctx.Repo.Repository.MarkAsBrokenEmpty() } - refName = brs[0] } ctx.Repo.RefName = refName ctx.Repo.BranchName = refName ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetBranchCommit(refName) - if err != nil { + if err == nil { + ctx.Repo.CommitID = ctx.Repo.Commit.ID.String() + } else if strings.Contains(err.Error(), "fatal: not a git repository") || strings.Contains(err.Error(), "object does not exist") { + // if the repository is broken, we can continue to the handler code, to show "Settings -> Delete Repository" for end users + log.Error("GetBranchCommit: %v", err) + ctx.Repo.Repository.MarkAsBrokenEmpty() + } else { ctx.ServerError("GetBranchCommit", err) return } - ctx.Repo.CommitID = ctx.Repo.Commit.ID.String() ctx.Repo.IsViewBranch = true - } else { refName = getRefName(ctx, refType) ctx.Repo.RefName = refName diff --git a/modules/doctor/paths.go b/modules/doctor/paths.go index d7bf2539e7..1558efc25b 100644 --- a/modules/doctor/paths.go +++ b/modules/doctor/paths.go @@ -106,7 +106,7 @@ func isWritableDir(path string) error { return err } if err := os.Remove(tmpFile.Name()); err != nil { - fmt.Printf("Warning: can't remove temporary file: '%s'\n", tmpFile.Name()) + fmt.Printf("Warning: can't remove temporary file: '%s'\n", tmpFile.Name()) //nolint:forbidigo } tmpFile.Close() return nil diff --git a/modules/git/command.go b/modules/git/command.go index a42d859f55..ac013d4ea1 100644 --- a/modules/git/command.go +++ b/modules/git/command.go @@ -211,10 +211,18 @@ type RunOpts struct { Env []string Timeout time.Duration UseContextTimeout bool - Dir string - Stdout, Stderr io.Writer - Stdin io.Reader - PipelineFunc func(context.Context, context.CancelFunc) error + + // Dir is the working dir for the git command, however: + // FIXME: this could be incorrect in many cases, for example: + // * /some/path/.git + // * /some/path/.git/gitea-data/data/repositories/user/repo.git + // If "user/repo.git" is invalid/broken, then running git command in it will use "/some/path/.git", and produce unexpected results + // The correct approach is to use `--git-dir" global argument + Dir string + + Stdout, Stderr io.Writer + Stdin io.Reader + PipelineFunc func(context.Context, context.CancelFunc) error } func commonBaseEnvs() []string { diff --git a/modules/git/repo.go b/modules/git/repo.go index c486d20b28..36e0d84dde 100644 --- a/modules/git/repo.go +++ b/modules/git/repo.go @@ -80,7 +80,7 @@ func InitRepository(ctx context.Context, repoPath string, bare bool) error { // IsEmpty Check if repository is empty. func (repo *Repository) IsEmpty() (bool, error) { var errbuf, output strings.Builder - if err := NewCommand(repo.Ctx, "show-ref", "--head", "^HEAD$"). + if err := NewCommand(repo.Ctx).AddOptionFormat("--git-dir=%s", repo.Path).AddArguments("show-ref", "--head", "^HEAD$"). Run(&RunOpts{ Dir: repo.Path, Stdout: &output, diff --git a/modules/git/repo_attribute_test.go b/modules/git/repo_attribute_test.go index f88ae95407..350b69dd25 100644 --- a/modules/git/repo_attribute_test.go +++ b/modules/git/repo_attribute_test.go @@ -19,7 +19,7 @@ func Test_nulSeparatedAttributeWriter_ReadAttribute(t *testing.T) { n, err := wr.Write([]byte(testStr)) - assert.Equal(t, n, len(testStr)) + assert.Len(t, testStr, n) assert.NoError(t, err) select { case attr := <-wr.ReadAttribute(): @@ -32,7 +32,7 @@ func Test_nulSeparatedAttributeWriter_ReadAttribute(t *testing.T) { // Write a second attribute again n, err = wr.Write([]byte(testStr)) - assert.Equal(t, n, len(testStr)) + assert.Len(t, testStr, n) assert.NoError(t, err) select { diff --git a/modules/git/repo_base_nogogit.go b/modules/git/repo_base_nogogit.go index a0216d14a6..e0f2d563b3 100644 --- a/modules/git/repo_base_nogogit.go +++ b/modules/git/repo_base_nogogit.go @@ -61,7 +61,7 @@ func OpenRepository(ctx context.Context, repoPath string) (*Repository, error) { } repo.batchWriter, repo.batchReader, repo.batchCancel = CatFileBatch(ctx, repoPath) - repo.checkWriter, repo.checkReader, repo.checkCancel = CatFileBatchCheck(ctx, repo.Path) + repo.checkWriter, repo.checkReader, repo.checkCancel = CatFileBatchCheck(ctx, repoPath) return repo, nil } diff --git a/modules/git/repo_commit.go b/modules/git/repo_commit.go index 2b780feb5c..153a116b06 100644 --- a/modules/git/repo_commit.go +++ b/modules/git/repo_commit.go @@ -84,6 +84,9 @@ func (repo *Repository) GetCommitByPath(relpath string) (*Commit, error) { if err != nil { return nil, err } + if len(commits) == 0 { + return nil, ErrNotExist{ID: relpath} + } return commits[0], nil } diff --git a/modules/git/repo_commit_test.go b/modules/git/repo_commit_test.go index 729fb0ba10..fee145e924 100644 --- a/modules/git/repo_commit_test.go +++ b/modules/git/repo_commit_test.go @@ -97,6 +97,6 @@ func TestRepository_CommitsBetweenIDs(t *testing.T) { for i, c := range cases { commits, err := bareRepo1.CommitsBetweenIDs(c.NewID, c.OldID) assert.NoError(t, err) - assert.Equal(t, c.ExpectedCommits, len(commits), "case %d", i) + assert.Len(t, commits, c.ExpectedCommits, "case %d", i) } } diff --git a/modules/git/repo_tag_test.go b/modules/git/repo_tag_test.go index 8ccfec3eb2..fb6a7cdd1a 100644 --- a/modules/git/repo_tag_test.go +++ b/modules/git/repo_tag_test.go @@ -26,7 +26,7 @@ func TestRepository_GetTags(t *testing.T) { return } assert.Len(t, tags, 2) - assert.Equal(t, len(tags), total) + assert.Len(t, tags, total) assert.EqualValues(t, "signed-tag", tags[0].Name) assert.EqualValues(t, "36f97d9a96457e2bab511db30fe2db03893ebc64", tags[0].ID.String()) assert.EqualValues(t, "tag", tags[0].Type) diff --git a/modules/graceful/manager_windows.go b/modules/graceful/manager_windows.go index 212ddf437c..f3606084aa 100644 --- a/modules/graceful/manager_windows.go +++ b/modules/graceful/manager_windows.go @@ -96,7 +96,7 @@ func (g *Manager) start() { run := svc.Run //lint:ignore SA1019 We use IsAnInteractiveSession because IsWindowsService has a different permissions profile - isAnInteractiveSession, err := svc.IsAnInteractiveSession() + isAnInteractiveSession, err := svc.IsAnInteractiveSession() //nolint:staticcheck if err != nil { log.Error("Unable to ascertain if running as an Windows Service: %v", err) return diff --git a/modules/indexer/code/indexer.go b/modules/indexer/code/indexer.go index 027d13555c..2c493ccf94 100644 --- a/modules/indexer/code/indexer.go +++ b/modules/indexer/code/indexer.go @@ -154,7 +154,9 @@ func Init() { log.Trace("IndexerData Process Repo: %d", indexerData.RepoID) if err := index(ctx, indexer, indexerData.RepoID); err != nil { - log.Error("index: %v", err) + if !setting.IsInTesting { + log.Error("indexer index error for repo %v: %v", indexerData.RepoID, err) + } if indexer.Ping() { continue } diff --git a/modules/indexer/code/indexer_test.go b/modules/indexer/code/indexer_test.go index c353d5b688..52f7e76e41 100644 --- a/modules/indexer/code/indexer_test.go +++ b/modules/indexer/code/indexer_test.go @@ -69,7 +69,7 @@ func testIndexer(name string, t *testing.T, indexer Indexer) { t.Run(kw.Keyword, func(t *testing.T) { total, res, langs, err := indexer.Search(context.TODO(), kw.RepoIDs, "", kw.Keyword, 1, 10, false) assert.NoError(t, err) - assert.EqualValues(t, len(kw.IDs), total) + assert.Len(t, kw.IDs, int(total)) assert.Len(t, langs, kw.Langs) ids := make([]int64, 0, len(res)) diff --git a/modules/indexer/issues/indexer_test.go b/modules/indexer/issues/indexer_test.go index bf9d6d0d16..c7c7cea90f 100644 --- a/modules/indexer/issues/indexer_test.go +++ b/modules/indexer/issues/indexer_test.go @@ -16,7 +16,6 @@ import ( _ "code.gitea.io/gitea/models" "github.com/stretchr/testify/assert" - "gopkg.in/ini.v1" ) func TestMain(m *testing.M) { @@ -27,7 +26,7 @@ func TestMain(m *testing.M) { func TestBleveSearchIssues(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - setting.CfgProvider = ini.Empty() + setting.CfgProvider = setting.NewEmptyConfigProvider() tmpIndexerDir := t.TempDir() diff --git a/modules/indexer/stats/db.go b/modules/indexer/stats/db.go index 9bbdcad60d..2a0475dea6 100644 --- a/modules/indexer/stats/db.go +++ b/modules/indexer/stats/db.go @@ -11,6 +11,7 @@ import ( "code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/process" + "code.gitea.io/gitea/modules/setting" ) // DBIndexer implements Indexer interface to use database's like search @@ -46,7 +47,7 @@ func (db *DBIndexer) Index(id int64) error { // Get latest commit for default branch commitID, err := gitRepo.GetBranchCommitID(repo.DefaultBranch) if err != nil { - if git.IsErrBranchNotExist(err) || git.IsErrNotExist(err) { + if git.IsErrBranchNotExist(err) || git.IsErrNotExist(err) || setting.IsInTesting { log.Debug("Unable to get commit ID for default branch %s in %s ... skipping this repository", repo.DefaultBranch, repo.RepoPath()) return nil } @@ -62,7 +63,9 @@ func (db *DBIndexer) Index(id int64) error { // Calculate and save language statistics to database stats, err := gitRepo.GetLanguageStats(commitID) if err != nil { - log.Error("Unable to get language stats for ID %s for default branch %s in %s. Error: %v", commitID, repo.DefaultBranch, repo.RepoPath(), err) + if !setting.IsInTesting { + log.Error("Unable to get language stats for ID %s for default branch %s in %s. Error: %v", commitID, repo.DefaultBranch, repo.RepoPath(), err) + } return err } err = repo_model.UpdateLanguageStats(repo, commitID, stats) diff --git a/modules/indexer/stats/indexer_test.go b/modules/indexer/stats/indexer_test.go index 50a5fade78..8d9b4e36d9 100644 --- a/modules/indexer/stats/indexer_test.go +++ b/modules/indexer/stats/indexer_test.go @@ -18,7 +18,6 @@ import ( _ "code.gitea.io/gitea/models" "github.com/stretchr/testify/assert" - "gopkg.in/ini.v1" ) func TestMain(m *testing.M) { @@ -29,7 +28,7 @@ func TestMain(m *testing.M) { func TestRepoStatsIndex(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - setting.CfgProvider = ini.Empty() + setting.CfgProvider = setting.NewEmptyConfigProvider() setting.LoadQueueSettings() diff --git a/modules/indexer/stats/queue.go b/modules/indexer/stats/queue.go index 32379f2859..a57338e07d 100644 --- a/modules/indexer/stats/queue.go +++ b/modules/indexer/stats/queue.go @@ -10,6 +10,7 @@ import ( "code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/queue" + "code.gitea.io/gitea/modules/setting" ) // statsQueue represents a queue to handle repository stats updates @@ -20,7 +21,9 @@ func handle(data ...queue.Data) []queue.Data { for _, datum := range data { opts := datum.(int64) if err := indexer.Index(opts); err != nil { - log.Error("stats queue indexer.Index(%d) failed: %v", opts, err) + if !setting.IsInTesting { + log.Error("stats queue indexer.Index(%d) failed: %v", opts, err) + } } } return nil diff --git a/modules/lfs/http_client_test.go b/modules/lfs/http_client_test.go index a8c7e379f6..cb71b9008a 100644 --- a/modules/lfs/http_client_test.go +++ b/modules/lfs/http_client_test.go @@ -162,7 +162,7 @@ func TestHTTPClientDownload(t *testing.T) { assert.NoError(t, err) assert.Equal(t, "download", batchRequest.Operation) - assert.Equal(t, 1, len(batchRequest.Objects)) + assert.Len(t, batchRequest.Objects, 1) assert.Equal(t, p.Oid, batchRequest.Objects[0].Oid) assert.Equal(t, p.Size, batchRequest.Objects[0].Size) @@ -269,7 +269,7 @@ func TestHTTPClientUpload(t *testing.T) { assert.NoError(t, err) assert.Equal(t, "upload", batchRequest.Operation) - assert.Equal(t, 1, len(batchRequest.Objects)) + assert.Len(t, batchRequest.Objects, 1) assert.Equal(t, p.Oid, batchRequest.Objects[0].Oid) assert.Equal(t, p.Size, batchRequest.Objects[0].Size) diff --git a/modules/log/buffer.go b/modules/log/buffer.go deleted file mode 100644 index 1eee2465f1..0000000000 --- a/modules/log/buffer.go +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright 2022 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package log - -import ( - "bytes" - "sync" -) - -type bufferWriteCloser struct { - mu sync.Mutex - buffer bytes.Buffer -} - -func (b *bufferWriteCloser) Write(p []byte) (int, error) { - b.mu.Lock() - defer b.mu.Unlock() - return b.buffer.Write(p) -} - -func (b *bufferWriteCloser) Close() error { - return nil -} - -func (b *bufferWriteCloser) String() string { - b.mu.Lock() - defer b.mu.Unlock() - return b.buffer.String() -} - -// BufferLogger implements LoggerProvider and writes messages in a buffer. -type BufferLogger struct { - WriterLogger -} - -// NewBufferLogger create BufferLogger returning as LoggerProvider. -func NewBufferLogger() LoggerProvider { - log := &BufferLogger{} - log.NewWriterLogger(&bufferWriteCloser{}) - return log -} - -// Init inits connection writer -func (log *BufferLogger) Init(string) error { - log.NewWriterLogger(log.out) - return nil -} - -// Content returns the content accumulated in the content provider -func (log *BufferLogger) Content() (string, error) { - return log.out.(*bufferWriteCloser).String(), nil -} - -// Flush when log should be flushed -func (log *BufferLogger) Flush() { -} - -// ReleaseReopen does nothing -func (log *BufferLogger) ReleaseReopen() error { - return nil -} - -// GetName returns the default name for this implementation -func (log *BufferLogger) GetName() string { - return "buffer" -} - -func init() { - Register("buffer", NewBufferLogger) -} diff --git a/modules/log/buffer_test.go b/modules/log/buffer_test.go deleted file mode 100644 index 1c39006050..0000000000 --- a/modules/log/buffer_test.go +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright 2022 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package log - -import ( - "fmt" - "strings" - "testing" - "time" - - "github.com/stretchr/testify/assert" -) - -func TestBufferLogger(t *testing.T) { - logger := NewBufferLogger() - bufferLogger := logger.(*BufferLogger) - assert.NotNil(t, bufferLogger) - - err := logger.Init("") - assert.NoError(t, err) - - location, _ := time.LoadLocation("EST") - date := time.Date(2019, time.January, 13, 22, 3, 30, 15, location) - - msg := "TEST MSG" - event := Event{ - level: INFO, - msg: msg, - caller: "CALLER", - filename: "FULL/FILENAME", - line: 1, - time: date, - } - logger.LogEvent(&event) - content, err := bufferLogger.Content() - assert.NoError(t, err) - assert.Contains(t, content, msg) - logger.Close() -} - -func TestBufferLoggerContent(t *testing.T) { - level := INFO - logger := NewLogger(0, "console", "console", fmt.Sprintf(`{"level":"%s"}`, level.String())) - - logger.SetLogger("buffer", "buffer", "{}") - defer logger.DelLogger("buffer") - - msg := "A UNIQUE MESSAGE" - Error(msg) - - found := false - for i := 0; i < 30000; i++ { - content, err := logger.GetLoggerProviderContent("buffer") - assert.NoError(t, err) - if strings.Contains(content, msg) { - found = true - break - } - time.Sleep(1 * time.Millisecond) - } - assert.True(t, found) -} diff --git a/modules/log/conn.go b/modules/log/conn.go index 39d5234214..b21a744037 100644 --- a/modules/log/conn.go +++ b/modules/log/conn.go @@ -118,11 +118,6 @@ func (log *ConnLogger) Init(jsonconfig string) error { return nil } -// Content returns the content accumulated in the content provider -func (log *ConnLogger) Content() (string, error) { - return "", fmt.Errorf("not supported") -} - // Flush does nothing for this implementation func (log *ConnLogger) Flush() { } diff --git a/modules/log/console.go b/modules/log/console.go index 8764d984ac..ce0415d139 100644 --- a/modules/log/console.go +++ b/modules/log/console.go @@ -65,11 +65,6 @@ func (log *ConsoleLogger) Init(config string) error { return nil } -// Content returns the content accumulated in the content provider -func (log *ConsoleLogger) Content() (string, error) { - return "", fmt.Errorf("not supported") -} - // Flush when log should be flushed func (log *ConsoleLogger) Flush() { } diff --git a/modules/log/event.go b/modules/log/event.go index 4466f7ff34..723c8810bc 100644 --- a/modules/log/event.go +++ b/modules/log/event.go @@ -82,7 +82,7 @@ func (l *ChannelledLog) Start() { l.closeLogger() return } - l.loggerProvider.LogEvent(event) + l.loggerProvider.LogEvent(event) //nolint:errcheck case _, ok := <-l.flush: if !ok { l.closeLogger() @@ -119,7 +119,7 @@ func (l *ChannelledLog) emptyQueue() bool { if !ok { return false } - l.loggerProvider.LogEvent(event) + l.loggerProvider.LogEvent(event) //nolint:errcheck default: return true } @@ -247,12 +247,6 @@ func (m *MultiChannelledLog) GetEventLogger(name string) EventLogger { return m.loggers[name] } -// GetEventProvider returns a sub logger provider content from this MultiChannelledLog -func (m *MultiChannelledLog) GetLoggerProviderContent(name string) (string, error) { - channelledLogger := m.GetEventLogger(name).(*ChannelledLog) - return channelledLogger.loggerProvider.Content() -} - // GetEventLoggerNames returns a list of names func (m *MultiChannelledLog) GetEventLoggerNames() []string { m.rwmutex.RLock() @@ -351,7 +345,7 @@ func (m *MultiChannelledLog) Start() { for _, logger := range m.loggers { err := logger.LogEvent(event) if err != nil { - fmt.Println(err) + fmt.Println(err) //nolint:forbidigo } } m.rwmutex.RUnlock() @@ -385,7 +379,7 @@ func (m *MultiChannelledLog) emptyQueue() bool { for _, logger := range m.loggers { err := logger.LogEvent(event) if err != nil { - fmt.Println(err) + fmt.Println(err) //nolint:forbidigo } } m.rwmutex.RUnlock() @@ -460,3 +454,7 @@ func (m *MultiChannelledLog) ResetLevel() Level { func (m *MultiChannelledLog) GetName() string { return m.name } + +func (e *Event) GetMsg() string { + return e.msg +} diff --git a/modules/log/file.go b/modules/log/file.go index 16fe26f84d..2ec6de450c 100644 --- a/modules/log/file.go +++ b/modules/log/file.go @@ -181,7 +181,7 @@ func (log *FileLogger) DoRotate() error { } if log.Compress { - go compressOldLogFile(fname, log.CompressionLevel) + go compressOldLogFile(fname, log.CompressionLevel) //nolint:errcheck } // re-start logger @@ -216,7 +216,7 @@ func compressOldLogFile(fname string, compressionLevel int) error { if err != nil { zw.Close() fw.Close() - util.Remove(fname + ".gz") + util.Remove(fname + ".gz") //nolint:errcheck return err } reader.Close() @@ -253,15 +253,6 @@ func (log *FileLogger) deleteOldLog() { }) } -// Content returns the content accumulated in the content provider -func (log *FileLogger) Content() (string, error) { - b, err := os.ReadFile(log.Filename) - if err != nil { - return "", err - } - return string(b), nil -} - // Flush flush file logger. // there are no buffering messages in file logger in memory. // flush file means sync file from disk. diff --git a/modules/log/file_test.go b/modules/log/file_test.go index f1ca23d342..34f7459806 100644 --- a/modules/log/file_test.go +++ b/modules/log/file_test.go @@ -51,7 +51,7 @@ func TestFileLogger(t *testing.T) { fileLogger := NewFileLogger() realFileLogger, ok := fileLogger.(*FileLogger) - assert.Equal(t, true, ok) + assert.True(t, ok) location, _ := time.LoadLocation("EST") diff --git a/modules/log/log.go b/modules/log/log.go index 9057cda37e..eee2728bf6 100644 --- a/modules/log/log.go +++ b/modules/log/log.go @@ -245,7 +245,7 @@ func Close() { func Log(skip int, level Level, format string, v ...interface{}) { l, ok := NamedLoggers.Load(DEFAULT) if ok { - l.Log(skip+1, level, format, v...) + l.Log(skip+1, level, format, v...) //nolint:errcheck } } @@ -273,7 +273,7 @@ func (l *LoggerAsWriter) Write(p []byte) (int, error) { for _, logger := range l.ourLoggers { // Skip = 3 because this presumes that we have been called by log.Println() // If the caller has used log.Output or the like this will be wrong - logger.Log(3, l.level, string(p)) + logger.Log(3, l.level, string(p)) //nolint:errcheck } return len(p), nil } diff --git a/modules/log/logger.go b/modules/log/logger.go index 56e39c392e..71949e29b8 100644 --- a/modules/log/logger.go +++ b/modules/log/logger.go @@ -53,7 +53,7 @@ type LevelLoggerLogger struct { // Trace records trace log func (l *LevelLoggerLogger) Trace(format string, v ...interface{}) { - l.Log(1, TRACE, format, v...) + l.Log(1, TRACE, format, v...) //nolint:errcheck } // IsTrace returns true if the logger is TRACE @@ -63,7 +63,7 @@ func (l *LevelLoggerLogger) IsTrace() bool { // Debug records debug log func (l *LevelLoggerLogger) Debug(format string, v ...interface{}) { - l.Log(1, DEBUG, format, v...) + l.Log(1, DEBUG, format, v...) //nolint:errcheck } // IsDebug returns true if the logger is DEBUG @@ -73,7 +73,7 @@ func (l *LevelLoggerLogger) IsDebug() bool { // Info records information log func (l *LevelLoggerLogger) Info(format string, v ...interface{}) { - l.Log(1, INFO, format, v...) + l.Log(1, INFO, format, v...) //nolint:errcheck } // IsInfo returns true if the logger is INFO @@ -83,7 +83,7 @@ func (l *LevelLoggerLogger) IsInfo() bool { // Warn records warning log func (l *LevelLoggerLogger) Warn(format string, v ...interface{}) { - l.Log(1, WARN, format, v...) + l.Log(1, WARN, format, v...) //nolint:errcheck } // IsWarn returns true if the logger is WARN @@ -93,12 +93,12 @@ func (l *LevelLoggerLogger) IsWarn() bool { // Error records error log func (l *LevelLoggerLogger) Error(format string, v ...interface{}) { - l.Log(1, ERROR, format, v...) + l.Log(1, ERROR, format, v...) //nolint:errcheck } // ErrorWithSkip records error log from "skip" calls back from this function func (l *LevelLoggerLogger) ErrorWithSkip(skip int, format string, v ...interface{}) { - l.Log(skip+1, ERROR, format, v...) + l.Log(skip+1, ERROR, format, v...) //nolint:errcheck } // IsError returns true if the logger is ERROR @@ -108,12 +108,12 @@ func (l *LevelLoggerLogger) IsError() bool { // Critical records critical log func (l *LevelLoggerLogger) Critical(format string, v ...interface{}) { - l.Log(1, CRITICAL, format, v...) + l.Log(1, CRITICAL, format, v...) //nolint:errcheck } // CriticalWithSkip records critical log from "skip" calls back from this function func (l *LevelLoggerLogger) CriticalWithSkip(skip int, format string, v ...interface{}) { - l.Log(skip+1, CRITICAL, format, v...) + l.Log(skip+1, CRITICAL, format, v...) //nolint:errcheck } // IsCritical returns true if the logger is CRITICAL @@ -123,14 +123,14 @@ func (l *LevelLoggerLogger) IsCritical() bool { // Fatal records fatal log and exit the process func (l *LevelLoggerLogger) Fatal(format string, v ...interface{}) { - l.Log(1, FATAL, format, v...) + l.Log(1, FATAL, format, v...) //nolint:errcheck l.Close() os.Exit(1) } // FatalWithSkip records fatal log from "skip" calls back from this function and exits the process func (l *LevelLoggerLogger) FatalWithSkip(skip int, format string, v ...interface{}) { - l.Log(skip+1, FATAL, format, v...) + l.Log(skip+1, FATAL, format, v...) //nolint:errcheck l.Close() os.Exit(1) } diff --git a/modules/log/multichannel.go b/modules/log/multichannel.go index 8ccb08179c..6b8a9b8246 100644 --- a/modules/log/multichannel.go +++ b/modules/log/multichannel.go @@ -99,6 +99,6 @@ func (l *MultiChannelledLogger) SendLog(level Level, caller, filename string, li time: time.Now(), stacktrace: stack, } - l.LogEvent(event) + l.LogEvent(event) //nolint:errcheck return nil } diff --git a/modules/log/provider.go b/modules/log/provider.go index 490c3fa71b..b5058139d7 100644 --- a/modules/log/provider.go +++ b/modules/log/provider.go @@ -6,7 +6,6 @@ package log // LoggerProvider represents behaviors of a logger provider. type LoggerProvider interface { Init(config string) error - Content() (string, error) EventLogger } diff --git a/modules/log/smtp.go b/modules/log/smtp.go index e385020c67..4e896496d7 100644 --- a/modules/log/smtp.go +++ b/modules/log/smtp.go @@ -95,11 +95,6 @@ func (log *SMTPLogger) sendMail(p []byte) (int, error) { ) } -// Content returns the content accumulated in the content provider -func (log *SMTPLogger) Content() (string, error) { - return "", fmt.Errorf("not supported") -} - // Flush when log should be flushed func (log *SMTPLogger) Flush() { } diff --git a/modules/log/writer.go b/modules/log/writer.go index ac4b3c8c4e..61f1d866ee 100644 --- a/modules/log/writer.go +++ b/modules/log/writer.go @@ -202,7 +202,7 @@ func (logger *WriterLogger) createMsg(buf *[]byte, event *Event) { (&protectedANSIWriter{ w: &baw, mode: pawMode, - }).Write(msg) + }).Write(msg) //nolint:errcheck *buf = baw if event.stacktrace != "" && logger.StacktraceLevel <= event.level { @@ -249,7 +249,7 @@ func (logger *WriterLogger) Match(event *Event) bool { (&protectedANSIWriter{ w: &baw, mode: removeColor, - }).Write([]byte(event.msg)) + }).Write([]byte(event.msg)) //nolint:errcheck msg = baw return logger.regexp.Match(msg) } diff --git a/modules/markup/html_test.go b/modules/markup/html_test.go index f8b84485c0..cb1216ec94 100644 --- a/modules/markup/html_test.go +++ b/modules/markup/html_test.go @@ -593,5 +593,5 @@ func TestIssue18471(t *testing.T) { }, strings.NewReader(data), &res) assert.NoError(t, err) - assert.Equal(t, res.String(), "783b039...da951ce") + assert.Equal(t, "783b039...da951ce", res.String()) } diff --git a/modules/packages/container/metadata_test.go b/modules/packages/container/metadata_test.go index 48809f4c99..195b3982cb 100644 --- a/modules/packages/container/metadata_test.go +++ b/modules/packages/container/metadata_test.go @@ -33,7 +33,7 @@ func TestParseImageConfig(t *testing.T) { assert.Equal(t, projectURL, metadata.ProjectURL) assert.Equal(t, repositoryURL, metadata.RepositoryURL) assert.Equal(t, documentationURL, metadata.DocumentationURL) - assert.Equal(t, []string{"do it 1", "do it 2"}, metadata.ImageLayers) + assert.ElementsMatch(t, []string{"do it 1", "do it 2"}, metadata.ImageLayers) assert.Equal( t, map[string]string{ diff --git a/modules/paginator/paginator_test.go b/modules/paginator/paginator_test.go index 41bb305337..8a56ee5121 100644 --- a/modules/paginator/paginator_test.go +++ b/modules/paginator/paginator_test.go @@ -70,19 +70,19 @@ func TestPaginator(t *testing.T) { t.Run("Generate pages", func(t *testing.T) { p := New(0, 10, 1, 0) pages := p.Pages() - assert.Equal(t, 0, len(pages)) + assert.Empty(t, pages) }) t.Run("Only current page", func(t *testing.T) { p := New(0, 10, 1, 1) pages := p.Pages() - assert.Equal(t, 1, len(pages)) + assert.Len(t, pages, 1) assert.Equal(t, 1, pages[0].Num()) assert.True(t, pages[0].IsCurrent()) p = New(1, 10, 1, 1) pages = p.Pages() - assert.Equal(t, 1, len(pages)) + assert.Len(t, pages, 1) assert.Equal(t, 1, pages[0].Num()) assert.True(t, pages[0].IsCurrent()) }) @@ -90,13 +90,13 @@ func TestPaginator(t *testing.T) { t.Run("Total page number is less or equal", func(t *testing.T) { p := New(1, 10, 1, 2) pages := p.Pages() - assert.Equal(t, 1, len(pages)) + assert.Len(t, pages, 1) assert.Equal(t, 1, pages[0].Num()) assert.True(t, pages[0].IsCurrent()) p = New(11, 10, 1, 2) pages = p.Pages() - assert.Equal(t, 2, len(pages)) + assert.Len(t, pages, 2) assert.Equal(t, 1, pages[0].Num()) assert.True(t, pages[0].IsCurrent()) assert.Equal(t, 2, pages[1].Num()) @@ -104,7 +104,7 @@ func TestPaginator(t *testing.T) { p = New(11, 10, 2, 2) pages = p.Pages() - assert.Equal(t, 2, len(pages)) + assert.Len(t, pages, 2) assert.Equal(t, 1, pages[0].Num()) assert.False(t, pages[0].IsCurrent()) assert.Equal(t, 2, pages[1].Num()) @@ -112,7 +112,7 @@ func TestPaginator(t *testing.T) { p = New(25, 10, 2, 3) pages = p.Pages() - assert.Equal(t, 3, len(pages)) + assert.Len(t, pages, 3) assert.Equal(t, 1, pages[0].Num()) assert.False(t, pages[0].IsCurrent()) assert.Equal(t, 2, pages[1].Num()) @@ -125,7 +125,7 @@ func TestPaginator(t *testing.T) { // ... 2 p := New(11, 10, 2, 1) pages := p.Pages() - assert.Equal(t, 2, len(pages)) + assert.Len(t, pages, 2) assert.Equal(t, -1, pages[0].Num()) assert.False(t, pages[0].IsCurrent()) assert.Equal(t, 2, pages[1].Num()) @@ -134,7 +134,7 @@ func TestPaginator(t *testing.T) { // ... 2 3 p = New(21, 10, 2, 2) pages = p.Pages() - assert.Equal(t, 3, len(pages)) + assert.Len(t, pages, 3) assert.Equal(t, -1, pages[0].Num()) assert.False(t, pages[0].IsCurrent()) assert.Equal(t, 2, pages[1].Num()) @@ -145,7 +145,7 @@ func TestPaginator(t *testing.T) { // ... 2 3 4 p = New(31, 10, 3, 3) pages = p.Pages() - assert.Equal(t, 4, len(pages)) + assert.Len(t, pages, 4) assert.Equal(t, -1, pages[0].Num()) assert.False(t, pages[0].IsCurrent()) assert.Equal(t, 2, pages[1].Num()) @@ -158,7 +158,7 @@ func TestPaginator(t *testing.T) { // ... 3 4 5 p = New(41, 10, 4, 3) pages = p.Pages() - assert.Equal(t, 4, len(pages)) + assert.Len(t, pages, 4) assert.Equal(t, -1, pages[0].Num()) assert.False(t, pages[0].IsCurrent()) assert.Equal(t, 3, pages[1].Num()) @@ -171,7 +171,7 @@ func TestPaginator(t *testing.T) { // ... 4 5 6 7 8 9 10 p = New(100, 10, 9, 7) pages = p.Pages() - assert.Equal(t, 8, len(pages)) + assert.Len(t, pages, 8) assert.Equal(t, -1, pages[0].Num()) assert.False(t, pages[0].IsCurrent()) assert.Equal(t, 4, pages[1].Num()) @@ -194,7 +194,7 @@ func TestPaginator(t *testing.T) { // 1 ... p := New(21, 10, 1, 1) pages := p.Pages() - assert.Equal(t, 2, len(pages)) + assert.Len(t, pages, 2) assert.Equal(t, 1, pages[0].Num()) assert.True(t, pages[0].IsCurrent()) assert.Equal(t, -1, pages[1].Num()) @@ -203,7 +203,7 @@ func TestPaginator(t *testing.T) { // 1 2 ... p = New(21, 10, 1, 2) pages = p.Pages() - assert.Equal(t, 3, len(pages)) + assert.Len(t, pages, 3) assert.Equal(t, 1, pages[0].Num()) assert.True(t, pages[0].IsCurrent()) assert.Equal(t, 2, pages[1].Num()) @@ -214,7 +214,7 @@ func TestPaginator(t *testing.T) { // 1 2 3 ... p = New(31, 10, 2, 3) pages = p.Pages() - assert.Equal(t, 4, len(pages)) + assert.Len(t, pages, 4) assert.Equal(t, 1, pages[0].Num()) assert.False(t, pages[0].IsCurrent()) assert.Equal(t, 2, pages[1].Num()) @@ -227,7 +227,7 @@ func TestPaginator(t *testing.T) { // 1 2 3 ... p = New(41, 10, 2, 3) pages = p.Pages() - assert.Equal(t, 4, len(pages)) + assert.Len(t, pages, 4) assert.Equal(t, 1, pages[0].Num()) assert.False(t, pages[0].IsCurrent()) assert.Equal(t, 2, pages[1].Num()) @@ -240,7 +240,7 @@ func TestPaginator(t *testing.T) { // 1 2 3 4 5 6 7 ... p = New(100, 10, 1, 7) pages = p.Pages() - assert.Equal(t, 8, len(pages)) + assert.Len(t, pages, 8) assert.Equal(t, 1, pages[0].Num()) assert.True(t, pages[0].IsCurrent()) assert.Equal(t, 2, pages[1].Num()) @@ -261,7 +261,7 @@ func TestPaginator(t *testing.T) { // 1 2 3 4 5 6 7 ... p = New(100, 10, 2, 7) pages = p.Pages() - assert.Equal(t, 8, len(pages)) + assert.Len(t, pages, 8) assert.Equal(t, 1, pages[0].Num()) assert.False(t, pages[0].IsCurrent()) assert.Equal(t, 2, pages[1].Num()) @@ -284,7 +284,7 @@ func TestPaginator(t *testing.T) { // ... 2 3 ... p := New(35, 10, 2, 2) pages := p.Pages() - assert.Equal(t, 4, len(pages)) + assert.Len(t, pages, 4) assert.Equal(t, -1, pages[0].Num()) assert.False(t, pages[0].IsCurrent()) assert.Equal(t, 2, pages[1].Num()) @@ -297,7 +297,7 @@ func TestPaginator(t *testing.T) { // ... 2 3 4 ... p = New(49, 10, 3, 3) pages = p.Pages() - assert.Equal(t, 5, len(pages)) + assert.Len(t, pages, 5) assert.Equal(t, -1, pages[0].Num()) assert.False(t, pages[0].IsCurrent()) assert.Equal(t, 2, pages[1].Num()) diff --git a/modules/queue/unique_queue_disk_channel_test.go b/modules/queue/unique_queue_disk_channel_test.go index fd76163f4a..f75c69f785 100644 --- a/modules/queue/unique_queue_disk_channel_test.go +++ b/modules/queue/unique_queue_disk_channel_test.go @@ -4,7 +4,6 @@ package queue import ( - "fmt" "strconv" "sync" "testing" @@ -17,7 +16,6 @@ import ( func TestPersistableChannelUniqueQueue(t *testing.T) { tmpDir := t.TempDir() - fmt.Printf("TempDir %s\n", tmpDir) _ = log.NewLogger(1000, "console", "console", `{"level":"warn","stacktracelevel":"NONE","stderr":true}`) // Common function to create the Queue @@ -208,7 +206,7 @@ func TestPersistableChannelUniqueQueue(t *testing.T) { mapLock.Lock() assert.Equal(t, 101, len(executedInitial[name])+len(executedEmpty[name])) - assert.Equal(t, 0, len(hasEmpty[name])) + assert.Empty(t, hasEmpty[name]) mapLock.Unlock() }) close(done) diff --git a/modules/secret/secret_test.go b/modules/secret/secret_test.go index 0a189ecebf..4b018fddb6 100644 --- a/modules/secret/secret_test.go +++ b/modules/secret/secret_test.go @@ -15,9 +15,9 @@ func TestEncryptDecrypt(t *testing.T) { hex, _ = EncryptSecret("foo", "baz") str, _ = DecryptSecret("foo", hex) - assert.Equal(t, str, "baz") + assert.Equal(t, "baz", str) hex, _ = EncryptSecret("bar", "baz") str, _ = DecryptSecret("foo", hex) - assert.NotEqual(t, str, "baz") + assert.NotEqual(t, "baz", str) } diff --git a/modules/setting/config_provider.go b/modules/setting/config_provider.go index 0244a8d06e..92c8c97fe9 100644 --- a/modules/setting/config_provider.go +++ b/modules/setting/config_provider.go @@ -4,20 +4,157 @@ package setting import ( + "fmt" + "os" + "path/filepath" + "strings" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/util" ini "gopkg.in/ini.v1" ) +type ConfigSection interface { + Name() string + MapTo(interface{}) error + HasKey(key string) bool + NewKey(name, value string) (*ini.Key, error) + Key(key string) *ini.Key + Keys() []*ini.Key + ChildSections() []*ini.Section +} + // ConfigProvider represents a config provider type ConfigProvider interface { - Section(section string) *ini.Section - NewSection(name string) (*ini.Section, error) - GetSection(name string) (*ini.Section, error) + Section(section string) ConfigSection + NewSection(name string) (ConfigSection, error) + GetSection(name string) (ConfigSection, error) + DeleteSection(name string) error + Save() error +} + +type iniFileConfigProvider struct { + *ini.File + filepath string // the ini file path + newFile bool // whether the file has not existed previously + allowEmpty bool // whether not finding configuration files is allowed (only true for the tests) +} + +// NewEmptyConfigProvider create a new empty config provider +func NewEmptyConfigProvider() ConfigProvider { + cp, _ := newConfigProviderFromData("") + return cp +} + +// newConfigProviderFromData this function is only for testing +func newConfigProviderFromData(configContent string) (ConfigProvider, error) { + var cfg *ini.File + var err error + if configContent == "" { + cfg = ini.Empty() + } else { + cfg, err = ini.Load(strings.NewReader(configContent)) + if err != nil { + return nil, err + } + } + cfg.NameMapper = ini.SnackCase + return &iniFileConfigProvider{ + File: cfg, + newFile: true, + }, nil +} + +// newConfigProviderFromFile load configuration from file. +// NOTE: do not print any log except error. +func newConfigProviderFromFile(customConf string, allowEmpty bool, extraConfig string) (*iniFileConfigProvider, error) { + cfg := ini.Empty() + newFile := true + + if customConf != "" { + isFile, err := util.IsFile(customConf) + if err != nil { + return nil, fmt.Errorf("unable to check if %s is a file. Error: %v", customConf, err) + } + if isFile { + if err := cfg.Append(customConf); err != nil { + return nil, fmt.Errorf("failed to load custom conf '%s': %v", customConf, err) + } + newFile = false + } + } + + if newFile && !allowEmpty { + return nil, fmt.Errorf("unable to find configuration file: %q, please ensure you are running in the correct environment or set the correct configuration file with -c", CustomConf) + } + + if extraConfig != "" { + if err := cfg.Append([]byte(extraConfig)); err != nil { + return nil, fmt.Errorf("unable to append more config: %v", err) + } + } + + cfg.NameMapper = ini.SnackCase + return &iniFileConfigProvider{ + File: cfg, + filepath: customConf, + newFile: newFile, + allowEmpty: allowEmpty, + }, nil +} + +func (p *iniFileConfigProvider) Section(section string) ConfigSection { + return p.File.Section(section) +} + +func (p *iniFileConfigProvider) NewSection(name string) (ConfigSection, error) { + return p.File.NewSection(name) +} + +func (p *iniFileConfigProvider) GetSection(name string) (ConfigSection, error) { + return p.File.GetSection(name) +} + +func (p *iniFileConfigProvider) DeleteSection(name string) error { + p.File.DeleteSection(name) + return nil +} + +// Save save the content into file +func (p *iniFileConfigProvider) Save() error { + if p.filepath == "" { + if !p.allowEmpty { + return fmt.Errorf("custom config path must not be empty") + } + return nil + } + + if p.newFile { + if err := os.MkdirAll(filepath.Dir(CustomConf), os.ModePerm); err != nil { + return fmt.Errorf("failed to create '%s': %v", CustomConf, err) + } + } + if err := p.SaveTo(p.filepath); err != nil { + return fmt.Errorf("failed to save '%s': %v", p.filepath, err) + } + + // Change permissions to be more restrictive + fi, err := os.Stat(CustomConf) + if err != nil { + return fmt.Errorf("failed to determine current conf file permissions: %v", err) + } + + if fi.Mode().Perm() > 0o600 { + if err = os.Chmod(CustomConf, 0o600); err != nil { + log.Warn("Failed changing conf file permissions to -rw-------. Consider changing them manually.") + } + } + return nil } // a file is an implementation ConfigProvider and other implementations are possible, i.e. from docker, k8s, … -var _ ConfigProvider = &ini.File{} +var _ ConfigProvider = &iniFileConfigProvider{} func mustMapSetting(rootCfg ConfigProvider, sectionName string, setting interface{}) { if err := rootCfg.Section(sectionName).MapTo(setting); err != nil { diff --git a/modules/setting/cors.go b/modules/setting/cors.go index 260848b5df..bafbbab64f 100644 --- a/modules/setting/cors.go +++ b/modules/setting/cors.go @@ -21,9 +21,10 @@ var CORSConfig = struct { Headers []string XFrameOptions string }{ - Enabled: false, - MaxAge: 10 * time.Minute, + AllowDomain: []string{"*"}, + Methods: []string{"GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"}, Headers: []string{"Content-Type", "User-Agent"}, + MaxAge: 10 * time.Minute, XFrameOptions: "SAMEORIGIN", } diff --git a/modules/setting/cron_test.go b/modules/setting/cron_test.go index be97e59bd9..8d58cf8b48 100644 --- a/modules/setting/cron_test.go +++ b/modules/setting/cron_test.go @@ -7,7 +7,6 @@ import ( "testing" "github.com/stretchr/testify/assert" - ini "gopkg.in/ini.v1" ) func Test_getCronSettings(t *testing.T) { @@ -23,11 +22,11 @@ func Test_getCronSettings(t *testing.T) { iniStr := ` [cron.test] -Base = true -Second = white rabbit -Extend = true +BASE = true +SECOND = white rabbit +EXTEND = true ` - cfg, err := ini.Load([]byte(iniStr)) + cfg, err := newConfigProviderFromData(iniStr) assert.NoError(t, err) extended := &Extended{ diff --git a/modules/setting/lfs.go b/modules/setting/lfs.go index e04cde100b..1b659dd22b 100644 --- a/modules/setting/lfs.go +++ b/modules/setting/lfs.go @@ -9,8 +9,6 @@ import ( "code.gitea.io/gitea/modules/generate" "code.gitea.io/gitea/modules/log" - - ini "gopkg.in/ini.v1" ) // LFS represents the configuration for Git LFS @@ -38,8 +36,7 @@ func loadLFSFrom(rootCfg ConfigProvider) { // DEPRECATED should not be removed because users maybe upgrade from lower version to the latest version // if these are removed, the warning will not be shown deprecatedSetting(rootCfg, "server", "LFS_CONTENT_PATH", "lfs", "PATH", "v1.19.0") - lfsSec.Key("PATH").MustString( - sec.Key("LFS_CONTENT_PATH").String()) + lfsSec.Key("PATH").MustString(sec.Key("LFS_CONTENT_PATH").String()) LFS.Storage = getStorage(rootCfg, "lfs", storageType, lfsSec) @@ -62,9 +59,11 @@ func loadLFSFrom(rootCfg ConfigProvider) { } // Save secret - CreateOrAppendToCustomConf("server.LFS_JWT_SECRET", func(cfg *ini.File) { - cfg.Section("server").Key("LFS_JWT_SECRET").SetValue(LFS.JWTSecretBase64) - }) + sec.Key("LFS_JWT_SECRET").SetValue(LFS.JWTSecretBase64) + if err := rootCfg.Save(); err != nil { + log.Fatal("Error saving JWT Secret for custom config: %v", err) + return + } } } } diff --git a/modules/setting/log.go b/modules/setting/log.go index 1ff710073e..d9a9e5af8f 100644 --- a/modules/setting/log.go +++ b/modules/setting/log.go @@ -15,8 +15,6 @@ import ( "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/util" - - ini "gopkg.in/ini.v1" ) var ( @@ -131,12 +129,12 @@ type LogDescription struct { SubLogDescriptions []SubLogDescription } -func getLogLevel(section *ini.Section, key string, defaultValue log.Level) log.Level { +func getLogLevel(section ConfigSection, key string, defaultValue log.Level) log.Level { value := section.Key(key).MustString(defaultValue.String()) return log.FromString(value) } -func getStacktraceLogLevel(section *ini.Section, key, defaultValue string) string { +func getStacktraceLogLevel(section ConfigSection, key, defaultValue string) string { value := section.Key(key).MustString(defaultValue) return log.FromString(value).String() } @@ -165,7 +163,7 @@ func loadLogFrom(rootCfg ConfigProvider) { Log.EnableXORMLog = rootCfg.Section("log").Key("ENABLE_XORM_LOG").MustBool(true) } -func generateLogConfig(sec *ini.Section, name string, defaults defaultLogOptions) (mode, jsonConfig, levelName string) { +func generateLogConfig(sec ConfigSection, name string, defaults defaultLogOptions) (mode, jsonConfig, levelName string) { level := getLogLevel(sec, "LEVEL", Log.Level) levelName = level.String() stacktraceLevelName := getStacktraceLogLevel(sec, "STACKTRACE_LEVEL", Log.StacktraceLogLevel) diff --git a/modules/setting/mailer_test.go b/modules/setting/mailer_test.go index 4cfd6142be..f65dbaf8f5 100644 --- a/modules/setting/mailer_test.go +++ b/modules/setting/mailer_test.go @@ -7,11 +7,10 @@ import ( "testing" "github.com/stretchr/testify/assert" - ini "gopkg.in/ini.v1" ) func Test_loadMailerFrom(t *testing.T) { - iniFile := ini.Empty() + iniFile := NewEmptyConfigProvider() kases := map[string]*Mailer{ "smtp.mydomain.com": { SMTPAddr: "smtp.mydomain.com", diff --git a/modules/setting/markup.go b/modules/setting/markup.go index b71a902be6..969e30e888 100644 --- a/modules/setting/markup.go +++ b/modules/setting/markup.go @@ -8,8 +8,6 @@ import ( "strings" "code.gitea.io/gitea/modules/log" - - "gopkg.in/ini.v1" ) // ExternalMarkupRenderers represents the external markup renderers @@ -82,7 +80,7 @@ func loadMarkupFrom(rootCfg ConfigProvider) { } } -func newMarkupSanitizer(name string, sec *ini.Section) { +func newMarkupSanitizer(name string, sec ConfigSection) { rule, ok := createMarkupSanitizerRule(name, sec) if ok { if strings.HasPrefix(name, "sanitizer.") { @@ -99,7 +97,7 @@ func newMarkupSanitizer(name string, sec *ini.Section) { } } -func createMarkupSanitizerRule(name string, sec *ini.Section) (MarkupSanitizerRule, bool) { +func createMarkupSanitizerRule(name string, sec ConfigSection) (MarkupSanitizerRule, bool) { var rule MarkupSanitizerRule ok := false @@ -141,7 +139,7 @@ func createMarkupSanitizerRule(name string, sec *ini.Section) (MarkupSanitizerRu return rule, true } -func newMarkupRenderer(name string, sec *ini.Section) { +func newMarkupRenderer(name string, sec ConfigSection) { extensionReg := regexp.MustCompile(`\.\w`) extensions := sec.Key("FILE_EXTENSIONS").Strings(",") diff --git a/modules/setting/oauth2.go b/modules/setting/oauth2.go index 44f5568ef4..4dab468c10 100644 --- a/modules/setting/oauth2.go +++ b/modules/setting/oauth2.go @@ -4,12 +4,12 @@ package setting import ( + "encoding/base64" "math" "path/filepath" + "code.gitea.io/gitea/modules/generate" "code.gitea.io/gitea/modules/log" - - "gopkg.in/ini.v1" ) // OAuth2UsernameType is enum describing the way gitea 'name' should be generated from oauth2 data @@ -80,7 +80,7 @@ func loadOAuth2ClientFrom(rootCfg ConfigProvider) { } } -func parseScopes(sec *ini.Section, name string) []string { +func parseScopes(sec ConfigSection, name string) []string { parts := sec.Key(name).Strings(" ") scopes := make([]string, 0, len(parts)) for _, scope := range parts { @@ -119,4 +119,19 @@ func loadOAuth2From(rootCfg ConfigProvider) { if !filepath.IsAbs(OAuth2.JWTSigningPrivateKeyFile) { OAuth2.JWTSigningPrivateKeyFile = filepath.Join(AppDataPath, OAuth2.JWTSigningPrivateKeyFile) } + + key := make([]byte, 32) + n, err := base64.RawURLEncoding.Decode(key, []byte(OAuth2.JWTSecretBase64)) + if err != nil || n != 32 { + key, err = generate.NewJwtSecret() + if err != nil { + log.Fatal("error generating JWT secret: %v", err) + } + + secretBase64 := base64.RawURLEncoding.EncodeToString(key) + rootCfg.Section("oauth2").Key("JWT_SECRET").SetValue(secretBase64) + if err := rootCfg.Save(); err != nil { + log.Fatal("save oauth2.JWT_SECRET failed: %v", err) + } + } } diff --git a/modules/setting/other.go b/modules/setting/other.go index 4fba754a08..706cb1e3d9 100644 --- a/modules/setting/other.go +++ b/modules/setting/other.go @@ -3,20 +3,25 @@ package setting -var ( - // Other settings - ShowFooterBranding bool +import "code.gitea.io/gitea/modules/log" + +type OtherConfig struct { ShowFooterVersion bool ShowFooterTemplateLoadTime bool EnableFeed bool EnableSitemap bool -) +} + +var Other = OtherConfig{ + ShowFooterVersion: true, + ShowFooterTemplateLoadTime: true, + EnableSitemap: true, + EnableFeed: true, +} func loadOtherFrom(rootCfg ConfigProvider) { sec := rootCfg.Section("other") - ShowFooterBranding = sec.Key("SHOW_FOOTER_BRANDING").MustBool(false) - ShowFooterVersion = sec.Key("SHOW_FOOTER_VERSION").MustBool(true) - ShowFooterTemplateLoadTime = sec.Key("SHOW_FOOTER_TEMPLATE_LOAD_TIME").MustBool(true) - EnableSitemap = sec.Key("ENABLE_SITEMAP").MustBool(true) - EnableFeed = sec.Key("ENABLE_FEED").MustBool(true) + if err := sec.MapTo(&Other); err != nil { + log.Fatal("Failed to map [other] settings: %v", err) + } } diff --git a/modules/setting/packages.go b/modules/setting/packages.go index ac0ad62bca..89601c3b99 100644 --- a/modules/setting/packages.go +++ b/modules/setting/packages.go @@ -12,7 +12,6 @@ import ( "code.gitea.io/gitea/modules/log" "github.com/dustin/go-humanize" - ini "gopkg.in/ini.v1" ) // Package registry settings @@ -86,7 +85,7 @@ func loadPackagesFrom(rootCfg ConfigProvider) { Packages.LimitSizeVagrant = mustBytes(sec, "LIMIT_SIZE_VAGRANT") } -func mustBytes(section *ini.Section, key string) int64 { +func mustBytes(section ConfigSection, key string) int64 { const noLimit = "-1" value := section.Key(key).MustString(noLimit) diff --git a/modules/setting/queue.go b/modules/setting/queue.go index bd4bf48e33..8c37e538bb 100644 --- a/modules/setting/queue.go +++ b/modules/setting/queue.go @@ -10,8 +10,6 @@ import ( "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/log" - - ini "gopkg.in/ini.v1" ) // QueueSettings represent the settings for a queue from the ini @@ -195,7 +193,7 @@ func handleOldLengthConfiguration(rootCfg ConfigProvider, queueName, oldSection, // toDirectlySetKeysSet returns a set of keys directly set by this section // Note: we cannot use section.HasKey(...) as that will immediately set the Key if a parent section has the Key // but this section does not. -func toDirectlySetKeysSet(section *ini.Section) container.Set[string] { +func toDirectlySetKeysSet(section ConfigSection) container.Set[string] { sections := make(container.Set[string]) for _, key := range section.Keys() { sections.Add(key.Name()) diff --git a/modules/setting/security.go b/modules/setting/security.go index b9841cdb95..ce2e7711f1 100644 --- a/modules/setting/security.go +++ b/modules/setting/security.go @@ -11,8 +11,6 @@ import ( "code.gitea.io/gitea/modules/auth/password/hash" "code.gitea.io/gitea/modules/generate" "code.gitea.io/gitea/modules/log" - - ini "gopkg.in/ini.v1" ) var ( @@ -43,7 +41,7 @@ var ( // loadSecret load the secret from ini by uriKey or verbatimKey, only one of them could be set // If the secret is loaded from uriKey (file), the file should be non-empty, to guarantee the behavior stable and clear. -func loadSecret(sec *ini.Section, uriKey, verbatimKey string) string { +func loadSecret(sec ConfigSection, uriKey, verbatimKey string) string { // don't allow setting both URI and verbatim string uri := sec.Key(uriKey).String() verbatim := sec.Key(verbatimKey).String() @@ -84,16 +82,17 @@ func loadSecret(sec *ini.Section, uriKey, verbatimKey string) string { } // generateSaveInternalToken generates and saves the internal token to app.ini -func generateSaveInternalToken() { +func generateSaveInternalToken(rootCfg ConfigProvider) { token, err := generate.NewInternalToken() if err != nil { log.Fatal("Error generate internal token: %v", err) } InternalToken = token - CreateOrAppendToCustomConf("security.INTERNAL_TOKEN", func(cfg *ini.File) { - cfg.Section("security").Key("INTERNAL_TOKEN").SetValue(token) - }) + rootCfg.Section("security").Key("INTERNAL_TOKEN").SetValue(token) + if err := rootCfg.Save(); err != nil { + log.Fatal("Error saving internal token: %v", err) + } } func loadSecurityFrom(rootCfg ConfigProvider) { @@ -141,7 +140,7 @@ func loadSecurityFrom(rootCfg ConfigProvider) { if InstallLock && InternalToken == "" { // if Gitea has been installed but the InternalToken hasn't been generated (upgrade from an old release), we should generate // some users do cluster deployment, they still depend on this auto-generating behavior. - generateSaveInternalToken() + generateSaveInternalToken(rootCfg) } cfgdata := sec.Key("PASSWORD_COMPLEXITY").Strings(",") diff --git a/modules/setting/setting.go b/modules/setting/setting.go index 4d7a7caab8..7a1b7d17a6 100644 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -18,16 +18,13 @@ import ( "code.gitea.io/gitea/modules/auth/password/hash" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/user" - "code.gitea.io/gitea/modules/util" - - ini "gopkg.in/ini.v1" ) // settings var ( // AppVer is the version of the current build of Gitea. It is set in main.go from main.Version. AppVer string - // AppBuiltWith represents a human readable version go runtime build version and build tags. (See main.go formatBuiltWith().) + // AppBuiltWith represents a human-readable version go runtime build version and build tags. (See main.go formatBuiltWith().) AppBuiltWith string // AppStartTime store time gitea has started AppStartTime time.Time @@ -40,7 +37,8 @@ var ( // AppWorkPath is used as the base path for several other paths. AppWorkPath string - // Global setting objects + // Other global setting objects + CfgProvider ConfigProvider CustomPath string // Custom directory path CustomConf string @@ -48,6 +46,10 @@ var ( RunUser string IsProd bool IsWindows bool + + // IsInTesting indicates whether the testing is running. A lot of unreliable code causes a lot of nonsense error logs during testing + // TODO: this is only a temporary solution, we should make the test code more reliable + IsInTesting = false ) func getAppPath() (string, error) { @@ -108,8 +110,12 @@ func getWorkPath(appPath string) string { func init() { IsWindows = runtime.GOOS == "windows" + if AppVer == "" { + AppVer = "dev" + } + // We can rely on log.CanColorStdout being set properly because modules/log/console_windows.go comes before modules/setting/setting.go lexicographically - // By default set this logger at Info - we'll change it later but we need to start with something. + // By default set this logger at Info - we'll change it later, but we need to start with something. log.NewLogger(0, "console", "console", fmt.Sprintf(`{"level": "info", "colorize": %t, "stacktraceLevel": "none"}`, log.CanColorStdout)) var err error @@ -199,17 +205,29 @@ func PrepareAppDataPath() error { // InitProviderFromExistingFile initializes config provider from an existing config file (app.ini) func InitProviderFromExistingFile() { - CfgProvider = newFileProviderFromConf(CustomConf, false, "") + var err error + CfgProvider, err = newConfigProviderFromFile(CustomConf, false, "") + if err != nil { + log.Fatal("InitProviderFromExistingFile: %v", err) + } } // InitProviderAllowEmpty initializes config provider from file, it's also fine that if the config file (app.ini) doesn't exist func InitProviderAllowEmpty() { - CfgProvider = newFileProviderFromConf(CustomConf, true, "") + var err error + CfgProvider, err = newConfigProviderFromFile(CustomConf, true, "") + if err != nil { + log.Fatal("InitProviderAllowEmpty: %v", err) + } } // InitProviderAndLoadCommonSettingsForTest initializes config provider and load common setttings for tests func InitProviderAndLoadCommonSettingsForTest(extraConfigs ...string) { - CfgProvider = newFileProviderFromConf(CustomConf, true, strings.Join(extraConfigs, "\n")) + var err error + CfgProvider, err = newConfigProviderFromFile(CustomConf, true, strings.Join(extraConfigs, "\n")) + if err != nil { + log.Fatal("InitProviderAndLoadCommonSettingsForTest: %v", err) + } loadCommonSettingsFrom(CfgProvider) if err := PrepareAppDataPath(); err != nil { log.Fatal("Can not prepare APP_DATA_PATH: %v", err) @@ -220,33 +238,6 @@ func InitProviderAndLoadCommonSettingsForTest(extraConfigs ...string) { PasswordHashAlgo, _ = hash.SetDefaultPasswordHashAlgorithm("dummy") } -// newFileProviderFromConf initializes configuration context. -// NOTE: do not print any log except error. -func newFileProviderFromConf(customConf string, allowEmpty bool, extraConfig string) *ini.File { - cfg := ini.Empty() - - isFile, err := util.IsFile(customConf) - if err != nil { - log.Error("Unable to check if %s is a file. Error: %v", customConf, err) - } - if isFile { - if err := cfg.Append(customConf); err != nil { - log.Fatal("Failed to load custom conf '%s': %v", customConf, err) - } - } else if !allowEmpty { - log.Fatal("Unable to find configuration file: %q.\nEnsure you are running in the correct environment or set the correct configuration file with -c.", CustomConf) - } // else: no config file, a config file might be created at CustomConf later (might not) - - if extraConfig != "" { - if err = cfg.Append([]byte(extraConfig)); err != nil { - log.Fatal("Unable to append more config: %v", err) - } - } - - cfg.NameMapper = ini.SnackCase - return cfg -} - // LoadCommonSettings loads common configurations from a configuration provider. func LoadCommonSettings() { loadCommonSettingsFrom(CfgProvider) @@ -310,51 +301,6 @@ func loadRunModeFrom(rootCfg ConfigProvider) { } } -// CreateOrAppendToCustomConf creates or updates the custom config. -// Use the callback to set individual values. -func CreateOrAppendToCustomConf(purpose string, callback func(cfg *ini.File)) { - if CustomConf == "" { - log.Error("Custom config path must not be empty") - return - } - - cfg := ini.Empty() - isFile, err := util.IsFile(CustomConf) - if err != nil { - log.Error("Unable to check if %s is a file. Error: %v", CustomConf, err) - } - if isFile { - if err := cfg.Append(CustomConf); err != nil { - log.Error("failed to load custom conf %s: %v", CustomConf, err) - return - } - } - - callback(cfg) - - if err := os.MkdirAll(filepath.Dir(CustomConf), os.ModePerm); err != nil { - log.Fatal("failed to create '%s': %v", CustomConf, err) - return - } - if err := cfg.SaveTo(CustomConf); err != nil { - log.Fatal("error saving to custom config: %v", err) - } - log.Info("Settings for %s saved to: %q", purpose, CustomConf) - - // Change permissions to be more restrictive - fi, err := os.Stat(CustomConf) - if err != nil { - log.Error("Failed to determine current conf file permissions: %v", err) - return - } - - if fi.Mode().Perm() > 0o600 { - if err = os.Chmod(CustomConf, 0o600); err != nil { - log.Warn("Failed changing conf file permissions to -rw-------. Consider changing them manually.") - } - } -} - // LoadSettings initializes the settings for normal start up func LoadSettings() { loadDBSetting(CfgProvider) diff --git a/modules/setting/storage.go b/modules/setting/storage.go index 50d4c8439e..6da52807ec 100644 --- a/modules/setting/storage.go +++ b/modules/setting/storage.go @@ -6,15 +6,13 @@ package setting import ( "path/filepath" "reflect" - - ini "gopkg.in/ini.v1" ) // Storage represents configuration of storages type Storage struct { Type string Path string - Section *ini.Section + Section ConfigSection ServeDirect bool } @@ -30,7 +28,7 @@ func (s *Storage) MapTo(v interface{}) error { return nil } -func getStorage(rootCfg ConfigProvider, name, typ string, targetSec *ini.Section) Storage { +func getStorage(rootCfg ConfigProvider, name, typ string, targetSec ConfigSection) Storage { const sectionName = "storage" sec := rootCfg.Section(sectionName) @@ -52,7 +50,7 @@ func getStorage(rootCfg ConfigProvider, name, typ string, targetSec *ini.Section storage.Section = targetSec storage.Type = typ - overrides := make([]*ini.Section, 0, 3) + overrides := make([]ConfigSection, 0, 3) nameSec, err := rootCfg.GetSection(sectionName + "." + name) if err == nil { overrides = append(overrides, nameSec) diff --git a/modules/setting/storage_test.go b/modules/setting/storage_test.go index 7737d233b9..9c51bbc081 100644 --- a/modules/setting/storage_test.go +++ b/modules/setting/storage_test.go @@ -7,7 +7,6 @@ import ( "testing" "github.com/stretchr/testify/assert" - ini "gopkg.in/ini.v1" ) func Test_getStorageCustomType(t *testing.T) { @@ -20,7 +19,7 @@ MINIO_BUCKET = gitea-attachment STORAGE_TYPE = minio MINIO_ENDPOINT = my_minio:9000 ` - cfg, err := ini.Load([]byte(iniStr)) + cfg, err := newConfigProviderFromData(iniStr) assert.NoError(t, err) sec := cfg.Section("attachment") @@ -43,7 +42,7 @@ MINIO_BUCKET = gitea-attachment [storage.minio] MINIO_BUCKET = gitea ` - cfg, err := ini.Load([]byte(iniStr)) + cfg, err := newConfigProviderFromData(iniStr) assert.NoError(t, err) sec := cfg.Section("attachment") @@ -65,7 +64,7 @@ MINIO_BUCKET = gitea-minio [storage] MINIO_BUCKET = gitea ` - cfg, err := ini.Load([]byte(iniStr)) + cfg, err := newConfigProviderFromData(iniStr) assert.NoError(t, err) sec := cfg.Section("attachment") @@ -88,7 +87,7 @@ MINIO_BUCKET = gitea [storage] STORAGE_TYPE = local ` - cfg, err := ini.Load([]byte(iniStr)) + cfg, err := newConfigProviderFromData(iniStr) assert.NoError(t, err) sec := cfg.Section("attachment") @@ -100,7 +99,7 @@ STORAGE_TYPE = local } func Test_getStorageGetDefaults(t *testing.T) { - cfg, err := ini.Load([]byte("")) + cfg, err := newConfigProviderFromData("") assert.NoError(t, err) sec := cfg.Section("attachment") @@ -121,7 +120,7 @@ MINIO_BUCKET = gitea-attachment [storage] MINIO_BUCKET = gitea-storage ` - cfg, err := ini.Load([]byte(iniStr)) + cfg, err := newConfigProviderFromData(iniStr) assert.NoError(t, err) { @@ -155,7 +154,7 @@ STORAGE_TYPE = lfs [storage.lfs] MINIO_BUCKET = gitea-storage ` - cfg, err := ini.Load([]byte(iniStr)) + cfg, err := newConfigProviderFromData(iniStr) assert.NoError(t, err) { @@ -179,7 +178,7 @@ func Test_getStorageInheritStorageType(t *testing.T) { [storage] STORAGE_TYPE = minio ` - cfg, err := ini.Load([]byte(iniStr)) + cfg, err := newConfigProviderFromData(iniStr) assert.NoError(t, err) sec := cfg.Section("attachment") @@ -194,7 +193,7 @@ func Test_getStorageInheritNameSectionType(t *testing.T) { [storage.attachments] STORAGE_TYPE = minio ` - cfg, err := ini.Load([]byte(iniStr)) + cfg, err := newConfigProviderFromData(iniStr) assert.NoError(t, err) sec := cfg.Section("attachment") diff --git a/modules/setting/ui.go b/modules/setting/ui.go index 90bad50d3b..bf2b2d4bb7 100644 --- a/modules/setting/ui.go +++ b/modules/setting/ui.go @@ -77,7 +77,7 @@ var UI = struct { GraphMaxCommitNum: 100, CodeCommentLines: 4, ReactionMaxUserNum: 10, - ThemeColorMetaTag: `#6cc644`, + ThemeColorMetaTag: ``, MaxDisplayFileSize: 8388608, DefaultTheme: `auto`, Themes: []string{`auto`, `gitea`, `arc-green`}, diff --git a/modules/storage/local_test.go b/modules/storage/local_test.go index 9649761a0f..0873f8e14e 100644 --- a/modules/storage/local_test.go +++ b/modules/storage/local_test.go @@ -89,6 +89,6 @@ func TestLocalStorageIterator(t *testing.T) { return nil }) assert.NoError(t, err) - assert.Equal(t, count, len(expected)) + assert.Len(t, expected, count) } } diff --git a/modules/templates/base.go b/modules/templates/base.go index e95ce31cfc..4254a56976 100644 --- a/modules/templates/base.go +++ b/modules/templates/base.go @@ -33,8 +33,7 @@ func BaseVars() Vars { "ShowRegistrationButton": setting.Service.ShowRegistrationButton, "ShowMilestonesDashboardPage": setting.Service.ShowMilestonesDashboardPage, - "ShowFooterBranding": setting.ShowFooterBranding, - "ShowFooterVersion": setting.ShowFooterVersion, + "ShowFooterVersion": setting.Other.ShowFooterVersion, "DisableDownloadSourceArchives": setting.Repository.DisableDownloadSourceArchives, "EnableSwagger": setting.API.EnableSwagger, diff --git a/modules/templates/helper.go b/modules/templates/helper.go index 24687a4606..b7bef20560 100644 --- a/modules/templates/helper.go +++ b/modules/templates/helper.go @@ -15,7 +15,6 @@ import ( "mime" "net/url" "path/filepath" - "reflect" "regexp" "strings" "time" @@ -68,12 +67,17 @@ func NewFuncMap() []template.FuncMap { "PathEscape": url.PathEscape, "PathEscapeSegments": util.PathEscapeSegments, + // utils + "StringUtils": NewStringUtils, + "SliceUtils": NewSliceUtils, + // ----------------------------------------------------------------- // string / json + // TODO: move string helper functions to StringUtils "Join": strings.Join, "DotEscape": DotEscape, - "HasPrefix": strings.HasPrefix, "EllipsisString": base.EllipsisString, + "DumpVar": dumpVar, "Json": func(in interface{}) string { out, err := json.Marshal(in) @@ -135,6 +139,7 @@ func NewFuncMap() []template.FuncMap { "CountFmt": base.FormatNumberSI, "TimeSince": timeutil.TimeSince, "TimeSinceUnix": timeutil.TimeSinceUnix, + "DateTime": timeutil.DateTime, "Sec2Time": util.SecToTime, "DateFmtLong": func(t time.Time) string { return t.Format(time.RFC3339) @@ -143,35 +148,6 @@ func NewFuncMap() []template.FuncMap { return fmt.Sprint(time.Since(startTime).Nanoseconds()/1e6) + "ms" }, - // ----------------------------------------------------------------- - // slice - "containGeneric": func(arr, v interface{}) bool { - arrV := reflect.ValueOf(arr) - if arrV.Kind() == reflect.String && reflect.ValueOf(v).Kind() == reflect.String { - return strings.Contains(arr.(string), v.(string)) - } - if arrV.Kind() == reflect.Slice { - for i := 0; i < arrV.Len(); i++ { - iV := arrV.Index(i) - if !iV.CanInterface() { - continue - } - if iV.Interface() == v { - return true - } - } - } - return false - }, - "contain": func(s []int64, id int64) bool { - for i := 0; i < len(s); i++ { - if s[i] == id { - return true - } - } - return false - }, - // ----------------------------------------------------------------- // setting "AppName": func() string { @@ -206,7 +182,7 @@ func NewFuncMap() []template.FuncMap { return setting.UI.DefaultShowFullName }, "ShowFooterTemplateLoadTime": func() bool { - return setting.ShowFooterTemplateLoadTime + return setting.Other.ShowFooterTemplateLoadTime }, "AllowedReactions": func() []string { return setting.UI.Reactions diff --git a/modules/templates/htmlrenderer.go b/modules/templates/htmlrenderer.go index 833c2acdca..2cecef5f84 100644 --- a/modules/templates/htmlrenderer.go +++ b/modules/templates/htmlrenderer.go @@ -9,7 +9,6 @@ import ( "context" "errors" "fmt" - "html/template" "io" "net/http" "path/filepath" @@ -22,13 +21,16 @@ import ( "code.gitea.io/gitea/modules/assetfs" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/templates/scopedtmpl" "code.gitea.io/gitea/modules/util" ) var rendererKey interface{} = "templatesHtmlRenderer" +type TemplateExecutor scopedtmpl.TemplateExecutor + type HTMLRender struct { - templates atomic.Pointer[template.Template] + templates atomic.Pointer[scopedtmpl.ScopedTemplate] } var ErrTemplateNotInitialized = errors.New("template system is not initialized, check your log for errors") @@ -47,22 +49,20 @@ func (h *HTMLRender) HTML(w io.Writer, status int, name string, data interface{} return t.Execute(w, data) } -func (h *HTMLRender) TemplateLookup(name string) (*template.Template, error) { +func (h *HTMLRender) TemplateLookup(name string) (TemplateExecutor, error) { tmpls := h.templates.Load() if tmpls == nil { return nil, ErrTemplateNotInitialized } - tmpl := tmpls.Lookup(name) - if tmpl == nil { - return nil, util.ErrNotExist - } - return tmpl, nil + + return tmpls.Executor(name, NewFuncMap()[0]) } func (h *HTMLRender) CompileTemplates() error { - extSuffix := ".tmpl" - tmpls := template.New("") assets := AssetFS() + extSuffix := ".tmpl" + tmpls := scopedtmpl.NewScopedTemplate() + tmpls.Funcs(NewFuncMap()[0]) files, err := ListWebTemplateAssetNames(assets) if err != nil { return nil @@ -73,9 +73,6 @@ func (h *HTMLRender) CompileTemplates() error { } name := strings.TrimSuffix(file, extSuffix) tmpl := tmpls.New(filepath.ToSlash(name)) - for _, fm := range NewFuncMap() { - tmpl.Funcs(fm) - } buf, err := assets.ReadFile(file) if err != nil { return err @@ -84,6 +81,7 @@ func (h *HTMLRender) CompileTemplates() error { return err } } + tmpls.Freeze() h.templates.Store(tmpls) return nil } diff --git a/modules/templates/scopedtmpl/scopedtmpl.go b/modules/templates/scopedtmpl/scopedtmpl.go new file mode 100644 index 0000000000..a8b67ad0f9 --- /dev/null +++ b/modules/templates/scopedtmpl/scopedtmpl.go @@ -0,0 +1,239 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package scopedtmpl + +import ( + "fmt" + "html/template" + "io" + "reflect" + "sync" + texttemplate "text/template" + "text/template/parse" + "unsafe" +) + +type TemplateExecutor interface { + Execute(wr io.Writer, data interface{}) error +} + +type ScopedTemplate struct { + all *template.Template + parseFuncs template.FuncMap // this func map is only used for parsing templates + frozen bool + + scopedMu sync.RWMutex + scopedTemplateSets map[string]*scopedTemplateSet +} + +func NewScopedTemplate() *ScopedTemplate { + return &ScopedTemplate{ + all: template.New(""), + parseFuncs: template.FuncMap{}, + scopedTemplateSets: map[string]*scopedTemplateSet{}, + } +} + +func (t *ScopedTemplate) Funcs(funcMap template.FuncMap) { + if t.frozen { + panic("cannot add new functions to frozen template set") + } + t.all.Funcs(funcMap) + for k, v := range funcMap { + t.parseFuncs[k] = v + } +} + +func (t *ScopedTemplate) New(name string) *template.Template { + if t.frozen { + panic("cannot add new template to frozen template set") + } + return t.all.New(name) +} + +func (t *ScopedTemplate) Freeze() { + t.frozen = true + // reset the exec func map, then `escapeTemplate` is safe to call `Execute` to do escaping + m := template.FuncMap{} + for k := range t.parseFuncs { + m[k] = func(v ...any) any { return nil } + } + t.all.Funcs(m) +} + +func (t *ScopedTemplate) Executor(name string, funcMap template.FuncMap) (TemplateExecutor, error) { + t.scopedMu.RLock() + scopedTmplSet, ok := t.scopedTemplateSets[name] + t.scopedMu.RUnlock() + + if !ok { + var err error + t.scopedMu.Lock() + if scopedTmplSet, ok = t.scopedTemplateSets[name]; !ok { + if scopedTmplSet, err = newScopedTemplateSet(t.all, name); err == nil { + t.scopedTemplateSets[name] = scopedTmplSet + } + } + t.scopedMu.Unlock() + if err != nil { + return nil, err + } + } + + if scopedTmplSet == nil { + return nil, fmt.Errorf("template %s not found", name) + } + return scopedTmplSet.newExecutor(funcMap), nil +} + +type scopedTemplateSet struct { + name string + htmlTemplates map[string]*template.Template + textTemplates map[string]*texttemplate.Template + execFuncs map[string]reflect.Value +} + +func escapeTemplate(t *template.Template) error { + // force the Golang HTML template to complete the escaping work + err := t.Execute(io.Discard, nil) + if _, ok := err.(*template.Error); ok { + return err + } + return nil +} + +//nolint:unused +type htmlTemplate struct { + escapeErr error + text *texttemplate.Template +} + +//nolint:unused +type textTemplateCommon struct { + tmpl map[string]*template.Template // Map from name to defined templates. + muTmpl sync.RWMutex // protects tmpl + option struct { + missingKey int + } + muFuncs sync.RWMutex // protects parseFuncs and execFuncs + parseFuncs texttemplate.FuncMap + execFuncs map[string]reflect.Value +} + +//nolint:unused +type textTemplate struct { + name string + *parse.Tree + *textTemplateCommon + leftDelim string + rightDelim string +} + +func ptr[T, P any](ptr *P) *T { + // https://pkg.go.dev/unsafe#Pointer + // (1) Conversion of a *T1 to Pointer to *T2. + // Provided that T2 is no larger than T1 and that the two share an equivalent memory layout, + // this conversion allows reinterpreting data of one type as data of another type. + return (*T)(unsafe.Pointer(ptr)) +} + +func newScopedTemplateSet(all *template.Template, name string) (*scopedTemplateSet, error) { + targetTmpl := all.Lookup(name) + if targetTmpl == nil { + return nil, fmt.Errorf("template %q not found", name) + } + if err := escapeTemplate(targetTmpl); err != nil { + return nil, fmt.Errorf("template %q has an error when escaping: %w", name, err) + } + + ts := &scopedTemplateSet{ + name: name, + htmlTemplates: map[string]*template.Template{}, + textTemplates: map[string]*texttemplate.Template{}, + } + + htmlTmpl := ptr[htmlTemplate](all) + textTmpl := htmlTmpl.text + textTmplPtr := ptr[textTemplate](textTmpl) + + textTmplPtr.muFuncs.Lock() + ts.execFuncs = map[string]reflect.Value{} + for k, v := range textTmplPtr.execFuncs { + ts.execFuncs[k] = v + } + textTmplPtr.muFuncs.Unlock() + + var collectTemplates func(nodes []parse.Node) + var collectErr error // only need to collect the one error + collectTemplates = func(nodes []parse.Node) { + for _, node := range nodes { + if node.Type() == parse.NodeTemplate { + nodeTemplate := node.(*parse.TemplateNode) + subName := nodeTemplate.Name + if ts.htmlTemplates[subName] == nil { + subTmpl := all.Lookup(subName) + if subTmpl == nil { + // HTML template will add some internal templates like "$delimDoubleQuote" into the text template + ts.textTemplates[subName] = textTmpl.Lookup(subName) + } else if subTmpl.Tree == nil || subTmpl.Tree.Root == nil { + collectErr = fmt.Errorf("template %q has no tree, it's usually caused by broken templates", subName) + } else { + ts.htmlTemplates[subName] = subTmpl + if err := escapeTemplate(subTmpl); err != nil { + collectErr = fmt.Errorf("template %q has an error when escaping: %w", subName, err) + return + } + collectTemplates(subTmpl.Tree.Root.Nodes) + } + } + } else if node.Type() == parse.NodeList { + nodeList := node.(*parse.ListNode) + collectTemplates(nodeList.Nodes) + } else if node.Type() == parse.NodeIf { + nodeIf := node.(*parse.IfNode) + collectTemplates(nodeIf.BranchNode.List.Nodes) + if nodeIf.BranchNode.ElseList != nil { + collectTemplates(nodeIf.BranchNode.ElseList.Nodes) + } + } else if node.Type() == parse.NodeRange { + nodeRange := node.(*parse.RangeNode) + collectTemplates(nodeRange.BranchNode.List.Nodes) + if nodeRange.BranchNode.ElseList != nil { + collectTemplates(nodeRange.BranchNode.ElseList.Nodes) + } + } else if node.Type() == parse.NodeWith { + nodeWith := node.(*parse.WithNode) + collectTemplates(nodeWith.BranchNode.List.Nodes) + if nodeWith.BranchNode.ElseList != nil { + collectTemplates(nodeWith.BranchNode.ElseList.Nodes) + } + } + } + } + ts.htmlTemplates[name] = targetTmpl + collectTemplates(targetTmpl.Tree.Root.Nodes) + return ts, collectErr +} + +func (ts *scopedTemplateSet) newExecutor(funcMap map[string]any) TemplateExecutor { + tmpl := texttemplate.New("") + tmplPtr := ptr[textTemplate](tmpl) + tmplPtr.execFuncs = map[string]reflect.Value{} + for k, v := range ts.execFuncs { + tmplPtr.execFuncs[k] = v + } + if funcMap != nil { + tmpl.Funcs(funcMap) + } + // after escapeTemplate, the html templates are also escaped text templates, so it could be added to the text template directly + for _, t := range ts.htmlTemplates { + _, _ = tmpl.AddParseTree(t.Name(), t.Tree) + } + for _, t := range ts.textTemplates { + _, _ = tmpl.AddParseTree(t.Name(), t.Tree) + } + + // now the text template has all necessary escaped templates, so we can safely execute, just like what the html template does + return tmpl.Lookup(ts.name) +} diff --git a/modules/templates/scopedtmpl/scopedtmpl_test.go b/modules/templates/scopedtmpl/scopedtmpl_test.go new file mode 100644 index 0000000000..774b8c7d42 --- /dev/null +++ b/modules/templates/scopedtmpl/scopedtmpl_test.go @@ -0,0 +1,98 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package scopedtmpl + +import ( + "bytes" + "html/template" + "strings" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestScopedTemplateSetFuncMap(t *testing.T) { + all := template.New("") + + all.Funcs(template.FuncMap{"CtxFunc": func(s string) string { + return "default" + }}) + + _, err := all.New("base").Parse(`{{CtxFunc "base"}}`) + assert.NoError(t, err) + + _, err = all.New("test").Parse(strings.TrimSpace(` +{{template "base"}} +{{CtxFunc "test"}} +{{template "base"}} +{{CtxFunc "test"}} +`)) + assert.NoError(t, err) + + ts, err := newScopedTemplateSet(all, "test") + assert.NoError(t, err) + + // try to use different CtxFunc to render concurrently + + funcMap1 := template.FuncMap{ + "CtxFunc": func(s string) string { + time.Sleep(100 * time.Millisecond) + return s + "1" + }, + } + + funcMap2 := template.FuncMap{ + "CtxFunc": func(s string) string { + time.Sleep(100 * time.Millisecond) + return s + "2" + }, + } + + out1 := bytes.Buffer{} + out2 := bytes.Buffer{} + wg := sync.WaitGroup{} + wg.Add(2) + go func() { + err := ts.newExecutor(funcMap1).Execute(&out1, nil) + assert.NoError(t, err) + wg.Done() + }() + go func() { + err := ts.newExecutor(funcMap2).Execute(&out2, nil) + assert.NoError(t, err) + wg.Done() + }() + wg.Wait() + assert.Equal(t, "base1\ntest1\nbase1\ntest1", out1.String()) + assert.Equal(t, "base2\ntest2\nbase2\ntest2", out2.String()) +} + +func TestScopedTemplateSetEscape(t *testing.T) { + all := template.New("") + _, err := all.New("base").Parse(`{{.text}}`) + assert.NoError(t, err) + + _, err = all.New("test").Parse(`{{template "base" .}}
{{.text}}
`) + assert.NoError(t, err) + + ts, err := newScopedTemplateSet(all, "test") + assert.NoError(t, err) + + out := bytes.Buffer{} + err = ts.newExecutor(nil).Execute(&out, map[string]string{"param": "/", "text": "<"}) + assert.NoError(t, err) + + assert.Equal(t, `<
<
`, out.String()) +} + +func TestScopedTemplateSetUnsafe(t *testing.T) { + all := template.New("") + _, err := all.New("test").Parse(``) + assert.NoError(t, err) + + _, err = newScopedTemplateSet(all, "test") + assert.ErrorContains(t, err, "appears in an ambiguous context within a URL") +} diff --git a/modules/templates/util.go b/modules/templates/util.go deleted file mode 100644 index 13f3a56808..0000000000 --- a/modules/templates/util.go +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright 2023 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package templates - -import ( - "fmt" - "reflect" -) - -func dictMerge(base map[string]any, arg any) bool { - if arg == nil { - return true - } - rv := reflect.ValueOf(arg) - if rv.Kind() == reflect.Map { - for _, k := range rv.MapKeys() { - base[k.String()] = rv.MapIndex(k).Interface() - } - return true - } - return false -} - -// dict is a helper function for creating a map[string]any from a list of key-value pairs. -// If the key is dot ".", the value is merged into the base map, just like Golang template's dot syntax: dot means current -// The dot syntax is highly discouraged because it might cause unclear key conflicts. It's always good to use explicit keys. -func dict(args ...any) (map[string]any, error) { - if len(args)%2 != 0 { - return nil, fmt.Errorf("invalid dict constructor syntax: must have key-value pairs") - } - m := make(map[string]any, len(args)/2) - for i := 0; i < len(args); i += 2 { - key, ok := args[i].(string) - if !ok { - return nil, fmt.Errorf("invalid dict constructor syntax: unable to merge args[%d]", i) - } - if key == "." { - if ok = dictMerge(m, args[i+1]); !ok { - return nil, fmt.Errorf("invalid dict constructor syntax: dot arg[%d] must be followed by a dict", i) - } - } else { - m[key] = args[i+1] - } - } - return m, nil -} diff --git a/modules/templates/util_dict.go b/modules/templates/util_dict.go new file mode 100644 index 0000000000..79c307b68d --- /dev/null +++ b/modules/templates/util_dict.go @@ -0,0 +1,121 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package templates + +import ( + "fmt" + "html" + "html/template" + "reflect" + + "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/setting" +) + +func dictMerge(base map[string]any, arg any) bool { + if arg == nil { + return true + } + rv := reflect.ValueOf(arg) + if rv.Kind() == reflect.Map { + for _, k := range rv.MapKeys() { + base[k.String()] = rv.MapIndex(k).Interface() + } + return true + } + return false +} + +// dict is a helper function for creating a map[string]any from a list of key-value pairs. +// If the key is dot ".", the value is merged into the base map, just like Golang template's dot syntax: dot means current +// The dot syntax is highly discouraged because it might cause unclear key conflicts. It's always good to use explicit keys. +func dict(args ...any) (map[string]any, error) { + if len(args)%2 != 0 { + return nil, fmt.Errorf("invalid dict constructor syntax: must have key-value pairs") + } + m := make(map[string]any, len(args)/2) + for i := 0; i < len(args); i += 2 { + key, ok := args[i].(string) + if !ok { + return nil, fmt.Errorf("invalid dict constructor syntax: unable to merge args[%d]", i) + } + if key == "." { + if ok = dictMerge(m, args[i+1]); !ok { + return nil, fmt.Errorf("invalid dict constructor syntax: dot arg[%d] must be followed by a dict", i) + } + } else { + m[key] = args[i+1] + } + } + return m, nil +} + +func dumpVarMarshalable(v any, dumped map[uintptr]bool) (ret any, ok bool) { + if v == nil { + return nil, true + } + e := reflect.ValueOf(v) + for e.Kind() == reflect.Pointer { + e = e.Elem() + } + if e.CanAddr() { + addr := e.UnsafeAddr() + if dumped[addr] { + return "[dumped]", false + } + dumped[addr] = true + defer delete(dumped, addr) + } + switch e.Kind() { + case reflect.Bool, reflect.String, + reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, + reflect.Float32, reflect.Float64: + return e.Interface(), true + case reflect.Struct: + m := map[string]any{} + for i := 0; i < e.NumField(); i++ { + k := e.Type().Field(i).Name + if !e.Type().Field(i).IsExported() { + continue + } + v := e.Field(i).Interface() + m[k], _ = dumpVarMarshalable(v, dumped) + } + return m, true + case reflect.Map: + m := map[string]any{} + for _, k := range e.MapKeys() { + m[k.String()], _ = dumpVarMarshalable(e.MapIndex(k).Interface(), dumped) + } + return m, true + case reflect.Array, reflect.Slice: + var m []any + for i := 0; i < e.Len(); i++ { + v, _ := dumpVarMarshalable(e.Index(i).Interface(), dumped) + m = append(m, v) + } + return m, true + default: + return "[" + reflect.TypeOf(v).String() + "]", false + } +} + +// dumpVar helps to dump a variable in a template, to help debugging and development. +func dumpVar(v any) template.HTML { + if setting.IsProd { + return "
dumpVar: only available in dev mode
" + } + m, ok := dumpVarMarshalable(v, map[uintptr]bool{}) + var dumpStr string + jsonBytes, err := json.MarshalIndent(m, "", " ") + if err != nil { + dumpStr = fmt.Sprintf("dumpVar: unable to marshal %T: %v", v, err) + } else if ok { + dumpStr = fmt.Sprintf("dumpVar: %T\n%s", v, string(jsonBytes)) + } else { + dumpStr = fmt.Sprintf("dumpVar: unmarshalable %T\n%s", v, string(jsonBytes)) + } + return template.HTML("
" + html.EscapeString(dumpStr) + "
") +} diff --git a/modules/templates/util_slice.go b/modules/templates/util_slice.go new file mode 100644 index 0000000000..a3318cc11a --- /dev/null +++ b/modules/templates/util_slice.go @@ -0,0 +1,35 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package templates + +import ( + "fmt" + "reflect" +) + +type SliceUtils struct{} + +func NewSliceUtils() *SliceUtils { + return &SliceUtils{} +} + +func (su *SliceUtils) Contains(s, v any) bool { + if s == nil { + return false + } + sv := reflect.ValueOf(s) + if sv.Kind() != reflect.Slice && sv.Kind() != reflect.Array { + panic(fmt.Sprintf("invalid type, expected slice or array, but got: %T", s)) + } + for i := 0; i < sv.Len(); i++ { + it := sv.Index(i) + if !it.CanInterface() { + continue + } + if it.Interface() == v { + return true + } + } + return false +} diff --git a/modules/templates/util_string.go b/modules/templates/util_string.go new file mode 100644 index 0000000000..e86bbe9e70 --- /dev/null +++ b/modules/templates/util_string.go @@ -0,0 +1,20 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package templates + +import "strings" + +type StringUtils struct{} + +func NewStringUtils() *StringUtils { + return &StringUtils{} +} + +func (su *StringUtils) HasPrefix(s, prefix string) bool { + return strings.HasPrefix(s, prefix) +} + +func (su *StringUtils) Contains(s, substr string) bool { + return strings.Contains(s, substr) +} diff --git a/modules/templates/util_test.go b/modules/templates/util_test.go index dfa691c5e2..febaf7fa88 100644 --- a/modules/templates/util_test.go +++ b/modules/templates/util_test.go @@ -4,6 +4,9 @@ package templates import ( + "html/template" + "io" + "strings" "testing" "github.com/stretchr/testify/assert" @@ -41,3 +44,36 @@ func TestDict(t *testing.T) { assert.Error(t, err) } } + +func TestUtils(t *testing.T) { + execTmpl := func(code string, data any) string { + tmpl := template.New("test") + tmpl.Funcs(template.FuncMap{"SliceUtils": NewSliceUtils, "StringUtils": NewStringUtils}) + template.Must(tmpl.Parse(code)) + w := &strings.Builder{} + assert.NoError(t, tmpl.Execute(w, data)) + return w.String() + } + + actual := execTmpl("{{SliceUtils.Contains .Slice .Value}}", map[string]any{"Slice": []string{"a", "b"}, "Value": "a"}) + assert.Equal(t, "true", actual) + + actual = execTmpl("{{SliceUtils.Contains .Slice .Value}}", map[string]any{"Slice": []string{"a", "b"}, "Value": "x"}) + assert.Equal(t, "false", actual) + + actual = execTmpl("{{SliceUtils.Contains .Slice .Value}}", map[string]any{"Slice": []int64{1, 2}, "Value": int64(2)}) + assert.Equal(t, "true", actual) + + actual = execTmpl("{{StringUtils.Contains .String .Value}}", map[string]any{"String": "abc", "Value": "b"}) + assert.Equal(t, "true", actual) + + actual = execTmpl("{{StringUtils.Contains .String .Value}}", map[string]any{"String": "abc", "Value": "x"}) + assert.Equal(t, "false", actual) + + tmpl := template.New("test") + tmpl.Funcs(template.FuncMap{"SliceUtils": NewSliceUtils, "StringUtils": NewStringUtils}) + template.Must(tmpl.Parse("{{SliceUtils.Contains .Slice .Value}}")) + // error is like this: `template: test:1:12: executing "test" at : error calling Contains: ...` + err := tmpl.Execute(io.Discard, map[string]any{"Slice": struct{}{}}) + assert.ErrorContains(t, err, "invalid type, expected slice or array") +} diff --git a/modules/test/context_tests.go b/modules/test/context_tests.go index e558bf1b9f..4af7651250 100644 --- a/modules/test/context_tests.go +++ b/modules/test/context_tests.go @@ -5,7 +5,6 @@ package test import ( scontext "context" - "html/template" "io" "net/http" "net/http/httptest" @@ -18,6 +17,7 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/translation" "code.gitea.io/gitea/modules/web/middleware" @@ -120,7 +120,7 @@ func (rw *mockResponseWriter) Push(target string, opts *http.PushOptions) error type mockRender struct{} -func (tr *mockRender) TemplateLookup(tmpl string) (*template.Template, error) { +func (tr *mockRender) TemplateLookup(tmpl string) (templates.TemplateExecutor, error) { return nil, nil } diff --git a/modules/test/logchecker.go b/modules/test/logchecker.go new file mode 100644 index 0000000000..8f8c753c76 --- /dev/null +++ b/modules/test/logchecker.go @@ -0,0 +1,116 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package test + +import ( + "strconv" + "strings" + "sync" + "sync/atomic" + "time" + + "code.gitea.io/gitea/modules/log" +) + +type LogChecker struct { + logger *log.MultiChannelledLogger + loggerName string + eventLoggerName string + + filterMessages []string + filtered []bool + + stopMark string + stopped bool + + mu sync.Mutex +} + +func (lc *LogChecker) LogEvent(event *log.Event) error { + lc.mu.Lock() + defer lc.mu.Unlock() + for i, msg := range lc.filterMessages { + if strings.Contains(event.GetMsg(), msg) { + lc.filtered[i] = true + } + } + if strings.Contains(event.GetMsg(), lc.stopMark) { + lc.stopped = true + } + return nil +} + +func (lc *LogChecker) Close() {} + +func (lc *LogChecker) Flush() {} + +func (lc *LogChecker) GetLevel() log.Level { + return log.TRACE +} + +func (lc *LogChecker) GetStacktraceLevel() log.Level { + return log.NONE +} + +func (lc *LogChecker) GetName() string { + return lc.eventLoggerName +} + +func (lc *LogChecker) ReleaseReopen() error { + return nil +} + +var checkerIndex int64 + +func NewLogChecker(loggerName string) (logChecker *LogChecker, cancel func()) { + logger := log.GetLogger(loggerName) + newCheckerIndex := atomic.AddInt64(&checkerIndex, 1) + lc := &LogChecker{ + logger: logger, + loggerName: loggerName, + eventLoggerName: "TestLogChecker-" + strconv.FormatInt(newCheckerIndex, 10), + } + if err := logger.AddLogger(lc); err != nil { + panic(err) // it's impossible + } + return lc, func() { _, _ = logger.DelLogger(lc.GetName()) } +} + +// Filter will make the `Check` function to check if these logs are outputted. +func (lc *LogChecker) Filter(msgs ...string) *LogChecker { + lc.mu.Lock() + defer lc.mu.Unlock() + lc.filterMessages = make([]string, len(msgs)) + copy(lc.filterMessages, msgs) + lc.filtered = make([]bool, len(lc.filterMessages)) + return lc +} + +func (lc *LogChecker) StopMark(msg string) *LogChecker { + lc.mu.Lock() + defer lc.mu.Unlock() + lc.stopMark = msg + lc.stopped = false + return lc +} + +// Check returns the filtered slice and whether the stop mark is reached. +func (lc *LogChecker) Check(d time.Duration) (filtered []bool, stopped bool) { + stop := time.Now().Add(d) + + for { + lc.mu.Lock() + stopped = lc.stopped + lc.mu.Unlock() + + if time.Now().After(stop) || stopped { + lc.mu.Lock() + f := make([]bool, len(lc.filtered)) + copy(f, lc.filtered) + lc.mu.Unlock() + return f, stopped + } + time.Sleep(10 * time.Millisecond) + } +} diff --git a/modules/test/logchecker_test.go b/modules/test/logchecker_test.go new file mode 100644 index 0000000000..4dfea8c3e3 --- /dev/null +++ b/modules/test/logchecker_test.go @@ -0,0 +1,47 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package test + +import ( + "testing" + "time" + + "code.gitea.io/gitea/modules/log" + + "github.com/stretchr/testify/assert" +) + +func TestLogChecker(t *testing.T) { + _ = log.NewLogger(1000, "console", "console", `{"level":"info","stacktracelevel":"NONE","stderr":true}`) + + lc, cleanup := NewLogChecker(log.DEFAULT) + defer cleanup() + + lc.Filter("First", "Third").StopMark("End") + log.Info("test") + + filtered, stopped := lc.Check(100 * time.Millisecond) + assert.ElementsMatch(t, []bool{false, false}, filtered) + assert.False(t, stopped) + + log.Info("First") + filtered, stopped = lc.Check(100 * time.Millisecond) + assert.ElementsMatch(t, []bool{true, false}, filtered) + assert.False(t, stopped) + + log.Info("Second") + filtered, stopped = lc.Check(100 * time.Millisecond) + assert.ElementsMatch(t, []bool{true, false}, filtered) + assert.False(t, stopped) + + log.Info("Third") + filtered, stopped = lc.Check(100 * time.Millisecond) + assert.ElementsMatch(t, []bool{true, true}, filtered) + assert.False(t, stopped) + + log.Info("End") + filtered, stopped = lc.Check(100 * time.Millisecond) + assert.ElementsMatch(t, []bool{true, true}, filtered) + assert.True(t, stopped) +} diff --git a/modules/timeutil/datetime.go b/modules/timeutil/datetime.go new file mode 100644 index 0000000000..80b96fa656 --- /dev/null +++ b/modules/timeutil/datetime.go @@ -0,0 +1,60 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package timeutil + +import ( + "fmt" + "html" + "html/template" + "time" +) + +// DateTime renders an absolute time HTML element by datetime. +func DateTime(format string, datetime any) template.HTML { + if p, ok := datetime.(*time.Time); ok { + datetime = *p + } + if p, ok := datetime.(*TimeStamp); ok { + datetime = *p + } + switch v := datetime.(type) { + case TimeStamp: + datetime = v.AsTime() + case int: + datetime = TimeStamp(v).AsTime() + case int64: + datetime = TimeStamp(v).AsTime() + } + + var datetimeEscaped, textEscaped string + switch v := datetime.(type) { + case nil: + return "N/A" + case string: + datetimeEscaped = html.EscapeString(v) + textEscaped = datetimeEscaped + case time.Time: + if v.IsZero() || v.Unix() == 0 { + return "N/A" + } + datetimeEscaped = html.EscapeString(v.Format(time.RFC3339)) + if format == "full" { + textEscaped = html.EscapeString(v.Format("2006-01-02 15:04:05 -07:00")) + } else { + textEscaped = html.EscapeString(v.Format("2006-01-02")) + } + default: + panic(fmt.Sprintf("Unsupported time type %T", datetime)) + } + + switch format { + case "short": + return template.HTML(fmt.Sprintf(`%s`, datetimeEscaped, textEscaped)) + case "long": + return template.HTML(fmt.Sprintf(`%s`, datetimeEscaped, textEscaped)) + case "full": + return template.HTML(fmt.Sprintf(`%s`, datetimeEscaped, textEscaped)) + } + panic(fmt.Sprintf("Unsupported format %s", format)) +} diff --git a/modules/timeutil/datetime_test.go b/modules/timeutil/datetime_test.go new file mode 100644 index 0000000000..a5639b34f1 --- /dev/null +++ b/modules/timeutil/datetime_test.go @@ -0,0 +1,45 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package timeutil + +import ( + "testing" + "time" + + "code.gitea.io/gitea/modules/setting" + + "github.com/stretchr/testify/assert" +) + +func TestDateTime(t *testing.T) { + oldTz := setting.DefaultUILocation + setting.DefaultUILocation, _ = time.LoadLocation("America/New_York") + defer func() { + setting.DefaultUILocation = oldTz + }() + + refTimeStr := "2018-01-01T00:00:00Z" + refTime, _ := time.Parse(time.RFC3339, refTimeStr) + refTimeStamp := TimeStamp(refTime.Unix()) + + assert.EqualValues(t, "N/A", DateTime("short", nil)) + assert.EqualValues(t, "N/A", DateTime("short", 0)) + assert.EqualValues(t, "N/A", DateTime("short", time.Time{})) + assert.EqualValues(t, "N/A", DateTime("short", TimeStamp(0))) + + actual := DateTime("short", "invalid") + assert.EqualValues(t, `invalid`, actual) + + actual = DateTime("short", refTimeStr) + assert.EqualValues(t, `2018-01-01T00:00:00Z`, actual) + + actual = DateTime("short", refTime) + assert.EqualValues(t, `2018-01-01`, actual) + + actual = DateTime("short", refTimeStamp) + assert.EqualValues(t, `2017-12-31`, actual) + + actual = DateTime("full", refTimeStamp) + assert.EqualValues(t, `2017-12-31 19:00:00 -05:00`, actual) +} diff --git a/modules/timeutil/since.go b/modules/timeutil/since.go index e6a2519d21..04fcff54a3 100644 --- a/modules/timeutil/since.go +++ b/modules/timeutil/since.go @@ -23,7 +23,7 @@ const ( ) func computeTimeDiffFloor(diff int64, lang translation.Locale) (int64, string) { - diffStr := "" + var diffStr string switch { case diff <= 0: diff = 0 @@ -115,7 +115,7 @@ func timeSincePro(then, now time.Time, lang translation.Locale) string { } func timeSinceUnix(then, now time.Time, lang translation.Locale) template.HTML { - friendlyText := then.Format("2006-01-02 15:04:05 +07:00") + friendlyText := then.Format("2006-01-02 15:04:05 -07:00") // document: https://github.com/github/relative-time-element attrs := `tense="past"` diff --git a/modules/translation/i18n/i18n_test.go b/modules/translation/i18n/i18n_test.go index 085d03811f..1d1be43318 100644 --- a/modules/translation/i18n/i18n_test.go +++ b/modules/translation/i18n/i18n_test.go @@ -51,8 +51,8 @@ sub = Changed Sub String assert.Equal(t, `test value; more text`, result) langs, descs := ls.ListLangNameDesc() - assert.Equal(t, []string{"lang1", "lang2"}, langs) - assert.Equal(t, []string{"Lang1", "Lang2"}, descs) + assert.ElementsMatch(t, []string{"lang1", "lang2"}, langs) + assert.ElementsMatch(t, []string{"Lang1", "Lang2"}, descs) found := ls.Has("lang1", "no-such") assert.False(t, found) diff --git a/modules/translation/i18n/localestore.go b/modules/translation/i18n/localestore.go index 90f574127d..0664bcfd1a 100644 --- a/modules/translation/i18n/localestore.go +++ b/modules/translation/i18n/localestore.go @@ -72,7 +72,6 @@ func (store *localeStore) AddLocaleByIni(langName, langDesc string, source, more l.idxToMsgMap[idx] = key.Value() } } - iniFile = nil return nil } diff --git a/modules/util/slice_test.go b/modules/util/slice_test.go index b0b771a79a..373c1a3b7b 100644 --- a/modules/util/slice_test.go +++ b/modules/util/slice_test.go @@ -74,15 +74,15 @@ func TestSliceEqual(t *testing.T) { } func TestSliceRemoveAll(t *testing.T) { - assert.Equal(t, SliceRemoveAll([]int{2, 0, 2, 3}, 0), []int{2, 2, 3}) - assert.Equal(t, SliceRemoveAll([]int{2, 0, 2, 3}, 2), []int{0, 3}) - assert.Equal(t, SliceRemoveAll([]int{0, 0, 0, 0}, 0), []int{}) - assert.Equal(t, SliceRemoveAll([]int{2, 0, 2, 3}, 4), []int{2, 0, 2, 3}) - assert.Equal(t, SliceRemoveAll([]int{}, 0), []int{}) - assert.Equal(t, SliceRemoveAll([]int(nil), 0), []int(nil)) - assert.Equal(t, SliceRemoveAll([]int{}, 0), []int{}) + assert.ElementsMatch(t, []int{2, 2, 3}, SliceRemoveAll([]int{2, 0, 2, 3}, 0)) + assert.ElementsMatch(t, []int{0, 3}, SliceRemoveAll([]int{2, 0, 2, 3}, 2)) + assert.Empty(t, SliceRemoveAll([]int{0, 0, 0, 0}, 0)) + assert.ElementsMatch(t, []int{2, 0, 2, 3}, SliceRemoveAll([]int{2, 0, 2, 3}, 4)) + assert.Empty(t, SliceRemoveAll([]int{}, 0)) + assert.ElementsMatch(t, []int(nil), SliceRemoveAll([]int(nil), 0)) + assert.Empty(t, SliceRemoveAll([]int{}, 0)) - assert.Equal(t, SliceRemoveAll([]string{"2", "0", "2", "3"}, "0"), []string{"2", "2", "3"}) - assert.Equal(t, SliceRemoveAll([]float64{2, 0, 2, 3}, 0), []float64{2, 2, 3}) - assert.Equal(t, SliceRemoveAll([]bool{false, true, false}, true), []bool{false, false}) + assert.ElementsMatch(t, []string{"2", "2", "3"}, SliceRemoveAll([]string{"2", "0", "2", "3"}, "0")) + assert.ElementsMatch(t, []float64{2, 2, 3}, SliceRemoveAll([]float64{2, 0, 2, 3}, 0)) + assert.ElementsMatch(t, []bool{false, false}, SliceRemoveAll([]bool{false, true, false}, true)) } diff --git a/modules/util/truncate_test.go b/modules/util/truncate_test.go index 05e2bc0301..dfe1230fd4 100644 --- a/modules/util/truncate_test.go +++ b/modules/util/truncate_test.go @@ -25,7 +25,7 @@ func TestSplitString(t *testing.T) { assert.Equal(t, c.ellipsis+c.input[len(c.leftSub):], r, "test split %s at %d, expected rightSub: %q", c.input, c.n, c.input[len(c.leftSub):]) } else { assert.Equal(t, c.leftSub, l, "test split %q at %d, expected leftSub: %q", c.input, c.n, c.leftSub) - assert.Equal(t, "", r, "test split %q at %d, expected rightSub: %q", c.input, c.n, "") + assert.Empty(t, r, "test split %q at %d, expected rightSub: %q", c.input, c.n, "") } } } diff --git a/modules/web/handler.go b/modules/web/handler.go new file mode 100644 index 0000000000..8a44673f12 --- /dev/null +++ b/modules/web/handler.go @@ -0,0 +1,200 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package web + +import ( + goctx "context" + "fmt" + "net/http" + "reflect" + "strings" + + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/web/routing" +) + +// ResponseStatusProvider is an interface to check whether the response has been written by the handler +type ResponseStatusProvider interface { + Written() bool +} + +// TODO: decouple this from the context package, let the context package register these providers +var argTypeProvider = map[reflect.Type]func(req *http.Request) ResponseStatusProvider{ + reflect.TypeOf(&context.APIContext{}): func(req *http.Request) ResponseStatusProvider { return context.GetAPIContext(req) }, + reflect.TypeOf(&context.Context{}): func(req *http.Request) ResponseStatusProvider { return context.GetContext(req) }, + reflect.TypeOf(&context.PrivateContext{}): func(req *http.Request) ResponseStatusProvider { return context.GetPrivateContext(req) }, +} + +// responseWriter is a wrapper of http.ResponseWriter, to check whether the response has been written +type responseWriter struct { + respWriter http.ResponseWriter + status int +} + +var _ ResponseStatusProvider = (*responseWriter)(nil) + +func (r *responseWriter) Written() bool { + return r.status > 0 +} + +func (r *responseWriter) Header() http.Header { + return r.respWriter.Header() +} + +func (r *responseWriter) Write(bytes []byte) (int, error) { + if r.status == 0 { + r.status = http.StatusOK + } + return r.respWriter.Write(bytes) +} + +func (r *responseWriter) WriteHeader(statusCode int) { + r.status = statusCode + r.respWriter.WriteHeader(statusCode) +} + +var ( + httpReqType = reflect.TypeOf((*http.Request)(nil)) + respWriterType = reflect.TypeOf((*http.ResponseWriter)(nil)).Elem() + cancelFuncType = reflect.TypeOf((*goctx.CancelFunc)(nil)).Elem() +) + +// preCheckHandler checks whether the handler is valid, developers could get first-time feedback, all mistakes could be found at startup +func preCheckHandler(fn reflect.Value, argsIn []reflect.Value) { + hasStatusProvider := false + for _, argIn := range argsIn { + if _, hasStatusProvider = argIn.Interface().(ResponseStatusProvider); hasStatusProvider { + break + } + } + if !hasStatusProvider { + panic(fmt.Sprintf("handler should have at least one ResponseStatusProvider argument, but got %s", fn.Type())) + } + if fn.Type().NumOut() != 0 && fn.Type().NumIn() != 1 { + panic(fmt.Sprintf("handler should have no return value or only one argument, but got %s", fn.Type())) + } + if fn.Type().NumOut() == 1 && fn.Type().Out(0) != cancelFuncType { + panic(fmt.Sprintf("handler should return a cancel function, but got %s", fn.Type())) + } +} + +func prepareHandleArgsIn(resp http.ResponseWriter, req *http.Request, fn reflect.Value) []reflect.Value { + isPreCheck := req == nil + + argsIn := make([]reflect.Value, fn.Type().NumIn()) + for i := 0; i < fn.Type().NumIn(); i++ { + argTyp := fn.Type().In(i) + switch argTyp { + case respWriterType: + argsIn[i] = reflect.ValueOf(resp) + case httpReqType: + argsIn[i] = reflect.ValueOf(req) + default: + if argFn, ok := argTypeProvider[argTyp]; ok { + if isPreCheck { + argsIn[i] = reflect.ValueOf(&responseWriter{}) + } else { + argsIn[i] = reflect.ValueOf(argFn(req)) + } + } else { + panic(fmt.Sprintf("unsupported argument type: %s", argTyp)) + } + } + } + return argsIn +} + +func handleResponse(fn reflect.Value, ret []reflect.Value) goctx.CancelFunc { + if len(ret) == 1 { + if cancelFunc, ok := ret[0].Interface().(goctx.CancelFunc); ok { + return cancelFunc + } + panic(fmt.Sprintf("unsupported return type: %s", ret[0].Type())) + } else if len(ret) > 1 { + panic(fmt.Sprintf("unsupported return values: %s", fn.Type())) + } + return nil +} + +func hasResponseBeenWritten(argsIn []reflect.Value) bool { + for _, argIn := range argsIn { + if statusProvider, ok := argIn.Interface().(ResponseStatusProvider); ok { + if statusProvider.Written() { + return true + } + } + } + return false +} + +// toHandlerProvider converts a handler to a handler provider +// A handler provider is a function that takes a "next" http.Handler, it can be used as a middleware +func toHandlerProvider(handler any) func(next http.Handler) http.Handler { + if hp, ok := handler.(func(next http.Handler) http.Handler); ok { + return hp + } + + funcInfo := routing.GetFuncInfo(handler) + fn := reflect.ValueOf(handler) + if fn.Type().Kind() != reflect.Func { + panic(fmt.Sprintf("handler must be a function, but got %s", fn.Type())) + } + + provider := func(next http.Handler) http.Handler { + return http.HandlerFunc(func(respOrig http.ResponseWriter, req *http.Request) { + // wrap the response writer to check whether the response has been written + resp := respOrig + if _, ok := resp.(ResponseStatusProvider); !ok { + resp = &responseWriter{respWriter: resp} + } + + // prepare the arguments for the handler and do pre-check + argsIn := prepareHandleArgsIn(resp, req, fn) + if req == nil { + preCheckHandler(fn, argsIn) + return // it's doing pre-check, just return + } + + routing.UpdateFuncInfo(req.Context(), funcInfo) + ret := fn.Call(argsIn) + + // handle the return value, and defer the cancel function if there is one + cancelFunc := handleResponse(fn, ret) + if cancelFunc != nil { + defer cancelFunc() + } + + // if the response has not been written, call the next handler + if next != nil && !hasResponseBeenWritten(argsIn) { + next.ServeHTTP(resp, req) + } + }) + } + + provider(nil).ServeHTTP(nil, nil) // do a pre-check to make sure all arguments and return values are supported + return provider +} + +// MiddlewareWithPrefix wraps a handler function at a prefix, and make it as a middleware +// TODO: this design is incorrect, the asset handler should not be a middleware +func MiddlewareWithPrefix(pathPrefix string, middleware func(handler http.Handler) http.Handler, handlerFunc http.HandlerFunc) func(next http.Handler) http.Handler { + funcInfo := routing.GetFuncInfo(handlerFunc) + handler := http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { + routing.UpdateFuncInfo(req.Context(), funcInfo) + handlerFunc(resp, req) + }) + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { + if !strings.HasPrefix(req.URL.Path, pathPrefix) { + next.ServeHTTP(resp, req) + return + } + if middleware != nil { + middleware(handler).ServeHTTP(resp, req) + } else { + handler.ServeHTTP(resp, req) + } + }) + } +} diff --git a/modules/web/route.go b/modules/web/route.go index 0f2fdc33b5..fe35880849 100644 --- a/modules/web/route.go +++ b/modules/web/route.go @@ -4,8 +4,6 @@ package web import ( - goctx "context" - "fmt" "net/http" "strings" @@ -17,13 +15,13 @@ import ( ) // Bind binding an obj to a handler -func Bind[T any](obj T) http.HandlerFunc { - return Wrap(func(ctx *context.Context) { +func Bind[T any](_ T) any { + return func(ctx *context.Context) { theObj := new(T) // create a new form obj for every request but not use obj directly binding.Bind(ctx.Req, theObj) SetForm(ctx, theObj) middleware.AssignForm(theObj, ctx.Data) - }) + } } // SetForm set the form object @@ -56,21 +54,12 @@ func NewRoute() *Route { // Use supports two middlewares func (r *Route) Use(middlewares ...interface{}) { if r.curGroupPrefix != "" { + // FIXME: this behavior is incorrect, should use "With" instead r.curMiddlewares = append(r.curMiddlewares, middlewares...) } else { - for _, middle := range middlewares { - switch t := middle.(type) { - case func(http.Handler) http.Handler: - r.R.Use(t) - case func(*context.Context): - r.R.Use(Middle(t)) - case func(*context.Context) goctx.CancelFunc: - r.R.Use(MiddleCancel(t)) - case func(*context.APIContext): - r.R.Use(MiddleAPI(t)) - default: - panic(fmt.Sprintf("Unsupported middleware type: %#v", t)) - } + // FIXME: another misuse, the "Use" with empty middlewares is called after "Mount" + for _, m := range middlewares { + r.R.Use(toHandlerProvider(m)) } } } @@ -99,6 +88,32 @@ func (r *Route) getPattern(pattern string) string { return strings.TrimSuffix(newPattern, "/") } +func (r *Route) wrapMiddlewareAndHandler(h []any) ([]func(http.Handler) http.Handler, http.HandlerFunc) { + handlerProviders := make([]func(http.Handler) http.Handler, 0, len(r.curMiddlewares)+len(h)) + for _, m := range r.curMiddlewares { + handlerProviders = append(handlerProviders, toHandlerProvider(m)) + } + for _, m := range h { + handlerProviders = append(handlerProviders, toHandlerProvider(m)) + } + middlewares := handlerProviders[:len(handlerProviders)-1] + handlerFunc := handlerProviders[len(handlerProviders)-1](nil).ServeHTTP + return middlewares, handlerFunc +} + +func (r *Route) Methods(method, pattern string, h []any) { + middlewares, handlerFunc := r.wrapMiddlewareAndHandler(h) + fullPattern := r.getPattern(pattern) + if strings.Contains(method, ",") { + methods := strings.Split(method, ",") + for _, method := range methods { + r.R.With(middlewares...).Method(strings.TrimSpace(method), fullPattern, handlerFunc) + } + } else { + r.R.With(middlewares...).Method(method, fullPattern, handlerFunc) + } +} + // Mount attaches another Route along ./pattern/* func (r *Route) Mount(pattern string, subR *Route) { middlewares := make([]interface{}, len(r.curMiddlewares)) @@ -109,81 +124,53 @@ func (r *Route) Mount(pattern string, subR *Route) { // Any delegate requests for all methods func (r *Route) Any(pattern string, h ...interface{}) { - middlewares := r.getMiddlewares(h) - r.R.HandleFunc(r.getPattern(pattern), Wrap(middlewares...)) + middlewares, handlerFunc := r.wrapMiddlewareAndHandler(h) + r.R.With(middlewares...).HandleFunc(r.getPattern(pattern), handlerFunc) } -// Route delegate special methods -func (r *Route) Route(pattern, methods string, h ...interface{}) { - p := r.getPattern(pattern) - ms := strings.Split(methods, ",") - middlewares := r.getMiddlewares(h) - for _, method := range ms { - r.R.MethodFunc(strings.TrimSpace(method), p, Wrap(middlewares...)) - } +// RouteMethods delegate special methods, it is an alias of "Methods", while the "pattern" is the first parameter +func (r *Route) RouteMethods(pattern, methods string, h ...interface{}) { + r.Methods(methods, pattern, h) } // Delete delegate delete method func (r *Route) Delete(pattern string, h ...interface{}) { - middlewares := r.getMiddlewares(h) - r.R.Delete(r.getPattern(pattern), Wrap(middlewares...)) -} - -func (r *Route) getMiddlewares(h []interface{}) []interface{} { - middlewares := make([]interface{}, len(r.curMiddlewares), len(r.curMiddlewares)+len(h)) - copy(middlewares, r.curMiddlewares) - middlewares = append(middlewares, h...) - return middlewares + r.Methods("DELETE", pattern, h) } // Get delegate get method func (r *Route) Get(pattern string, h ...interface{}) { - middlewares := r.getMiddlewares(h) - r.R.Get(r.getPattern(pattern), Wrap(middlewares...)) -} - -// Options delegate options method -func (r *Route) Options(pattern string, h ...interface{}) { - middlewares := r.getMiddlewares(h) - r.R.Options(r.getPattern(pattern), Wrap(middlewares...)) + r.Methods("GET", pattern, h) } // GetOptions delegate get and options method func (r *Route) GetOptions(pattern string, h ...interface{}) { - middlewares := r.getMiddlewares(h) - r.R.Get(r.getPattern(pattern), Wrap(middlewares...)) - r.R.Options(r.getPattern(pattern), Wrap(middlewares...)) + r.Methods("GET,OPTIONS", pattern, h) } // PostOptions delegate post and options method func (r *Route) PostOptions(pattern string, h ...interface{}) { - middlewares := r.getMiddlewares(h) - r.R.Post(r.getPattern(pattern), Wrap(middlewares...)) - r.R.Options(r.getPattern(pattern), Wrap(middlewares...)) + r.Methods("POST,OPTIONS", pattern, h) } // Head delegate head method func (r *Route) Head(pattern string, h ...interface{}) { - middlewares := r.getMiddlewares(h) - r.R.Head(r.getPattern(pattern), Wrap(middlewares...)) + r.Methods("HEAD", pattern, h) } // Post delegate post method func (r *Route) Post(pattern string, h ...interface{}) { - middlewares := r.getMiddlewares(h) - r.R.Post(r.getPattern(pattern), Wrap(middlewares...)) + r.Methods("POST", pattern, h) } // Put delegate put method func (r *Route) Put(pattern string, h ...interface{}) { - middlewares := r.getMiddlewares(h) - r.R.Put(r.getPattern(pattern), Wrap(middlewares...)) + r.Methods("PUT", pattern, h) } // Patch delegate patch method func (r *Route) Patch(pattern string, h ...interface{}) { - middlewares := r.getMiddlewares(h) - r.R.Patch(r.getPattern(pattern), Wrap(middlewares...)) + r.Methods("PATCH", pattern, h) } // ServeHTTP implements http.Handler @@ -191,19 +178,12 @@ func (r *Route) ServeHTTP(w http.ResponseWriter, req *http.Request) { r.R.ServeHTTP(w, req) } -// NotFound defines a handler to respond whenever a route could -// not be found. +// NotFound defines a handler to respond whenever a route could not be found. func (r *Route) NotFound(h http.HandlerFunc) { r.R.NotFound(h) } -// MethodNotAllowed defines a handler to respond whenever a method is -// not allowed. -func (r *Route) MethodNotAllowed(h http.HandlerFunc) { - r.R.MethodNotAllowed(h) -} - -// Combo deletegate requests to Combo +// Combo delegates requests to Combo func (r *Route) Combo(pattern string, h ...interface{}) *Combo { return &Combo{r, pattern, h} } @@ -215,31 +195,31 @@ type Combo struct { h []interface{} } -// Get deletegate Get method +// Get delegates Get method func (c *Combo) Get(h ...interface{}) *Combo { c.r.Get(c.pattern, append(c.h, h...)...) return c } -// Post deletegate Post method +// Post delegates Post method func (c *Combo) Post(h ...interface{}) *Combo { c.r.Post(c.pattern, append(c.h, h...)...) return c } -// Delete deletegate Delete method +// Delete delegates Delete method func (c *Combo) Delete(h ...interface{}) *Combo { c.r.Delete(c.pattern, append(c.h, h...)...) return c } -// Put deletegate Put method +// Put delegates Put method func (c *Combo) Put(h ...interface{}) *Combo { c.r.Put(c.pattern, append(c.h, h...)...) return c } -// Patch deletegate Patch method +// Patch delegates Patch method func (c *Combo) Patch(h ...interface{}) *Combo { c.r.Patch(c.pattern, append(c.h, h...)...) return c diff --git a/modules/web/route_test.go b/modules/web/route_test.go index 232444cb83..cc0e26a12e 100644 --- a/modules/web/route_test.go +++ b/modules/web/route_test.go @@ -7,6 +7,7 @@ import ( "bytes" "net/http" "net/http/httptest" + "strconv" "testing" chi "github.com/go-chi/chi/v5" @@ -39,7 +40,7 @@ func TestRoute2(t *testing.T) { recorder := httptest.NewRecorder() recorder.Body = buff - var route int + hit := -1 r := NewRoute() r.Group("/{username}/{reponame}", func() { @@ -51,7 +52,7 @@ func TestRoute2(t *testing.T) { assert.EqualValues(t, "gitea", reponame) tp := chi.URLParam(req, "type") assert.EqualValues(t, "issues", tp) - route = 0 + hit = 0 }) r.Get("/{type:issues|pulls}/{index}", func(resp http.ResponseWriter, req *http.Request) { @@ -63,10 +64,13 @@ func TestRoute2(t *testing.T) { assert.EqualValues(t, "issues", tp) index := chi.URLParam(req, "index") assert.EqualValues(t, "1", index) - route = 1 + hit = 1 }) }, func(resp http.ResponseWriter, req *http.Request) { - resp.WriteHeader(http.StatusOK) + if stop, err := strconv.Atoi(req.FormValue("stop")); err == nil { + hit = stop + resp.WriteHeader(http.StatusOK) + } }) r.Group("/issues/{index}", func() { @@ -77,7 +81,7 @@ func TestRoute2(t *testing.T) { assert.EqualValues(t, "gitea", reponame) index := chi.URLParam(req, "index") assert.EqualValues(t, "1", index) - route = 2 + hit = 2 }) }) }) @@ -86,19 +90,25 @@ func TestRoute2(t *testing.T) { assert.NoError(t, err) r.ServeHTTP(recorder, req) assert.EqualValues(t, http.StatusOK, recorder.Code) - assert.EqualValues(t, 0, route) + assert.EqualValues(t, 0, hit) req, err = http.NewRequest("GET", "http://localhost:8000/gitea/gitea/issues/1", nil) assert.NoError(t, err) r.ServeHTTP(recorder, req) assert.EqualValues(t, http.StatusOK, recorder.Code) - assert.EqualValues(t, 1, route) + assert.EqualValues(t, 1, hit) + + req, err = http.NewRequest("GET", "http://localhost:8000/gitea/gitea/issues/1?stop=100", nil) + assert.NoError(t, err) + r.ServeHTTP(recorder, req) + assert.EqualValues(t, http.StatusOK, recorder.Code) + assert.EqualValues(t, 100, hit) req, err = http.NewRequest("GET", "http://localhost:8000/gitea/gitea/issues/1/view", nil) assert.NoError(t, err) r.ServeHTTP(recorder, req) assert.EqualValues(t, http.StatusOK, recorder.Code) - assert.EqualValues(t, 2, route) + assert.EqualValues(t, 2, hit) } func TestRoute3(t *testing.T) { @@ -106,7 +116,7 @@ func TestRoute3(t *testing.T) { recorder := httptest.NewRecorder() recorder.Body = buff - var route int + hit := -1 m := NewRoute() r := NewRoute() @@ -116,20 +126,20 @@ func TestRoute3(t *testing.T) { m.Group("/{username}/{reponame}", func() { m.Group("/branch_protections", func() { m.Get("", func(resp http.ResponseWriter, req *http.Request) { - route = 0 + hit = 0 }) m.Post("", func(resp http.ResponseWriter, req *http.Request) { - route = 1 + hit = 1 }) m.Group("/{name}", func() { m.Get("", func(resp http.ResponseWriter, req *http.Request) { - route = 2 + hit = 2 }) m.Patch("", func(resp http.ResponseWriter, req *http.Request) { - route = 3 + hit = 3 }) m.Delete("", func(resp http.ResponseWriter, req *http.Request) { - route = 4 + hit = 4 }) }) }) @@ -140,29 +150,29 @@ func TestRoute3(t *testing.T) { assert.NoError(t, err) r.ServeHTTP(recorder, req) assert.EqualValues(t, http.StatusOK, recorder.Code) - assert.EqualValues(t, 0, route) + assert.EqualValues(t, 0, hit) req, err = http.NewRequest("POST", "http://localhost:8000/api/v1/repos/gitea/gitea/branch_protections", nil) assert.NoError(t, err) r.ServeHTTP(recorder, req) assert.EqualValues(t, http.StatusOK, recorder.Code, http.StatusOK) - assert.EqualValues(t, 1, route) + assert.EqualValues(t, 1, hit) req, err = http.NewRequest("GET", "http://localhost:8000/api/v1/repos/gitea/gitea/branch_protections/master", nil) assert.NoError(t, err) r.ServeHTTP(recorder, req) assert.EqualValues(t, http.StatusOK, recorder.Code) - assert.EqualValues(t, 2, route) + assert.EqualValues(t, 2, hit) req, err = http.NewRequest("PATCH", "http://localhost:8000/api/v1/repos/gitea/gitea/branch_protections/master", nil) assert.NoError(t, err) r.ServeHTTP(recorder, req) assert.EqualValues(t, http.StatusOK, recorder.Code) - assert.EqualValues(t, 3, route) + assert.EqualValues(t, 3, hit) req, err = http.NewRequest("DELETE", "http://localhost:8000/api/v1/repos/gitea/gitea/branch_protections/master", nil) assert.NoError(t, err) r.ServeHTTP(recorder, req) assert.EqualValues(t, http.StatusOK, recorder.Code) - assert.EqualValues(t, 4, route) + assert.EqualValues(t, 4, hit) } diff --git a/modules/web/routing/logger_manager.go b/modules/web/routing/logger_manager.go index aa25ec3a27..acd8c4b7dc 100644 --- a/modules/web/routing/logger_manager.go +++ b/modules/web/routing/logger_manager.go @@ -31,7 +31,7 @@ const ( type Printer func(trigger Event, record *requestRecord) type requestRecordsManager struct { - print Printer + print Printer //nolint:forbidigo lock sync.Mutex diff --git a/modules/web/wrap.go b/modules/web/wrap.go deleted file mode 100644 index 0ff9529fae..0000000000 --- a/modules/web/wrap.go +++ /dev/null @@ -1,116 +0,0 @@ -// Copyright 2021 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package web - -import ( - goctx "context" - "net/http" - "strings" - - "code.gitea.io/gitea/modules/context" - "code.gitea.io/gitea/modules/web/routing" -) - -// Wrap converts all kinds of routes to standard library one -func Wrap(handlers ...interface{}) http.HandlerFunc { - if len(handlers) == 0 { - panic("No handlers found") - } - - ourHandlers := make([]wrappedHandlerFunc, 0, len(handlers)) - - for _, handler := range handlers { - ourHandlers = append(ourHandlers, convertHandler(handler)) - } - return wrapInternal(ourHandlers) -} - -func wrapInternal(handlers []wrappedHandlerFunc) http.HandlerFunc { - return func(resp http.ResponseWriter, req *http.Request) { - var defers []func() - defer func() { - for i := len(defers) - 1; i >= 0; i-- { - defers[i]() - } - }() - for i := 0; i < len(handlers); i++ { - handler := handlers[i] - others := handlers[i+1:] - done, deferrable := handler(resp, req, others...) - if deferrable != nil { - defers = append(defers, deferrable) - } - if done { - return - } - } - } -} - -// Middle wrap a context function as a chi middleware -func Middle(f func(ctx *context.Context)) func(next http.Handler) http.Handler { - funcInfo := routing.GetFuncInfo(f) - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { - routing.UpdateFuncInfo(req.Context(), funcInfo) - ctx := context.GetContext(req) - f(ctx) - if ctx.Written() { - return - } - next.ServeHTTP(ctx.Resp, ctx.Req) - }) - } -} - -// MiddleCancel wrap a context function as a chi middleware -func MiddleCancel(f func(ctx *context.Context) goctx.CancelFunc) func(netx http.Handler) http.Handler { - funcInfo := routing.GetFuncInfo(f) - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { - routing.UpdateFuncInfo(req.Context(), funcInfo) - ctx := context.GetContext(req) - cancel := f(ctx) - if cancel != nil { - defer cancel() - } - if ctx.Written() { - return - } - next.ServeHTTP(ctx.Resp, ctx.Req) - }) - } -} - -// MiddleAPI wrap a context function as a chi middleware -func MiddleAPI(f func(ctx *context.APIContext)) func(next http.Handler) http.Handler { - funcInfo := routing.GetFuncInfo(f) - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { - routing.UpdateFuncInfo(req.Context(), funcInfo) - ctx := context.GetAPIContext(req) - f(ctx) - if ctx.Written() { - return - } - next.ServeHTTP(ctx.Resp, ctx.Req) - }) - } -} - -// WrapWithPrefix wraps a provided handler function at a prefix -func WrapWithPrefix(pathPrefix string, handler http.HandlerFunc, friendlyName ...string) func(next http.Handler) http.Handler { - funcInfo := routing.GetFuncInfo(handler, friendlyName...) - - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { - if !strings.HasPrefix(req.URL.Path, pathPrefix) { - next.ServeHTTP(resp, req) - return - } - routing.UpdateFuncInfo(req.Context(), funcInfo) - handler(resp, req) - }) - } -} diff --git a/modules/web/wrap_convert.go b/modules/web/wrap_convert.go deleted file mode 100644 index 6778e208cf..0000000000 --- a/modules/web/wrap_convert.go +++ /dev/null @@ -1,109 +0,0 @@ -// Copyright 2021 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package web - -import ( - goctx "context" - "fmt" - "net/http" - - "code.gitea.io/gitea/modules/context" - "code.gitea.io/gitea/modules/web/routing" -) - -type wrappedHandlerFunc func(resp http.ResponseWriter, req *http.Request, others ...wrappedHandlerFunc) (done bool, deferrable func()) - -func convertHandler(handler interface{}) wrappedHandlerFunc { - funcInfo := routing.GetFuncInfo(handler) - switch t := handler.(type) { - case http.HandlerFunc: - return func(resp http.ResponseWriter, req *http.Request, others ...wrappedHandlerFunc) (done bool, deferrable func()) { - routing.UpdateFuncInfo(req.Context(), funcInfo) - if _, ok := resp.(context.ResponseWriter); !ok { - resp = context.NewResponse(resp) - } - t(resp, req) - if r, ok := resp.(context.ResponseWriter); ok && r.Status() > 0 { - done = true - } - return done, deferrable - } - case func(http.ResponseWriter, *http.Request): - return func(resp http.ResponseWriter, req *http.Request, others ...wrappedHandlerFunc) (done bool, deferrable func()) { - routing.UpdateFuncInfo(req.Context(), funcInfo) - t(resp, req) - if r, ok := resp.(context.ResponseWriter); ok && r.Status() > 0 { - done = true - } - return done, deferrable - } - - case func(ctx *context.Context): - return func(resp http.ResponseWriter, req *http.Request, others ...wrappedHandlerFunc) (done bool, deferrable func()) { - routing.UpdateFuncInfo(req.Context(), funcInfo) - ctx := context.GetContext(req) - t(ctx) - done = ctx.Written() - return done, deferrable - } - case func(ctx *context.Context) goctx.CancelFunc: - return func(resp http.ResponseWriter, req *http.Request, others ...wrappedHandlerFunc) (done bool, deferrable func()) { - routing.UpdateFuncInfo(req.Context(), funcInfo) - ctx := context.GetContext(req) - deferrable = t(ctx) - done = ctx.Written() - return done, deferrable - } - case func(*context.APIContext): - return func(resp http.ResponseWriter, req *http.Request, others ...wrappedHandlerFunc) (done bool, deferrable func()) { - routing.UpdateFuncInfo(req.Context(), funcInfo) - ctx := context.GetAPIContext(req) - t(ctx) - done = ctx.Written() - return done, deferrable - } - case func(*context.APIContext) goctx.CancelFunc: - return func(resp http.ResponseWriter, req *http.Request, others ...wrappedHandlerFunc) (done bool, deferrable func()) { - routing.UpdateFuncInfo(req.Context(), funcInfo) - ctx := context.GetAPIContext(req) - deferrable = t(ctx) - done = ctx.Written() - return done, deferrable - } - case func(*context.PrivateContext): - return func(resp http.ResponseWriter, req *http.Request, others ...wrappedHandlerFunc) (done bool, deferrable func()) { - routing.UpdateFuncInfo(req.Context(), funcInfo) - ctx := context.GetPrivateContext(req) - t(ctx) - done = ctx.Written() - return done, deferrable - } - case func(*context.PrivateContext) goctx.CancelFunc: - return func(resp http.ResponseWriter, req *http.Request, others ...wrappedHandlerFunc) (done bool, deferrable func()) { - routing.UpdateFuncInfo(req.Context(), funcInfo) - ctx := context.GetPrivateContext(req) - deferrable = t(ctx) - done = ctx.Written() - return done, deferrable - } - case func(http.Handler) http.Handler: - return func(resp http.ResponseWriter, req *http.Request, others ...wrappedHandlerFunc) (done bool, deferrable func()) { - next := http.HandlerFunc(func(http.ResponseWriter, *http.Request) {}) - if len(others) > 0 { - next = wrapInternal(others) - } - routing.UpdateFuncInfo(req.Context(), funcInfo) - if _, ok := resp.(context.ResponseWriter); !ok { - resp = context.NewResponse(resp) - } - t(next).ServeHTTP(resp, req) - if r, ok := resp.(context.ResponseWriter); ok && r.Status() > 0 { - done = true - } - return done, deferrable - } - default: - panic(fmt.Sprintf("Unsupported handler type: %#v", t)) - } -} diff --git a/options/locale/locale_cs-CZ.ini b/options/locale/locale_cs-CZ.ini index 2e720709f4..c4a65b90ef 100644 --- a/options/locale/locale_cs-CZ.ini +++ b/options/locale/locale_cs-CZ.ini @@ -116,6 +116,8 @@ footer=Patička footer.software=O softwaru footer.links=Odkazy +[heatmap] + [editor] [filter] @@ -229,7 +231,6 @@ install_btn_confirm=Nainstalovat Gitea test_git_failed=Chyba při testu příkazu 'git': %v sqlite3_not_available=Tato verze Gitea nepodporuje SQLite3. Stáhněte si oficiální binární verzi od %s (nikoli verzi „gobuild“). invalid_db_setting=Nastavení databáze je neplatné: %v -invalid_db_table=Databázová tabulka „%s“ je neplatná: %v invalid_repo_path=Kořenový adresář repozitářů není správný: %v invalid_app_data_path=Cesta k datům aplikace je neplatná: %v run_user_not_match=`"Run as" uživatelské jméno není aktuální uživatelské jméno: %s -> %s` @@ -293,7 +294,6 @@ repo_no_results=Nebyly nalezeny žádné odpovídající repozitáře. user_no_results=Nebyly nalezeni žádní odpovídající uživatelé. org_no_results=Nebyly nalezeny žádné odpovídající organizace. code_no_results=Nebyl nalezen žádný zdrojový kód odpovídající hledanému výrazu. -code_search_results=Výsledky hledání pro „%s“ code_last_indexed_at=Naposledy indexováno %s relevant_repositories_tooltip=Repozitáře, které jsou rozštěpení nebo nemají žádné téma, ikonu a žádný popis jsou skryty. relevant_repositories=Zobrazují se pouze relevantní repositáře, zobrazit nefiltrované výsledky. @@ -437,6 +437,7 @@ team_invite.text_3=Poznámka: Tato pozvánka byla určena pro %[1]s. Pokud jste [modal] yes=Ano no=Ne +cancel=Zrušit modify=Aktualizovat [form] @@ -470,8 +471,6 @@ size_error=` musí být minimálně velikosti %s.` min_size_error=` musí obsahovat nejméně %s znaků.` max_size_error=` musí obsahovat maximálně %s znaků.` email_error=` není správná e-mailová adresa.` -url_error=`„%s“ není platná adresa URL.` -include_error=` musí obsahovat řetězec „%s“.` glob_pattern_error=`zástupný vzor je neplatný: %s.` regex_pattern_error=` regex vzor je neplatný: %s.` invalid_group_team_map_error=` mapování je neplatné: %s` @@ -495,7 +494,6 @@ team_name_been_taken=Název týmu je již použit. team_no_units_error=Povolit přístup alespoň do jedné sekce repozitáře. email_been_used=Tato e-mailová adresa je již používána. email_invalid=Emailová adresa je neplatná. -openid_been_used=OpenID addresa „%s“ je již použita. username_password_incorrect=Uživatelské jméno nebo heslo není správné. password_complexity=Heslo nesplňuje požadavky na složitost: password_lowercase_one=Alespoň jedno malé písmeno @@ -539,9 +537,6 @@ heatmap.loading=Načítání teplotní mapy… user_bio=Životopis disabled_public_activity=Tento uživatel zakázal veřejnou viditelnost aktivity. -form.name_reserved=Uživatelské jméno „%s“ je rezervováno. -form.name_pattern_not_allowed=Vzor „%s“ není povolen v uživatelském jméně. -form.name_chars_not_allowed=Uživatelské jméno ‚%s‘ obsahuje neplatné znaky. [settings] profile=Profil @@ -572,7 +567,6 @@ location=Místo update_theme=Aktualizovat motiv vzhledu update_profile=Aktualizovat profil update_language=Aktualizovat jazyk -update_language_not_found=Jazyk „%s“ není k dispozici. update_language_success=Jazyk byl aktualizován. update_profile_success=Váš profil byl aktualizován. change_username=Vaše uživatelské jméno bylo změněno. @@ -646,7 +640,6 @@ add_new_email=Přidat novou e-mailovou adresu add_new_openid=Přidat novou OpenID URI add_email=Přidat e-mailovou adresu add_openid=Přidat OpenID URI -add_email_confirmation_sent=Potvrzovací e-mail byl odeslán na „%s“. Prosím zkontrolujte příchozí poštu během následujících %s pro potvrzení vaší e-mailové adresy. add_email_success=Nová e-mailová adresa byla přidána. email_preference_set_success=Nastavení e-mailu bylo úspěšně nastaveno. add_openid_success=Nová OpenID adresa byla přidána. @@ -684,7 +677,6 @@ gpg_token_help=Podpis můžete vygenerovat pomocí: gpg_token_code=echo "%s" | gpg -a --default-key %s --detach-sig gpg_token_signature=Zakódovaný podpis GPG key_signature_gpg_placeholder=Začíná s „-----BEGIN PGP SIGNATURE-----“ -verify_gpg_key_success=GPG klíč „%s“ byl ověřen. ssh_key_verified=Ověřený klíč ssh_key_verified_long=Klíč byl ověřen pomocí tokenu a může být použit k ověření commitů shodujících se s libovolnou vaší aktivovanou e-mailovou adresou pro tohoto uživatele. ssh_key_verify=Ověřit @@ -694,15 +686,11 @@ ssh_token=Token ssh_token_help=Podpis můžete vygenerovat pomocí: ssh_token_signature=Zakódovaný podpis SSH key_signature_ssh_placeholder=Začíná s „-----BEGIN SSH SIGNATURE-----“ -verify_ssh_key_success=SSH klíč „%s“ byl ověřen. subkeys=Podklíče key_id=ID klíče key_name=Název klíče key_content=Obsah principal_content=Obsah -add_key_success=SSH klíč „%s“ byl přidán. -add_gpg_key_success=GPG klíč „%s“ byl přidán. -add_principal_success=Byl přidán SSH Principal certifikát „%s“. delete_key=Odstranit ssh_key_deletion=Odstraňte SSH klíč gpg_key_deletion=Odstraňte GPG klíč @@ -950,8 +938,6 @@ archive.pull.nocomment=Tento repozitář je archivovaný. Nemůžete komentovat form.reach_limit_of_creation_1=Již jste dosáhli svůj limit %d repozitář. form.reach_limit_of_creation_n=Již jste dosáhli svůj limit %d repozitářů. -form.name_reserved=Jméno repozitáře „%s“ je rezervované. -form.name_pattern_not_allowed=Vzor „%s“ není povolený v názvu repozitáře. need_auth=Ověření migrate_options=Možnosti migrace @@ -1098,7 +1084,6 @@ editor.must_be_on_a_branch=Musíte mít zvolenu větev pro úpravu či návrh zm editor.fork_before_edit=Musíte rozštěpit tento repozitář pro vytvoření nebo navržení změny tohoto souboru. editor.delete_this_file=Smazat soubor editor.must_have_write_access=Musíte mít přístup pro zápis pro dělání či navrhování změn tohoto souboru. -editor.file_delete_success=Soubor „%s“ byl smazán. editor.name_your_file=Pojmenujte váš soubor… editor.filename_help=Přidejte adresář pomocí zapsání jeho jména následovaného lomítkem („/“). Smažte adresář pomocí stisku backspace na začátku vstupního pole. editor.or=nebo @@ -1106,12 +1091,8 @@ editor.cancel_lower=Zrušit editor.commit_signed_changes=Odevzdat podepsané změny editor.commit_changes=Odevzdat změny editor.add_tmpl=Přidán „“ -editor.add=Přidat „%s“ -editor.update=Aktualizovat „%s“ -editor.delete=Smazat „%s“ editor.patch=Použít záplatu editor.patching=Záplatování: -editor.fail_to_apply_patch=Nelze použít záplatu „%s“ editor.new_patch=Nová záplata editor.commit_message_desc=Přidat volitelný rozšířený popis… editor.signoff_desc=Přidat Signed-off-by podpis přispěvatele na konec zprávy o commitu. @@ -1122,29 +1103,15 @@ editor.propose_file_change=Navrhnout změnu souboru editor.new_branch_name_desc=Název nové větve… editor.cancel=Zrušit editor.filename_cannot_be_empty=Jméno nemůže být prázdné. -editor.filename_is_invalid=Jméno souborů je neplatné: '%s'. -editor.branch_does_not_exist=Větev „%s“ v tomto repozitáři neexistuje. -editor.branch_already_exists=Repozitář větev „%s“ již obsahuje. -editor.directory_is_a_file=Jméno adresáře „%s“ je již použito jako jméno souboru v tomto repozitáři. -editor.file_is_a_symlink=„%s“ je symbolický odkaz. Symbolické odkazy nemohou být upravovány ve webovém editoru -editor.filename_is_a_directory=Jméno souboru „%s“ je již použito jako jméno adresáře v tomto repozitáři. -editor.file_editing_no_longer_exists=Upravovaný soubor „%s“ již není součástí tohoto repozitáře. -editor.file_deleting_no_longer_exists=Odstraňovaný soubor „%s“ již není součástí tohoto repozitáře. editor.file_changed_while_editing=Obsah souboru byl změněn od doby, kdy jste začaly s úpravou. Klikněte zde, abyste je zobrazili, nebo potvrďte změny ještě jednou pro jejich přepsání. -editor.file_already_exists=Soubor „%s“ již existuje v tomto repozitáři. editor.commit_empty_file_header=Odevzdat prázdný soubor editor.commit_empty_file_text=Soubor, který se chystáte odevzdat, je prázdný. Pokračovat? editor.no_changes_to_show=Žádné změny k zobrazení. -editor.fail_to_update_file=Nepodařilo se aktualizovat/vytvořit soubor „%s“. editor.fail_to_update_file_summary=Chybové hlášení: editor.push_rejected_no_message=Změna byla serverem zamítnuta bez zprávy. Prosím, zkontrolujte háčky Gitu. editor.push_rejected=Změna byla serverem zamítnuta. Prosím, zkontrolujte háčky Gitu. editor.push_rejected_summary=Úplná zpráva o odmítnutí: editor.add_subdir=Přidat adresář… -editor.unable_to_upload_files=Nepodařilo se nahrát soubor „%s“. Chyba: %v -editor.upload_file_is_locked=Soubor '%s' uzamkl %s. -editor.upload_files_to_dir=Nahrát soubory do „%s“ -editor.cannot_commit_to_protected_branch=Nelze vytvořit commit v chráněné větvi „%s“. editor.no_commit_to_branch=Nelze odevzdat přímo do větve, protože: editor.user_no_push_to_branch=Uživatel nemůže nahrávat do větve editor.require_signed_commit=Větev vyžaduje podepsaný commit @@ -1153,7 +1120,6 @@ editor.revert=Vrátit %s na: commits.desc=Procházet historii změn zdrojového kódu. commits.commits=Commity -commits.no_commits=Žádné společné commity. '%s' a '%s' mají zcela odlišnou historii. commits.nothing_to_compare=Tyto větve jsou stejné. commits.search=Hledání commitů… commits.find=Vyhledat @@ -1188,14 +1154,12 @@ projects.create=Vytvořit projekt projects.title=Název projects.new=Nový projekt projects.new_subheader=Koordinujte, sledujte a aktualizujte svou práci na jednom místě, aby projekty zůstaly transparentní a v plánu. -projects.create_success=Projekt '%s' byl vytvořen. projects.deletion=Odstranit projekt projects.deletion_desc=Odstranění projektu jej odstraní ze všech souvisejících úkolů. Pokračovat? projects.deletion_success=Projekt byl odstraněn. projects.edit=Upravit projekty projects.edit_subheader=Projekty organizují úkoly a sledují pokrok. projects.modify=Aktualizovat projekt -projects.edit_success=Projekt '%s' byl aktualizován. projects.type.none=Žádný projects.type.basic_kanban=Základní Kanban projects.type.bug_triage=Třídění chyb @@ -1265,7 +1229,6 @@ issues.label_templates.title=Nahrát předdefinovanou sadu značek issues.label_templates.info=Neexistují žádné štítky. Vytvořte štítek pomocí „Nový štítek“ nebo použijte přednastavenou sadu štítků: issues.label_templates.helper=Vyberte sadu značek issues.label_templates.use=Použít sadu štítků -issues.label_templates.fail_to_load_file=Nepodařilo se nahrát soubor šablony značek „%s“: %v issues.add_label=přidal/a %s štítek %s issues.add_labels=přidal/a %s štítky %s issues.remove_label=odstranil/a %s štítek %s @@ -1500,7 +1463,6 @@ issues.review.add_review_request=vyžádal posouzení od %s %s issues.review.remove_review_request=odstranil žádost o posouzení na %s %s issues.review.remove_review_request_self=odmítl posoudit %s issues.review.pending=Čekající -issues.review.pending.tooltip=Tento komentář není momentálně viditelný pro ostatní uživatele. Chcete-li odeslat Vaše čekající komentáře, vyberte „%s“ → „%s/%s/%s“ v horní části stránky. issues.review.review=Posouzení issues.review.reviewers=Posuzovatelé issues.review.outdated=Zastaralé @@ -1554,11 +1516,8 @@ pulls.tab_files=Změněné soubory pulls.reopen_to_merge=Prosíme, otevřete znovu tento požadavek na natažení, aby se provedlo sloučení. pulls.cant_reopen_deleted_branch=Tento požadavek na natažení nemůže být znovu otevřen protože větev byla smazána. pulls.merged=Sloučený -pulls.merged_as=Požadavek na natažení byl sloučen jako %[2]s. pulls.manually_merged=Sloučeno ručně -pulls.manually_merged_as=Požadavek na natažení byl ručně sloučen jako %[2]s. pulls.is_closed=Požadavek na natažení byl uzavřen. -pulls.has_merged=Požadavek na natažení byl sloučen. pulls.title_wip_desc=`Začněte název s %s a zamezíte tak nechtěnému sloučení požadavku na natažení.` pulls.cannot_merge_work_in_progress=Tento požadavek na natažení je označen jako probíhající práce. pulls.still_in_progress=Stále probíhá? @@ -1662,12 +1621,10 @@ milestones.desc=Popis milestones.due_date=Termín (volitelný) milestones.clear=Zrušit milestones.invalid_due_date_format=Termín dokončení musí být ve formátu 'rrrr-mm-dd'. -milestones.create_success=Milník „%s“ byl vytvořen. milestones.edit=Upravit milník milestones.edit_subheader=Milník organizuje úkoly a sledují pokrok. milestones.cancel=Zrušit milestones.modify=Aktualizovat milník -milestones.edit_success=Milník „%s“ byl aktualizován. milestones.deletion=Smazat milník milestones.deletion_desc=Odstranění milníku jej smaže ze všech souvisejících úkolů. Pokračovat? milestones.deletion_success=Milník byl odstraněn. @@ -1678,7 +1635,6 @@ milestones.filter_sort.most_complete=Nejvíce dokončené milestones.filter_sort.most_issues=Nejvíce úkolů milestones.filter_sort.least_issues=Nejméně úkolů -signing.will_sign=Tento commit bude podepsána klíčem '%s' signing.wont_sign.error=Došlo k chybě při kontrole, zda může být commit podepsán signing.wont_sign.nokey=K podpisu tohoto commitu není k dispozici žádný klíč signing.wont_sign.never=Commity nejsou nikdy podepsány @@ -1712,9 +1668,7 @@ wiki.file_revision=Revize stránky wiki.wiki_page_revisions=Revize Wiki stránky wiki.back_to_wiki=Zpět na wiki stránku wiki.delete_page_button=Smazat stránku -wiki.delete_page_notice_1=Odstranění Wiki stránky „%s“ nemůže být vráceno zpět. Pokračovat? wiki.page_already_exists=Stránka Wiki se stejným názvem již existuje. -wiki.reserved_page=Jméno Wiki stránky „%s“ je rezervováno. wiki.pages=Stránky wiki.last_updated=Naposledy aktualizováno: %s wiki.page_name_desc=Zadejte název této Wiki stránky. Některé speciální názvy jsou: „Home“, „_Sidebar“ a „_Footer“. @@ -2057,7 +2011,6 @@ settings.title=Název settings.deploy_key_content=Obsah settings.key_been_used=Klíč pro nasazení se stejným obsahem je již používán. settings.key_name_used=Klíč pro nasazení se stejným jménem již existuje. -settings.add_key_success=Klíč pro nasazení „%s“ byl přidán. settings.deploy_key_deletion=Odstranit klíč pro nasazení settings.deploy_key_deletion_desc=Odstranění klíče pro nasazení zruší jeho přístup k repozitáři. Pokračovat? settings.deploy_key_deletion_success=Klíč pro nasazení byl odstraněn. @@ -2101,8 +2054,6 @@ settings.require_signed_commits=Vyžadovat podepsané revize settings.require_signed_commits_desc=Odmítnout nahrání do této větve pokud nejsou podepsaná nebo jsou neověřitelná. settings.add_protected_branch=Zapnout ochranu settings.delete_protected_branch=Vypnout ochranu -settings.update_protect_branch_success=Ochrana větví pro větev „%s“ byla aktualizována. -settings.remove_protected_branch_success=Ochrana větví pro větev „%s“ byla zakázána. settings.protected_branch_deletion=Zakázat ochranu větví settings.protected_branch_deletion_desc=Zakázání ochrany větví umožní uživatelům s právem zápisu nahrávat do této větve. Pokračovat? settings.block_rejected_reviews=Blokovat sloučení při zamítavých posouzeních @@ -2272,42 +2223,23 @@ release.add_tag=Vytvořit pouze značku branch.name=Jméno větve branch.search=Hledat větve -branch.already_exists=Větev pojmenovaná „%s“ již existuje. branch.delete_head=Smazat -branch.delete=Smazat větev „%s“ branch.delete_html=Smazat větev branch.delete_desc=Smazání větve je trvalé. NEMŮŽE být vráceno zpět. Pokračovat? -branch.deletion_success=Větev „%s“ byla smazána. -branch.deletion_failed=Nepodařilo se odstranit větev „%s“. -branch.delete_branch_has_new_commits=Větev „%s“ nemůže být smazána, protože byly přidány nové revize po sloučení. branch.create_branch=Vytvořit větev %s -branch.create_from=z „%s“ -branch.create_success=Větev „%s“ byla vytvořena. -branch.branch_already_exists=Větev „%s“ již existuje v tomto repozitáři. -branch.branch_name_conflict=Jméno větve „%s“ koliduje s již existující větví „%s“. -branch.tag_collision=Větev „%s“ nemůže být vytvořena, protože v repozitáři existuje značka se stejným jménem. branch.deleted_by=Odstranil %s -branch.restore_success=Větev „%s“ byla obnovena. -branch.restore_failed=Nepodařilo se obnovit větev „%s“. -branch.protected_deletion_failed=Větev „%s“ je chráněna. Nemůže být smazána. -branch.default_deletion_failed=Větev „%s“ je výchozí větev. Nelze ji odstranit. -branch.restore=Obnovit větev „%s“ -branch.download=Stáhnout větev „%s“ branch.included_desc=Tato větev je součástí výchozí větve branch.included=Zahrnuje branch.create_new_branch=Vytvořit větev z větve: branch.confirm_create_branch=Vytvořit větev branch.create_branch_operation=Vytvořit větev branch.new_branch=Vytvořit novou větev -branch.new_branch_from=Vytvořit novou větev z „%s“ branch.renamed=Větev %s byla přejmenována na %s. tag.create_tag=Vytvořit značku %s tag.create_tag_operation=Vytvořit značku tag.confirm_create_tag=Vytvořit značku -tag.create_tag_from=Vytvořit novou značku z „%s“ -tag.create_success=Značka „%s“ byla vytvořena. topic.manage_topics=Spravovat témata topic.done=Hotovo @@ -2344,8 +2276,6 @@ team_permission_desc=Oprávnění team_unit_desc=Povolit přístup do částí repozitáře team_unit_disabled=(zakázaná) -form.name_reserved=Název organizace „%s“ je rezervován. -form.name_pattern_not_allowed=Vzor „%s“ není povolený v názvu organizace. form.create_org_not_allowed=Nemáte oprávnění vytvářet nové organizace. settings=Nastavení @@ -2550,7 +2480,6 @@ users.created=Vytvořen users.last_login=Poslední přihlášení users.never_login=Nikdy nepřihlášen users.send_register_notify=Odeslat upozornění při registraci uživatele -users.new_success=Uživatelský účet „%s“ byl vytvořen. users.edit=Upravit users.auth_source=Zdroj ověřování users.local=Místní @@ -2747,7 +2676,6 @@ auths.tip.yandex=Vytvořte novou aplikaci na https://oauth.yandex.com/client/new auths.tip.mastodon=Vložte vlastní URL instance pro mastodon, kterou se chcete autentizovat (nebo použijte výchozí) auths.edit=Upravit zdroj ověřování auths.activated=Tento zdroj ověřování je aktivován -auths.new_success=Zdroj ověřování „%s“ byl přidán. auths.update_success=Zdroj ověřování byl aktualizován. auths.update=Aktualizovat zdroj ověřování auths.delete=Smazat zdroj ověřování @@ -2755,7 +2683,6 @@ auths.delete_auth_title=Smazat zdroj ověřování auths.delete_auth_desc=Zamezíte přihlášení uživatelům pomocí tohoto zdroje ověřování, pokud ho smažete. Pokračovat? auths.still_in_used=Zdroj ověřování je stále používán. Nejprve převeďte nebo smažte všechny uživatele, kteří používají tento způsob ověřování. auths.deletion_success=Zdroj ověřování byl smazán. -auths.login_source_exist=Zdroj ověřování '%s' již existuje. auths.login_source_of_type_exist=Zdroj ověřování tohoto typu již existuje. config.server_config=Nastavení serveru @@ -2846,8 +2773,6 @@ config.mailer_sendmail_timeout=Časový limit Sandmail config.mailer_use_dummy=Fiktivní config.test_email_placeholder=E-mail (např.: test@example.com) config.send_test_mail=Odeslat zkušební e-mail -config.test_mail_failed=Odeslání testovacího e-mailu na „%s“ selhalo: %v -config.test_mail_sent=Zkušební e-mail byl odeslán na „%s“. config.oauth_config=Nastavení ověření OAuth config.oauth_enabled=Zapnutý @@ -3266,3 +3191,5 @@ runs.closed_tab=%d uzavřeno runs.commit=Commit +[projects] + diff --git a/options/locale/locale_de-DE.ini b/options/locale/locale_de-DE.ini index 5b2eaf316f..4e7a3a32bd 100644 --- a/options/locale/locale_de-DE.ini +++ b/options/locale/locale_de-DE.ini @@ -109,6 +109,8 @@ rss_feed=RSS Feed [aria] +[heatmap] + [editor] [filter] @@ -220,7 +222,6 @@ install_btn_confirm=Gitea installieren test_git_failed=Fehler beim Test des „git“-Befehls: %v sqlite3_not_available=Diese Gitea-Version unterstützt SQLite3 nicht. Bitte lade die offizielle binäre Version von %s herunter (nicht die „gobuild“-Version). invalid_db_setting=Datenbankeinstellungen sind ungültig: %v -invalid_db_table=Die Datenbanktabelle '%s' ist ungültig: %v invalid_repo_path=Repository-Verzeichnis ist ungültig: %v invalid_app_data_path=Der App-Daten-Pfad ist ungültig: %v run_user_not_match=Der „Ausführen als“-Benutzername ist nicht der aktuelle Benutzername: %s -> %s @@ -279,7 +280,6 @@ repo_no_results=Keine passenden Repositorys gefunden. user_no_results=Keine passenden Benutzer gefunden. org_no_results=Keine passenden Organisationen gefunden. code_no_results=Es konnte kein passender Code für deinen Suchbegriff gefunden werden. -code_search_results=Suchergebnisse für „%s“ code_last_indexed_at=Zuletzt indexiert %s relevant_repositories_tooltip=Repositorys, die Forks sind oder die kein Thema, kein Symbol und keine Beschreibung haben, werden ausgeblendet. relevant_repositories=Es werden nur relevante Repositorys angezeigt, zeigt ungefilterte Ergebnisse an. @@ -413,6 +413,7 @@ repo.collaborator.added.text=Du wurdest als Mitarbeiter für folgendes Repositor [modal] yes=Ja no=Abbrechen +cancel=Abbrechen modify=Aktualisieren [form] @@ -446,8 +447,6 @@ size_error=` muss die Größe %s haben.` min_size_error=` muss mindestens %s Zeichen enthalten.` max_size_error=` darf höchstens %s Zeichen enthalten.` email_error=` ist keine gültige E-Mail-Adresse.` -url_error=`'%s' ist keine valide URL.` -include_error=` muss den Text „%s“ enthalten.` glob_pattern_error=` Der Glob Pattern ist ungültig: %s.` regex_pattern_error=` regex ist ungültig: %s.` unknown_error=Unbekannter Fehler: @@ -470,7 +469,6 @@ team_name_been_taken=Der Teamname ist bereits vergeben. team_no_units_error=Das Team muss auf mindestens einen Bereich Zugriff haben. email_been_used=Die E-Mail-Adresse wird bereits verwendet. email_invalid=Die E-Mail-Adresse ist ungültig. -openid_been_used=Die OpenID-Adresse „%s“ wird bereits verwendet. username_password_incorrect=Benutzername oder Passwort ist falsch. password_complexity=Das Passwort erfüllt nicht die Komplexitätsanforderungen: password_lowercase_one=Mindestens ein Kleinbuchstabe @@ -511,9 +509,6 @@ heatmap.loading=Heatmap wird geladen… user_bio=Biografie disabled_public_activity=Dieser Benutzer hat die öffentliche Sichtbarkeit der Aktivität deaktiviert. -form.name_reserved=Der Benutzername „%s“ ist reserviert. -form.name_pattern_not_allowed=Das Muster „%s“ ist nicht in einem Benutzernamen erlaubt. -form.name_chars_not_allowed=Benutzername "%s" enthält ungültige Zeichen. [settings] profile=Profil @@ -544,7 +539,6 @@ location=Standort update_theme=Theme ändern update_profile=Profil aktualisieren update_language=Sprache aktualisieren -update_language_not_found=Sprache '%s' ist nicht verfügbar. update_language_success=Sprache wurde aktualisiert. update_profile_success=Dein Profil wurde aktualisiert. change_username=Dein Benutzername wurde geändert. @@ -618,7 +612,6 @@ add_new_email=Neue E-Mail-Adresse hinzufügen add_new_openid=Neue OpenID-URI hinzufügen add_email=E-Mail-Adresse hinzufügen add_openid=OpenID-URI hinzufügen -add_email_confirmation_sent=Eine Bestätigungs-E-Mail wurde an „%s“ gesendet. Bitte überprüfe dein Postfach innerhalb der nächsten %s, um die E-Mail-Adresse zu bestätigen. add_email_success=Die neue E-Mail-Addresse wurde hinzugefügt. email_preference_set_success=E-Mail-Einstellungen wurden erfolgreich aktualisiert. add_openid_success=Die neue OpenID-Adresse wurde hinzugefügt. @@ -656,7 +649,6 @@ gpg_token_help=Du kannst eine Signatur wie folgt generieren: gpg_token_code=echo "%s" | gpg -a --default-key %s --detach-sig gpg_token_signature=GPG Textsignatur (armored signature) key_signature_gpg_placeholder=Beginnt mit '-----BEGIN PGP SIGNATURE-----' -verify_gpg_key_success=Der GPG-Schlüssel „%s“ wurde verifiziert. ssh_key_verified=Verifizierter Schlüssel ssh_key_verified_long=Der Schlüssel wurde mit einem Token verifiziert. Er kann verwendet werden, um Commits zu verifizieren, die mit irgendeiner für diesen Nutzer aktivierten E-Mail-Adresse und irgendeiner Identität dieses Schlüssels übereinstimmen. ssh_key_verify=Verifizieren @@ -666,15 +658,11 @@ ssh_token=Token ssh_token_help=Du kannst eine Signatur wie folgt generieren: ssh_token_signature=SSH Textsignatur (armored signature) key_signature_ssh_placeholder=Beginnt mit „-----BEGIN PGP SIGNATURE-----“ -verify_ssh_key_success=Der SSH-Key "%s" wurde verifiziert. subkeys=Unterschlüssel key_id=Schlüssel-ID key_name=Schlüsselname key_content=Inhalt principal_content=Inhalt -add_key_success=Der SSH-Schlüssel „%s“ wurde hinzugefügt. -add_gpg_key_success=Der GPG-Key „%s“ wurde hinzugefügt. -add_principal_success=Die SSH-Zertifikat-Identität '%s' wurde hinzugefügt. delete_key=Entfernen ssh_key_deletion=SSH-Schlüssel entfernen gpg_key_deletion=GPG-Schlüssel entfernen @@ -919,8 +907,6 @@ archive.pull.nocomment=Dieses Repo ist archiviert. Du kannst Pull-Requests nicht form.reach_limit_of_creation_1=Du hast bereits dein Limit von %d Repository erreicht. form.reach_limit_of_creation_n=Du hast bereits dein Limit von %d Repositories erreicht. -form.name_reserved=Der Repository-Name „%s“ ist reserviert. -form.name_pattern_not_allowed='%s' ist nicht erlaubt für Repository-Namen. need_auth=Authentifizierung migrate_options=Migrationsoptionen @@ -1060,7 +1046,6 @@ editor.must_be_on_a_branch=Du musst dich in einem Branch befinden, um Änderunge editor.fork_before_edit=Du musst dieses Repository forken, um Änderungen an dieser Datei vorzuschlagen oder vorzunehmen. editor.delete_this_file=Datei löschen editor.must_have_write_access=Du benötigst Schreibzugriff, um Änderungen an dieser Datei vorzuschlagen oder vorzunehmen. -editor.file_delete_success=Datei '%s' wurde gelöscht. editor.name_your_file=Dateinamen eingeben… editor.filename_help=Füge einen Ordner hinzu, indem du seinen Namen und anschließend '/' eingibst. Entferne einen Ordner indem du die Zurücktaste am Anfang des Feldes drückst. editor.or=oder @@ -1068,12 +1053,8 @@ editor.cancel_lower=Abbrechen editor.commit_signed_changes=Committe signierte Änderungen editor.commit_changes=Änderungen committen editor.add_tmpl='' hinzufügen -editor.add=„%s“ hinzufügen -editor.update=„%s“ ändern -editor.delete=„%s“ löschen editor.patch=Patch anwenden editor.patching=Patche: -editor.fail_to_apply_patch=Kann Patch '%s' nicht anwenden editor.new_patch=Neuer Patch editor.commit_message_desc=Eine ausführlichere (optionale) Beschreibung hinzufügen… editor.signoff_desc=Am Ende der Commit Nachricht einen Signed-off-by Anhang vom Committer hinzufügen. @@ -1084,29 +1065,15 @@ editor.propose_file_change=Dateiänderung vorschlagen editor.new_branch_name_desc=Neuer Branchname… editor.cancel=Abbrechen editor.filename_cannot_be_empty=Der Dateiname darf nicht leer sein. -editor.filename_is_invalid=Der Dateiname ist ungültig: '%s'. -editor.branch_does_not_exist=Der Branch '%s' existiert nicht in diesem Repository. -editor.branch_already_exists=Branch „%s“ existiert bereits in diesem Repository. -editor.directory_is_a_file=Der Verzeichnisname „%s“ wird bereits als Dateiname in diesem Repository verwendet. -editor.file_is_a_symlink='%s' ist ein symbolischer Link. Symbolische Links können mit dem Web Editor nicht bearbeitet werden -editor.filename_is_a_directory=Der Dateiname „%s“ wird bereits als Verzeichnisname in diesem Repository verwendet. -editor.file_editing_no_longer_exists=Die bearbeitete Datei „%s“ existiert nicht mehr in diesem Repository. -editor.file_deleting_no_longer_exists=Die Datei '%s' existiert in diesem Repository nicht mehr. editor.file_changed_while_editing=Der Inhalt der Datei hat sich seit dem Beginn der Bearbeitung geändert. Hier klicken, um die Änderungen anzusehen, oder Änderungen erneut comitten, um sie zu überschreiben. -editor.file_already_exists=Eine Datei mit dem Namen „%s“ ist bereits in diesem Repository vorhanden. editor.commit_empty_file_header=Leere Datei committen editor.commit_empty_file_text=Die Datei, die du commiten willst, ist leer. Fortfahren? editor.no_changes_to_show=Keine Änderungen vorhanden. -editor.fail_to_update_file=Fehler beim Aktualisieren/Erstellen der Datei '%s'. editor.fail_to_update_file_summary=Fehlermeldung: editor.push_rejected_no_message=Die Änderung wurde vom Server ohne Nachricht abgelehnt. Bitte überprüfe die Git Hooks. editor.push_rejected=Die Änderung wurde vom Server abgelehnt. Bitte überprüfe die Git Hooks. editor.push_rejected_summary=Vollständige Ablehnungsmeldung: editor.add_subdir=Verzeichnis erstellen… -editor.unable_to_upload_files=Fehler beim Hochladen der Dateien nach „%s“. Fehler: %v -editor.upload_file_is_locked=Datei „%s” ist durch %s gesperrt. -editor.upload_files_to_dir=Dateien hochladen nach „%s“ -editor.cannot_commit_to_protected_branch=Commit in den geschützten Branch „%s“ ist nicht möglich. editor.no_commit_to_branch=Kann nicht direkt zum Branch committen, da: editor.user_no_push_to_branch=Benutzer kann nicht in die Branch pushen editor.require_signed_commit=Branch erfordert einen signierten Commit @@ -1115,7 +1082,6 @@ editor.revert=%s zurücksetzen auf: commits.desc=Durchsuche die Quellcode-Änderungshistorie. commits.commits=Commits -commits.no_commits=Keine gemeinsamen Commits. '%s' and '%s' haben vollständig unterschiedliche Historien. commits.nothing_to_compare=Diese Branches sind auf demselben Stand. commits.search=Commits durchsuchen… commits.find=Suchen @@ -1149,14 +1115,12 @@ projects.create=Projekt erstellen projects.title=Titel projects.new=Neues Projekt projects.new_subheader=Koordiniere, verfolge und aktualisiere deine Arbeit an einem Ort, so dass Projekte transparent und planmäßig bleiben. -projects.create_success=Das Projekt '%s' wurde erstellt. projects.deletion=Projekt löschen projects.deletion_desc=Das Löschen eines Projekts entfernt es von allen damit zusammenhängenden Issues. Fortfahren? projects.deletion_success=Das Projekt wurde gelöscht. projects.edit=Projekte bearbeiten projects.edit_subheader=Benutze Projekte, um Issues zu organisieren und den Fortschritt darzustellen. projects.modify=Projekt aktualisieren -projects.edit_success=Projekt '%s' wurde aktualisiert. projects.type.none=Ohne projects.type.basic_kanban=Einfaches Kanban projects.type.bug_triage=Bug Triage @@ -1214,7 +1178,6 @@ issues.label_templates.title=Lade vordefinierte Label issues.label_templates.info=Es existieren noch keine Label. Erstelle ein neues Label („Neues Label“) oder verwende das Standard-Label-Set: issues.label_templates.helper=Wähle ein Label-Set issues.label_templates.use=Label-Set verwenden -issues.label_templates.fail_to_load_file=Fehler beim Laden der Label-Vorlagendatei „%s“: %v issues.add_label=hat das Label %s %s hinzugefügt issues.add_labels=hat die Labels %s %s hinzugefügt issues.remove_label=hat das Label %s %s entfernt @@ -1447,7 +1410,6 @@ issues.review.add_review_request=hat ein Review von %s %s angefragt issues.review.remove_review_request=hat die Aufforderung zum Review an %s %s entfernt issues.review.remove_review_request_self=hat das Review verweigert %s issues.review.pending=Ausstehend -issues.review.pending.tooltip=Dieser Kommentar ist derzeit nicht für andere Benutzer sichtbar. Um Ihre ausstehenden Kommentare einzureichen, wählen Sie '%s' -> '%s/%s/%s' oben auf der Seite. issues.review.review=Review issues.review.reviewers=Reviewer issues.review.outdated=Veraltet @@ -1501,11 +1463,8 @@ pulls.tab_files=Geänderte Dateien pulls.reopen_to_merge=Bitte diesen Pull-Request wieder öffnen, um zu mergen. pulls.cant_reopen_deleted_branch=Dieser Pull-Request kann nicht wieder geöffnet werden, da die Branch bereits gelöscht wurde. pulls.merged=Zusammengeführt -pulls.merged_as=Der Pull Request wurde als %[2]s gemergt. pulls.manually_merged=Manuell gemergt -pulls.manually_merged_as=Dieser Pull Request wurde manuell als %[2]s gemergt. pulls.is_closed=Der Pull-Request wurde geschlossen. -pulls.has_merged=Der Pull-Request wurde gemergt. pulls.title_wip_desc=`Beginne den Titel mit %s um zu verhindern, dass der Pull Request versehentlich gemergt wird.` pulls.cannot_merge_work_in_progress=Dieser Pull Request ist als Work in Progress markiert. pulls.still_in_progress=Noch in Bearbeitung? @@ -1608,12 +1567,10 @@ milestones.desc=Beschreibung milestones.due_date=Fälligkeitsdatum (optional) milestones.clear=Feld leeren milestones.invalid_due_date_format=Das Fälligkeitsdatum muss das Format „JJJJ-MM-TT“ haben. -milestones.create_success=Der Meilenstein „%s“ wurde erstellt. milestones.edit=Meilenstein bearbeiten milestones.edit_subheader=Benutze Meilensteine, um Issues zu organisieren und den Fortschritt darzustellen. milestones.cancel=Abbrechen milestones.modify=Meilenstein bearbeiten -milestones.edit_success=Die Änderungen am Meilenstein „%s“ wurden gespeichert. milestones.deletion=Meilenstein löschen milestones.deletion_desc=Das Löschen des Meilensteins entfernt ihn von allen Issues. Fortfahren? milestones.deletion_success=Der Meilenstein wurde gelöscht. @@ -1624,7 +1581,6 @@ milestones.filter_sort.most_complete=Vollständigste milestones.filter_sort.most_issues=Meiste Issues milestones.filter_sort.least_issues=Wenigste Issues -signing.will_sign=Dieser Commit wird mit dem Schlüssel '%s' signiert signing.wont_sign.error=Es gab einen Fehler bei der Prüfung, ob der Commit signiert werden kann signing.wont_sign.nokey=Es ist kein Schlüssel zum Signieren dieses Commits verfügbar signing.wont_sign.never=Commits werden nie signiert @@ -1658,9 +1614,7 @@ wiki.file_revision=Seitenversion wiki.wiki_page_revisions=Wiki Änderungsverlauf wiki.back_to_wiki=Zurück zur Wiki-Seite wiki.delete_page_button=Seite löschen -wiki.delete_page_notice_1=Das Löschen der Wiki-Seite „%s“ kann nicht rückgängig gemacht werden. Fortfahren? wiki.page_already_exists=Eine Wiki-Seite mit dem gleichen Namen existiert bereits. -wiki.reserved_page=Der Wiki-Seitenname „%s“ ist reserviert. wiki.pages=Seiten wiki.last_updated=Zuletzt aktualisiert %s wiki.page_name_desc=Geben Sie einen Namen für diese Wiki-Seite ein. Spezielle Namen sind: 'Home', '_Sidebar' und '_Footer'. @@ -1996,7 +1950,6 @@ settings.title=Titel settings.deploy_key_content=Inhalt settings.key_been_used=Ein Deploy-Key mit identischem Inhalt wird bereits verwendet. settings.key_name_used=Ein Deploy-Key mit diesem Namen existiert bereits. -settings.add_key_success=Der Deploy-Key „%s“ wurde erfolgreich hinzugefügt. settings.deploy_key_deletion=Deploy-Key löschen settings.deploy_key_deletion_desc=Nach dem Löschen wird dieser Deploy-Key keinen Zugriff mehr auf dieses Repository haben. Fortfahren? settings.deploy_key_deletion_success=Der Deploy-Key wurde entfernt. @@ -2038,8 +1991,6 @@ settings.require_signed_commits=Signierte Commits erforderlich settings.require_signed_commits_desc=Pushes auf diesen Branch ablehnen, wenn Commits nicht signiert oder nicht überprüfbar sind. settings.add_protected_branch=Schutz aktivieren settings.delete_protected_branch=Schutz deaktivieren -settings.update_protect_branch_success=Branch-Schutz für den Branch „%s“ wurde geändert. -settings.remove_protected_branch_success=Branch-Schutz für den Branch „%s“ wurde deaktiviert. settings.protected_branch_deletion=Branch-Schutz deaktivieren settings.protected_branch_deletion_desc=Wenn du den Branch-Schutz deaktivierst, können alle Nutzer mit Schreibrechten auf den Branch pushen. Fortfahren? settings.block_rejected_reviews=Merge bei abgelehnten Reviews blockieren @@ -2209,42 +2160,23 @@ release.add_tag=Nur Tag erstellen branch.name=Branchname branch.search=Branches durchsuchen -branch.already_exists=Ein Branch mit dem Namen „%s“ existiert bereits. branch.delete_head=Löschen -branch.delete=Branch „%s“ löschen branch.delete_html=Branch löschen branch.delete_desc=Das Löschen eines Branches ist permanent. Es KANN NICHT rückgängig gemacht werden. Fortfahren? -branch.deletion_success=Branch „%s“ wurde gelöscht. -branch.deletion_failed=Branch „%s“ konnte nicht gelöscht werden. -branch.delete_branch_has_new_commits=Der Branch „%s“ kann nicht gelöscht werden, da seit dem letzten Merge neue Commits hinzugefügt wurden. branch.create_branch=Erstelle Branch %s -branch.create_from=von „%s“ -branch.create_success=Branch „%s“ wurde erstellt. -branch.branch_already_exists=Branch „%s“ existiert bereits in diesem Repository. -branch.branch_name_conflict=Der Branch-Name „%s“ steht in Konflikt mit dem bestehenden Branch „%s“. -branch.tag_collision=Branch „%s“ kann nicht erstellt werden, da in diesem Repository bereits ein Tag mit dem selben Namen existiert. branch.deleted_by=Von %s gelöscht -branch.restore_success=Branch „%s“ wurde wiederhergestellt. -branch.restore_failed=Wiederherstellung des Branches „%s“ fehlgeschlagen. -branch.protected_deletion_failed=Branch „%s“ ist geschützt und kann nicht gelöscht werden. -branch.default_deletion_failed=Branch "%s" kann nicht gelöscht werden, da dieser Branch der Standardbranch ist. -branch.restore=Branch „%s“ wiederherstellen -branch.download=Branch „%s“ herunterladen branch.included_desc=Dieser Branch ist im Standard-Branch enthalten branch.included=Enthalten branch.create_new_branch=Branch aus Branch erstellen: branch.confirm_create_branch=Branch erstellen branch.create_branch_operation=Branch erstellen branch.new_branch=Neue Branch erstellen -branch.new_branch_from=Neue Branch von '%s' erstellen branch.renamed=Branch %s wurde in %s umbenannt. tag.create_tag=Tag %s erstellen tag.create_tag_operation=Tag erstellen tag.confirm_create_tag=Tag erstellen -tag.create_tag_from=Neuen Tag von '%s' erstellen -tag.create_success=Tag "%s" wurde erstellt. topic.manage_topics=Themen verwalten topic.done=Fertig @@ -2281,8 +2213,6 @@ team_permission_desc=Berechtigungen team_unit_desc=Zugriff auf Repositorybereiche erlauben team_unit_disabled=(Deaktiviert) -form.name_reserved=Der Organisationsname „%s“ ist reserviert. -form.name_pattern_not_allowed=Das Muster „%s“ ist in Organisationsnamen nicht erlaubt. form.create_org_not_allowed=Du bist nicht berechtigt, eine Organisation zu erstellen. settings=Einstellungen @@ -2484,7 +2414,6 @@ users.created=Registriert am users.last_login=Letzte Anmeldung users.never_login=Hat sich noch nie eingeloggt users.send_register_notify=Benutzer-Registrierungsbenachrichtigung senden -users.new_success=Der Account „%s“ wurde erstellt. users.edit=Bearbeiten users.auth_source=Authentifizierungsquelle users.local=Lokal @@ -2680,7 +2609,6 @@ auths.tip.yandex=`Erstelle eine neue Anwendung auf https://oauth.yandex.com/clie auths.tip.mastodon=Gebe eine benutzerdefinierte URL für die Mastodon-Instanz ein, mit der du dich authentifizieren möchtest (oder benutze die standardmäßige) auths.edit=Authentifikationsquelle bearbeiten auths.activated=Diese Authentifikationsquelle ist aktiviert -auths.new_success=Die Authentifizierung „%s“ wurde hinzugefügt. auths.update_success=Diese Authentifizierungsquelle wurde aktualisiert. auths.update=Authentifizierungsquelle aktualisieren auths.delete=Authentifikationsquelle löschen @@ -2688,7 +2616,6 @@ auths.delete_auth_title=Authentifizierungsquelle löschen auths.delete_auth_desc=Das Löschen einer Authentifizierungsquelle verhindert, dass Benutzer sich darüber anmelden können. Fortfahren? auths.still_in_used=Diese Authentifizierungsquelle wird noch verwendet. Bearbeite oder lösche zuerst alle Benutzer, die diese Authentifizierungsquelle benutzen. auths.deletion_success=Die Authentifizierungsquelle „%s“ wurde gelöscht. -auths.login_source_exist=Die Authentifizierungsquelle „%s“ existiert bereits. auths.login_source_of_type_exist=Eine Authentifizierungart dieses Typs existiert bereits. config.server_config=Serverkonfiguration @@ -2777,8 +2704,6 @@ config.mailer_sendmail_args=Zusätzliche Argumente für Sendmail config.mailer_sendmail_timeout=Sendmail Timeout config.test_email_placeholder=E-Mail (z.B. test@example.com) config.send_test_mail=Test-E-Mail senden -config.test_mail_failed=Das Senden der Test-E-Mail an „%s“ ist fehlgeschlagen: %v -config.test_mail_sent=Eine Test-E-Mail wurde an „%s“ gesendet. config.oauth_config=OAuth-Konfiguration config.oauth_enabled=Aktiviert @@ -3127,3 +3052,5 @@ runners.version=Version runs.commit=Commit +[projects] + diff --git a/options/locale/locale_el-GR.ini b/options/locale/locale_el-GR.ini index e3fe183e59..f0c8efda74 100644 --- a/options/locale/locale_el-GR.ini +++ b/options/locale/locale_el-GR.ini @@ -19,6 +19,7 @@ active_stopwatch=Ενεργή Καταγραφή Χρόνου create_new=Δημιουργία… user_profile_and_more=Προφίλ και ρυθμίσεις… signed_in_as=Είσοδος ως +enable_javascript=Απαιτείται JavaScript για να εμφανιστεί αυτή η ιστοσελίδα. toc=Πίνακας Περιεχομένων licenses=Άδειες return_to_gitea=Επιστροφή στο Gitea @@ -83,6 +84,7 @@ add=Προσθήκη add_all=Προσθήκη Όλων remove=Αφαίρεση remove_all=Αφαίρεση Όλων +remove_label_str=`Αφαίρεση του αντικειμένου "%s"` edit=Επεξεργασία enabled=Ενεργοποιημένο @@ -116,7 +118,27 @@ footer=Υποσέλιδο footer.software=Σχετικά με το Λογισμικό footer.links=Συνδέσεις +[heatmap] +number_of_contributions_in_the_last_12_months=%s συνεισφορές τους τελευταίους 12 μήνες +no_contributions=Χωρίς συνεισφορές +less=Λιγότερα +more=Περισσότερα + [editor] +buttons.heading.tooltip=Προσθήκη επικεφαλίδας +buttons.bold.tooltip=Προσθήκη έντονου κειμένου +buttons.italic.tooltip=Προσθήκη πλαγίου κειμένου +buttons.quote.tooltip=Παράθεση κειμένου +buttons.code.tooltip=Προσθήκη κώδικα +buttons.link.tooltip=Προσθήκη συνδέσμου +buttons.list.unordered.tooltip=Προσθήκη απλής λίστας +buttons.list.ordered.tooltip=Προσθήκη αριθμημένης λίστας +buttons.list.task.tooltip=Προσθήκη λίστας εργασιών +buttons.mention.tooltip=Μνημόνευση ενός χρήστη ή ομάδας +buttons.ref.tooltip=Μνημόνευση ενός θέματος ή pull request +buttons.switch_to_legacy.tooltip=Χρήση του κλασσικού κειμενογράφου +buttons.enable_monospace_font=Ενεργοποίηση σταθερής γραμματοσειράς +buttons.disable_monospace_font=Απενεργοποίηση σταθερής γραμματοσειράς [filter] string.asc=A - Z @@ -219,6 +241,7 @@ openid_signup_popup=Ενεργοποίηση ιδιοεγγραφής χρηστ enable_captcha=Ενεργοποίηση CAPTCHA στην εγγραφή enable_captcha_popup=Απαιτείται ένα CAPTCHA για τη ιδιοεγγραφή του χρήστη. require_sign_in_view=Απαιτείται Είσοδος για τη Προβολή Σελίδων +require_sign_in_view_popup=Περιορισμός της πρόσβασης σε συνδεδεμένους χρήστες. Οι επισκέπτες θα μπορούν μόνο να δουν τις σελίδες εισόδου και εγγραφής. admin_setting_desc=Η δημιουργία ενός λογαριασμού διαχειριστή είναι προαιρετική. Ο πρώτος εγγεγραμμένος χρήστης θα γίνει αυτόματα διαχειριστής. admin_title=Ρυθμίσεις Λογαριασμού Διαχειριστή admin_name=Όνομα Χρήστη Διαχειριστή @@ -229,7 +252,7 @@ install_btn_confirm=Εγκατάσταση Gitea test_git_failed=Αδυναμία δοκιμής της εντολής 'git': %v sqlite3_not_available=Αυτή η έκδοση Gitea δεν υποστηρίζει την SQLite3. Παρακαλώ κατεβάστε την επίσημη δυαδική έκδοση από το %s (όχι την έκδοση 'gobuild'). invalid_db_setting=Οι ρυθμίσεις της βάσης δεδομένων δεν είναι έγκυρες: %v -invalid_db_table=Ο πίνακας της βάσης δεδομένων '%s' δεν είναι έγκυρος: %v +invalid_db_table=Ο πίνακας βάσης δεδομένων "%s" δεν είναι έγκυρος: %v invalid_repo_path=Η αρχική διαδρομή των αποθετηρίων δεν είναι έγκυρη: %v invalid_app_data_path=Η διαδρομή δεδομένων εφαρμογής (app data) δεν είναι έγκυρη: %v run_user_not_match=Το όνομα χρήστη 'εκτέλεση ως' δεν είναι το τρέχον όνομα χρήστη: %s -> %s @@ -248,6 +271,7 @@ no_reply_address=Κρυφό Όνομα Τομέα Email no_reply_address_helper=Όνομα τομέα για χρήστες με μια κρυφή διεύθυνση email. Για παράδειγμα, το όνομα χρήστη 'nikos' θα συνδεθεί στο Git ως 'nikos@noreply.example.org' αν ο κρυφός τομέας email έχει οριστεί ως 'noreply.example.org'. password_algorithm=Αλγόριθμος Hash Κωδικού Πρόσβασης invalid_password_algorithm=Μη έγκυρος αλγόριθμος κωδικού πρόσβασης +password_algorithm_helper=Ορίστε τον αλγόριθμο κατακερματισμού για το κωδικό πρόσβασης. Οι αλγόριθμοι διαφέρουν σε απαιτήσεις και αντοχή. Ο αλγόριθμος argon2 είναι αρκετά ασφαλής, αλλά χρησιμοποιεί πολλή μνήμη και μπορεί να είναι ακατάλληλος για μικρά συστήματα. enable_update_checker=Ενεργοποίηση Ελεγκτή Ενημερώσεων enable_update_checker_helper=Ελέγχει περιοδικά για νέες εκδόσεις κάνοντας σύνδεση στο gitea.io. @@ -294,7 +318,7 @@ repo_no_results=Δεν βρέθηκαν αποθετήρια που να ται user_no_results=Δεν βρέθηκαν χρήστες που να ταιριάζουν με τα κριτήρια. org_no_results=Δεν βρέθηκαν οργανισμοί που να ταιριάζουν με τα κριτήρια. code_no_results=Δεν βρέθηκε πηγαίος κώδικας που να ταιριάζει με τον όρο αναζήτησης. -code_search_results=Αποτελέσματα αναζήτησης για '%s' +code_search_results=`Αποτελέσματα αναζήτησης για "%s"` code_last_indexed_at=Τελευταίο δημιουργία ευρετηρίου στις %s relevant_repositories_tooltip=Τα αποθετήρια που είναι forks ή που δεν έχουν θέμα, εικονίδιο και περιγραφή είναι κρυμμένα. relevant_repositories=Εμφανίζονται μόνο τα σχετικά αποθετήρια, εμφάνιση χωρίς φίλτρο. @@ -438,6 +462,8 @@ team_invite.text_3=Σημείωση: Αυτή η πρόσκληση προορι [modal] yes=Ναι no=Όχι +confirm=Επιβεβαίωση +cancel=Ακύρωση modify=Ενημέρωση [form] @@ -471,8 +497,8 @@ size_error=`πρέπει να έχει μέγεθος %s.` min_size_error=` πρέπει να περιέχει τουλάχιστον %s χαρακτήρες.` max_size_error=` πρέπει να περιέχει το πολύ %s χαρακτήρες.` email_error=` δεν είναι έγκυρη διεύθυνση email.` -url_error=`'%s' δεν είναι έγκυρο URL.` -include_error=` πρέπει να περιέχει τη σειρά χαρακτήρων '%s'.` +url_error=`Το "%s" δεν είναι ένα έγκυρο URL.` +include_error=` πρέπει να περιέχει το μέρος "%s".` glob_pattern_error=` το μοτίβο ταιριάσματος (glob) δεν είναι έγκυρο: %s.` regex_pattern_error=` το μοτίβο regex δεν είναι έγκυρο: %s.` username_error=` μπορεί να περιέχει μόνο αλφαριθμητικούς χαρακτήρες ('0-9','a-z','A-Z'), παύλα ('-'), κάτω παύλα ('_') και τελεία ('.'). Δεν μπορεί να ξεκινά ή να τελειώνει με μη αλφαριθμητικούς χαρακτήρες, επίσης απαγορεύονται οι διαδοχικοί μη αλφαριθμητικοί χαρακτήρες.` @@ -497,7 +523,7 @@ team_name_been_taken=Το όνομα της ομάδας χρησιμοποιε team_no_units_error=Να επιτρέπεται η πρόσβαση σε τουλάχιστον μία ενότητα αποθετηρίου. email_been_used=Η διεύθυνση email χρησιμοποιείται ήδη. email_invalid=Η διεύθυνση email δεν είναι έγκυρη. -openid_been_used=Η διεύθυνση OpenID '%s' χρησιμοποιείται ήδη. +openid_been_used=Η διεύθυνση OpenID "%s" χρησιμοποιείται ήδη. username_password_incorrect=Το όνομα χρήστη ή ο κωδικός πρόσβασης δεν είναι σωστά. password_complexity=Ο κωδικός πρόσβασης δεν περνά τις απαιτήσεις πολυπλοκότητας: password_lowercase_one=Τουλάχιστον ένα πεζό γράμμα @@ -517,10 +543,16 @@ organization_leave_success=Έχετε εγκαταλείψει με επιτυχ invalid_ssh_key=Δεν είναι δυνατή η επαλήθευση του SSH κλειδιού σας: %s invalid_gpg_key=Δεν είναι δυνατή η επαλήθευση του GPG κλειδιού σας: %s -invalid_ssh_principal=Μη έγκυρος ssh principal: %s +invalid_ssh_principal=Μη έγκυρη αρχή: %s must_use_public_key=Το κλειδί που δώσατε είναι ένα ιδιωτικό κλειδί. Παρακαλώ μην ανεβάζετε το ιδιωτικό σας κλειδί οπουδήποτε. Χρησιμοποιήστε το δημόσιο κλειδί αντί αυτού. +unable_verify_ssh_key=Αδυναμία επαλήθευσης του κλειδιού SSH, ελέγξτε το ξανά για λάθη. auth_failed=Αποτυχία ταυτοποίησης: %v +still_own_repo=Ο λογαριασμός σας κατέχει ένα ή περισσότερα αποθετήρια, διαγράψτε ή μεταφέρετε τα πρώτα. +still_has_org=Ο λογαριασμός σας είναι μέλος σε ένα ή περισσότερους οργανισμούς, φύγετε από αυτούς πρώτα. +still_own_packages=Ο λογαριασμός σας κατέχει ένα ή περισσότερα πακέτα, διαγράψτε τα πρώτα. +org_still_own_repo=Αυτός ο οργανισμός κατέχει ακόμα ένα ή περισσότερα αποθετήρια, διαγράψτε τα ή μεταφέρετε τα πρώτα. +org_still_own_packages=Αυτός ο οργανισμός κατέχει ακόμα ένα ή περισσότερα πακέτα, διαγράψτε τα πρώτα. target_branch_not_exist=Ο κλάδος προορισμού δεν υπάρχει. @@ -540,10 +572,12 @@ unfollow=Να μην ακολουθώ heatmap.loading=Φόρτωση heatmap… user_bio=Βιογραφικό disabled_public_activity=Αυτός ο χρήστης έχει απενεργοποιήσει τη δημόσια προβολή της δραστηριότητας. +email_visibility.limited=Η διεύθυνση email σας είναι ορατή σε όλους τους ταυτοποιημένους χρήστες +email_visibility.private=Η διεύθυνση email σας είναι ορατή μόνο σε εσάς και στους διαχειριστές -form.name_reserved=Το όνομα χρήστη '%s' είναι δεσμευμένο. -form.name_pattern_not_allowed=Το μοτίβο '%s' δεν επιτρέπεται μέσα σε όνομα χρήστη. -form.name_chars_not_allowed=Το όνομα χρήστη '%s' περιέχει μη έγκυρους χαρακτήρες. +form.name_reserved=Το όνομα χρήστη "%s" είναι δεσμευμένο. +form.name_pattern_not_allowed=Το μοτίβο "%s" δεν επιτρέπεται μέσα σε ένα όνομα χρήστη. +form.name_chars_not_allowed=Το όνομα χρήστη "%s" περιέχει μη έγκυρους χαρακτήρες. [settings] profile=Προφίλ @@ -574,7 +608,7 @@ location=Τοποθεσία update_theme=Ενημέρωση Θέματος Διεπαφής update_profile=Ενημέρωση Προφίλ update_language=Ενημέρωση Γλώσσας -update_language_not_found=Η γλώσσα '%s' δεν είναι διαθέσιμη. +update_language_not_found=Η γλώσσα "%s" δεν είναι διαθέσιμη. update_language_success=Η γλώσσα ενημερώθηκε. update_profile_success=Το προφίλ σας έχει ενημερωθεί. change_username=Το όνομα χρήστη σας έχει αλλάξει. @@ -585,6 +619,9 @@ cancel=Ακύρωση language=Γλώσσα ui=Θέμα Διεπαφής hidden_comment_types=Κρυμμένοι τύποι σχολίων +hidden_comment_types_description=Οι τύποι σχολίων που επιλέγονται εδώ δε θα εμφανίζονται μέσα στις σελίδες ζητημάτων. Επιλέγοντας π.χ το "Σήματα", θα αφαιρεθούν όλα τα σχόλια σαν το " πρόσθεσε/αφαίρεσε τα σήματα