diff --git a/.changelog.yml b/.changelog.yml index bfdee0c0ca2..a7df8779de1 100644 --- a/.changelog.yml +++ b/.changelog.yml @@ -22,20 +22,25 @@ groups: name: FEATURES labels: - type/feature - - - name: API - labels: - - modifies/api - name: ENHANCEMENTS labels: - type/enhancement - - type/refactoring - - topic/ui + - + name: PERFORMANCE + labels: + - performance/memory + - performance/speed + - performance/bigrepo + - performance/cpu - name: BUGFIXES labels: - type/bug + - + name: API + labels: + - modifies/api - name: TESTING labels: diff --git a/.eslintrc.cjs b/.eslintrc.cjs index f52da3fa5da..abb9bde2322 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -403,7 +403,7 @@ module.exports = { 'github/a11y-svg-has-accessible-name': [0], 'github/array-foreach': [0], 'github/async-currenttarget': [2], - 'github/async-preventdefault': [2], + 'github/async-preventdefault': [0], // https://github.com/github/eslint-plugin-github/issues/599 'github/authenticity-token': [0], 'github/get-attribute': [0], 'github/js-class-name': [0], diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index d37ce219c39..d409f18cd91 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -13,5 +13,5 @@ contact_links: url: https://docs.gitea.com/help/faq about: Please check if your question isn't mentioned here. - name: Crowdin Translations - url: https://crowdin.com/project/gitea + url: https://translate.gitea.com about: Translations are managed here. diff --git a/.github/workflows/cron-licenses.yml b/.github/workflows/cron-licenses.yml index cd8386ecc52..7e57f48aa99 100644 --- a/.github/workflows/cron-licenses.yml +++ b/.github/workflows/cron-licenses.yml @@ -1,8 +1,8 @@ name: cron-licenses on: - schedule: - - cron: "7 0 * * 1" # every Monday at 00:07 UTC + #schedule: + # - cron: "7 0 * * 1" # every Monday at 00:07 UTC workflow_dispatch: jobs: diff --git a/.gitignore b/.gitignore index 619cb1cabbe..d2154683778 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ _test # IntelliJ .idea +.run # IntelliJ Gateway .uuid diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 60146276dba..11c99d1e3a9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -182,7 +182,7 @@ Here's how to run the test suite: ## Translation -All translation work happens on [Crowdin](https://crowdin.com/project/gitea). +All translation work happens on [Crowdin](https://translate.gitea.com). The only translation that is maintained in this repository is [the English translation](https://github.com/go-gitea/gitea/blob/main/options/locale/locale_en-US.ini). It is synced regularly with Crowdin. \ Other locales on main branch **should not** be updated manually as they will be overwritten with each sync. \ diff --git a/MAINTAINERS b/MAINTAINERS index ad02ecc7557..7d21f449fe4 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -31,7 +31,6 @@ Gary Kim (@gary-kim) Guillermo Prandi (@guillep2k) Mura Li (@typeless) 6543 <6543@obermui.de> (@6543) -jaqra (@jaqra) David Svantesson (@davidsvantesson) a1012112796 <1012112796@qq.com> (@a1012112796) Karl Heinz Marbaise (@khmarbaise) @@ -63,3 +62,5 @@ Yu Liu <1240335630@qq.com> (@HEREYUA) Kemal Zebari (@kemzeb) Rowan Bohde (@bohde) hiifong (@hiifong) +metiftikci (@metiftikci) +Christopher Homberger (@ChristopherHX) diff --git a/Makefile b/Makefile index 5524cfb08cd..8a7855bd5ae 100644 --- a/Makefile +++ b/Makefile @@ -26,9 +26,9 @@ COMMA := , XGO_VERSION := go-1.23.x AIR_PACKAGE ?= github.com/air-verse/air@v1 -EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/v3/cmd/editorconfig-checker@v3.0.3 +EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/v3/cmd/editorconfig-checker@v3.1.2 GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.7.0 -GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/cmd/golangci-lint@v1.62.2 +GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/cmd/golangci-lint@v1.63.4 GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.12 MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.6.0 SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.31.0 @@ -36,7 +36,7 @@ XGO_PACKAGE ?= src.techknowlogick.com/xgo@latest GO_LICENSES_PACKAGE ?= github.com/google/go-licenses@v1 GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1 ACTIONLINT_PACKAGE ?= github.com/rhysd/actionlint/cmd/actionlint@v1 -GOPLS_PACKAGE ?= golang.org/x/tools/gopls@v0.17.0 +GOPLS_PACKAGE ?= golang.org/x/tools/gopls@v0.17.1 DOCKER_IMAGE ?= gitea/gitea DOCKER_TAG ?= latest @@ -189,67 +189,11 @@ TEST_MSSQL_PASSWORD ?= MwantsaSecurePassword1 all: build .PHONY: help -help: - @echo "Make Routines:" - @echo " - \"\" equivalent to \"build\"" - @echo " - build build everything" - @echo " - frontend build frontend files" - @echo " - backend build backend files" - @echo " - watch watch everything and continuously rebuild" - @echo " - watch-frontend watch frontend files and continuously rebuild" - @echo " - watch-backend watch backend files and continuously rebuild" - @echo " - clean delete backend and integration files" - @echo " - clean-all delete backend, frontend and integration files" - @echo " - deps install dependencies" - @echo " - deps-frontend install frontend dependencies" - @echo " - deps-backend install backend dependencies" - @echo " - deps-tools install tool dependencies" - @echo " - deps-py install python dependencies" - @echo " - lint lint everything" - @echo " - lint-fix lint everything and fix issues" - @echo " - lint-actions lint action workflow files" - @echo " - lint-frontend lint frontend files" - @echo " - lint-frontend-fix lint frontend files and fix issues" - @echo " - lint-backend lint backend files" - @echo " - lint-backend-fix lint backend files and fix issues" - @echo " - lint-go lint go files" - @echo " - lint-go-fix lint go files and fix issues" - @echo " - lint-go-vet lint go files with vet" - @echo " - lint-go-gopls lint go files with gopls" - @echo " - lint-js lint js files" - @echo " - lint-js-fix lint js files and fix issues" - @echo " - lint-css lint css files" - @echo " - lint-css-fix lint css files and fix issues" - @echo " - lint-md lint markdown files" - @echo " - lint-swagger lint swagger files" - @echo " - lint-templates lint template files" - @echo " - lint-yaml lint yaml files" - @echo " - lint-spell lint spelling" - @echo " - lint-spell-fix lint spelling and fix issues" - @echo " - checks run various consistency checks" - @echo " - checks-frontend check frontend files" - @echo " - checks-backend check backend files" - @echo " - test test everything" - @echo " - test-frontend test frontend files" - @echo " - test-backend test backend files" - @echo " - test-e2e[\#TestSpecificName] test end to end using playwright" - @echo " - update update js and py dependencies" - @echo " - update-js update js dependencies" - @echo " - update-py update py dependencies" - @echo " - webpack build webpack files" - @echo " - svg build svg files" - @echo " - fomantic build fomantic files" - @echo " - generate run \"go generate\"" - @echo " - fmt format the Go code" - @echo " - generate-license update license files" - @echo " - generate-gitignore update gitignore files" - @echo " - generate-manpage generate manpage" - @echo " - generate-swagger generate the swagger spec from code comments" - @echo " - swagger-validate check if the swagger spec is valid" - @echo " - go-licenses regenerate go licenses" - @echo " - tidy run go mod tidy" - @echo " - test[\#TestSpecificName] run unit test" - @echo " - test-sqlite[\#TestSpecificName] run integration test for sqlite" +help: Makefile ## print Makefile help information. + @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m[TARGETS] default target: build\033[0m\n\n\033[35mTargets:\033[0m\n"} /^[0-9A-Za-z._-]+:.*?##/ { printf " \033[36m%-45s\033[0m %s\n", $$1, $$2 }' Makefile #$(MAKEFILE_LIST) + @printf " \033[36m%-46s\033[0m %s\n" "test-e2e[#TestSpecificName]" "test end to end using playwright" + @printf " \033[36m%-46s\033[0m %s\n" "test[#TestSpecificName]" "run unit test" + @printf " \033[36m%-46s\033[0m %s\n" "test-sqlite[#TestSpecificName]" "run integration test for sqlite" .PHONY: go-check go-check: @@ -280,11 +224,11 @@ node-check: fi .PHONY: clean-all -clean-all: clean +clean-all: clean ## delete backend, frontend and integration files rm -rf $(WEBPACK_DEST_ENTRIES) node_modules .PHONY: clean -clean: +clean: ## delete backend and integration files rm -rf $(EXECUTABLE) $(DIST) $(BINDATA_DEST) $(BINDATA_HASH) \ integrations*.test \ e2e*.test \ @@ -296,7 +240,7 @@ clean: tests/e2e/reports/ tests/e2e/test-artifacts/ tests/e2e/test-snapshots/ .PHONY: fmt -fmt: +fmt: ## format the Go code @GOFUMPT_PACKAGE=$(GOFUMPT_PACKAGE) $(GO) run build/code-batch-process.go gitea-fmt -w '{file-list}' $(eval TEMPLATES := $(shell find templates -type f -name '*.tmpl')) @# strip whitespace after '{{' or '(' and before '}}' or ')' unless there is only @@ -325,7 +269,7 @@ TAGS_PREREQ := $(TAGS_EVIDENCE) endif .PHONY: generate-swagger -generate-swagger: $(SWAGGER_SPEC) +generate-swagger: $(SWAGGER_SPEC) ## generate the swagger spec from code comments $(SWAGGER_SPEC): $(GO_SOURCES_NO_BINDATA) $(GO) run $(SWAGGER_PACKAGE) generate spec -x "$(SWAGGER_EXCLUDE)" -o './$(SWAGGER_SPEC)' @@ -342,78 +286,78 @@ swagger-check: generate-swagger fi .PHONY: swagger-validate -swagger-validate: +swagger-validate: ## check if the swagger spec is valid $(SED_INPLACE) '$(SWAGGER_SPEC_S_JSON)' './$(SWAGGER_SPEC)' $(GO) run $(SWAGGER_PACKAGE) validate './$(SWAGGER_SPEC)' $(SED_INPLACE) '$(SWAGGER_SPEC_S_TMPL)' './$(SWAGGER_SPEC)' .PHONY: checks -checks: checks-frontend checks-backend +checks: checks-frontend checks-backend ## run various consistency checks .PHONY: checks-frontend -checks-frontend: lockfile-check svg-check +checks-frontend: lockfile-check svg-check ## check frontend files .PHONY: checks-backend -checks-backend: tidy-check swagger-check fmt-check swagger-validate security-check +checks-backend: tidy-check swagger-check fmt-check swagger-validate security-check ## check backend files .PHONY: lint -lint: lint-frontend lint-backend lint-spell +lint: lint-frontend lint-backend lint-spell ## lint everything .PHONY: lint-fix -lint-fix: lint-frontend-fix lint-backend-fix lint-spell-fix +lint-fix: lint-frontend-fix lint-backend-fix lint-spell-fix ## lint everything and fix issues .PHONY: lint-frontend -lint-frontend: lint-js lint-css +lint-frontend: lint-js lint-css ## lint frontend files .PHONY: lint-frontend-fix -lint-frontend-fix: lint-js-fix lint-css-fix +lint-frontend-fix: lint-js-fix lint-css-fix ## lint frontend files and fix issues .PHONY: lint-backend -lint-backend: lint-go lint-go-vet lint-go-gopls lint-editorconfig +lint-backend: lint-go lint-go-vet lint-go-gopls lint-editorconfig ## lint backend files .PHONY: lint-backend-fix -lint-backend-fix: lint-go-fix lint-go-vet lint-editorconfig +lint-backend-fix: lint-go-fix lint-go-vet lint-editorconfig ## lint backend files and fix issues .PHONY: lint-js -lint-js: node_modules +lint-js: node_modules ## lint js files npx eslint --color --max-warnings=0 --ext js,ts,vue $(ESLINT_FILES) npx vue-tsc .PHONY: lint-js-fix -lint-js-fix: node_modules +lint-js-fix: node_modules ## lint js files and fix issues npx eslint --color --max-warnings=0 --ext js,ts,vue $(ESLINT_FILES) --fix npx vue-tsc .PHONY: lint-css -lint-css: node_modules +lint-css: node_modules ## lint css files npx stylelint --color --max-warnings=0 $(STYLELINT_FILES) .PHONY: lint-css-fix -lint-css-fix: node_modules +lint-css-fix: node_modules ## lint css files and fix issues npx stylelint --color --max-warnings=0 $(STYLELINT_FILES) --fix .PHONY: lint-swagger -lint-swagger: node_modules +lint-swagger: node_modules ## lint swagger files npx spectral lint -q -F hint $(SWAGGER_SPEC) .PHONY: lint-md -lint-md: node_modules +lint-md: node_modules ## lint markdown files npx markdownlint *.md .PHONY: lint-spell -lint-spell: +lint-spell: ## lint spelling @go run $(MISSPELL_PACKAGE) -dict tools/misspellings.csv -error $(SPELLCHECK_FILES) .PHONY: lint-spell-fix -lint-spell-fix: +lint-spell-fix: ## lint spelling and fix issues @go run $(MISSPELL_PACKAGE) -dict tools/misspellings.csv -w $(SPELLCHECK_FILES) .PHONY: lint-go -lint-go: +lint-go: ## lint go files $(GO) run $(GOLANGCI_LINT_PACKAGE) run .PHONY: lint-go-fix -lint-go-fix: +lint-go-fix: ## lint go files and fix issues $(GO) run $(GOLANGCI_LINT_PACKAGE) run --fix # workaround step for the lint-go-windows CI task because 'go run' can not @@ -424,13 +368,13 @@ lint-go-windows: golangci-lint run .PHONY: lint-go-vet -lint-go-vet: +lint-go-vet: ## lint go files with vet @echo "Running go vet..." @GOOS= GOARCH= $(GO) build code.gitea.io/gitea-vet @$(GO) vet -vettool=gitea-vet ./... .PHONY: lint-go-gopls -lint-go-gopls: +lint-go-gopls: ## lint go files with gopls @echo "Running gopls check..." @GO=$(GO) GOPLS_PACKAGE=$(GOPLS_PACKAGE) tools/lint-go-gopls.sh $(GO_SOURCES_NO_BINDATA) @@ -439,41 +383,41 @@ lint-editorconfig: @$(GO) run $(EDITORCONFIG_CHECKER_PACKAGE) $(EDITORCONFIG_FILES) .PHONY: lint-actions -lint-actions: +lint-actions: ## lint action workflow files $(GO) run $(ACTIONLINT_PACKAGE) .PHONY: lint-templates -lint-templates: .venv node_modules +lint-templates: .venv node_modules ## lint template files @node tools/lint-templates-svg.js @poetry run djlint $(shell find templates -type f -iname '*.tmpl') .PHONY: lint-yaml -lint-yaml: .venv +lint-yaml: .venv ## lint yaml files @poetry run yamllint . .PHONY: watch -watch: +watch: ## watch everything and continuously rebuild @bash tools/watch.sh .PHONY: watch-frontend -watch-frontend: node-check node_modules +watch-frontend: node-check node_modules ## watch frontend files and continuously rebuild @rm -rf $(WEBPACK_DEST_ENTRIES) NODE_ENV=development npx webpack --watch --progress .PHONY: watch-backend -watch-backend: go-check +watch-backend: go-check ## watch backend files and continuously rebuild GITEA_RUN_MODE=dev $(GO) run $(AIR_PACKAGE) -c .air.toml .PHONY: test -test: test-frontend test-backend +test: test-frontend test-backend ## test everything .PHONY: test-backend -test-backend: +test-backend: ## test frontend files @echo "Running go test with $(GOTESTFLAGS) -tags '$(TEST_TAGS)'..." @$(GO) test $(GOTESTFLAGS) -tags='$(TEST_TAGS)' $(GO_TEST_PACKAGES) .PHONY: test-frontend -test-frontend: node_modules +test-frontend: node_modules ## test backend files npx vitest .PHONY: test-check @@ -505,7 +449,7 @@ unit-test-coverage: @$(GO) test $(GOTESTFLAGS) -timeout=20m -tags='$(TEST_TAGS)' -cover -coverprofile coverage.out $(GO_TEST_PACKAGES) && echo "\n==>\033[32m Ok\033[m\n" || exit 1 .PHONY: tidy -tidy: +tidy: ## run go mod tidy $(eval MIN_GO_VERSION := $(shell grep -Eo '^go\s+[0-9]+\.[0-9.]+' go.mod | cut -d' ' -f2)) $(GO) mod tidy -compat=$(MIN_GO_VERSION) @$(MAKE) --no-print-directory $(GO_LICENSE_FILE) @@ -524,7 +468,7 @@ tidy-check: tidy fi .PHONY: go-licenses -go-licenses: $(GO_LICENSE_FILE) +go-licenses: $(GO_LICENSE_FILE) ## regenerate go licenses $(GO_LICENSE_FILE): go.mod go.sum -$(GO) run $(GO_LICENSES_PACKAGE) save . --force --save_path=$(GO_LICENSE_TMP_DIR) 2>/dev/null @@ -771,17 +715,17 @@ install: $(wildcard *.go) CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) install -v -tags '$(TAGS)' -ldflags '-s -w $(LDFLAGS)' .PHONY: build -build: frontend backend +build: frontend backend ## build everything .PHONY: frontend -frontend: $(WEBPACK_DEST) +frontend: $(WEBPACK_DEST) ## build frontend files .PHONY: backend -backend: go-check generate-backend $(EXECUTABLE) +backend: go-check generate-backend $(EXECUTABLE) ## build backend files # We generate the backend before the frontend in case we in future we want to generate things in the frontend from generated files in backend .PHONY: generate -generate: generate-backend +generate: generate-backend ## run "go generate" .PHONY: generate-backend generate-backend: $(TAGS_PREREQ) generate-go @@ -846,20 +790,20 @@ release-sources: | $(DIST_DIRS) rm -f $(STORED_VERSION_FILE) .PHONY: deps -deps: deps-frontend deps-backend deps-tools deps-py +deps: deps-frontend deps-backend deps-tools deps-py ## install dependencies .PHONY: deps-py -deps-py: .venv +deps-py: .venv ## install python dependencies .PHONY: deps-frontend -deps-frontend: node_modules +deps-frontend: node_modules ## install frontend dependencies .PHONY: deps-backend -deps-backend: +deps-backend: ## install backend dependencies $(GO) mod download .PHONY: deps-tools -deps-tools: +deps-tools: ## install tool dependencies $(GO) install $(AIR_PACKAGE) & \ $(GO) install $(EDITORCONFIG_CHECKER_PACKAGE) & \ $(GO) install $(GOFUMPT_PACKAGE) & \ @@ -883,10 +827,10 @@ node_modules: package-lock.json @touch .venv .PHONY: update -update: update-js update-py +update: update-js update-py ## update js and py dependencies .PHONY: update-js -update-js: node-check | node_modules +update-js: node-check | node_modules ## update js dependencies npx updates -u -f package.json rm -rf node_modules package-lock.json npm install --package-lock @@ -895,14 +839,14 @@ update-js: node-check | node_modules @touch node_modules .PHONY: update-py -update-py: node-check | node_modules +update-py: node-check | node_modules ## update py dependencies npx updates -u -f pyproject.toml rm -rf .venv poetry.lock poetry install @touch .venv .PHONY: fomantic -fomantic: +fomantic: ## build fomantic files rm -rf $(FOMANTIC_WORK_DIR)/build cd $(FOMANTIC_WORK_DIR) && npm install --no-save cp -f $(FOMANTIC_WORK_DIR)/theme.config.less $(FOMANTIC_WORK_DIR)/node_modules/fomantic-ui/src/theme.config @@ -915,7 +859,7 @@ fomantic: rm -f $(FOMANTIC_WORK_DIR)/build/*.min.* .PHONY: webpack -webpack: $(WEBPACK_DEST) +webpack: $(WEBPACK_DEST) ## build webpack files $(WEBPACK_DEST): $(WEBPACK_SOURCES) $(WEBPACK_CONFIGS) package-lock.json @$(MAKE) -s node-check node_modules @@ -925,7 +869,7 @@ $(WEBPACK_DEST): $(WEBPACK_SOURCES) $(WEBPACK_CONFIGS) package-lock.json @touch $(WEBPACK_DEST) .PHONY: svg -svg: node-check | node_modules +svg: node-check | node_modules ## build svg files rm -rf $(SVG_DEST_DIR) node tools/generate-svg.js @@ -961,11 +905,11 @@ update-translations: rmdir ./translations .PHONY: generate-license -generate-license: +generate-license: ## update license files $(GO) run build/generate-licenses.go .PHONY: generate-gitignore -generate-gitignore: +generate-gitignore: ## update gitignore files $(GO) run build/generate-gitignores.go .PHONY: generate-images @@ -974,7 +918,7 @@ generate-images: | node_modules node tools/generate-images.js $(TAGS) .PHONY: generate-manpage -generate-manpage: +generate-manpage: ## generate manpage @[ -f gitea ] || make backend @mkdir -p man/man1/ man/man5 @./gitea docs --man > man/man1/gitea.1 diff --git a/README.md b/README.md index c280c832ac7..f747d993d79 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ [![](https://opencollective.com/gitea/tiers/backers/badge.svg?label=backers&color=brightgreen)](https://opencollective.com/gitea "Become a backer/sponsor of gitea") [![](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT "License: MIT") [![Contribute with Gitpod](https://img.shields.io/badge/Contribute%20with-Gitpod-908a85?logo=gitpod&color=green)](https://gitpod.io/#https://github.com/go-gitea/gitea) -[![](https://badges.crowdin.net/gitea/localized.svg)](https://crowdin.com/project/gitea "Crowdin") +[![](https://badges.crowdin.net/gitea/localized.svg)](https://translate.gitea.com "Crowdin") [View this document in Chinese](./README_ZH.md) @@ -31,6 +31,14 @@ For accessing free Gitea service (with a limited number of repositories), you ca To quickly deploy your own dedicated Gitea instance on Gitea Cloud, you can start a free trial at [cloud.gitea.com](https://cloud.gitea.com). +## Documentation + +You can find comprehensive documentation on our official [documentation website](https://docs.gitea.com/). + +It includes installation, administration, usage, development, contributing guides, and more to help you get started and explore all features effectively. + +If you have any suggestions or would like to contribute to it, you can visit the [documentation repository](https://gitea.com/gitea/docs) + ## Building From the root of the source tree, run: @@ -52,6 +60,8 @@ More info: https://docs.gitea.com/installation/install-from-source ## Using +After building, a binary file named `gitea` will be generated in the root of the source tree by default. To run it, use: + ./gitea web > [!NOTE] @@ -68,22 +78,25 @@ Expected workflow is: Fork -> Patch -> Push -> Pull Request ## Translating -Translations are done through Crowdin. If you want to translate to a new language ask one of the managers in the Crowdin project to add a new language there. +[![Crowdin](https://badges.crowdin.net/gitea/localized.svg)](https://translate.gitea.com) + +Translations are done through [Crowdin](https://translate.gitea.com). If you want to translate to a new language ask one of the managers in the Crowdin project to add a new language there. You can also just create an issue for adding a language or ask on discord on the #translation channel. If you need context or find some translation issues, you can leave a comment on the string or ask on Discord. For general translation questions there is a section in the docs. Currently a bit empty but we hope to fill it as questions pop up. -https://docs.gitea.com/contributing/localization +Get more information from [documentation](https://docs.gitea.com/contributing/localization). -[![Crowdin](https://badges.crowdin.net/gitea/localized.svg)](https://crowdin.com/project/gitea) +## Official and Third-Party Projects -## Further information +We provide an official [go-sdk](https://gitea.com/gitea/go-sdk), a CLI tool called [tea](https://gitea.com/gitea/tea) and an [action runner](https://gitea.com/gitea/act_runner) for Gitea Action. -For more information and instructions about how to install Gitea, please look at our [documentation](https://docs.gitea.com/). -If you have questions that are not covered by the documentation, you can get in contact with us on our [Discord server](https://discord.gg/Gitea) or create a post in the [discourse forum](https://forum.gitea.com/). +We maintain a list of Gitea-related projects at [gitea/awesome-gitea](https://gitea.com/gitea/awesome-gitea), where you can discover more third-party projects, including SDKs, plugins, themes, and more. -We maintain a list of Gitea-related projects at [gitea/awesome-gitea](https://gitea.com/gitea/awesome-gitea). +## Communication -The official Gitea CLI is developed at [gitea/tea](https://gitea.com/gitea/tea). +[![](https://img.shields.io/discord/322538954119184384.svg?logo=discord&logoColor=white&label=Discord&color=5865F2)](https://discord.gg/Gitea "Join the Discord chat at https://discord.gg/Gitea") + +If you have questions that are not covered by the [documentation](https://docs.gitea.com/), you can get in contact with us on our [Discord server](https://discord.gg/Gitea) or create a post in the [discourse forum](https://forum.gitea.com/). ## Authors @@ -122,18 +135,25 @@ Gitea is pronounced [/ɡɪ’ti:/](https://youtu.be/EM71-2uDAoY) as in "gi-tea" We're [working on it](https://github.com/go-gitea/gitea/issues/1029). +**Where can I find the security patches?** + +In the [release log](https://github.com/go-gitea/gitea/releases) or the [change log](https://github.com/go-gitea/gitea/blob/main/CHANGELOG.md), search for the keyword `SECURITY` to find the security patches. + ## License This project is licensed under the MIT License. See the [LICENSE](https://github.com/go-gitea/gitea/blob/main/LICENSE) file for the full license text. -## Screenshots +## Further information -Looking for an overview of the interface? Check it out! +
+Looking for an overview of the interface? Check it out! |![Dashboard](https://dl.gitea.com/screenshots/home_timeline.png)|![User Profile](https://dl.gitea.com/screenshots/user_profile.png)|![Global Issues](https://dl.gitea.com/screenshots/global_issues.png)| |:---:|:---:|:---:| |![Branches](https://dl.gitea.com/screenshots/branches.png)|![Web Editor](https://dl.gitea.com/screenshots/web_editor.png)|![Activity](https://dl.gitea.com/screenshots/activity.png)| |![New Migration](https://dl.gitea.com/screenshots/migration.png)|![Migrating](https://dl.gitea.com/screenshots/migration.gif)|![Pull Request View](https://image.ibb.co/e02dSb/6.png)| |![Pull Request Dark](https://dl.gitea.com/screenshots/pull_requests_dark.png)|![Diff Review Dark](https://dl.gitea.com/screenshots/review_dark.png)|![Diff Dark](https://dl.gitea.com/screenshots/diff_dark.png)| + +
diff --git a/README_ZH.md b/README_ZH.md index a2e36dc22f1..2dd60fd564f 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -9,13 +9,13 @@ [![](https://opencollective.com/gitea/tiers/backers/badge.svg?label=backers&color=brightgreen)](https://opencollective.com/gitea "Become a backer/sponsor of gitea") [![](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT "License: MIT") [![Contribute with Gitpod](https://img.shields.io/badge/Contribute%20with-Gitpod-908a85?logo=gitpod&color=green)](https://gitpod.io/#https://github.com/go-gitea/gitea) -[![](https://badges.crowdin.net/gitea/localized.svg)](https://crowdin.com/project/gitea "Crowdin") +[![](https://badges.crowdin.net/gitea/localized.svg)](https://translate.gitea.com "Crowdin") [View this document in English](./README.md) ## 目标 -Gitea 的首要目标是创建一个极易安装,运行非常快速,安装和使用体验良好的自建 Git 服务。我们采用 Go 作为后端语言,这使我们只要生成一个可执行程序即可。并且他还支持跨平台,支持 Linux, macOS 和 Windows 以及各种架构,除了 x86,amd64,还包括 ARM 和 PowerPC。 +Gitea 的首要目标是创建一个极易安装,运行非常快速,安装和使用体验良好的自建 Git 服务。我们采用 Go 作为后端语言,这使我们只要生成一个可执行程序即可。并且他还支持跨平台,支持 Linux、macOS 和 Windows 以及各种架构,除了 x86 和 amd64,还包括 ARM 和 PowerPC。 如果你想试用在线演示和报告问题,请访问 [demo.gitea.com](https://demo.gitea.com/)。 @@ -23,39 +23,80 @@ Gitea 的首要目标是创建一个极易安装,运行非常快速,安装 如果你想在 Gitea Cloud 上快速部署你自己独享的 Gitea 实例,请访问 [cloud.gitea.com](https://cloud.gitea.com) 开始免费试用。 -## 提示 - -1. **开始贡献代码之前请确保你已经看过了 [贡献者向导(英文)](CONTRIBUTING.md)**. -2. 所有的安全问题,请私下发送邮件给 **security@gitea.io**。谢谢! -3. 如果你要使用API,请参见 [API 文档](https://godoc.org/code.gitea.io/sdk/gitea). - ## 文档 关于如何安装请访问我们的 [文档站](https://docs.gitea.com/zh-cn/category/installation),如果没有找到对应的文档,你也可以通过 [Discord - 英文](https://discord.gg/gitea) 和 QQ群 328432459 来和我们交流。 -## 贡献流程 +## 编译 -Fork -> Patch -> Push -> Pull Request +在源代码的根目录下执行: + + TAGS="bindata" make build + +或者如果需要SQLite支持: + + TAGS="bindata sqlite sqlite_unlock_notify" make build + +编译过程会分成2个子任务: + +- `make backend`,需要 [Go Stable](https://go.dev/dl/),最低版本需求可查看 [go.mod](/go.mod)。 +- `make frontend`,需要 [Node.js LTS](https://nodejs.org/en/download/) 或更高版本。 + +你需要连接网络来下载 go 和 npm modules。当从 tar 格式的源文件编译时,其中包含了预编译的前端文件,因此 `make frontend` 将不会被执行。这允许编译时不需要 Node.js。 + +更多信息: https://docs.gitea.com/installation/install-from-source + +## 使用 + +编译之后,默认会在根目录下生成一个名为 `gitea` 的文件。你可以这样执行它: + + ./gitea web + +> [!注意] +> 如果你要使用API,请参见 [API 文档](https://godoc.org/code.gitea.io/sdk/gitea)。 + +## 贡献 + +贡献流程:Fork -> Patch -> Push -> Pull Request + +> [!注意] +> +> 1. **开始贡献代码之前请确保你已经看过了 [贡献者向导(英文)](CONTRIBUTING.md)**。 +> 2. 所有的安全问题,请私下发送邮件给 **security@gitea.io**。 谢谢! ## 翻译 -多语言翻译是基于Crowdin进行的. -[![Crowdin](https://badges.crowdin.net/gitea/localized.svg)](https://crowdin.com/project/gitea) +[![Crowdin](https://badges.crowdin.net/gitea/localized.svg)](https://translate.gitea.com) + +多语言翻译是基于Crowdin进行的。 + +从 [文档](https://docs.gitea.com/contributing/localization) 中获取更多信息。 + +## 官方和第三方项目 + +Gitea 提供官方的 [go-sdk](https://gitea.com/gitea/go-sdk),以及名为 [tea](https://gitea.com/gitea/tea) 的 CLI 工具 和 用于 Gitea Action 的 [action runner](https://gitea.com/gitea/act_runner)。 + +[gitea/awesome-gitea](https://gitea.com/gitea/awesome-gitea) 是一个 Gitea 相关项目的列表,你可以在这里找到更多的第三方项目,包括 SDK、插件、主题等等。 ## 作者 -* [Maintainers](https://github.com/orgs/go-gitea/people) -* [Contributors](https://github.com/go-gitea/gitea/graphs/contributors) -* [Translators](options/locale/TRANSLATORS) +- [Maintainers](https://github.com/orgs/go-gitea/people) +- [Contributors](https://github.com/go-gitea/gitea/graphs/contributors) +- [Translators](options/locale/TRANSLATORS) ## 授权许可 本项目采用 MIT 开源授权许可证,完整的授权说明已放置在 [LICENSE](https://github.com/go-gitea/gitea/blob/main/LICENSE) 文件中。 -## 截图 +## 更多信息 + +
+截图 |![Dashboard](https://dl.gitea.com/screenshots/home_timeline.png)|![User Profile](https://dl.gitea.com/screenshots/user_profile.png)|![Global Issues](https://dl.gitea.com/screenshots/global_issues.png)| |:---:|:---:|:---:| |![Branches](https://dl.gitea.com/screenshots/branches.png)|![Web Editor](https://dl.gitea.com/screenshots/web_editor.png)|![Activity](https://dl.gitea.com/screenshots/activity.png)| |![New Migration](https://dl.gitea.com/screenshots/migration.png)|![Migrating](https://dl.gitea.com/screenshots/migration.gif)|![Pull Request View](https://image.ibb.co/e02dSb/6.png)| |![Pull Request Dark](https://dl.gitea.com/screenshots/pull_requests_dark.png)|![Diff Review Dark](https://dl.gitea.com/screenshots/review_dark.png)|![Diff Dark](https://dl.gitea.com/screenshots/diff_dark.png)| + +
diff --git a/assets/go-licenses.json b/assets/go-licenses.json index 8c155ceadb1..29ed0848b84 100644 --- a/assets/go-licenses.json +++ b/assets/go-licenses.json @@ -744,11 +744,6 @@ "path": "github.com/kevinburke/ssh_config/LICENSE", "licenseText": "Copyright (c) 2017 Kevin Burke.\n\nPermission is hereby granted, free of charge, to any person\nobtaining a copy of this software and associated documentation\nfiles (the \"Software\"), to deal in the Software without\nrestriction, including without limitation the rights to use,\ncopy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the\nSoftware is furnished to do so, subject to the following\nconditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES\nOF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\nHOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nWHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\nOTHER DEALINGS IN THE SOFTWARE.\n\n===================\n\nThe lexer and parser borrow heavily from github.com/pelletier/go-toml. The\nlicense for that project is copied below.\n\nThe MIT License (MIT)\n\nCopyright (c) 2013 - 2017 Thomas Pelletier, Eric Anderton\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n" }, - { - "name": "github.com/keybase/go-crypto", - "path": "github.com/keybase/go-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": "github.com/klauspost/compress", "path": "github.com/klauspost/compress/LICENSE", @@ -874,6 +869,11 @@ "path": "github.com/mitchellh/mapstructure/LICENSE", "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2013 Mitchell Hashimoto\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n" }, + { + "name": "github.com/mmcloughlin/avo", + "path": "github.com/mmcloughlin/avo/LICENSE", + "licenseText": "BSD 3-Clause License\n\nCopyright (c) 2018, Michael McLoughlin\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n* Redistributions of source code must retain the above copyright notice, this\n list of conditions and the following disclaimer.\n\n* Redistributions in binary form must reproduce the above copyright notice,\n this list of conditions and the following disclaimer in the documentation\n and/or other materials provided with the distribution.\n\n* Neither the name of the copyright holder nor the names of its\n contributors may be used to endorse or promote products derived from\n this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (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": "github.com/modern-go/concurrent", "path": "github.com/modern-go/concurrent/LICENSE", @@ -937,7 +937,7 @@ { "name": "github.com/pjbgf/sha1cd", "path": "github.com/pjbgf/sha1cd/LICENSE", - "licenseText": " Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n" + "licenseText": " Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright 2023 pjbgf\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n" }, { "name": "github.com/pkg/errors", @@ -1089,11 +1089,6 @@ "path": "github.com/x448/float16/LICENSE", "licenseText": "MIT License\n\nCopyright (c) 2019 Montgomery Edwards⁴⁴⁸ and Faye Amacker\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n" }, - { - "name": "github.com/xanzy/go-gitlab", - "path": "github.com/xanzy/go-gitlab/LICENSE", - "licenseText": " Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"{}\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright {yyyy} {name of copyright owner}\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n" - }, { "name": "github.com/xanzy/ssh-agent", "path": "github.com/xanzy/ssh-agent/LICENSE", @@ -1129,6 +1124,11 @@ "path": "github.com/zeebo/blake3/LICENSE", "licenseText": "This work is released into the public domain with CC0 1.0.\n\n-------------------------------------------------------------------------------\n\nCreative Commons Legal Code\n\nCC0 1.0 Universal\n\n CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE\n LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN\n ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS\n INFORMATION ON AN \"AS-IS\" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES\n REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS\n PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM\n THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED\n HEREUNDER.\n\nStatement of Purpose\n\nThe laws of most jurisdictions throughout the world automatically confer\nexclusive Copyright and Related Rights (defined below) upon the creator\nand subsequent owner(s) (each and all, an \"owner\") of an original work of\nauthorship and/or a database (each, a \"Work\").\n\nCertain owners wish to permanently relinquish those rights to a Work for\nthe purpose of contributing to a commons of creative, cultural and\nscientific works (\"Commons\") that the public can reliably and without fear\nof later claims of infringement build upon, modify, incorporate in other\nworks, reuse and redistribute as freely as possible in any form whatsoever\nand for any purposes, including without limitation commercial purposes.\nThese owners may contribute to the Commons to promote the ideal of a free\nculture and the further production of creative, cultural and scientific\nworks, or to gain reputation or greater distribution for their Work in\npart through the use and efforts of others.\n\nFor these and/or other purposes and motivations, and without any\nexpectation of additional consideration or compensation, the person\nassociating CC0 with a Work (the \"Affirmer\"), to the extent that he or she\nis an owner of Copyright and Related Rights in the Work, voluntarily\nelects to apply CC0 to the Work and publicly distribute the Work under its\nterms, with knowledge of his or her Copyright and Related Rights in the\nWork and the meaning and intended legal effect of CC0 on those rights.\n\n1. Copyright and Related Rights. A Work made available under CC0 may be\nprotected by copyright and related or neighboring rights (\"Copyright and\nRelated Rights\"). Copyright and Related Rights include, but are not\nlimited to, the following:\n\n i. the right to reproduce, adapt, distribute, perform, display,\n communicate, and translate a Work;\n ii. moral rights retained by the original author(s) and/or performer(s);\niii. publicity and privacy rights pertaining to a person's image or\n likeness depicted in a Work;\n iv. rights protecting against unfair competition in regards to a Work,\n subject to the limitations in paragraph 4(a), below;\n v. rights protecting the extraction, dissemination, use and reuse of data\n in a Work;\n vi. database rights (such as those arising under Directive 96/9/EC of the\n European Parliament and of the Council of 11 March 1996 on the legal\n protection of databases, and under any national implementation\n thereof, including any amended or successor version of such\n directive); and\nvii. other similar, equivalent or corresponding rights throughout the\n world based on applicable law or treaty, and any national\n implementations thereof.\n\n2. Waiver. To the greatest extent permitted by, but not in contravention\nof, applicable law, Affirmer hereby overtly, fully, permanently,\nirrevocably and unconditionally waives, abandons, and surrenders all of\nAffirmer's Copyright and Related Rights and associated claims and causes\nof action, whether now known or unknown (including existing as well as\nfuture claims and causes of action), in the Work (i) in all territories\nworldwide, (ii) for the maximum duration provided by applicable law or\ntreaty (including future time extensions), (iii) in any current or future\nmedium and for any number of copies, and (iv) for any purpose whatsoever,\nincluding without limitation commercial, advertising or promotional\npurposes (the \"Waiver\"). Affirmer makes the Waiver for the benefit of each\nmember of the public at large and to the detriment of Affirmer's heirs and\nsuccessors, fully intending that such Waiver shall not be subject to\nrevocation, rescission, cancellation, termination, or any other legal or\nequitable action to disrupt the quiet enjoyment of the Work by the public\nas contemplated by Affirmer's express Statement of Purpose.\n\n3. Public License Fallback. Should any part of the Waiver for any reason\nbe judged legally invalid or ineffective under applicable law, then the\nWaiver shall be preserved to the maximum extent permitted taking into\naccount Affirmer's express Statement of Purpose. In addition, to the\nextent the Waiver is so judged Affirmer hereby grants to each affected\nperson a royalty-free, non transferable, non sublicensable, non exclusive,\nirrevocable and unconditional license to exercise Affirmer's Copyright and\nRelated Rights in the Work (i) in all territories worldwide, (ii) for the\nmaximum duration provided by applicable law or treaty (including future\ntime extensions), (iii) in any current or future medium and for any number\nof copies, and (iv) for any purpose whatsoever, including without\nlimitation commercial, advertising or promotional purposes (the\n\"License\"). The License shall be deemed effective as of the date CC0 was\napplied by Affirmer to the Work. Should any part of the License for any\nreason be judged legally invalid or ineffective under applicable law, such\npartial invalidity or ineffectiveness shall not invalidate the remainder\nof the License, and in such case Affirmer hereby affirms that he or she\nwill not (i) exercise any of his or her remaining Copyright and Related\nRights in the Work or (ii) assert any associated claims and causes of\naction with respect to the Work, in either case contrary to Affirmer's\nexpress Statement of Purpose.\n\n4. Limitations and Disclaimers.\n\n a. No trademark or patent rights held by Affirmer are waived, abandoned,\n surrendered, licensed or otherwise affected by this document.\n b. Affirmer offers the Work as-is and makes no representations or\n warranties of any kind concerning the Work, express, implied,\n statutory or otherwise, including without limitation warranties of\n title, merchantability, fitness for a particular purpose, non\n infringement, or the absence of latent or other defects, accuracy, or\n the present or absence of errors, whether or not discoverable, all to\n the greatest extent permissible under applicable law.\n c. Affirmer disclaims responsibility for clearing rights of other persons\n that may apply to the Work or any use thereof, including without\n limitation any person's Copyright and Related Rights in the Work.\n Further, Affirmer disclaims responsibility for obtaining any necessary\n consents, permissions or other rights required for any use of the\n Work.\n d. Affirmer understands and acknowledges that Creative Commons is not a\n party to this document and has no duty or obligation with respect to\n this CC0 or use of the Work.\n" }, + { + "name": "gitlab.com/gitlab-org/api/client-go", + "path": "gitlab.com/gitlab-org/api/client-go/LICENSE", + "licenseText": " Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"{}\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright {yyyy} {name of copyright owner}\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n" + }, { "name": "go.etcd.io/bbolt", "path": "go.etcd.io/bbolt/LICENSE", @@ -1194,6 +1194,11 @@ "path": "golang.org/x/time/rate/LICENSE", "licenseText": "Copyright 2009 The Go Authors.\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 LLC 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/tools", + "path": "golang.org/x/tools/LICENSE", + "licenseText": "Copyright 2009 The Go Authors.\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 LLC 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": "google.golang.org/genproto/googleapis/rpc/status", "path": "google.golang.org/genproto/googleapis/rpc/status/LICENSE", diff --git a/cmd/admin_user_create.go b/cmd/admin_user_create.go index bf8cbc7c4c0..5e03d6ca3f6 100644 --- a/cmd/admin_user_create.go +++ b/cmd/admin_user_create.go @@ -31,6 +31,11 @@ var microcmdUserCreate = &cli.Command{ Name: "username", Usage: "Username", }, + &cli.StringFlag{ + Name: "user-type", + Usage: "Set user's type: individual or bot", + Value: "individual", + }, &cli.StringFlag{ Name: "password", Usage: "User password", @@ -77,6 +82,22 @@ func runCreateUser(c *cli.Context) error { return err } + userTypes := map[string]user_model.UserType{ + "individual": user_model.UserTypeIndividual, + "bot": user_model.UserTypeBot, + } + userType, ok := userTypes[c.String("user-type")] + if !ok { + return fmt.Errorf("invalid user type: %s", c.String("user-type")) + } + if userType != user_model.UserTypeIndividual { + // Some other commands like "change-password" also only support individual users. + // It needs to clarify the "password" behavior for bot users in the future. + // At the moment, we do not allow setting password for bot users. + if c.IsSet("password") || c.IsSet("random-password") { + return errors.New("password can only be set for individual users") + } + } if c.IsSet("name") && c.IsSet("username") { return errors.New("cannot set both --name and --username flags") } @@ -118,16 +139,19 @@ func runCreateUser(c *cli.Context) error { return err } fmt.Printf("generated random password is '%s'\n", password) - } else { + } else if userType == user_model.UserTypeIndividual { return errors.New("must set either password or random-password flag") } isAdmin := c.Bool("admin") mustChangePassword := true // always default to true if c.IsSet("must-change-password") { + if userType != user_model.UserTypeIndividual { + return errors.New("must-change-password flag can only be set for individual users") + } // if the flag is set, use the value provided by the user mustChangePassword = c.Bool("must-change-password") - } else { + } else if userType == user_model.UserTypeIndividual { // check whether there are users in the database hasUserRecord, err := db.IsTableNotEmpty(&user_model.User{}) if err != nil { @@ -151,8 +175,9 @@ func runCreateUser(c *cli.Context) error { u := &user_model.User{ Name: username, Email: c.String("email"), - Passwd: password, IsAdmin: isAdmin, + Type: userType, + Passwd: password, MustChangePassword: mustChangePassword, Visibility: visibility, } diff --git a/cmd/admin_user_create_test.go b/cmd/admin_user_create_test.go index 83754e97b10..d8044e8de7e 100644 --- a/cmd/admin_user_create_test.go +++ b/cmd/admin_user_create_test.go @@ -13,32 +13,54 @@ import ( user_model "code.gitea.io/gitea/models/user" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestAdminUserCreate(t *testing.T) { app := NewMainApp(AppVersion{}) reset := func() { - assert.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.User{})) - assert.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.EmailAddress{})) + require.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.User{})) + require.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.EmailAddress{})) } - type createCheck struct{ IsAdmin, MustChangePassword bool } - createUser := func(name, args string) createCheck { - assert.NoError(t, app.Run(strings.Fields(fmt.Sprintf("./gitea admin user create --username %s --email %s@gitea.local %s --password foobar", name, name, args)))) - u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: name}) - return createCheck{u.IsAdmin, u.MustChangePassword} - } - reset() - assert.Equal(t, createCheck{IsAdmin: false, MustChangePassword: false}, createUser("u", ""), "first non-admin user doesn't need to change password") + t.Run("MustChangePassword", func(t *testing.T) { + type check struct { + IsAdmin bool + MustChangePassword bool + } + createCheck := func(name, args string) check { + require.NoError(t, app.Run(strings.Fields(fmt.Sprintf("./gitea admin user create --username %s --email %s@gitea.local %s --password foobar", name, name, args)))) + u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: name}) + return check{IsAdmin: u.IsAdmin, MustChangePassword: u.MustChangePassword} + } + reset() + assert.Equal(t, check{IsAdmin: false, MustChangePassword: false}, createCheck("u", ""), "first non-admin user doesn't need to change password") - reset() - assert.Equal(t, createCheck{IsAdmin: true, MustChangePassword: false}, createUser("u", "--admin"), "first admin user doesn't need to change password") + reset() + assert.Equal(t, check{IsAdmin: true, MustChangePassword: false}, createCheck("u", "--admin"), "first admin user doesn't need to change password") - reset() - assert.Equal(t, createCheck{IsAdmin: true, MustChangePassword: true}, createUser("u", "--admin --must-change-password")) - assert.Equal(t, createCheck{IsAdmin: true, MustChangePassword: true}, createUser("u2", "--admin")) - assert.Equal(t, createCheck{IsAdmin: true, MustChangePassword: false}, createUser("u3", "--admin --must-change-password=false")) - assert.Equal(t, createCheck{IsAdmin: false, MustChangePassword: true}, createUser("u4", "")) - assert.Equal(t, createCheck{IsAdmin: false, MustChangePassword: false}, createUser("u5", "--must-change-password=false")) + reset() + assert.Equal(t, check{IsAdmin: true, MustChangePassword: true}, createCheck("u", "--admin --must-change-password")) + assert.Equal(t, check{IsAdmin: true, MustChangePassword: true}, createCheck("u2", "--admin")) + assert.Equal(t, check{IsAdmin: true, MustChangePassword: false}, createCheck("u3", "--admin --must-change-password=false")) + assert.Equal(t, check{IsAdmin: false, MustChangePassword: true}, createCheck("u4", "")) + assert.Equal(t, check{IsAdmin: false, MustChangePassword: false}, createCheck("u5", "--must-change-password=false")) + }) + + t.Run("UserType", func(t *testing.T) { + createUser := func(name, args string) error { + return app.Run(strings.Fields(fmt.Sprintf("./gitea admin user create --username %s --email %s@gitea.local %s", name, name, args))) + } + + reset() + assert.ErrorContains(t, createUser("u", "--user-type invalid"), "invalid user type") + assert.ErrorContains(t, createUser("u", "--user-type bot --password 123"), "can only be set for individual users") + assert.ErrorContains(t, createUser("u", "--user-type bot --must-change-password"), "can only be set for individual users") + + assert.NoError(t, createUser("u", "--user-type bot")) + u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "u"}) + assert.Equal(t, user_model.UserTypeBot, u.Type) + assert.Equal(t, "", u.Passwd) + }) } diff --git a/cmd/main.go b/cmd/main.go index fd648946efa..7251bd09a3f 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -165,6 +165,7 @@ func NewMainApp(appVer AppVersion) *cli.App { app.Commands = append(app.Commands, subCmdWithConfig...) app.Commands = append(app.Commands, subCmdStandalone...) + setting.InitGiteaEnvVars() return app } diff --git a/cmd/main_test.go b/cmd/main_test.go index c182b440199..3ec584d3236 100644 --- a/cmd/main_test.go +++ b/cmd/main_test.go @@ -6,7 +6,6 @@ package cmd import ( "fmt" "io" - "os" "path/filepath" "strings" "testing" @@ -113,37 +112,17 @@ func TestCliCmd(t *testing.T) { _, _ = fmt.Fprint(ctx.App.Writer, makePathOutput(setting.AppWorkPath, setting.CustomPath, setting.CustomConf)) return nil }) - var envBackup []string - for _, s := range os.Environ() { - if strings.HasPrefix(s, "GITEA_") && strings.Contains(s, "=") { - envBackup = append(envBackup, s) - } - } - clearGiteaEnv := func() { - for _, s := range os.Environ() { - if strings.HasPrefix(s, "GITEA_") { - _ = os.Unsetenv(s) - } - } - } - defer func() { - clearGiteaEnv() - for _, s := range envBackup { - k, v, _ := strings.Cut(s, "=") - _ = os.Setenv(k, v) - } - }() - for _, c := range cases { - clearGiteaEnv() - for k, v := range c.env { - _ = os.Setenv(k, v) - } - args := strings.Split(c.cmd, " ") // for test only, "split" is good enough - r, err := runTestApp(app, args...) - assert.NoError(t, err, c.cmd) - assert.NotEmpty(t, c.exp, c.cmd) - assert.Contains(t, r.Stdout, c.exp, c.cmd) + t.Run(c.cmd, func(t *testing.T) { + for k, v := range c.env { + t.Setenv(k, v) + } + args := strings.Split(c.cmd, " ") // for test only, "split" is good enough + r, err := runTestApp(app, args...) + assert.NoError(t, err, c.cmd) + assert.NotEmpty(t, c.exp, c.cmd) + assert.Contains(t, r.Stdout, c.exp, c.cmd) + }) } } diff --git a/cmd/migrate.go b/cmd/migrate.go index 4e4dd45af3a..459805a76d7 100644 --- a/cmd/migrate.go +++ b/cmd/migrate.go @@ -18,7 +18,7 @@ import ( var CmdMigrate = &cli.Command{ Name: "migrate", Usage: "Migrate the database", - Description: "This is a command for migrating the database, so that you can run gitea admin create-user before starting the server.", + Description: `This is a command for migrating the database, so that you can run "gitea admin create user" before starting the server.`, Action: runMigrate, } diff --git a/cmd/serv.go b/cmd/serv.go index d2271b68d29..476fd9a2eae 100644 --- a/cmd/serv.go +++ b/cmd/serv.go @@ -104,7 +104,10 @@ func fail(ctx context.Context, userMessage, logMsgFmt string, args ...any) error // There appears to be a chance to cause a zombie process and failure to read the Exit status // if nothing is outputted on stdout. _, _ = fmt.Fprintln(os.Stdout, "") - _, _ = fmt.Fprintln(os.Stderr, "Gitea:", userMessage) + // add extra empty lines to separate our message from other git errors to get more attention + _, _ = fmt.Fprintln(os.Stderr, "error:") + _, _ = fmt.Fprintln(os.Stderr, "error:", userMessage) + _, _ = fmt.Fprintln(os.Stderr, "error:") if logMsgFmt != "" { logMsg := fmt.Sprintf(logMsgFmt, args...) diff --git a/cmd/web.go b/cmd/web.go index f8217758e55..dc5c6de48a3 100644 --- a/cmd/web.go +++ b/cmd/web.go @@ -18,10 +18,12 @@ import ( "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/graceful" + "code.gitea.io/gitea/modules/gtprof" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/process" "code.gitea.io/gitea/modules/public" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/routers" "code.gitea.io/gitea/routers/install" @@ -218,6 +220,8 @@ func serveInstalled(ctx *cli.Context) error { } } + gtprof.EnableBuiltinTracer(util.Iif(setting.IsProd, 2000*time.Millisecond, 100*time.Millisecond)) + // Set up Chi routes webRoutes := routers.NormalRoutes() err := listen(webRoutes, true) diff --git a/cmd/web_acme.go b/cmd/web_acme.go index 90e4a02764b..5daf0f55f24 100644 --- a/cmd/web_acme.go +++ b/cmd/web_acme.go @@ -54,8 +54,10 @@ func runACME(listenAddr string, m http.Handler) error { altTLSALPNPort = p } + // FIXME: this path is not right, it uses "AppWorkPath" incorrectly, and writes the data into "AppWorkPath/https" + // Ideally it should migrate to AppDataPath write to "AppDataPath/https" + certmagic.Default.Storage = &certmagic.FileStorage{Path: setting.AcmeLiveDirectory} magic := certmagic.NewDefault() - magic.Storage = &certmagic.FileStorage{Path: setting.AcmeLiveDirectory} // Try to use private CA root if provided, otherwise defaults to system's trust var certPool *x509.CertPool if setting.AcmeCARoot != "" { diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index d819b55d282..899209874f7 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -78,8 +78,9 @@ RUN_USER = ; git ;; Set the domain for the server ;DOMAIN = localhost ;; -;; Overwrite the automatically generated public URL. Necessary for proxies and docker. -;ROOT_URL = %(PROTOCOL)s://%(DOMAIN)s:%(HTTP_PORT)s/ +;; The AppURL used by Gitea to generate absolute links, defaults to "{PROTOCOL}://{DOMAIN}:{HTTP_PORT}/". +;; Most users should set it to the real website URL of their Gitea instance. +;ROOT_URL = ;; ;; For development purpose only. It makes Gitea handle sub-path ("/sub-path/owner/repo/...") directly when debugging without a reverse proxy. ;; DO NOT USE IT IN PRODUCTION!!! @@ -103,8 +104,8 @@ RUN_USER = ; git ;REDIRECT_OTHER_PORT = false ;PORT_TO_REDIRECT = 80 ;; -;; expect PROXY protocol header on connections to https redirector. -;REDIRECTOR_USE_PROXY_PROTOCOL = %(USE_PROXY_PROTOCOL)s +;; expect PROXY protocol header on connections to https redirector, defaults to USE_PROXY_PROTOCOL +;REDIRECTOR_USE_PROXY_PROTOCOL = ;; Minimum and maximum supported TLS versions ;SSL_MIN_VERSION=TLSv1.2 ;SSL_MAX_VERSION= @@ -128,13 +129,14 @@ RUN_USER = ; git ;; most cases you do not need to change the default value. Alter it only if ;; your SSH server node is not the same as HTTP node. For different protocol, the default ;; values are different. If `PROTOCOL` is `http+unix`, the default value is `http://unix/`. -;; If `PROTOCOL` is `fcgi` or `fcgi+unix`, the default value is `%(PROTOCOL)s://%(HTTP_ADDR)s:%(HTTP_PORT)s/`. -;; If listen on `0.0.0.0`, the default value is `%(PROTOCOL)s://localhost:%(HTTP_PORT)s/`, Otherwise the default -;; value is `%(PROTOCOL)s://%(HTTP_ADDR)s:%(HTTP_PORT)s/`. -;LOCAL_ROOT_URL = %(PROTOCOL)s://%(HTTP_ADDR)s:%(HTTP_PORT)s/ +;; If `PROTOCOL` is `fcgi` or `fcgi+unix`, the default value is `{PROTOCOL}://{HTTP_ADDR}:{HTTP_PORT}/`. +;; If listen on `0.0.0.0`, the default value is `{PROTOCOL}://localhost:{HTTP_PORT}/`. +;; Otherwise the default value is `{PROTOCOL}://{HTTP_ADDR}:{HTTP_PORT}/`. +;; Most users don't need (and shouldn't) set this value. +;LOCAL_ROOT_URL = ;; -;; When making local connections pass the PROXY protocol header. -;LOCAL_USE_PROXY_PROTOCOL = %(USE_PROXY_PROTOCOL)s +;; When making local connections pass the PROXY protocol header, defaults to USE_PROXY_PROTOCOL +;LOCAL_USE_PROXY_PROTOCOL = ;; ;; Disable SSH feature when not available ;DISABLE_SSH = false @@ -146,13 +148,17 @@ RUN_USER = ; git ;SSH_SERVER_USE_PROXY_PROTOCOL = false ;; ;; Username to use for the builtin SSH server. If blank, then it is the value of RUN_USER. -;BUILTIN_SSH_SERVER_USER = %(RUN_USER)s +;BUILTIN_SSH_SERVER_USER = ;; -;; Domain name to be exposed in clone URL -;SSH_DOMAIN = %(DOMAIN)s +;; Domain name to be exposed in clone URL, defaults to DOMAIN or the domain part of ROOT_URL +;SSH_DOMAIN = ;; -;; SSH username displayed in clone URLs. -;SSH_USER = %(BUILTIN_SSH_SERVER_USER)s +;; SSH username displayed in clone URLs. It defaults to BUILTIN_SSH_SERVER_USER or RUN_USER. +;; If it is set to "(DOER_USERNAME)", it will use current signed-in user's username. +;; This option is only for some advanced users who have configured their SSH reverse-proxy +;; and need to use different usernames for git SSH clone. +;; Most users should just leave it blank. +;SSH_USER = ;; ;; The network interface the builtin SSH server should listen on ;SSH_LISTEN_HOST = @@ -160,8 +166,8 @@ RUN_USER = ; git ;; Port number to be exposed in clone URL ;SSH_PORT = 22 ;; -;; The port number the builtin SSH server should listen on -;SSH_LISTEN_PORT = %(SSH_PORT)s +;; The port number the builtin SSH server should listen on, defaults to SSH_PORT +;SSH_LISTEN_PORT = ;; ;; Root path of SSH directory, default is '~/.ssh', but you have to use '/home/git/.ssh'. ;SSH_ROOT_PATH = @@ -188,7 +194,7 @@ RUN_USER = ; git ;; ;; For the built-in SSH server, choose the keypair to offer as the host key ;; The private key should be at SSH_SERVER_HOST_KEY and the public SSH_SERVER_HOST_KEY.pub -;; relative paths are made absolute relative to the %(APP_DATA_PATH)s +;; relative paths are made absolute relative to the APP_DATA_PATH ;SSH_SERVER_HOST_KEYS=ssh/gitea.rsa, ssh/gogs.rsa ;; ;; Directory to create temporary files in when testing public keys using ssh-keygen, @@ -582,7 +588,7 @@ ENABLED = true [log] ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; Root path for the log files - defaults to %(GITEA_WORK_DIR)/log +;; Root path for the log files - defaults to "{AppWorkPath}/log" ;ROOT_PATH = ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -682,8 +688,8 @@ LEVEL = Info ;; The path of git executable. If empty, Gitea searches through the PATH environment. ;PATH = ;; -;; The HOME directory for Git -;HOME_PATH = %(APP_DATA_PATH)s/home +;; The HOME directory for Git, defaults to "{APP_DATA_PATH}/home" +;HOME_PATH = ;; ;; Disables highlight of added and removed changes ;DISABLE_DIFF_HIGHLIGHT = false @@ -784,10 +790,13 @@ LEVEL = Info ;; Please note that setting this to false will not disable OAuth Basic or Basic authentication using a token ;ENABLE_BASIC_AUTHENTICATION = true ;; -;; Show the password sign-in form (for password-based login), otherwise, only show OAuth2 login methods. +;; Show the password sign-in form (for password-based login), otherwise, only show OAuth2 or passkey login methods if they are enabled. ;; If you set it to false, maybe it also needs to set ENABLE_BASIC_AUTHENTICATION to false to completely disable password-based authentication. ;ENABLE_PASSWORD_SIGNIN_FORM = true ;; +;; Allow users to sign-in with a passkey +;ENABLE_PASSKEY_AUTHENTICATION = true +;; ;; More detail: https://github.com/gogits/gogs/issues/165 ;ENABLE_REVERSE_PROXY_AUTHENTICATION = false ; Enable this to allow reverse proxy authentication for API requests, the reverse proxy is responsible for ensuring that no CSRF is possible. @@ -946,8 +955,8 @@ LEVEL = Info ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;[repository] ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; Root path for storing all repository data. By default, it is set to %(APP_DATA_PATH)s/gitea-repositories. -;; A relative path is interpreted as _`AppWorkPath`_/%(ROOT)s +;; Root path for storing all repository data. By default, it is set to "{APP_DATA_PATH}/gitea-repositories". +;; A relative path is interpreted as "{AppWorkPath}/{ROOT}" (use AppWorkPath as base path). ;ROOT = ;; ;; The script type this server supports. Usually this is `bash`, but some users report that only `sh` is available. @@ -1120,6 +1129,9 @@ LEVEL = Info ;; In default merge messages only include approvers who are official ;DEFAULT_MERGE_MESSAGE_OFFICIAL_APPROVERS_ONLY = true ;; +;; In default squash-merge messages include the commit message of all commits comprising the pull request. +;POPULATE_SQUASH_COMMENT_WITH_COMMIT_MESSAGES = false +;; ;; Add co-authored-by and co-committed-by trailers if committer does not match author ;ADD_CO_COMMITTER_TRAILERS = true ;; @@ -1485,6 +1497,10 @@ LEVEL = Info ;REPO_INDEXER_EXCLUDE = ;; ;MAX_FILE_SIZE = 1048576 +;; +;; Bleve engine has performance problems with fuzzy search, so we limit the fuzziness to 0 by default to disable it. +;; If you'd like to enable it, you can set it to a value between 0 and 2. +;TYPE_BLEVE_MAX_FUZZINESS = 0 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -1502,7 +1518,8 @@ LEVEL = Info ;TYPE = persistable-channel ;; ;; data-dir for storing persistable queues and level queues, individual queues will default to `queues/common` meaning the queue is shared. -;DATADIR = queues/ ; Relative paths will be made absolute against `%(APP_DATA_PATH)s`. +;; Relative paths will be made absolute against "APP_DATA_PATH" +;DATADIR = queues/ ;; ;; Default queue length before a channel queue will block ;LENGTH = 100000 diff --git a/docker/root/usr/bin/entrypoint b/docker/root/usr/bin/entrypoint index d9dbb3ebe0b..08587fc4f4d 100755 --- a/docker/root/usr/bin/entrypoint +++ b/docker/root/usr/bin/entrypoint @@ -37,5 +37,5 @@ done if [ $# -gt 0 ]; then exec "$@" else - exec /bin/s6-svscan /etc/s6 + exec /usr/bin/s6-svscan /etc/s6 fi diff --git a/flake.lock b/flake.lock index 1890b82dcfa..4319737c26e 100644 --- a/flake.lock +++ b/flake.lock @@ -5,11 +5,11 @@ "systems": "systems" }, "locked": { - "lastModified": 1726560853, - "narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=", + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", "owner": "numtide", "repo": "flake-utils", - "rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", "type": "github" }, "original": { @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1731139594, - "narHash": "sha256-IigrKK3vYRpUu+HEjPL/phrfh7Ox881er1UEsZvw9Q4=", + "lastModified": 1736798957, + "narHash": "sha256-qwpCtZhSsSNQtK4xYGzMiyEDhkNzOCz/Vfu4oL2ETsQ=", "owner": "nixos", "repo": "nixpkgs", - "rev": "76612b17c0ce71689921ca12d9ffdc9c23ce40b2", + "rev": "9abb87b552b7f55ac8916b6fc9e5cb486656a2f3", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index e3655b627eb..f54eba1c3bc 100644 --- a/flake.nix +++ b/flake.nix @@ -29,9 +29,14 @@ poetry # backend + go_1_23 gofumpt sqlite ]; + shellHook = '' + export GO="${pkgs.go_1_23}/bin/go" + export GOROOT="${pkgs.go_1_23}/share/go" + ''; }; } ); diff --git a/go.mod b/go.mod index dc80d2ca2bb..e2ccdd5ae65 100644 --- a/go.mod +++ b/go.mod @@ -24,10 +24,10 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/azcore v1.16.0 github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.4.1 github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 - github.com/ProtonMail/go-crypto v1.0.0 + github.com/ProtonMail/go-crypto v1.1.4 github.com/PuerkitoBio/goquery v1.10.0 github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.7.3 - github.com/alecthomas/chroma/v2 v2.14.0 + github.com/alecthomas/chroma/v2 v2.15.0 github.com/aws/aws-sdk-go-v2/credentials v1.17.42 github.com/aws/aws-sdk-go-v2/service/codecommit v1.27.3 github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb @@ -54,8 +54,8 @@ require ( github.com/go-chi/cors v1.2.1 github.com/go-co-op/gocron v1.37.0 github.com/go-enry/go-enry/v2 v2.9.1 - github.com/go-git/go-billy/v5 v5.6.0 - github.com/go-git/go-git/v5 v5.12.0 + github.com/go-git/go-billy/v5 v5.6.1 + github.com/go-git/go-git/v5 v5.13.1 github.com/go-ldap/ldap/v3 v3.4.8 github.com/go-redsync/redsync/v4 v4.13.0 github.com/go-sql-driver/mysql v1.8.1 @@ -71,7 +71,6 @@ require ( github.com/google/uuid v1.6.0 github.com/gorilla/feeds v1.2.0 github.com/gorilla/sessions v1.4.0 - github.com/h2non/gock v1.2.0 github.com/hashicorp/go-version v1.7.0 github.com/hashicorp/golang-lru/v2 v2.0.7 github.com/huandu/xstrings v1.5.0 @@ -79,7 +78,6 @@ require ( github.com/jhillyerd/enmime v1.3.0 github.com/json-iterator/go v1.1.12 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 - github.com/keybase/go-crypto v0.0.0-20200123153347-de78d2cb44f4 github.com/klauspost/compress v1.17.11 github.com/klauspost/cpuid/v2 v2.2.8 github.com/lib/pq v1.10.9 @@ -107,28 +105,28 @@ require ( github.com/sassoftware/go-rpmutils v0.4.0 github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 github.com/shurcooL/vfsgen v0.0.0-20230704071429-0000e147ea92 - github.com/stretchr/testify v1.9.0 + github.com/stretchr/testify v1.10.0 github.com/syndtr/goleveldb v1.0.0 github.com/tstranex/u2f v1.0.0 github.com/ulikunitz/xz v0.5.12 github.com/urfave/cli/v2 v2.27.5 github.com/wneessen/go-mail v0.5.2 - github.com/xanzy/go-gitlab v0.112.0 github.com/xeipuuv/gojsonschema v1.2.0 github.com/yohcop/openid-go v1.0.1 github.com/yuin/goldmark v1.7.8 github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc github.com/yuin/goldmark-meta v1.1.0 - golang.org/x/crypto v0.31.0 + gitlab.com/gitlab-org/api/client-go v0.119.0 + golang.org/x/crypto v0.32.0 golang.org/x/image v0.21.0 - golang.org/x/net v0.33.0 - golang.org/x/oauth2 v0.23.0 + golang.org/x/net v0.34.0 + golang.org/x/oauth2 v0.24.0 golang.org/x/sync v0.10.0 - golang.org/x/sys v0.28.0 + golang.org/x/sys v0.29.0 golang.org/x/text v0.21.0 - golang.org/x/tools v0.26.0 + golang.org/x/tools v0.29.0 google.golang.org/grpc v1.67.1 - google.golang.org/protobuf v1.35.1 + google.golang.org/protobuf v1.36.0 gopkg.in/ini.v1 v1.67.0 gopkg.in/yaml.v3 v3.0.1 mvdan.cc/xurls/v2 v2.5.0 @@ -187,7 +185,7 @@ require ( github.com/couchbase/gomemcached v0.3.2 // indirect github.com/couchbase/goutils v0.1.2 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect - github.com/cyphar/filepath-securejoin v0.3.4 // indirect + github.com/cyphar/filepath-securejoin v0.3.6 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/davidmz/go-pageant v1.0.2 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect @@ -220,7 +218,7 @@ require ( github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect github.com/golang-sql/sqlexp v0.1.0 // indirect github.com/golang/geo v0.0.0-20230421003525-6adc56603217 // indirect - github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/google/btree v1.1.3 // indirect @@ -230,7 +228,6 @@ require ( github.com/gorilla/handlers v1.5.2 // indirect github.com/gorilla/mux v1.8.1 // indirect github.com/gorilla/securecookie v1.1.2 // indirect - github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect @@ -255,6 +252,7 @@ require ( github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/mmcloughlin/avo v0.6.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/mrjones/oauth v0.0.0-20190623134757-126b35219450 // indirect @@ -266,7 +264,7 @@ require ( github.com/onsi/ginkgo v1.16.5 // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/pierrec/lz4/v4 v4.1.21 // indirect - github.com/pjbgf/sha1cd v0.3.0 // indirect + github.com/pjbgf/sha1cd v0.3.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.60.1 // indirect @@ -306,8 +304,8 @@ require ( go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect - golang.org/x/mod v0.21.0 // indirect - golang.org/x/time v0.7.0 // indirect + golang.org/x/mod v0.22.0 // indirect + golang.org/x/time v0.8.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index b5e64321b53..bc0265c51fb 100644 --- a/go.sum +++ b/go.sum @@ -71,8 +71,8 @@ github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSC github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= -github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78= -github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= +github.com/ProtonMail/go-crypto v1.1.4 h1:G5U5asvD5N/6/36oIw3k2bOfBn5XVcZrb7PBjzzKKoE= +github.com/ProtonMail/go-crypto v1.1.4/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= github.com/PuerkitoBio/goquery v1.10.0 h1:6fiXdLuUvYs2OJSvNRqlNPoBm6YABE226xrbavY5Wv4= github.com/PuerkitoBio/goquery v1.10.0/go.mod h1:TjZZl68Q3eGHNBA8CWaxAN7rOU1EbDz3CWuolcO5Yu4= github.com/RoaringBitmap/roaring v0.4.23/go.mod h1:D0gp8kJQgE1A4LQ5wFLggQEyvDi06Mq5mKs52e1TwOo= @@ -81,11 +81,11 @@ github.com/RoaringBitmap/roaring v1.9.4 h1:yhEIoH4YezLYT04s1nHehNO64EKFTop/wBhxv github.com/RoaringBitmap/roaring v1.9.4/go.mod h1:6AXUsoIEzDTFFQCe1RbGA6uFONMhvejWj5rqITANK90= github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.7.3 h1:BP0HiyNT3AQEYi+if3wkRcIdQFHtsw6xX3Kx0glckgA= github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.7.3/go.mod h1:hMNtySovKkn2gdDuLqnqveP+mfhUSaBdoBcr2I7Zt0E= -github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE= -github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= +github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= +github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs= -github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E= -github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I= +github.com/alecthomas/chroma/v2 v2.15.0 h1:LxXTQHFoYrstG2nnV9y2X5O94sOBzf0CIUpSTbpxvMc= +github.com/alecthomas/chroma/v2 v2.15.0/go.mod h1:gUhVLrPDXPtp/f+L1jo9xepo9gL4eLwRuGAunSZMkio= github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8= github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= @@ -188,7 +188,6 @@ github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= github.com/buildkite/terminal-to-html/v3 v3.16.3 h1:IGuJjboHjuMLWOGsKZKNxbbn41emOLiHzXPmQZk31fk= github.com/buildkite/terminal-to-html/v3 v3.16.3/go.mod h1:r/J7cC9c3EzBzP3/wDz0RJLPwv5PUAMp+KF2w+ntMc0= -github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/caddyserver/certmagic v0.21.4 h1:e7VobB8rffHv8ZZpSiZtEwnLDHUwLVYLWzWSa1FfKI0= github.com/caddyserver/certmagic v0.21.4/go.mod h1:swUXjQ1T9ZtMv95qj7/InJvWLXURU85r+CfG0T+ZbDE= github.com/caddyserver/zerossl v0.1.3 h1:onS+pxp3M8HnHpN5MMbOMyNjmTheJyWRaZYwn+YTAyA= @@ -205,7 +204,6 @@ github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moA github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= -github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= github.com/cloudflare/circl v1.5.0 h1:hxIWksrX6XN5a1L2TI/h53AGPhNHoUBo+TD1ms9+pys= github.com/cloudflare/circl v1.5.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= @@ -223,8 +221,8 @@ github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwc github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/cyphar/filepath-securejoin v0.3.4 h1:VBWugsJh2ZxJmLFSM06/0qzQyiQX2Qs0ViKrUAcqdZ8= -github.com/cyphar/filepath-securejoin v0.3.4/go.mod h1:8s/MCNJREmFK0H02MF6Ihv1nakJe4L/w3WZLHNkvlYM= +github.com/cyphar/filepath-securejoin v0.3.6 h1:4d9N5ykBnSp5Xn2JkhocYDkOpURL/18CYMpo6xB9uWM= +github.com/cyphar/filepath-securejoin v0.3.6/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -254,8 +252,8 @@ github.com/dvyukov/go-fuzz v0.0.0-20210429054444-fca39067bc72/go.mod h1:11Gm+ccJ github.com/editorconfig/editorconfig-core-go/v2 v2.6.2 h1:dKG8sc7n321deIVRcQtwlMNoBEra7j0qQ8RwxO8RN0w= github.com/editorconfig/editorconfig-core-go/v2 v2.6.2/go.mod h1:7dvD3GCm7eBw53xZ/lsiq72LqobdMg3ITbMBxnmJmqY= github.com/elazarl/go-bindata-assetfs v1.0.1/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4= -github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU= -github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= +github.com/elazarl/goproxy v1.2.3 h1:xwIyKHbaP5yfT6O9KIeYJR5549MXRQkoQMRXGztz8YQ= +github.com/elazarl/goproxy v1.2.3/go.mod h1:YfEbZtqP4AetfO6d40vWchF3znWX7C7Vd6ZMfdL8z64= github.com/emersion/go-imap v1.2.1 h1:+s9ZjMEjOB8NzZMVTM3cCenz2JrQIGGo5j1df19WjTA= github.com/emersion/go-imap v1.2.1/go.mod h1:Qlx1FSx2FTxjnjWpIlVNEuX+ylerZQNFE5NsmKFSejY= github.com/emersion/go-message v0.15.0/go.mod h1:wQUEfE+38+7EW8p8aZ96ptg6bAb1iwdgej19uXASlE4= @@ -313,12 +311,12 @@ github.com/go-fed/httpsig v1.1.1-0.20201223112313-55836744818e h1:oRq/fiirun5Hql github.com/go-fed/httpsig v1.1.1-0.20201223112313-55836744818e/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= -github.com/go-git/go-billy/v5 v5.6.0 h1:w2hPNtoehvJIxR00Vb4xX94qHQi/ApZfX+nBE2Cjio8= -github.com/go-git/go-billy/v5 v5.6.0/go.mod h1:sFDq7xD3fn3E0GOwUSZqHo9lrkmx8xJhA0ZrfvjBRGM= +github.com/go-git/go-billy/v5 v5.6.1 h1:u+dcrgaguSSkbjzHwelEjc0Yj300NUevrrPphk/SoRA= +github.com/go-git/go-billy/v5 v5.6.1/go.mod h1:0AsLr1z2+Uksi4NlElmMblP5rPcDZNRCD8ujZCRR2BE= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= -github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys= -github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY= +github.com/go-git/go-git/v5 v5.13.1 h1:DAQ9APonnlvSWpvolXWIuV6Q6zXy2wHbN4cVlNR5Q+M= +github.com/go-git/go-git/v5 v5.13.1/go.mod h1:qryJB4cSBoq3FRoBRf5A77joojuBcmPJ0qu3XXXVixc= github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/go-ldap/ldap/v3 v3.4.8 h1:loKJyspcRezt2Q3ZRMq2p/0v8iOurlmeXDPw6fikSvQ= @@ -385,8 +383,8 @@ github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= github.com/golang/geo v0.0.0-20230421003525-6adc56603217 h1:HKlyj6in2JV6wVkmQ4XmG/EIm+SCYlPZ+V4GWit7Z+I= github.com/golang/geo v0.0.0-20230421003525-6adc56603217/go.mod h1:8wI0hitZ3a1IxZfeH3/5I97CI8i5cLGsYe7xNhQGs9U= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= @@ -452,10 +450,6 @@ github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/z github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ= github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik= -github.com/h2non/gock v1.2.0 h1:K6ol8rfrRkUOefooBC8elXoaNGYkpp7y2qcxGG6BzUE= -github.com/h2non/gock v1.2.0/go.mod h1:tNhoxHYW2W42cYkYb1WqzdbYIieALC99kpYr7rH/BQk= -github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= -github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -512,8 +506,6 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNU github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= -github.com/keybase/go-crypto v0.0.0-20200123153347-de78d2cb44f4 h1:cTxwSmnaqLoo+4tLukHoB9iqHOu3LmLhRmgUxZo6Vp4= -github.com/keybase/go-crypto v0.0.0-20200123153347-de78d2cb44f4/go.mod h1:ghbZscTyKdM07+Fw3KSi0hcJm+AlEUWj8QLlPtijN/M= github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= @@ -585,6 +577,8 @@ github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyua github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/mmcloughlin/avo v0.6.0 h1:QH6FU8SKoTLaVs80GA8TJuLNkUYl4VokHKlPhVDg4YY= +github.com/mmcloughlin/avo v0.6.0/go.mod h1:8CoAGaCSYXtCPR+8y18Y9aB/kxb8JSS6FRI7mSkvD+8= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -599,8 +593,6 @@ github.com/msteinert/pam v1.2.0 h1:mYfjlvN2KYs2Pb9G6nb/1f/nPfAttT/Jee5Sq9r3bGE= github.com/msteinert/pam v1.2.0/go.mod h1:d2n0DCUK8rGecChV3JzvmsDjOY4R7AYbsNxAT+ftQl0= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4= -github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms= github.com/niklasfasching/go-org v1.7.0 h1:vyMdcMWWTe/XmANk19F4k8XGBYg0GQ/gJGMimOjGMek= github.com/niklasfasching/go-org v1.7.0/go.mod h1:WuVm4d45oePiE0eX25GqTDQIt/qPW1T9DGkRscqLW5o= github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0= @@ -637,8 +629,8 @@ github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG github.com/pierrec/lz4/v4 v4.1.2/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= -github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= -github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= +github.com/pjbgf/sha1cd v0.3.1 h1:Dh2GYdpJnO84lIw0LJwTFXjcNbasP/bklicSznyAaPI= +github.com/pjbgf/sha1cd v0.3.1/go.mod h1:Y8t7jSB/dEI/lQE04A1HVKteqjj9bX5O4+Cex0TCu8s= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= @@ -743,8 +735,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stvp/tempredis v0.0.0-20181119212430-b82af8480203 h1:QVqDTf3h2WHt08YuiTGPZLls0Wq99X9bWd0Q5ZSBesM= github.com/stvp/tempredis v0.0.0-20181119212430-b82af8480203/go.mod h1:oqN97ltKNihBbwlX8dLpwxCl3+HnXKV/R0e+sRLd9C8= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= @@ -772,8 +764,6 @@ github.com/wneessen/go-mail v0.5.2 h1:MZKwgHJoRboLJ+EHMLuHpZc95wo+u1xViL/4XSswDT github.com/wneessen/go-mail v0.5.2/go.mod h1:kRroJvEq2hOSEPFRiKjN7Csrz0G1w+RpiGR3b6yo+Ck= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= -github.com/xanzy/go-gitlab v0.112.0 h1:6Z0cqEooCvBMfBIHw+CgO4AKGRV8na/9781xOb0+DKw= -github.com/xanzy/go-gitlab v0.112.0/go.mod h1:wKNKh3GkYDMOsGmnfuX+ITCmDuSDWFO0G+C4AygL9RY= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= @@ -808,6 +798,8 @@ github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI= github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE= github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo= github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4= +gitlab.com/gitlab-org/api/client-go v0.119.0 h1:YBZyx9XUTtEDBBYtY36cZWz6JmT7om/8HPSk37IS95g= +gitlab.com/gitlab-org/api/client-go v0.119.0/go.mod h1:ygHmS3AU3TpvK+AC6DYO1QuAxLlv6yxYK+/Votr/WFQ= go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0= go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I= @@ -829,16 +821,14 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= -golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= -golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= -golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= +golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY= golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8= golang.org/x/image v0.21.0 h1:c5qV36ajHpdj4Qi0GnE0jUc/yuo33OLFaa0d+crTD5s= @@ -852,8 +842,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= -golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= +golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -865,20 +855,18 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -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.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= -golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= -golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= -golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= -golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= +golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= +golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= +golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -916,8 +904,6 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/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/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -927,14 +913,12 @@ golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/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/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= @@ -942,15 +926,13 @@ golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= -golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= -golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= +golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg= +golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/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.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= @@ -958,8 +940,8 @@ golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= -golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= +golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= @@ -970,8 +952,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= -golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= +golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE= +golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -986,8 +968,8 @@ google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQ google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= -google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.0 h1:mjIs9gYtt56AzC4ZaffQuh88TZurBGhIJMBZGSxNerQ= +google.golang.org/protobuf v1.36.0/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/main_timezones.go b/main_timezones.go new file mode 100644 index 00000000000..e1233007c62 --- /dev/null +++ b/main_timezones.go @@ -0,0 +1,16 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +//go:build windows + +package main + +// Golang has the ability to load OS's timezone data from most UNIX systems (https://github.com/golang/go/blob/master/src/time/zoneinfo_unix.go) +// Even if the timezone data is missing, users could install the related packages to get it. +// But on Windows, although `zoneinfo_windows.go` tries to load the timezone data from Windows registry, +// some users still suffer from the issue that the timezone data is missing: https://github.com/go-gitea/gitea/issues/33235 +// So we import the tzdata package to make sure the timezone data is included in the binary. +// +// For non-Windows package builders, they could still use the "TAGS=timetzdata" to include the tzdata package in the binary. +// If we decided to add the tzdata for other platforms, modify the "go:build" directive above. +import _ "time/tzdata" diff --git a/models/actions/run.go b/models/actions/run.go index a224a910ab5..60fbbcd3233 100644 --- a/models/actions/run.go +++ b/models/actions/run.go @@ -88,7 +88,7 @@ func (run *ActionRun) RefLink() string { if refName.IsPull() { return run.Repo.Link() + "/pulls/" + refName.ShortName() } - return git.RefURL(run.Repo.Link(), run.Ref) + return run.Repo.Link() + "/src/" + refName.RefWebLinkPath() } // PrettyRef return #id for pull ref or ShortName for others @@ -154,7 +154,7 @@ func (run *ActionRun) GetPushEventPayload() (*api.PushPayload, error) { } func (run *ActionRun) GetPullRequestEventPayload() (*api.PullRequestPayload, error) { - if run.Event == webhook_module.HookEventPullRequest || run.Event == webhook_module.HookEventPullRequestSync { + if run.Event.IsPullRequest() { var payload api.PullRequestPayload if err := json.Unmarshal([]byte(run.EventPayload), &payload); err != nil { return nil, err diff --git a/models/activities/action.go b/models/activities/action.go index 8304210188b..adc442b88bc 100644 --- a/models/activities/action.go +++ b/models/activities/action.go @@ -72,9 +72,9 @@ func (at ActionType) String() string { case ActionRenameRepo: return "rename_repo" case ActionStarRepo: - return "star_repo" + return "star_repo" // will not displayed in feeds.tmpl case ActionWatchRepo: - return "watch_repo" + return "watch_repo" // will not displayed in feeds.tmpl case ActionCommitRepo: return "commit_repo" case ActionCreateIssue: @@ -355,7 +355,7 @@ func (a *Action) GetBranch() string { // GetRefLink returns the action's ref link. func (a *Action) GetRefLink(ctx context.Context) string { - return git.RefURL(a.GetRepoLink(ctx), a.RefName) + return a.GetRepoLink(ctx) + "/src/" + git.RefName(a.RefName).RefWebLinkPath() } // GetTag returns the action's repository tag. diff --git a/models/asymkey/gpg_key.go b/models/asymkey/gpg_key.go index 5236b2d4500..e921340730b 100644 --- a/models/asymkey/gpg_key.go +++ b/models/asymkey/gpg_key.go @@ -13,8 +13,8 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/timeutil" - "github.com/keybase/go-crypto/openpgp" - "github.com/keybase/go-crypto/openpgp/packet" + "github.com/ProtonMail/go-crypto/openpgp" + "github.com/ProtonMail/go-crypto/openpgp/packet" "xorm.io/builder" ) @@ -141,7 +141,11 @@ func parseGPGKey(ctx context.Context, ownerID int64, e *openpgp.Entity, verified // Parse Subkeys subkeys := make([]*GPGKey, len(e.Subkeys)) for i, k := range e.Subkeys { - subs, err := parseSubGPGKey(ownerID, pubkey.KeyIdString(), k.PublicKey, expiry) + subkeyExpiry := expiry + if k.Sig.KeyLifetimeSecs != nil { + subkeyExpiry = k.PublicKey.CreationTime.Add(time.Duration(*k.Sig.KeyLifetimeSecs) * time.Second) + } + subs, err := parseSubGPGKey(ownerID, pubkey.KeyIdString(), k.PublicKey, subkeyExpiry) if err != nil { return nil, ErrGPGKeyParsing{ParseError: err} } @@ -156,7 +160,7 @@ func parseGPGKey(ctx context.Context, ownerID int64, e *openpgp.Entity, verified emails := make([]*user_model.EmailAddress, 0, len(e.Identities)) for _, ident := range e.Identities { - if ident.Revocation != nil { + if ident.Revoked(time.Now()) { continue } email := strings.ToLower(strings.TrimSpace(ident.UserId.Email)) diff --git a/models/asymkey/gpg_key_add.go b/models/asymkey/gpg_key_add.go index 11124b13665..6c0f6e01a7c 100644 --- a/models/asymkey/gpg_key_add.go +++ b/models/asymkey/gpg_key_add.go @@ -10,7 +10,7 @@ import ( "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/modules/log" - "github.com/keybase/go-crypto/openpgp" + "github.com/ProtonMail/go-crypto/openpgp" ) // __________________ ________ ____ __. @@ -83,12 +83,12 @@ func AddGPGKey(ctx context.Context, ownerID int64, content, token, signature str verified := false // Handle provided signature if signature != "" { - signer, err := openpgp.CheckArmoredDetachedSignature(ekeys, strings.NewReader(token), strings.NewReader(signature)) + signer, err := openpgp.CheckArmoredDetachedSignature(ekeys, strings.NewReader(token), strings.NewReader(signature), nil) if err != nil { - signer, err = openpgp.CheckArmoredDetachedSignature(ekeys, strings.NewReader(token+"\n"), strings.NewReader(signature)) + signer, err = openpgp.CheckArmoredDetachedSignature(ekeys, strings.NewReader(token+"\n"), strings.NewReader(signature), nil) } if err != nil { - signer, err = openpgp.CheckArmoredDetachedSignature(ekeys, strings.NewReader(token+"\r\n"), strings.NewReader(signature)) + signer, err = openpgp.CheckArmoredDetachedSignature(ekeys, strings.NewReader(token+"\r\n"), strings.NewReader(signature), nil) } if err != nil { log.Error("Unable to validate token signature. Error: %v", err) diff --git a/models/asymkey/gpg_key_commit_verification.go b/models/asymkey/gpg_key_commit_verification.go index 26fad3bb3f2..9219a509df0 100644 --- a/models/asymkey/gpg_key_commit_verification.go +++ b/models/asymkey/gpg_key_commit_verification.go @@ -16,7 +16,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" - "github.com/keybase/go-crypto/openpgp/packet" + "github.com/ProtonMail/go-crypto/openpgp/packet" ) // __________________ ________ ____ __. diff --git a/models/asymkey/gpg_key_common.go b/models/asymkey/gpg_key_common.go index 28cb8f4e766..92c34a2569e 100644 --- a/models/asymkey/gpg_key_common.go +++ b/models/asymkey/gpg_key_common.go @@ -13,9 +13,9 @@ import ( "strings" "time" - "github.com/keybase/go-crypto/openpgp" - "github.com/keybase/go-crypto/openpgp/armor" - "github.com/keybase/go-crypto/openpgp/packet" + "github.com/ProtonMail/go-crypto/openpgp" + "github.com/ProtonMail/go-crypto/openpgp/armor" + "github.com/ProtonMail/go-crypto/openpgp/packet" ) // __________________ ________ ____ __. @@ -80,7 +80,7 @@ func base64DecPubKey(content string) (*packet.PublicKey, error) { return pkey, nil } -// getExpiryTime extract the expire time of primary key based on sig +// getExpiryTime extract the expiry time of primary key based on sig func getExpiryTime(e *openpgp.Entity) time.Time { expiry := time.Time{} // Extract self-sign for expire date based on : https://github.com/golang/crypto/blob/master/openpgp/keys.go#L165 @@ -88,12 +88,12 @@ func getExpiryTime(e *openpgp.Entity) time.Time { for _, ident := range e.Identities { if selfSig == nil { selfSig = ident.SelfSignature - } else if ident.SelfSignature.IsPrimaryId != nil && *ident.SelfSignature.IsPrimaryId { + } else if ident.SelfSignature != nil && ident.SelfSignature.IsPrimaryId != nil && *ident.SelfSignature.IsPrimaryId { selfSig = ident.SelfSignature break } } - if selfSig.KeyLifetimeSecs != nil { + if selfSig != nil && selfSig.KeyLifetimeSecs != nil { expiry = e.PrimaryKey.CreationTime.Add(time.Duration(*selfSig.KeyLifetimeSecs) * time.Second) } return expiry diff --git a/models/asymkey/gpg_key_test.go b/models/asymkey/gpg_key_test.go index d3fbb01d82b..d7f0ff53649 100644 --- a/models/asymkey/gpg_key_test.go +++ b/models/asymkey/gpg_key_test.go @@ -13,8 +13,10 @@ import ( "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" - "github.com/keybase/go-crypto/openpgp/packet" + "github.com/ProtonMail/go-crypto/openpgp" + "github.com/ProtonMail/go-crypto/openpgp/packet" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestCheckArmoredGPGKeyString(t *testing.T) { @@ -107,9 +109,8 @@ MkM/fdpyc2hY7Dl/+qFmN5MG5yGmMpQcX+RNNR222ibNC1D3wg== =i9b7 -----END PGP PUBLIC KEY BLOCK-----` keys, err := checkArmoredGPGKeyString(testGPGArmor) - if !assert.NotEmpty(t, keys) { - return - } + require.NotEmpty(t, keys) + ekey := keys[0] assert.NoError(t, err, "Could not parse a valid GPG armored key", ekey) @@ -403,3 +404,25 @@ func TestTryGetKeyIDFromSignature(t *testing.T) { IssuerFingerprint: []uint8{0xb, 0x23, 0x24, 0xc7, 0xe6, 0xfe, 0x4f, 0x3a, 0x6, 0x26, 0xc1, 0x21, 0x3, 0x8d, 0x1a, 0x3e, 0xad, 0xdb, 0xea, 0x9c}, })) } + +func TestParseGPGKey(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + assert.NoError(t, db.Insert(db.DefaultContext, &user_model.EmailAddress{UID: 1, Email: "email1@example.com", IsActivated: true})) + + // create a key for test email + e, err := openpgp.NewEntity("name", "comment", "email1@example.com", nil) + require.NoError(t, err) + k, err := parseGPGKey(db.DefaultContext, 1, e, true) + require.NoError(t, err) + assert.NotEmpty(t, k.KeyID) + assert.NotEmpty(t, k.Emails) // the key is valid, matches the email + + // then revoke the key + for _, id := range e.Identities { + id.Revocations = append(id.Revocations, &packet.Signature{RevocationReason: util.ToPointer(packet.KeyCompromised)}) + } + k, err = parseGPGKey(db.DefaultContext, 1, e, true) + require.NoError(t, err) + assert.NotEmpty(t, k.KeyID) + assert.Empty(t, k.Emails) // the key is revoked, matches no email +} diff --git a/models/db/engine_hook.go b/models/db/engine_hook.go index b4c543c3dd8..2c9fc09c99d 100644 --- a/models/db/engine_hook.go +++ b/models/db/engine_hook.go @@ -7,23 +7,36 @@ import ( "context" "time" + "code.gitea.io/gitea/modules/gtprof" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" "xorm.io/xorm/contexts" ) -type SlowQueryHook struct { +type EngineHook struct { Threshold time.Duration Logger log.Logger } -var _ contexts.Hook = (*SlowQueryHook)(nil) +var _ contexts.Hook = (*EngineHook)(nil) -func (*SlowQueryHook) BeforeProcess(c *contexts.ContextHook) (context.Context, error) { - return c.Ctx, nil +func (*EngineHook) BeforeProcess(c *contexts.ContextHook) (context.Context, error) { + ctx, _ := gtprof.GetTracer().Start(c.Ctx, gtprof.TraceSpanDatabase) + return ctx, nil } -func (h *SlowQueryHook) AfterProcess(c *contexts.ContextHook) error { +func (h *EngineHook) AfterProcess(c *contexts.ContextHook) error { + span := gtprof.GetContextSpan(c.Ctx) + if span != nil { + // Do not record SQL parameters here: + // * It shouldn't expose the parameters because they contain sensitive information, end users need to report the trace details safely. + // * Some parameters contain quite long texts, waste memory and are difficult to display. + span.SetAttributeString(gtprof.TraceAttrDbSQL, c.SQL) + span.End() + } else { + setting.PanicInDevOrTesting("span in database engine hook is nil") + } if c.ExecuteTime >= h.Threshold { // 8 is the amount of skips passed to runtime.Caller, so that in the log the correct function // is being displayed (the function that ultimately wants to execute the query in the code) diff --git a/models/db/engine_init.go b/models/db/engine_init.go index da85018957b..edca6979342 100644 --- a/models/db/engine_init.go +++ b/models/db/engine_init.go @@ -72,7 +72,7 @@ func InitEngine(ctx context.Context) error { xe.SetDefaultContext(ctx) if setting.Database.SlowQueryThreshold > 0 { - xe.AddHook(&SlowQueryHook{ + xe.AddHook(&EngineHook{ Threshold: setting.Database.SlowQueryThreshold, Logger: log.GetLogger("xorm"), }) diff --git a/models/db/engine_test.go b/models/db/engine_test.go index e3dbfbdc242..10a1a33ff04 100644 --- a/models/db/engine_test.go +++ b/models/db/engine_test.go @@ -15,6 +15,7 @@ import ( _ "code.gitea.io/gitea/cmd" // for TestPrimaryKeys "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestDumpDatabase(t *testing.T) { @@ -62,9 +63,7 @@ func TestPrimaryKeys(t *testing.T) { // Import "code.gitea.io/gitea/cmd" to make sure each db.RegisterModel in init functions has been called. beans, err := db.NamesToBean() - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) whitelist := map[string]string{ "the_table_name_to_skip_checking": "Write a note here to explain why", @@ -79,8 +78,6 @@ func TestPrimaryKeys(t *testing.T) { t.Logf("ignore %q because %q", table.Name, why) continue } - if len(table.PrimaryKeys) == 0 { - t.Errorf("table %q has no primary key", table.Name) - } + assert.NotEmpty(t, table.PrimaryKeys, "table %q has no primary key", table.Name) } } diff --git a/models/db/name.go b/models/db/name.go index 55c9dffb6ab..e2165fd76bb 100644 --- a/models/db/name.go +++ b/models/db/name.go @@ -5,21 +5,12 @@ package db import ( "fmt" - "regexp" "strings" "unicode/utf8" "code.gitea.io/gitea/modules/util" ) -var ( - // ErrNameEmpty name is empty error - ErrNameEmpty = util.SilentWrap{Message: "name is empty", Err: util.ErrInvalidArgument} - - // AlphaDashDotPattern characters prohibited in a username (anything except A-Za-z0-9_.-) - AlphaDashDotPattern = regexp.MustCompile(`[^\w-\.]`) -) - // ErrNameReserved represents a "reserved name" error. type ErrNameReserved struct { Name string @@ -82,20 +73,20 @@ func (err ErrNameCharsNotAllowed) Unwrap() error { // IsUsableName checks if name is reserved or pattern of name is not allowed // based on given reserved names and patterns. -// Names are exact match, patterns can be prefix or suffix match with placeholder '*'. -func IsUsableName(names, patterns []string, name string) error { +// Names are exact match, patterns can be a prefix or suffix match with placeholder '*'. +func IsUsableName(reservedNames, reservedPatterns []string, name string) error { name = strings.TrimSpace(strings.ToLower(name)) if utf8.RuneCountInString(name) == 0 { - return ErrNameEmpty + return util.SilentWrap{Message: "name is empty", Err: util.ErrInvalidArgument} } - for i := range names { - if name == names[i] { + for i := range reservedNames { + if name == reservedNames[i] { return ErrNameReserved{name} } } - for _, pat := range patterns { + for _, pat := range reservedPatterns { if pat[0] == '*' && strings.HasSuffix(name, pat[1:]) || (pat[len(pat)-1] == '*' && strings.HasPrefix(name, pat[:len(pat)-1])) { return ErrNamePatternNotAllowed{pat} diff --git a/models/fixtures/access.yml b/models/fixtures/access.yml index 4171e31fef7..596046e9502 100644 --- a/models/fixtures/access.yml +++ b/models/fixtures/access.yml @@ -171,3 +171,9 @@ user_id: 40 repo_id: 61 mode: 4 + +- + id: 30 + user_id: 40 + repo_id: 1 + mode: 2 diff --git a/models/fixtures/repository.yml b/models/fixtures/repository.yml index bbb028eb7bb..552a78cbd27 100644 --- a/models/fixtures/repository.yml +++ b/models/fixtures/repository.yml @@ -1694,19 +1694,6 @@ is_fsck_enabled: true close_issues_via_commit_in_any_branch: false -- - id: 59 - owner_id: 2 - owner_name: user2 - lower_name: test_commit_revert - name: test_commit_revert - default_branch: main - is_empty: false - is_archived: false - is_private: true - status: 0 - num_issues: 0 - - id: 60 owner_id: 40 diff --git a/models/fixtures/user.yml b/models/fixtures/user.yml index b3bece5589c..976a236011c 100644 --- a/models/fixtures/user.yml +++ b/models/fixtures/user.yml @@ -67,7 +67,7 @@ num_followers: 2 num_following: 1 num_stars: 2 - num_repos: 15 + num_repos: 14 num_teams: 0 num_members: 0 visibility: 0 diff --git a/models/fixtures/webhook.yml b/models/fixtures/webhook.yml index f62bae1f311..ebc4062b60b 100644 --- a/models/fixtures/webhook.yml +++ b/models/fixtures/webhook.yml @@ -22,6 +22,7 @@ content_type: 1 # json events: '{"push_only":false,"send_everything":false,"choose_events":false,"events":{"create":false,"push":true,"pull_request":true}}' is_active: true + - id: 4 repo_id: 2 @@ -29,3 +30,23 @@ content_type: 1 # json events: '{"push_only":true,"branch_filter":"{master,feature*}"}' is_active: true + +- + id: 5 + repo_id: 0 + owner_id: 0 + url: www.example.com/url5 + content_type: 1 # json + events: '{"push_only":true,"branch_filter":"{master,feature*}"}' + is_active: true + is_system_webhook: true + +- + id: 6 + repo_id: 0 + owner_id: 0 + url: www.example.com/url6 + content_type: 1 # json + events: '{"push_only":true,"branch_filter":"{master,feature*}"}' + is_active: true + is_system_webhook: false diff --git a/models/git/branch.go b/models/git/branch.go index e683ce47e65..d1caa35947b 100644 --- a/models/git/branch.go +++ b/models/git/branch.go @@ -167,6 +167,9 @@ func GetBranch(ctx context.Context, repoID int64, branchName string) (*Branch, e BranchName: branchName, } } + // FIXME: this design is not right: it doesn't check `branch.IsDeleted`, it doesn't make sense to make callers to check IsDeleted again and again. + // It causes inconsistency with `GetBranches` and `git.GetBranch`, and will lead to strange bugs + // In the future, there should be 2 functions: `GetBranchExisting` and `GetBranchWithDeleted` return &branch, nil } @@ -440,6 +443,8 @@ type FindRecentlyPushedNewBranchesOptions struct { } type RecentlyPushedNewBranch struct { + BranchRepo *repo_model.Repository + BranchName string BranchDisplayName string BranchLink string BranchCompareURL string @@ -540,7 +545,9 @@ func FindRecentlyPushedNewBranches(ctx context.Context, doer *user_model.User, o branchDisplayName = fmt.Sprintf("%s:%s", branch.Repo.FullName(), branchDisplayName) } newBranches = append(newBranches, &RecentlyPushedNewBranch{ + BranchRepo: branch.Repo, BranchDisplayName: branchDisplayName, + BranchName: branch.Name, BranchLink: fmt.Sprintf("%s/src/branch/%s", branch.Repo.Link(), util.PathEscapeSegments(branch.Name)), BranchCompareURL: branch.Repo.ComposeBranchCompareURL(opts.BaseRepo, branch.Name), CommitTime: branch.CommitTime, diff --git a/models/issues/comment.go b/models/issues/comment.go index a7ec8f57fc4..a2487088205 100644 --- a/models/issues/comment.go +++ b/models/issues/comment.go @@ -112,8 +112,8 @@ const ( CommentTypePRScheduledToAutoMerge // 34 pr was scheduled to auto merge when checks succeed CommentTypePRUnScheduledToAutoMerge // 35 pr was un scheduled to auto merge when checks succeed - CommentTypePin // 36 pin Issue - CommentTypeUnpin // 37 unpin Issue + CommentTypePin // 36 pin Issue/PullRequest + CommentTypeUnpin // 37 unpin Issue/PullRequest CommentTypeChangeTimeEstimate // 38 Change time estimate ) diff --git a/models/issues/comment_list.go b/models/issues/comment_list.go index 61ac1c8f562..c483ada75aa 100644 --- a/models/issues/comment_list.go +++ b/models/issues/comment_list.go @@ -26,14 +26,14 @@ func (comments CommentList) LoadPosters(ctx context.Context) error { return c.PosterID, c.Poster == nil && c.PosterID > 0 }) - posterMaps, err := getPostersByIDs(ctx, posterIDs) + posterMaps, err := user_model.GetUsersMapByIDs(ctx, posterIDs) if err != nil { return err } for _, comment := range comments { if comment.Poster == nil { - comment.Poster = getPoster(comment.PosterID, posterMaps) + comment.Poster = user_model.GetPossibleUserFromMap(comment.PosterID, posterMaps) } } return nil @@ -41,7 +41,7 @@ func (comments CommentList) LoadPosters(ctx context.Context) error { func (comments CommentList) getLabelIDs() []int64 { return container.FilterSlice(comments, func(comment *Comment) (int64, bool) { - return comment.LabelID, comment.LabelID > 0 + return comment.LabelID, comment.LabelID > 0 && comment.Label == nil }) } @@ -51,6 +51,9 @@ func (comments CommentList) loadLabels(ctx context.Context) error { } labelIDs := comments.getLabelIDs() + if len(labelIDs) == 0 { + return nil + } commentLabels := make(map[int64]*Label, len(labelIDs)) left := len(labelIDs) for left > 0 { @@ -118,8 +121,8 @@ func (comments CommentList) loadMilestones(ctx context.Context) error { milestoneIDs = milestoneIDs[limit:] } - for _, issue := range comments { - issue.Milestone = milestoneMaps[issue.MilestoneID] + for _, comment := range comments { + comment.Milestone = milestoneMaps[comment.MilestoneID] } return nil } @@ -175,6 +178,9 @@ func (comments CommentList) loadAssignees(ctx context.Context) error { } assigneeIDs := comments.getAssigneeIDs() + if len(assigneeIDs) == 0 { + return nil + } assignees := make(map[int64]*user_model.User, len(assigneeIDs)) left := len(assigneeIDs) for left > 0 { @@ -301,6 +307,9 @@ func (comments CommentList) loadDependentIssues(ctx context.Context) error { e := db.GetEngine(ctx) issueIDs := comments.getDependentIssueIDs() + if len(issueIDs) == 0 { + return nil + } issues := make(map[int64]*Issue, len(issueIDs)) left := len(issueIDs) for left > 0 { @@ -427,6 +436,9 @@ func (comments CommentList) loadReviews(ctx context.Context) error { } reviewIDs := comments.getReviewIDs() + if len(reviewIDs) == 0 { + return nil + } reviews := make(map[int64]*Review, len(reviewIDs)) if err := db.GetEngine(ctx).In("id", reviewIDs).Find(&reviews); err != nil { return err diff --git a/models/issues/issue.go b/models/issues/issue.go index fe347c27156..564a9fb8359 100644 --- a/models/issues/issue.go +++ b/models/issues/issue.go @@ -46,23 +46,6 @@ func (err ErrIssueNotExist) Unwrap() error { return util.ErrNotExist } -// ErrIssueIsClosed represents a "IssueIsClosed" kind of error. -type ErrIssueIsClosed struct { - ID int64 - RepoID int64 - Index int64 -} - -// IsErrIssueIsClosed checks if an error is a ErrIssueNotExist. -func IsErrIssueIsClosed(err error) bool { - _, ok := err.(ErrIssueIsClosed) - return ok -} - -func (err ErrIssueIsClosed) Error() string { - return fmt.Sprintf("issue is closed [id: %d, repo_id: %d, index: %d]", err.ID, err.RepoID, err.Index) -} - // ErrNewIssueInsert is used when the INSERT statement in newIssue fails type ErrNewIssueInsert struct { OriginalError error @@ -78,22 +61,6 @@ func (err ErrNewIssueInsert) Error() string { return err.OriginalError.Error() } -// ErrIssueWasClosed is used when close a closed issue -type ErrIssueWasClosed struct { - ID int64 - Index int64 -} - -// IsErrIssueWasClosed checks if an error is a ErrIssueWasClosed. -func IsErrIssueWasClosed(err error) bool { - _, ok := err.(ErrIssueWasClosed) - return ok -} - -func (err ErrIssueWasClosed) Error() string { - return fmt.Sprintf("Issue [%d] %d was already closed", err.ID, err.Index) -} - var ErrIssueAlreadyChanged = util.NewInvalidArgumentErrorf("the issue is already changed") // Issue represents an issue or pull request of repository. @@ -271,6 +238,9 @@ func (issue *Issue) loadCommentsByType(ctx context.Context, tp CommentType) (err IssueID: issue.ID, Type: tp, }) + for _, comment := range issue.Comments { + comment.Issue = issue + } return err } diff --git a/models/issues/issue_list.go b/models/issues/issue_list.go index 22a4548adc2..02fd330f0a7 100644 --- a/models/issues/issue_list.go +++ b/models/issues/issue_list.go @@ -81,53 +81,19 @@ func (issues IssueList) LoadPosters(ctx context.Context) error { return issue.PosterID, issue.Poster == nil && issue.PosterID > 0 }) - posterMaps, err := getPostersByIDs(ctx, posterIDs) + posterMaps, err := user_model.GetUsersMapByIDs(ctx, posterIDs) if err != nil { return err } for _, issue := range issues { if issue.Poster == nil { - issue.Poster = getPoster(issue.PosterID, posterMaps) + issue.Poster = user_model.GetPossibleUserFromMap(issue.PosterID, posterMaps) } } return nil } -func getPostersByIDs(ctx context.Context, posterIDs []int64) (map[int64]*user_model.User, error) { - posterMaps := make(map[int64]*user_model.User, len(posterIDs)) - left := len(posterIDs) - for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } - err := db.GetEngine(ctx). - In("id", posterIDs[:limit]). - Find(&posterMaps) - if err != nil { - return nil, err - } - left -= limit - posterIDs = posterIDs[limit:] - } - return posterMaps, nil -} - -func getPoster(posterID int64, posterMaps map[int64]*user_model.User) *user_model.User { - if posterID == user_model.ActionsUserID { - return user_model.NewActionsUser() - } - if posterID <= 0 { - return nil - } - poster, ok := posterMaps[posterID] - if !ok { - return user_model.NewGhostUser() - } - return poster -} - func (issues IssueList) getIssueIDs() []int64 { ids := make([]int64, 0, len(issues)) for _, issue := range issues { diff --git a/models/issues/issue_project.go b/models/issues/issue_project.go index c4515fd8985..f520604321c 100644 --- a/models/issues/issue_project.go +++ b/models/issues/issue_project.go @@ -38,13 +38,15 @@ func (issue *Issue) projectID(ctx context.Context) int64 { } // ProjectColumnID return project column id if issue was assigned to one -func (issue *Issue) ProjectColumnID(ctx context.Context) int64 { +func (issue *Issue) ProjectColumnID(ctx context.Context) (int64, error) { var ip project_model.ProjectIssue has, err := db.GetEngine(ctx).Where("issue_id=?", issue.ID).Get(&ip) - if err != nil || !has { - return 0 + if err != nil { + return 0, err + } else if !has { + return 0, nil } - return ip.ProjectColumnID + return ip.ProjectColumnID, nil } // LoadIssuesFromColumn load issues assigned to this column diff --git a/models/issues/issue_stats.go b/models/issues/issue_stats.go index 9ef9347a16c..50409fbbd89 100644 --- a/models/issues/issue_stats.go +++ b/models/issues/issue_stats.go @@ -107,7 +107,7 @@ func GetIssueStats(ctx context.Context, opts *IssuesOptions) (*IssueStats, error accum.YourRepositoriesCount += stats.YourRepositoriesCount accum.AssignCount += stats.AssignCount accum.CreateCount += stats.CreateCount - accum.OpenCount += stats.MentionCount + accum.MentionCount += stats.MentionCount accum.ReviewRequestedCount += stats.ReviewRequestedCount accum.ReviewedCount += stats.ReviewedCount i = chunk diff --git a/models/issues/issue_update.go b/models/issues/issue_update.go index 53a6125ca89..d0a6a247289 100644 --- a/models/issues/issue_update.go +++ b/models/issues/issue_update.go @@ -28,38 +28,40 @@ import ( // UpdateIssueCols updates cols of issue func UpdateIssueCols(ctx context.Context, issue *Issue, cols ...string) error { - if _, err := db.GetEngine(ctx).ID(issue.ID).Cols(cols...).Update(issue); err != nil { - return err - } - return nil + _, err := db.GetEngine(ctx).ID(issue.ID).Cols(cols...).Update(issue) + return err } -func ChangeIssueStatus(ctx context.Context, issue *Issue, doer *user_model.User, isClosed, isMergePull bool) (*Comment, error) { - // Reload the issue - currentIssue, err := GetIssueByID(ctx, issue.ID) - if err != nil { - return nil, err - } +// ErrIssueIsClosed is used when close a closed issue +type ErrIssueIsClosed struct { + ID int64 + RepoID int64 + Index int64 + IsPull bool +} - // Nothing should be performed if current status is same as target status - if currentIssue.IsClosed == isClosed { - if !issue.IsPull { - return nil, ErrIssueWasClosed{ - ID: issue.ID, - } - } - return nil, ErrPullWasClosed{ - ID: issue.ID, +// IsErrIssueIsClosed checks if an error is a ErrIssueIsClosed. +func IsErrIssueIsClosed(err error) bool { + _, ok := err.(ErrIssueIsClosed) + return ok +} + +func (err ErrIssueIsClosed) Error() string { + return fmt.Sprintf("%s [id: %d, repo_id: %d, index: %d] is already closed", util.Iif(err.IsPull, "Pull Request", "Issue"), err.ID, err.RepoID, err.Index) +} + +func SetIssueAsClosed(ctx context.Context, issue *Issue, doer *user_model.User, isMergePull bool) (*Comment, error) { + if issue.IsClosed { + return nil, ErrIssueIsClosed{ + ID: issue.ID, + RepoID: issue.RepoID, + Index: issue.Index, + IsPull: issue.IsPull, } } - issue.IsClosed = isClosed - return doChangeIssueStatus(ctx, issue, doer, isMergePull) -} - -func doChangeIssueStatus(ctx context.Context, issue *Issue, doer *user_model.User, isMergePull bool) (*Comment, error) { // Check for open dependencies - if issue.IsClosed && issue.Repo.IsDependenciesEnabled(ctx) { + if issue.Repo.IsDependenciesEnabled(ctx) { // only check if dependencies are enabled and we're about to close an issue, otherwise reopening an issue would fail when there are unsatisfied dependencies noDeps, err := IssueNoDependenciesLeft(ctx, issue) if err != nil { @@ -71,16 +73,63 @@ func doChangeIssueStatus(ctx context.Context, issue *Issue, doer *user_model.Use } } - if issue.IsClosed { - issue.ClosedUnix = timeutil.TimeStampNow() - } else { - issue.ClosedUnix = 0 - } + issue.IsClosed = true + issue.ClosedUnix = timeutil.TimeStampNow() - if err := UpdateIssueCols(ctx, issue, "is_closed", "closed_unix"); err != nil { + if cnt, err := db.GetEngine(ctx).ID(issue.ID).Cols("is_closed", "closed_unix"). + Where("is_closed = ?", false). + Update(issue); err != nil { return nil, err + } else if cnt != 1 { + return nil, ErrIssueAlreadyChanged } + return updateIssueNumbers(ctx, issue, doer, util.Iif(isMergePull, CommentTypeMergePull, CommentTypeClose)) +} + +// ErrIssueIsOpen is used when reopen an opened issue +type ErrIssueIsOpen struct { + ID int64 + RepoID int64 + IsPull bool + Index int64 +} + +// IsErrIssueIsOpen checks if an error is a ErrIssueIsOpen. +func IsErrIssueIsOpen(err error) bool { + _, ok := err.(ErrIssueIsOpen) + return ok +} + +func (err ErrIssueIsOpen) Error() string { + return fmt.Sprintf("%s [id: %d, repo_id: %d, index: %d] is already open", util.Iif(err.IsPull, "Pull Request", "Issue"), err.ID, err.RepoID, err.Index) +} + +func setIssueAsReopen(ctx context.Context, issue *Issue, doer *user_model.User) (*Comment, error) { + if !issue.IsClosed { + return nil, ErrIssueIsOpen{ + ID: issue.ID, + RepoID: issue.RepoID, + Index: issue.Index, + IsPull: issue.IsPull, + } + } + + issue.IsClosed = false + issue.ClosedUnix = 0 + + if cnt, err := db.GetEngine(ctx).ID(issue.ID).Cols("is_closed", "closed_unix"). + Where("is_closed = ?", true). + Update(issue); err != nil { + return nil, err + } else if cnt != 1 { + return nil, ErrIssueAlreadyChanged + } + + return updateIssueNumbers(ctx, issue, doer, CommentTypeReopen) +} + +func updateIssueNumbers(ctx context.Context, issue *Issue, doer *user_model.User, cmtType CommentType) (*Comment, error) { // Update issue count of labels if err := issue.LoadLabels(ctx); err != nil { return nil, err @@ -103,14 +152,6 @@ func doChangeIssueStatus(ctx context.Context, issue *Issue, doer *user_model.Use return nil, err } - // New action comment - cmtType := CommentTypeClose - if !issue.IsClosed { - cmtType = CommentTypeReopen - } else if isMergePull { - cmtType = CommentTypeMergePull - } - return CreateComment(ctx, &CreateCommentOptions{ Type: cmtType, Doer: doer, @@ -134,7 +175,7 @@ func CloseIssue(ctx context.Context, issue *Issue, doer *user_model.User) (*Comm } defer committer.Close() - comment, err := ChangeIssueStatus(ctx, issue, doer, true, false) + comment, err := SetIssueAsClosed(ctx, issue, doer, false) if err != nil { return nil, err } @@ -159,7 +200,7 @@ func ReopenIssue(ctx context.Context, issue *Issue, doer *user_model.User) (*Com } defer committer.Close() - comment, err := ChangeIssueStatus(ctx, issue, doer, false, false) + comment, err := setIssueAsReopen(ctx, issue, doer) if err != nil { return nil, err } diff --git a/models/issues/pull.go b/models/issues/pull.go index 8c43eb19ddf..e3af00224de 100644 --- a/models/issues/pull.go +++ b/models/issues/pull.go @@ -80,22 +80,6 @@ func (err ErrPullRequestAlreadyExists) Unwrap() error { return util.ErrAlreadyExist } -// ErrPullWasClosed is used close a closed pull request -type ErrPullWasClosed struct { - ID int64 - Index int64 -} - -// IsErrPullWasClosed checks if an error is a ErrErrPullWasClosed. -func IsErrPullWasClosed(err error) bool { - _, ok := err.(ErrPullWasClosed) - return ok -} - -func (err ErrPullWasClosed) Error() string { - return fmt.Sprintf("Pull request [%d] %d was already closed", err.ID, err.Index) -} - // PullRequestType defines pull request type type PullRequestType int @@ -301,7 +285,7 @@ func (pr *PullRequest) LoadRequestedReviewers(ctx context.Context) error { return nil } - reviews, err := GetReviewsByIssueID(ctx, pr.Issue.ID) + reviews, _, err := GetReviewsByIssueID(ctx, pr.Issue.ID) if err != nil { return err } @@ -320,7 +304,7 @@ func (pr *PullRequest) LoadRequestedReviewers(ctx context.Context) error { // LoadRequestedReviewersTeams loads the requested reviewers teams. func (pr *PullRequest) LoadRequestedReviewersTeams(ctx context.Context) error { - reviews, err := GetReviewsByIssueID(ctx, pr.Issue.ID) + reviews, _, err := GetReviewsByIssueID(ctx, pr.Issue.ID) if err != nil { return err } diff --git a/models/issues/pull_list.go b/models/issues/pull_list.go index 59010aa9d06..1ddb94e566b 100644 --- a/models/issues/pull_list.go +++ b/models/issues/pull_list.go @@ -166,6 +166,23 @@ func (prs PullRequestList) getRepositoryIDs() []int64 { return repoIDs.Values() } +func (prs PullRequestList) SetBaseRepo(baseRepo *repo_model.Repository) { + for _, pr := range prs { + if pr.BaseRepo == nil { + pr.BaseRepo = baseRepo + } + } +} + +func (prs PullRequestList) SetHeadRepo(headRepo *repo_model.Repository) { + for _, pr := range prs { + if pr.HeadRepo == nil { + pr.HeadRepo = headRepo + pr.isHeadRepoLoaded = true + } + } +} + func (prs PullRequestList) LoadRepositories(ctx context.Context) error { repoIDs := prs.getRepositoryIDs() reposMap := make(map[int64]*repo_model.Repository, len(repoIDs)) diff --git a/models/issues/reaction.go b/models/issues/reaction.go index 11b3c6be203..f24001fd231 100644 --- a/models/issues/reaction.go +++ b/models/issues/reaction.go @@ -7,6 +7,7 @@ import ( "bytes" "context" "fmt" + "strings" "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" @@ -321,6 +322,11 @@ func valuesUser(m map[int64]*user_model.User) []*user_model.User { return values } +// newMigrationOriginalUser creates and returns a fake user for external user +func newMigrationOriginalUser(name string) *user_model.User { + return &user_model.User{ID: 0, Name: name, LowerName: strings.ToLower(name)} +} + // LoadUsers loads reactions' all users func (list ReactionList) LoadUsers(ctx context.Context, repo *repo_model.Repository) ([]*user_model.User, error) { if len(list) == 0 { @@ -338,7 +344,7 @@ func (list ReactionList) LoadUsers(ctx context.Context, repo *repo_model.Reposit for _, reaction := range list { if reaction.OriginalAuthor != "" { - reaction.User = user_model.NewReplaceUser(fmt.Sprintf("%s(%s)", reaction.OriginalAuthor, repo.OriginalServiceType.Name())) + reaction.User = newMigrationOriginalUser(fmt.Sprintf("%s(%s)", reaction.OriginalAuthor, repo.OriginalServiceType.Name())) } else if user, ok := userMaps[reaction.UserID]; ok { reaction.User = user } else { diff --git a/models/issues/review.go b/models/issues/review.go index 3e787273be8..1c5c2ee30ac 100644 --- a/models/issues/review.go +++ b/models/issues/review.go @@ -930,17 +930,19 @@ func MarkConversation(ctx context.Context, comment *Comment, doer *user_model.Us } // CanMarkConversation Add or remove Conversation mark for a code comment permission check -// the PR writer , offfcial reviewer and poster can do it +// the PR writer , official reviewer and poster can do it func CanMarkConversation(ctx context.Context, issue *Issue, doer *user_model.User) (permResult bool, err error) { if doer == nil || issue == nil { return false, fmt.Errorf("issue or doer is nil") } + if err = issue.LoadRepo(ctx); err != nil { + return false, err + } + if issue.Repo.IsArchived { + return false, nil + } if doer.ID != issue.PosterID { - if err = issue.LoadRepo(ctx); err != nil { - return false, err - } - p, err := access_model.GetUserRepoPermission(ctx, issue.Repo, doer) if err != nil { return false, err diff --git a/models/issues/review_list.go b/models/issues/review_list.go index bc7d7ec0f01..928f24fb2dc 100644 --- a/models/issues/review_list.go +++ b/models/issues/review_list.go @@ -5,6 +5,8 @@ package issues import ( "context" + "slices" + "sort" "code.gitea.io/gitea/models/db" organization_model "code.gitea.io/gitea/models/organization" @@ -153,43 +155,60 @@ func CountReviews(ctx context.Context, opts FindReviewOptions) (int64, error) { return db.GetEngine(ctx).Where(opts.toCond()).Count(&Review{}) } -// GetReviewersFromOriginalAuthorsByIssueID gets the latest review of each original authors for a pull request -func GetReviewersFromOriginalAuthorsByIssueID(ctx context.Context, issueID int64) (ReviewList, error) { - reviews := make([]*Review, 0, 10) - - // Get latest review of each reviewer, sorted in order they were made - if err := db.GetEngine(ctx).SQL("SELECT * FROM review WHERE id IN (SELECT max(id) as id FROM review WHERE issue_id = ? AND reviewer_team_id = 0 AND type in (?, ?, ?) AND original_author_id <> 0 GROUP BY issue_id, original_author_id) ORDER BY review.updated_unix ASC", - issueID, ReviewTypeApprove, ReviewTypeReject, ReviewTypeRequest). - Find(&reviews); err != nil { - return nil, err - } - - return reviews, nil -} - // GetReviewsByIssueID gets the latest review of each reviewer for a pull request -func GetReviewsByIssueID(ctx context.Context, issueID int64) (ReviewList, error) { +// The first returned parameter is the latest review of each individual reviewer or team +// The second returned parameter is the latest review of each original author which is migrated from other systems +// The reviews are sorted by updated time +func GetReviewsByIssueID(ctx context.Context, issueID int64) (latestReviews, migratedOriginalReviews ReviewList, err error) { reviews := make([]*Review, 0, 10) - sess := db.GetEngine(ctx) - - // Get latest review of each reviewer, sorted in order they were made - if err := sess.SQL("SELECT * FROM review WHERE id IN (SELECT max(id) as id FROM review WHERE issue_id = ? AND reviewer_team_id = 0 AND type in (?, ?, ?) AND dismissed = ? AND original_author_id = 0 GROUP BY issue_id, reviewer_id) ORDER BY review.updated_unix ASC", - issueID, ReviewTypeApprove, ReviewTypeReject, ReviewTypeRequest, false). - Find(&reviews); err != nil { - return nil, err + // Get all reviews for the issue id + if err := db.GetEngine(ctx).Where("issue_id=?", issueID).OrderBy("updated_unix ASC").Find(&reviews); err != nil { + return nil, nil, err } + // filter them in memory to get the latest review of each reviewer + // Since the reviews should not be too many for one issue, less than 100 commonly, it's acceptable to do this in memory + // And since there are too less indexes in review table, it will be very slow to filter in the database + reviewersMap := make(map[int64][]*Review) // key is reviewer id + originalReviewersMap := make(map[int64][]*Review) // key is original author id + reviewTeamsMap := make(map[int64][]*Review) // key is reviewer team id + countedReivewTypes := []ReviewType{ReviewTypeApprove, ReviewTypeReject, ReviewTypeRequest} + for _, review := range reviews { + if review.ReviewerTeamID == 0 && slices.Contains(countedReivewTypes, review.Type) && !review.Dismissed { + if review.OriginalAuthorID != 0 { + originalReviewersMap[review.OriginalAuthorID] = append(originalReviewersMap[review.OriginalAuthorID], review) + } else { + reviewersMap[review.ReviewerID] = append(reviewersMap[review.ReviewerID], review) + } + } else if review.ReviewerTeamID != 0 && review.OriginalAuthorID == 0 { + reviewTeamsMap[review.ReviewerTeamID] = append(reviewTeamsMap[review.ReviewerTeamID], review) + } + } + + individualReviews := make([]*Review, 0, 10) + for _, reviews := range reviewersMap { + individualReviews = append(individualReviews, reviews[len(reviews)-1]) + } + sort.Slice(individualReviews, func(i, j int) bool { + return individualReviews[i].UpdatedUnix < individualReviews[j].UpdatedUnix + }) + + originalReviews := make([]*Review, 0, 10) + for _, reviews := range originalReviewersMap { + originalReviews = append(originalReviews, reviews[len(reviews)-1]) + } + sort.Slice(originalReviews, func(i, j int) bool { + return originalReviews[i].UpdatedUnix < originalReviews[j].UpdatedUnix + }) + teamReviewRequests := make([]*Review, 0, 5) - if err := sess.SQL("SELECT * FROM review WHERE id IN (SELECT max(id) as id FROM review WHERE issue_id = ? AND reviewer_team_id <> 0 AND original_author_id = 0 GROUP BY issue_id, reviewer_team_id) ORDER BY review.updated_unix ASC", - issueID). - Find(&teamReviewRequests); err != nil { - return nil, err + for _, reviews := range reviewTeamsMap { + teamReviewRequests = append(teamReviewRequests, reviews[len(reviews)-1]) } + sort.Slice(teamReviewRequests, func(i, j int) bool { + return teamReviewRequests[i].UpdatedUnix < teamReviewRequests[j].UpdatedUnix + }) - if len(teamReviewRequests) > 0 { - reviews = append(reviews, teamReviewRequests...) - } - - return reviews, nil + return append(individualReviews, teamReviewRequests...), originalReviews, nil } diff --git a/models/issues/review_test.go b/models/issues/review_test.go index 50330e3ff2c..2588b8ba41b 100644 --- a/models/issues/review_test.go +++ b/models/issues/review_test.go @@ -162,8 +162,9 @@ func TestGetReviewersByIssueID(t *testing.T) { }, ) - allReviews, err := issues_model.GetReviewsByIssueID(db.DefaultContext, issue.ID) + allReviews, migratedReviews, err := issues_model.GetReviewsByIssueID(db.DefaultContext, issue.ID) assert.NoError(t, err) + assert.Empty(t, migratedReviews) for _, review := range allReviews { assert.NoError(t, review.LoadReviewer(db.DefaultContext)) } diff --git a/models/issues/stopwatch.go b/models/issues/stopwatch.go index 629af95b577..7c05a3a883d 100644 --- a/models/issues/stopwatch.go +++ b/models/issues/stopwatch.go @@ -46,11 +46,6 @@ func (s Stopwatch) Seconds() int64 { return int64(timeutil.TimeStampNow() - s.CreatedUnix) } -// Duration returns a human-readable duration string based on local server time -func (s Stopwatch) Duration() string { - return util.SecToTime(s.Seconds()) -} - func getStopwatch(ctx context.Context, userID, issueID int64) (sw *Stopwatch, exists bool, err error) { sw = new(Stopwatch) exists, err = db.GetEngine(ctx). @@ -201,7 +196,7 @@ func FinishIssueStopwatch(ctx context.Context, user *user_model.User, issue *Iss Doer: user, Issue: issue, Repo: issue.Repo, - Content: util.SecToTime(timediff), + Content: util.SecToHours(timediff), Type: CommentTypeStopTracking, TimeID: tt.ID, }); err != nil { diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index c667b606bbb..3aa6b5a7165 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -372,7 +372,8 @@ func prepareMigrationTasks() []*migration { newMigration(311, "Add TimeEstimate to Issue table", v1_23.AddTimeEstimateColumnToIssueTable), // Gitea 1.23.0-rc0 ends at migration ID number 311 (database version 312) - newMigration(312, "Add table issue_dev_link", v1_24.CreateTableIssueDevLink), + newMigration(312, "Add DeleteBranchAfterMerge to AutoMerge", v1_24.AddDeleteBranchAfterMergeForAutoMerge), + newMigration(313, "Add table issue_dev_link", v1_24.CreateTableIssueDevLink), } return preparedMigrations } diff --git a/models/migrations/v1_21/v276.go b/models/migrations/v1_21/v276.go index 15177bf0406..9d22c9052e1 100644 --- a/models/migrations/v1_21/v276.go +++ b/models/migrations/v1_21/v276.go @@ -172,7 +172,7 @@ func getRemoteAddress(ownerName, repoName, remoteName string) (string, error) { return "", fmt.Errorf("get remote %s's address of %s/%s failed: %v", remoteName, ownerName, repoName, err) } - u, err := giturl.Parse(remoteURL) + u, err := giturl.ParseGitURL(remoteURL) if err != nil { return "", err } diff --git a/models/migrations/v1_24/v312.go b/models/migrations/v1_24/v312.go index 1eb936f0fef..9766dc1ccfb 100644 --- a/models/migrations/v1_24/v312.go +++ b/models/migrations/v1_24/v312.go @@ -4,19 +4,18 @@ package v1_24 //nolint import ( - "code.gitea.io/gitea/modules/timeutil" - "xorm.io/xorm" ) -func CreateTableIssueDevLink(x *xorm.Engine) error { - type IssueDevLink struct { - ID int64 `xorm:"pk autoincr"` - IssueID int64 `xorm:"INDEX"` - LinkType int - LinkedRepoID int64 `xorm:"INDEX"` // it can link to self repo or other repo - LinkIndex string // branch name, pull request number or commit sha - CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` - } - return x.Sync(new(IssueDevLink)) +type pullAutoMerge struct { + DeleteBranchAfterMerge bool +} + +// TableName return database table name for xorm +func (pullAutoMerge) TableName() string { + return "pull_auto_merge" +} + +func AddDeleteBranchAfterMergeForAutoMerge(x *xorm.Engine) error { + return x.Sync(new(pullAutoMerge)) } diff --git a/models/migrations/v1_24/v313.go b/models/migrations/v1_24/v313.go new file mode 100644 index 00000000000..1eb936f0fef --- /dev/null +++ b/models/migrations/v1_24/v313.go @@ -0,0 +1,22 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_24 //nolint + +import ( + "code.gitea.io/gitea/modules/timeutil" + + "xorm.io/xorm" +) + +func CreateTableIssueDevLink(x *xorm.Engine) error { + type IssueDevLink struct { + ID int64 `xorm:"pk autoincr"` + IssueID int64 `xorm:"INDEX"` + LinkType int + LinkedRepoID int64 `xorm:"INDEX"` // it can link to self repo or other repo + LinkIndex string // branch name, pull request number or commit sha + CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` + } + return x.Sync(new(IssueDevLink)) +} diff --git a/models/organization/org_test.go b/models/organization/org_test.go index 2c5b4090df3..b882a25be32 100644 --- a/models/organization/org_test.go +++ b/models/organization/org_test.go @@ -16,6 +16,7 @@ import ( "code.gitea.io/gitea/modules/structs" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestUser_IsOwnedBy(t *testing.T) { @@ -180,9 +181,8 @@ func TestRestrictedUserOrgMembers(t *testing.T) { ID: 29, IsRestricted: true, }) - if !assert.True(t, restrictedUser.IsRestricted) { - return // ensure fixtures return restricted user - } + // ensure fixtures return restricted user + require.True(t, restrictedUser.IsRestricted) testCases := []struct { name string diff --git a/models/organization/org_worktime.go b/models/organization/org_worktime.go new file mode 100644 index 00000000000..7b57182a8a7 --- /dev/null +++ b/models/organization/org_worktime.go @@ -0,0 +1,103 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package organization + +import ( + "sort" + + "code.gitea.io/gitea/models/db" + + "xorm.io/builder" +) + +type WorktimeSumByRepos struct { + RepoName string + SumTime int64 +} + +func GetWorktimeByRepos(org *Organization, unitFrom, unixTo int64) (results []WorktimeSumByRepos, err error) { + err = db.GetEngine(db.DefaultContext). + Select("repository.name AS repo_name, SUM(tracked_time.time) AS sum_time"). + Table("tracked_time"). + Join("INNER", "issue", "tracked_time.issue_id = issue.id"). + Join("INNER", "repository", "issue.repo_id = repository.id"). + Where(builder.Eq{"repository.owner_id": org.ID}). + And(builder.Eq{"tracked_time.deleted": false}). + And(builder.Gte{"tracked_time.created_unix": unitFrom}). + And(builder.Lte{"tracked_time.created_unix": unixTo}). + GroupBy("repository.name"). + OrderBy("repository.name"). + Find(&results) + return results, err +} + +type WorktimeSumByMilestones struct { + RepoName string + MilestoneName string + MilestoneID int64 + MilestoneDeadline int64 + SumTime int64 + HideRepoName bool +} + +func GetWorktimeByMilestones(org *Organization, unitFrom, unixTo int64) (results []WorktimeSumByMilestones, err error) { + err = db.GetEngine(db.DefaultContext). + Select("repository.name AS repo_name, milestone.name AS milestone_name, milestone.id AS milestone_id, milestone.deadline_unix as milestone_deadline, SUM(tracked_time.time) AS sum_time"). + Table("tracked_time"). + Join("INNER", "issue", "tracked_time.issue_id = issue.id"). + Join("INNER", "repository", "issue.repo_id = repository.id"). + Join("LEFT", "milestone", "issue.milestone_id = milestone.id"). + Where(builder.Eq{"repository.owner_id": org.ID}). + And(builder.Eq{"tracked_time.deleted": false}). + And(builder.Gte{"tracked_time.created_unix": unitFrom}). + And(builder.Lte{"tracked_time.created_unix": unixTo}). + GroupBy("repository.name, milestone.name, milestone.deadline_unix, milestone.id"). + OrderBy("repository.name, milestone.deadline_unix, milestone.id"). + Find(&results) + + // TODO: pgsql: NULL values are sorted last in default ascending order, so we need to sort them manually again. + sort.Slice(results, func(i, j int) bool { + if results[i].RepoName != results[j].RepoName { + return results[i].RepoName < results[j].RepoName + } + if results[i].MilestoneDeadline != results[j].MilestoneDeadline { + return results[i].MilestoneDeadline < results[j].MilestoneDeadline + } + return results[i].MilestoneID < results[j].MilestoneID + }) + + // Show only the first RepoName, for nicer output. + prevRepoName := "" + for i := 0; i < len(results); i++ { + res := &results[i] + res.MilestoneDeadline = 0 // clear the deadline because we do not really need it + if prevRepoName == res.RepoName { + res.HideRepoName = true + } + prevRepoName = res.RepoName + } + return results, err +} + +type WorktimeSumByMembers struct { + UserName string + SumTime int64 +} + +func GetWorktimeByMembers(org *Organization, unitFrom, unixTo int64) (results []WorktimeSumByMembers, err error) { + err = db.GetEngine(db.DefaultContext). + Select("`user`.name AS user_name, SUM(tracked_time.time) AS sum_time"). + Table("tracked_time"). + Join("INNER", "issue", "tracked_time.issue_id = issue.id"). + Join("INNER", "repository", "issue.repo_id = repository.id"). + Join("INNER", "`user`", "tracked_time.user_id = `user`.id"). + Where(builder.Eq{"repository.owner_id": org.ID}). + And(builder.Eq{"tracked_time.deleted": false}). + And(builder.Gte{"tracked_time.created_unix": unitFrom}). + And(builder.Lte{"tracked_time.created_unix": unixTo}). + GroupBy("`user`.name"). + OrderBy("sum_time DESC"). + Find(&results) + return results, err +} diff --git a/models/packages/package.go b/models/packages/package.go index c12f345f0e2..31e1277a6e3 100644 --- a/models/packages/package.go +++ b/models/packages/package.go @@ -248,6 +248,18 @@ func GetPackageByID(ctx context.Context, packageID int64) (*Package, error) { return p, nil } +// UpdatePackageNameByID updates the package's name, it is only for internal usage, for example: rename some legacy packages +func UpdatePackageNameByID(ctx context.Context, ownerID int64, packageType Type, packageID int64, name string) error { + var cond builder.Cond = builder.Eq{ + "package.id": packageID, + "package.owner_id": ownerID, + "package.type": packageType, + "package.is_internal": false, + } + _, err := db.GetEngine(ctx).Where(cond).Update(&Package{Name: name, LowerName: strings.ToLower(name)}) + return err +} + // GetPackageByName gets a package by name func GetPackageByName(ctx context.Context, ownerID int64, packageType Type, name string) (*Package, error) { var cond builder.Cond = builder.Eq{ diff --git a/models/perm/access/repo_permission.go b/models/perm/access/repo_permission.go index 0ed116a1324..e00b7c5320f 100644 --- a/models/perm/access/repo_permission.go +++ b/models/perm/access/repo_permission.go @@ -175,10 +175,14 @@ func (p *Permission) LogString() string { return fmt.Sprintf(format, args...) } -func applyEveryoneRepoPermission(user *user_model.User, perm *Permission) { +func finalProcessRepoUnitPermission(user *user_model.User, perm *Permission) { if user == nil || user.ID <= 0 { + // for anonymous access, it could be: + // AccessMode is None or Read, units has repo units, unitModes is nil return } + + // apply everyone access permissions for _, u := range perm.units { if u.EveryoneAccessMode >= perm_model.AccessModeRead && u.EveryoneAccessMode > perm.everyoneAccessMode[u.Type] { if perm.everyoneAccessMode == nil { @@ -187,17 +191,40 @@ func applyEveryoneRepoPermission(user *user_model.User, perm *Permission) { perm.everyoneAccessMode[u.Type] = u.EveryoneAccessMode } } + + if perm.unitsMode == nil { + // if unitsMode is not set, then it means that the default p.AccessMode applies to all units + return + } + + // remove no permission units + origPermUnits := perm.units + perm.units = make([]*repo_model.RepoUnit, 0, len(perm.units)) + for _, u := range origPermUnits { + shouldKeep := false + for t := range perm.unitsMode { + if shouldKeep = u.Type == t; shouldKeep { + break + } + } + for t := range perm.everyoneAccessMode { + if shouldKeep = shouldKeep || u.Type == t; shouldKeep { + break + } + } + if shouldKeep { + perm.units = append(perm.units, u) + } + } } // GetUserRepoPermission returns the user permissions to the repository func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, user *user_model.User) (perm Permission, err error) { defer func() { if err == nil { - applyEveryoneRepoPermission(user, &perm) - } - if log.IsTrace() { - log.Trace("Permission Loaded for user %-v in repo %-v, permissions: %-+v", user, repo, perm) + finalProcessRepoUnitPermission(user, &perm) } + log.Trace("Permission Loaded for user %-v in repo %-v, permissions: %-+v", user, repo, perm) }() if err = repo.LoadUnits(ctx); err != nil { @@ -294,16 +321,6 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use } } - // remove no permission units - perm.units = make([]*repo_model.RepoUnit, 0, len(repo.Units)) - for t := range perm.unitsMode { - for _, u := range repo.Units { - if u.Type == t { - perm.units = append(perm.units, u) - } - } - } - return perm, err } diff --git a/models/perm/access/repo_permission_test.go b/models/perm/access/repo_permission_test.go index 50070c43684..9862da06731 100644 --- a/models/perm/access/repo_permission_test.go +++ b/models/perm/access/repo_permission_test.go @@ -50,7 +50,7 @@ func TestApplyEveryoneRepoPermission(t *testing.T) { {Type: unit.TypeWiki, EveryoneAccessMode: perm_model.AccessModeRead}, }, } - applyEveryoneRepoPermission(nil, &perm) + finalProcessRepoUnitPermission(nil, &perm) assert.False(t, perm.CanRead(unit.TypeWiki)) perm = Permission{ @@ -59,7 +59,7 @@ func TestApplyEveryoneRepoPermission(t *testing.T) { {Type: unit.TypeWiki, EveryoneAccessMode: perm_model.AccessModeRead}, }, } - applyEveryoneRepoPermission(&user_model.User{ID: 0}, &perm) + finalProcessRepoUnitPermission(&user_model.User{ID: 0}, &perm) assert.False(t, perm.CanRead(unit.TypeWiki)) perm = Permission{ @@ -68,7 +68,7 @@ func TestApplyEveryoneRepoPermission(t *testing.T) { {Type: unit.TypeWiki, EveryoneAccessMode: perm_model.AccessModeRead}, }, } - applyEveryoneRepoPermission(&user_model.User{ID: 1}, &perm) + finalProcessRepoUnitPermission(&user_model.User{ID: 1}, &perm) assert.True(t, perm.CanRead(unit.TypeWiki)) perm = Permission{ @@ -77,20 +77,22 @@ func TestApplyEveryoneRepoPermission(t *testing.T) { {Type: unit.TypeWiki, EveryoneAccessMode: perm_model.AccessModeRead}, }, } - applyEveryoneRepoPermission(&user_model.User{ID: 1}, &perm) + finalProcessRepoUnitPermission(&user_model.User{ID: 1}, &perm) // it should work the same as "EveryoneAccessMode: none" because the default AccessMode should be applied to units assert.True(t, perm.CanWrite(unit.TypeWiki)) perm = Permission{ units: []*repo_model.RepoUnit{ + {Type: unit.TypeCode}, // will be removed {Type: unit.TypeWiki, EveryoneAccessMode: perm_model.AccessModeRead}, }, unitsMode: map[unit.Type]perm_model.AccessMode{ unit.TypeWiki: perm_model.AccessModeWrite, }, } - applyEveryoneRepoPermission(&user_model.User{ID: 1}, &perm) + finalProcessRepoUnitPermission(&user_model.User{ID: 1}, &perm) assert.True(t, perm.CanWrite(unit.TypeWiki)) + assert.Len(t, perm.units, 1) } func TestUnitAccessMode(t *testing.T) { diff --git a/models/project/project.go b/models/project/project.go index 87e679e1b73..20b5df0b6e4 100644 --- a/models/project/project.go +++ b/models/project/project.go @@ -126,6 +126,14 @@ func (p *Project) LoadRepo(ctx context.Context) (err error) { return err } +func ProjectLinkForOrg(org *user_model.User, projectID int64) string { //nolint + return fmt.Sprintf("%s/-/projects/%d", org.HomeLink(), projectID) +} + +func ProjectLinkForRepo(repo *repo_model.Repository, projectID int64) string { //nolint + return fmt.Sprintf("%s/projects/%d", repo.Link(), projectID) +} + // Link returns the project's relative URL. func (p *Project) Link(ctx context.Context) string { if p.OwnerID > 0 { @@ -134,7 +142,7 @@ func (p *Project) Link(ctx context.Context) string { log.Error("LoadOwner: %v", err) return "" } - return fmt.Sprintf("%s/-/projects/%d", p.Owner.HomeLink(), p.ID) + return ProjectLinkForOrg(p.Owner, p.ID) } if p.RepoID > 0 { err := p.LoadRepo(ctx) @@ -142,7 +150,7 @@ func (p *Project) Link(ctx context.Context) string { log.Error("LoadRepo: %v", err) return "" } - return fmt.Sprintf("%s/projects/%d", p.Repo.Link(), p.ID) + return ProjectLinkForRepo(p.Repo, p.ID) } return "" } @@ -236,6 +244,10 @@ func GetSearchOrderByBySortType(sortType string) db.SearchOrderBy { return db.SearchOrderByRecentUpdated case "leastupdate": return db.SearchOrderByLeastUpdated + case "alphabetically": + return "title ASC" + case "reversealphabetically": + return "title DESC" default: return db.SearchOrderByNewest } diff --git a/models/pull/automerge.go b/models/pull/automerge.go index f69fcb60d16..3cafacc3a41 100644 --- a/models/pull/automerge.go +++ b/models/pull/automerge.go @@ -15,13 +15,14 @@ import ( // AutoMerge represents a pull request scheduled for merging when checks succeed type AutoMerge struct { - ID int64 `xorm:"pk autoincr"` - PullID int64 `xorm:"UNIQUE"` - DoerID int64 `xorm:"INDEX NOT NULL"` - Doer *user_model.User `xorm:"-"` - MergeStyle repo_model.MergeStyle `xorm:"varchar(30)"` - Message string `xorm:"LONGTEXT"` - CreatedUnix timeutil.TimeStamp `xorm:"created"` + ID int64 `xorm:"pk autoincr"` + PullID int64 `xorm:"UNIQUE"` + DoerID int64 `xorm:"INDEX NOT NULL"` + Doer *user_model.User `xorm:"-"` + MergeStyle repo_model.MergeStyle `xorm:"varchar(30)"` + Message string `xorm:"LONGTEXT"` + DeleteBranchAfterMerge bool + CreatedUnix timeutil.TimeStamp `xorm:"created"` } // TableName return database table name for xorm @@ -49,7 +50,7 @@ func IsErrAlreadyScheduledToAutoMerge(err error) bool { } // ScheduleAutoMerge schedules a pull request to be merged when all checks succeed -func ScheduleAutoMerge(ctx context.Context, doer *user_model.User, pullID int64, style repo_model.MergeStyle, message string) error { +func ScheduleAutoMerge(ctx context.Context, doer *user_model.User, pullID int64, style repo_model.MergeStyle, message string, deleteBranchAfterMerge bool) error { // Check if we already have a merge scheduled for that pull request if exists, _, err := GetScheduledMergeByPullID(ctx, pullID); err != nil { return err @@ -58,10 +59,11 @@ func ScheduleAutoMerge(ctx context.Context, doer *user_model.User, pullID int64, } _, err := db.GetEngine(ctx).Insert(&AutoMerge{ - DoerID: doer.ID, - PullID: pullID, - MergeStyle: style, - Message: message, + DoerID: doer.ID, + PullID: pullID, + MergeStyle: style, + Message: message, + DeleteBranchAfterMerge: deleteBranchAfterMerge, }) return err } diff --git a/models/repo/archiver.go b/models/repo/archiver.go index 14ffa1d89b1..5a3eac9f148 100644 --- a/models/repo/archiver.go +++ b/models/repo/archiver.go @@ -56,16 +56,11 @@ func repoArchiverForRelativePath(relativePath string) (*RepoArchiver, error) { if err != nil { return nil, util.SilentWrap{Message: fmt.Sprintf("invalid storage path: %s", relativePath), Err: util.ErrInvalidArgument} } - nameExts := strings.SplitN(parts[2], ".", 2) - if len(nameExts) != 2 { + commitID, archiveType := git.SplitArchiveNameType(parts[2]) + if archiveType == git.ArchiveUnknown { return nil, util.SilentWrap{Message: fmt.Sprintf("invalid storage path: %s", relativePath), Err: util.ErrInvalidArgument} } - - return &RepoArchiver{ - RepoID: repoID, - CommitID: parts[1] + nameExts[0], - Type: git.ToArchiveType(nameExts[1]), - }, nil + return &RepoArchiver{RepoID: repoID, CommitID: commitID, Type: archiveType}, nil } // GetRepoArchiver get an archiver diff --git a/models/repo/license.go b/models/repo/license.go index 366b4598cc4..9bcf0f7bc97 100644 --- a/models/repo/license.go +++ b/models/repo/license.go @@ -54,6 +54,7 @@ func UpdateRepoLicenses(ctx context.Context, repo *Repository, commitID string, for _, o := range oldLicenses { // Update already existing license if o.License == license { + o.CommitID = commitID if _, err := db.GetEngine(ctx).ID(o.ID).Cols("`commit_id`").Update(o); err != nil { return err } diff --git a/models/repo/repo.go b/models/repo/repo.go index dd5636a7fa4..58a911b0ac8 100644 --- a/models/repo/repo.go +++ b/models/repo/repo.go @@ -11,14 +11,17 @@ import ( "net" "net/url" "path/filepath" + "regexp" "strconv" "strings" + "sync" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/git" + giturl "code.gitea.io/gitea/modules/git/url" "code.gitea.io/gitea/modules/httplib" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/markup" @@ -59,18 +62,30 @@ func (err ErrRepoIsArchived) Error() string { return fmt.Sprintf("%s is archived", err.Repo.LogString()) } -var ( - reservedRepoNames = []string{".", "..", "-"} - reservedRepoPatterns = []string{"*.git", "*.wiki", "*.rss", "*.atom"} -) +type globalVarsStruct struct { + validRepoNamePattern *regexp.Regexp + invalidRepoNamePattern *regexp.Regexp + reservedRepoNames []string + reservedRepoPatterns []string +} -// IsUsableRepoName returns true when repository is usable +var globalVars = sync.OnceValue(func() *globalVarsStruct { + return &globalVarsStruct{ + validRepoNamePattern: regexp.MustCompile(`[-.\w]+`), + invalidRepoNamePattern: regexp.MustCompile(`[.]{2,}`), + reservedRepoNames: []string{".", "..", "-"}, + reservedRepoPatterns: []string{"*.git", "*.wiki", "*.rss", "*.atom"}, + } +}) + +// IsUsableRepoName returns true when name is usable func IsUsableRepoName(name string) error { - if db.AlphaDashDotPattern.MatchString(name) { + vars := globalVars() + if !vars.validRepoNamePattern.MatchString(name) || vars.invalidRepoNamePattern.MatchString(name) { // Note: usually this error is normally caught up earlier in the UI return db.ErrNameCharsNotAllowed{Name: name} } - return db.IsUsableName(reservedRepoNames, reservedRepoPatterns, name) + return db.IsUsableName(vars.reservedRepoNames, vars.reservedRepoPatterns, name) } // TrustModelType defines the types of trust model for this repository @@ -276,6 +291,8 @@ func (repo *Repository) IsBroken() bool { } // MarkAsBrokenEmpty marks the repo as broken and empty +// FIXME: the status "broken" and "is_empty" were abused, +// The code always set them together, no way to distinguish whether a repo is really "empty" or "broken" func (repo *Repository) MarkAsBrokenEmpty() { repo.Status = RepositoryBroken repo.IsEmpty = true @@ -632,14 +649,26 @@ type CloneLink struct { } // ComposeHTTPSCloneURL returns HTTPS clone URL based on given owner and repository name. -func ComposeHTTPSCloneURL(owner, repo string) string { - return fmt.Sprintf("%s%s/%s.git", setting.AppURL, url.PathEscape(owner), url.PathEscape(repo)) +func ComposeHTTPSCloneURL(ctx context.Context, owner, repo string) string { + return fmt.Sprintf("%s%s/%s.git", httplib.GuessCurrentAppURL(ctx), url.PathEscape(owner), url.PathEscape(repo)) } -func ComposeSSHCloneURL(ownerName, repoName string) string { +func ComposeSSHCloneURL(doer *user_model.User, ownerName, repoName string) string { sshUser := setting.SSH.User sshDomain := setting.SSH.Domain + if sshUser == "(DOER_USERNAME)" { + // Some users use SSH reverse-proxy and need to use the current signed-in username as the SSH user + // to make the SSH reverse-proxy could prepare the user's public keys ahead. + // For most cases we have the correct "doer", then use it as the SSH user. + // If we can't get the doer, then use the built-in SSH user. + if doer != nil { + sshUser = doer.Name + } else { + sshUser = setting.SSH.BuiltinServerUser + } + } + // non-standard port, it must use full URI if setting.SSH.Port != 22 { sshHost := net.JoinHostPort(sshDomain, strconv.Itoa(setting.SSH.Port)) @@ -657,21 +686,20 @@ func ComposeSSHCloneURL(ownerName, repoName string) string { return fmt.Sprintf("%s@%s:%s/%s.git", sshUser, sshHost, url.PathEscape(ownerName), url.PathEscape(repoName)) } -func (repo *Repository) cloneLink(isWiki bool) *CloneLink { - repoName := repo.Name - if isWiki { - repoName += ".wiki" - } - +func (repo *Repository) cloneLink(ctx context.Context, doer *user_model.User, repoPathName string) *CloneLink { cl := new(CloneLink) - cl.SSH = ComposeSSHCloneURL(repo.OwnerName, repoName) - cl.HTTPS = ComposeHTTPSCloneURL(repo.OwnerName, repoName) + cl.SSH = ComposeSSHCloneURL(doer, repo.OwnerName, repoPathName) + cl.HTTPS = ComposeHTTPSCloneURL(ctx, repo.OwnerName, repoPathName) return cl } // CloneLink returns clone URLs of repository. -func (repo *Repository) CloneLink() (cl *CloneLink) { - return repo.cloneLink(false) +func (repo *Repository) CloneLink(ctx context.Context, doer *user_model.User) (cl *CloneLink) { + return repo.cloneLink(ctx, doer, repo.Name) +} + +func (repo *Repository) CloneLinkGeneral(ctx context.Context) (cl *CloneLink) { + return repo.cloneLink(ctx, nil /* no doer, use a general git user */, repo.Name) } // GetOriginalURLHostname returns the hostname of a URL or the URL @@ -767,47 +795,25 @@ func GetRepositoryByName(ctx context.Context, ownerID int64, name string) (*Repo return &repo, err } -// getRepositoryURLPathSegments returns segments (owner, reponame) extracted from a url -func getRepositoryURLPathSegments(repoURL string) []string { - if strings.HasPrefix(repoURL, setting.AppURL) { - return strings.Split(strings.TrimPrefix(repoURL, setting.AppURL), "/") - } - - sshURLVariants := [4]string{ - setting.SSH.Domain + ":", - setting.SSH.User + "@" + setting.SSH.Domain + ":", - "git+ssh://" + setting.SSH.Domain + "/", - "git+ssh://" + setting.SSH.User + "@" + setting.SSH.Domain + "/", - } - - for _, sshURL := range sshURLVariants { - if strings.HasPrefix(repoURL, sshURL) { - return strings.Split(strings.TrimPrefix(repoURL, sshURL), "/") - } - } - - return nil -} - // GetRepositoryByURL returns the repository by given url func GetRepositoryByURL(ctx context.Context, repoURL string) (*Repository, error) { - // possible urls for git: - // https://my.domain/sub-path//.git - // https://my.domain/sub-path// - // git+ssh://user@my.domain//.git - // git+ssh://user@my.domain// - // user@my.domain:/.git - // user@my.domain:/ - - pathSegments := getRepositoryURLPathSegments(repoURL) - - if len(pathSegments) != 2 { + ret, err := giturl.ParseRepositoryURL(ctx, repoURL) + if err != nil || ret.OwnerName == "" { return nil, fmt.Errorf("unknown or malformed repository URL") } + return GetRepositoryByOwnerAndName(ctx, ret.OwnerName, ret.RepoName) +} - ownerName := pathSegments[0] - repoName := strings.TrimSuffix(pathSegments[1], ".git") - return GetRepositoryByOwnerAndName(ctx, ownerName, repoName) +// GetRepositoryByURLRelax also accepts an SSH clone URL without user part +func GetRepositoryByURLRelax(ctx context.Context, repoURL string) (*Repository, error) { + if !strings.Contains(repoURL, "://") && !strings.Contains(repoURL, "@") { + // convert "example.com:owner/repo" to "@example.com:owner/repo" + p1, p2, p3 := strings.Index(repoURL, "."), strings.Index(repoURL, ":"), strings.Index(repoURL, "/") + if 0 < p1 && p1 < p2 && p2 < p3 { + repoURL = "@" + repoURL + } + } + return GetRepositoryByURL(ctx, repoURL) } // GetRepositoryByID returns the repository by given id if exists. diff --git a/models/repo/repo_test.go b/models/repo/repo_test.go index 6d88d170da3..dffbd182613 100644 --- a/models/repo/repo_test.go +++ b/models/repo/repo_test.go @@ -16,6 +16,7 @@ import ( "code.gitea.io/gitea/modules/test" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) var ( @@ -132,60 +133,43 @@ func TestGetRepositoryByURL(t *testing.T) { t.Run("InvalidPath", func(t *testing.T) { repo, err := GetRepositoryByURL(db.DefaultContext, "something") - assert.Nil(t, repo) assert.Error(t, err) }) + testRepo2 := func(t *testing.T, url string) { + repo, err := GetRepositoryByURL(db.DefaultContext, url) + require.NoError(t, err) + assert.EqualValues(t, 2, repo.ID) + assert.EqualValues(t, 2, repo.OwnerID) + } + t.Run("ValidHttpURL", func(t *testing.T) { - test := func(t *testing.T, url string) { - repo, err := GetRepositoryByURL(db.DefaultContext, url) - - assert.NotNil(t, repo) - assert.NoError(t, err) - - assert.Equal(t, int64(2), repo.ID) - assert.Equal(t, int64(2), repo.OwnerID) - } - - test(t, "https://try.gitea.io/user2/repo2") - test(t, "https://try.gitea.io/user2/repo2.git") + testRepo2(t, "https://try.gitea.io/user2/repo2") + testRepo2(t, "https://try.gitea.io/user2/repo2.git") }) t.Run("ValidGitSshURL", func(t *testing.T) { - test := func(t *testing.T, url string) { - repo, err := GetRepositoryByURL(db.DefaultContext, url) + testRepo2(t, "git+ssh://sshuser@try.gitea.io/user2/repo2") + testRepo2(t, "git+ssh://sshuser@try.gitea.io/user2/repo2.git") - assert.NotNil(t, repo) - assert.NoError(t, err) - - assert.Equal(t, int64(2), repo.ID) - assert.Equal(t, int64(2), repo.OwnerID) - } - - test(t, "git+ssh://sshuser@try.gitea.io/user2/repo2") - test(t, "git+ssh://sshuser@try.gitea.io/user2/repo2.git") - - test(t, "git+ssh://try.gitea.io/user2/repo2") - test(t, "git+ssh://try.gitea.io/user2/repo2.git") + testRepo2(t, "git+ssh://try.gitea.io/user2/repo2") + testRepo2(t, "git+ssh://try.gitea.io/user2/repo2.git") }) t.Run("ValidImplicitSshURL", func(t *testing.T) { - test := func(t *testing.T, url string) { - repo, err := GetRepositoryByURL(db.DefaultContext, url) - - assert.NotNil(t, repo) - assert.NoError(t, err) + testRepo2(t, "sshuser@try.gitea.io:user2/repo2") + testRepo2(t, "sshuser@try.gitea.io:user2/repo2.git") + testRelax := func(t *testing.T, url string) { + repo, err := GetRepositoryByURLRelax(db.DefaultContext, url) + require.NoError(t, err) assert.Equal(t, int64(2), repo.ID) assert.Equal(t, int64(2), repo.OwnerID) } - - test(t, "sshuser@try.gitea.io:user2/repo2") - test(t, "sshuser@try.gitea.io:user2/repo2.git") - - test(t, "try.gitea.io:user2/repo2") - test(t, "try.gitea.io:user2/repo2.git") + // TODO: it doesn't seem to be common git ssh URL, should we really support this? + testRelax(t, "try.gitea.io:user2/repo2") + testRelax(t, "try.gitea.io:user2/repo2.git") }) } @@ -199,21 +183,41 @@ func TestComposeSSHCloneURL(t *testing.T) { setting.SSH.Domain = "domain" setting.SSH.Port = 22 setting.Repository.UseCompatSSHURI = false - assert.Equal(t, "git@domain:user/repo.git", ComposeSSHCloneURL("user", "repo")) + assert.Equal(t, "git@domain:user/repo.git", ComposeSSHCloneURL(&user_model.User{Name: "doer"}, "user", "repo")) setting.Repository.UseCompatSSHURI = true - assert.Equal(t, "ssh://git@domain/user/repo.git", ComposeSSHCloneURL("user", "repo")) + assert.Equal(t, "ssh://git@domain/user/repo.git", ComposeSSHCloneURL(&user_model.User{Name: "doer"}, "user", "repo")) // test SSH_DOMAIN while use non-standard SSH port setting.SSH.Port = 123 setting.Repository.UseCompatSSHURI = false - assert.Equal(t, "ssh://git@domain:123/user/repo.git", ComposeSSHCloneURL("user", "repo")) + assert.Equal(t, "ssh://git@domain:123/user/repo.git", ComposeSSHCloneURL(nil, "user", "repo")) setting.Repository.UseCompatSSHURI = true - assert.Equal(t, "ssh://git@domain:123/user/repo.git", ComposeSSHCloneURL("user", "repo")) + assert.Equal(t, "ssh://git@domain:123/user/repo.git", ComposeSSHCloneURL(nil, "user", "repo")) // test IPv6 SSH_DOMAIN setting.Repository.UseCompatSSHURI = false setting.SSH.Domain = "::1" setting.SSH.Port = 22 - assert.Equal(t, "git@[::1]:user/repo.git", ComposeSSHCloneURL("user", "repo")) + assert.Equal(t, "git@[::1]:user/repo.git", ComposeSSHCloneURL(nil, "user", "repo")) setting.SSH.Port = 123 - assert.Equal(t, "ssh://git@[::1]:123/user/repo.git", ComposeSSHCloneURL("user", "repo")) + assert.Equal(t, "ssh://git@[::1]:123/user/repo.git", ComposeSSHCloneURL(nil, "user", "repo")) + + setting.SSH.User = "(DOER_USERNAME)" + setting.SSH.Domain = "domain" + setting.SSH.Port = 22 + assert.Equal(t, "doer@domain:user/repo.git", ComposeSSHCloneURL(&user_model.User{Name: "doer"}, "user", "repo")) + setting.SSH.Port = 123 + assert.Equal(t, "ssh://doer@domain:123/user/repo.git", ComposeSSHCloneURL(&user_model.User{Name: "doer"}, "user", "repo")) +} + +func TestIsUsableRepoName(t *testing.T) { + assert.NoError(t, IsUsableRepoName("a")) + assert.NoError(t, IsUsableRepoName("-1_.")) + assert.NoError(t, IsUsableRepoName(".profile")) + + assert.Error(t, IsUsableRepoName("-")) + assert.Error(t, IsUsableRepoName("🌞")) + assert.Error(t, IsUsableRepoName("the..repo")) + assert.Error(t, IsUsableRepoName("foo.wiki")) + assert.Error(t, IsUsableRepoName("foo.git")) + assert.Error(t, IsUsableRepoName("foo.RSS")) } diff --git a/models/repo/transfer.go b/models/repo/transfer.go index 43e15b33bcf..b669145d68a 100644 --- a/models/repo/transfer.go +++ b/models/repo/transfer.go @@ -68,6 +68,7 @@ type RepoTransfer struct { //nolint RecipientID int64 Recipient *user_model.User `xorm:"-"` RepoID int64 + Repo *Repository `xorm:"-"` TeamIDs []int64 Teams []*organization.Team `xorm:"-"` @@ -79,48 +80,65 @@ func init() { db.RegisterModel(new(RepoTransfer)) } -// LoadAttributes fetches the transfer recipient from the database -func (r *RepoTransfer) LoadAttributes(ctx context.Context) error { +func (r *RepoTransfer) LoadRecipient(ctx context.Context) error { if r.Recipient == nil { u, err := user_model.GetUserByID(ctx, r.RecipientID) if err != nil { return err } - r.Recipient = u } - if r.Recipient.IsOrganization() && len(r.TeamIDs) != len(r.Teams) { - for _, v := range r.TeamIDs { - team, err := organization.GetTeamByID(ctx, v) - if err != nil { - return err - } + return nil +} - if team.OrgID != r.Recipient.ID { - return fmt.Errorf("team %d belongs not to org %d", v, r.Recipient.ID) - } +func (r *RepoTransfer) LoadRepo(ctx context.Context) error { + if r.Repo == nil { + repo, err := GetRepositoryByID(ctx, r.RepoID) + if err != nil { + return err + } + r.Repo = repo + } + return nil +} + +// LoadAttributes fetches the transfer recipient from the database +func (r *RepoTransfer) LoadAttributes(ctx context.Context) error { + if err := r.LoadRecipient(ctx); err != nil { + return err + } + + if r.Recipient.IsOrganization() && r.Teams == nil { + teamsMap, err := organization.GetTeamsByIDs(ctx, r.TeamIDs) + if err != nil { + return err + } + for _, team := range teamsMap { r.Teams = append(r.Teams, team) } } + if err := r.LoadRepo(ctx); err != nil { + return err + } + if r.Doer == nil { u, err := user_model.GetUserByID(ctx, r.DoerID) if err != nil { return err } - r.Doer = u } return nil } -// CanUserAcceptTransfer checks if the user has the rights to accept/decline a repo transfer. +// CanUserAcceptOrRejectTransfer checks if the user has the rights to accept/decline a repo transfer. // For user, it checks if it's himself // For organizations, it checks if the user is able to create repos -func (r *RepoTransfer) CanUserAcceptTransfer(ctx context.Context, u *user_model.User) bool { +func (r *RepoTransfer) CanUserAcceptOrRejectTransfer(ctx context.Context, u *user_model.User) bool { if err := r.LoadAttributes(ctx); err != nil { log.Error("LoadAttributes: %v", err) return false @@ -166,6 +184,10 @@ func GetPendingRepositoryTransfers(ctx context.Context, opts *PendingRepositoryT Find(&transfers) } +func IsRepositoryTransferExist(ctx context.Context, repoID int64) (bool, error) { + return db.GetEngine(ctx).Where("repo_id = ?", repoID).Exist(new(RepoTransfer)) +} + // GetPendingRepositoryTransfer fetches the most recent and ongoing transfer // process for the repository func GetPendingRepositoryTransfer(ctx context.Context, repo *Repository) (*RepoTransfer, error) { @@ -206,11 +228,26 @@ func CreatePendingRepositoryTransfer(ctx context.Context, doer, newOwner *user_m return err } + if _, err := user_model.GetUserByID(ctx, newOwner.ID); err != nil { + return err + } + // Make sure repo is ready to transfer if err := TestRepositoryReadyForTransfer(repo.Status); err != nil { return err } + exist, err := IsRepositoryTransferExist(ctx, repo.ID) + if err != nil { + return err + } + if exist { + return ErrRepoTransferInProgress{ + Uname: repo.Owner.LowerName, + Name: repo.Name, + } + } + repo.Status = RepositoryPendingTransfer if err := UpdateRepositoryCols(ctx, repo, "status"); err != nil { return err diff --git a/models/repo/update.go b/models/repo/update.go index e7ca2240282..fce357a1acf 100644 --- a/models/repo/update.go +++ b/models/repo/update.go @@ -46,6 +46,12 @@ func UpdateRepositoryCols(ctx context.Context, repo *Repository, cols ...string) return err } +// UpdateRepositoryColsNoAutoTime updates repository's columns and but applies time change automatically +func UpdateRepositoryColsNoAutoTime(ctx context.Context, repo *Repository, cols ...string) error { + _, err := db.GetEngine(ctx).ID(repo.ID).Cols(cols...).NoAutoTime().Update(repo) + return err +} + // ErrReachLimitOfRepo represents a "ReachLimitOfRepo" kind of error. type ErrReachLimitOfRepo struct { Limit int diff --git a/models/repo/wiki.go b/models/repo/wiki.go index b378666a206..4239a815b28 100644 --- a/models/repo/wiki.go +++ b/models/repo/wiki.go @@ -5,6 +5,7 @@ package repo import ( + "context" "fmt" "path/filepath" "strings" @@ -72,8 +73,8 @@ func (err ErrWikiInvalidFileName) Unwrap() error { } // WikiCloneLink returns clone URLs of repository wiki. -func (repo *Repository) WikiCloneLink() *CloneLink { - return repo.cloneLink(true) +func (repo *Repository) WikiCloneLink(ctx context.Context, doer *user_model.User) *CloneLink { + return repo.cloneLink(ctx, doer, repo.Name+".wiki") } // WikiPath returns wiki data path by given user and repository name. diff --git a/models/repo/wiki_test.go b/models/repo/wiki_test.go index 629986f741a..0157b7735d0 100644 --- a/models/repo/wiki_test.go +++ b/models/repo/wiki_test.go @@ -4,6 +4,7 @@ package repo_test import ( + "context" "path/filepath" "testing" @@ -18,7 +19,7 @@ func TestRepository_WikiCloneLink(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) - cloneLink := repo.WikiCloneLink() + cloneLink := repo.WikiCloneLink(context.Background(), nil) assert.Equal(t, "ssh://sshuser@try.gitea.io:3000/user2/repo1.wiki.git", cloneLink.SSH) assert.Equal(t, "https://try.gitea.io/user2/repo1.wiki.git", cloneLink.HTTPS) } diff --git a/models/system/notice_test.go b/models/system/notice_test.go index 599b2fb65c3..9fc9e6cce19 100644 --- a/models/system/notice_test.go +++ b/models/system/notice_test.go @@ -45,8 +45,6 @@ func TestCreateRepositoryNotice(t *testing.T) { unittest.AssertExistsAndLoadBean(t, noticeBean) } -// TODO TestRemoveAllWithNotice - func TestCountNotices(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) assert.Equal(t, int64(3), system.CountNotices(db.DefaultContext)) diff --git a/models/unittest/fscopy.go b/models/unittest/fscopy.go index 4d7ee2151dc..b7ba6b7ef54 100644 --- a/models/unittest/fscopy.go +++ b/models/unittest/fscopy.go @@ -11,35 +11,13 @@ import ( "code.gitea.io/gitea/modules/util" ) -// Copy copies file from source to target path. -func Copy(src, dest string) error { - // Gather file information to set back later. - si, err := os.Lstat(src) - if err != nil { - return err - } - - // Handle symbolic link. - if si.Mode()&os.ModeSymlink != 0 { - target, err := os.Readlink(src) - if err != nil { - return err - } - // NOTE: os.Chmod and os.Chtimes don't recognize symbolic link, - // which will lead "no such file or directory" error. - return os.Symlink(target, dest) - } - - return util.CopyFile(src, dest) -} - -// Sync synchronizes the two files. This is skipped if both files +// SyncFile synchronizes the two files. This is skipped if both files // exist and the size, modtime, and mode match. -func Sync(srcPath, destPath string) error { +func SyncFile(srcPath, destPath string) error { dest, err := os.Stat(destPath) if err != nil { if os.IsNotExist(err) { - return Copy(srcPath, destPath) + return util.CopyFile(srcPath, destPath) } return err } @@ -55,7 +33,7 @@ func Sync(srcPath, destPath string) error { return nil } - return Copy(srcPath, destPath) + return util.CopyFile(srcPath, destPath) } // SyncDirs synchronizes files recursively from source to target directory. @@ -66,37 +44,45 @@ func SyncDirs(srcPath, destPath string) error { return err } + // the keep file is used to keep the directory in a git repository, it doesn't need to be synced + // and go-git doesn't work with the ".keep" file (it would report errors like "ref is empty") + const keepFile = ".keep" + // find and delete all untracked files - destFiles, err := util.StatDir(destPath, true) + destFiles, err := util.ListDirRecursively(destPath, &util.ListDirOptions{IncludeDir: true}) if err != nil { return err } for _, destFile := range destFiles { destFilePath := filepath.Join(destPath, destFile) + shouldRemove := filepath.Base(destFilePath) == keepFile if _, err = os.Stat(filepath.Join(srcPath, destFile)); err != nil { if os.IsNotExist(err) { - // if src file does not exist, remove dest file - if err = os.RemoveAll(destFilePath); err != nil { - return err - } + shouldRemove = true } else { return err } } + // if src file does not exist, remove dest file + if shouldRemove { + if err = os.RemoveAll(destFilePath); err != nil { + return err + } + } } // sync src files to dest - srcFiles, err := util.StatDir(srcPath, true) + srcFiles, err := util.ListDirRecursively(srcPath, &util.ListDirOptions{IncludeDir: true}) if err != nil { return err } for _, srcFile := range srcFiles { destFilePath := filepath.Join(destPath, srcFile) - // util.StatDir appends a slash to the directory name + // util.ListDirRecursively appends a slash to the directory name if strings.HasSuffix(srcFile, "/") { err = os.MkdirAll(destFilePath, os.ModePerm) - } else { - err = Sync(filepath.Join(srcPath, srcFile), destFilePath) + } else if filepath.Base(destFilePath) != keepFile { + err = SyncFile(filepath.Join(srcPath, srcFile), destFilePath) } if err != nil { return err diff --git a/models/unittest/testdb.go b/models/unittest/testdb.go index 75044d19e8e..7a9ca9698d0 100644 --- a/models/unittest/testdb.go +++ b/models/unittest/testdb.go @@ -59,6 +59,7 @@ func InitSettings() { _ = hash.Register("dummy", hash.NewDummyHasher) setting.PasswordHashAlgo, _ = hash.SetDefaultPasswordHashAlgorithm("dummy") + setting.InitGiteaEnvVarsForTesting() } // TestOptions represents test options @@ -83,6 +84,7 @@ func MainTest(m *testing.M, testOptsArg ...*TestOptions) { setting.IsInTesting = true setting.AppURL = "https://try.gitea.io/" + setting.Domain = "try.gitea.io" setting.RunUser = "runuser" setting.SSH.User = "sshuser" setting.SSH.BuiltinServerUser = "builtinuser" diff --git a/models/user/avatar.go b/models/user/avatar.go index 5453c78fc61..2a41b991292 100644 --- a/models/user/avatar.go +++ b/models/user/avatar.go @@ -38,27 +38,30 @@ func GenerateRandomAvatar(ctx context.Context, u *User) error { u.Avatar = avatars.HashEmail(seed) - // Don't share the images so that we can delete them easily - if err := storage.SaveFrom(storage.Avatars, u.CustomAvatarRelativePath(), func(w io.Writer) error { - if err := png.Encode(w, img); err != nil { - log.Error("Encode: %v", err) + _, err = storage.Avatars.Stat(u.CustomAvatarRelativePath()) + if err != nil { + // If unable to Stat the avatar file (usually it means non-existing), then try to save a new one + // Don't share the images so that we can delete them easily + if err := storage.SaveFrom(storage.Avatars, u.CustomAvatarRelativePath(), func(w io.Writer) error { + if err := png.Encode(w, img); err != nil { + log.Error("Encode: %v", err) + } + return nil + }); err != nil { + return fmt.Errorf("failed to save avatar %s: %w", u.CustomAvatarRelativePath(), err) } - return err - }); err != nil { - return fmt.Errorf("Failed to create dir %s: %w", u.CustomAvatarRelativePath(), err) } if _, err := db.GetEngine(ctx).ID(u.ID).Cols("avatar").Update(u); err != nil { return err } - log.Info("New random avatar created: %d", u.ID) return nil } // AvatarLinkWithSize returns a link to the user's avatar with size. size <= 0 means default size func (u *User) AvatarLinkWithSize(ctx context.Context, size int) string { - if u.IsGhost() { + if u.IsGhost() || u.IsGiteaActions() { return avatars.DefaultAvatarLink() } diff --git a/models/user/avatar_test.go b/models/user/avatar_test.go index 1078875ee16..a1cc01316f3 100644 --- a/models/user/avatar_test.go +++ b/models/user/avatar_test.go @@ -4,13 +4,19 @@ package user import ( + "context" + "io" + "strings" "testing" "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/storage" "code.gitea.io/gitea/modules/test" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestUserAvatarLink(t *testing.T) { @@ -26,3 +32,37 @@ func TestUserAvatarLink(t *testing.T) { link = u.AvatarLink(db.DefaultContext) assert.Equal(t, "https://localhost/sub-path/avatars/avatar.png", link) } + +func TestUserAvatarGenerate(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + var err error + tmpDir := t.TempDir() + storage.Avatars, err = storage.NewLocalStorage(context.Background(), &setting.Storage{Path: tmpDir}) + require.NoError(t, err) + + u := unittest.AssertExistsAndLoadBean(t, &User{ID: 2}) + + // there was no avatar, generate a new one + assert.Empty(t, u.Avatar) + err = GenerateRandomAvatar(db.DefaultContext, u) + require.NoError(t, err) + assert.NotEmpty(t, u.Avatar) + + // make sure the generated one exists + oldAvatarPath := u.CustomAvatarRelativePath() + _, err = storage.Avatars.Stat(u.CustomAvatarRelativePath()) + require.NoError(t, err) + // and try to change its content + _, err = storage.Avatars.Save(u.CustomAvatarRelativePath(), strings.NewReader("abcd"), 4) + require.NoError(t, err) + + // try to generate again + err = GenerateRandomAvatar(db.DefaultContext, u) + require.NoError(t, err) + assert.Equal(t, oldAvatarPath, u.CustomAvatarRelativePath()) + f, err := storage.Avatars.Open(u.CustomAvatarRelativePath()) + require.NoError(t, err) + defer f.Close() + content, _ := io.ReadAll(f) + assert.Equal(t, "abcd", string(content)) +} diff --git a/models/user/email_address.go b/models/user/email_address.go index 74ba5f617a5..2ba6a564502 100644 --- a/models/user/email_address.go +++ b/models/user/email_address.go @@ -8,7 +8,6 @@ import ( "context" "fmt" "net/mail" - "regexp" "strings" "time" @@ -153,8 +152,6 @@ func UpdateEmailAddress(ctx context.Context, email *EmailAddress) error { return err } -var emailRegexp = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]*@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$") - // ValidateEmail check if email is a valid & allowed address func ValidateEmail(email string) error { if err := validateEmailBasic(email); err != nil { @@ -514,7 +511,7 @@ func validateEmailBasic(email string) error { return ErrEmailInvalid{email} } - if !emailRegexp.MatchString(email) { + if !globalVars().emailRegexp.MatchString(email) { return ErrEmailCharIsNotSupported{email} } @@ -545,3 +542,13 @@ func IsEmailDomainAllowed(email string) bool { return validation.IsEmailDomainListed(setting.Service.EmailDomainAllowList, email) } + +func GetActivatedEmailAddresses(ctx context.Context, uid int64) ([]string, error) { + emails := make([]string, 0, 2) + if err := db.GetEngine(ctx).Table("email_address").Select("email"). + Where("uid=? AND is_activated=?", uid, true).Asc("id"). + Find(&emails); err != nil { + return nil, err + } + return emails, nil +} diff --git a/models/user/openid.go b/models/user/openid.go index ee4ecabae0b..420c67ca183 100644 --- a/models/user/openid.go +++ b/models/user/openid.go @@ -11,9 +11,6 @@ import ( "code.gitea.io/gitea/modules/util" ) -// ErrOpenIDNotExist openid is not known -var ErrOpenIDNotExist = util.NewNotExistErrorf("OpenID is unknown") - // UserOpenID is the list of all OpenID identities of a user. // Since this is a middle table, name it OpenID is not suitable, so we ignore the lint here type UserOpenID struct { //revive:disable-line:exported @@ -99,7 +96,7 @@ func DeleteUserOpenID(ctx context.Context, openid *UserOpenID) (err error) { if err != nil { return err } else if deleted != 1 { - return ErrOpenIDNotExist + return util.NewNotExistErrorf("OpenID is unknown") } return nil } diff --git a/models/user/openid_test.go b/models/user/openid_test.go index 27e6edd1e00..708af9e6530 100644 --- a/models/user/openid_test.go +++ b/models/user/openid_test.go @@ -11,6 +11,7 @@ import ( user_model "code.gitea.io/gitea/models/user" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestGetUserOpenIDs(t *testing.T) { @@ -34,30 +35,23 @@ func TestGetUserOpenIDs(t *testing.T) { func TestToggleUserOpenIDVisibility(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) oids, err := user_model.GetUserOpenIDs(db.DefaultContext, int64(2)) - if !assert.NoError(t, err) || !assert.Len(t, oids, 1) { - return - } + require.NoError(t, err) + require.Len(t, oids, 1) assert.True(t, oids[0].Show) err = user_model.ToggleUserOpenIDVisibility(db.DefaultContext, oids[0].ID) - if !assert.NoError(t, err) { - return - } + require.NoError(t, err) oids, err = user_model.GetUserOpenIDs(db.DefaultContext, int64(2)) - if !assert.NoError(t, err) || !assert.Len(t, oids, 1) { - return - } + require.NoError(t, err) + require.Len(t, oids, 1) + assert.False(t, oids[0].Show) err = user_model.ToggleUserOpenIDVisibility(db.DefaultContext, oids[0].ID) - if !assert.NoError(t, err) { - return - } + require.NoError(t, err) oids, err = user_model.GetUserOpenIDs(db.DefaultContext, int64(2)) - if !assert.NoError(t, err) { - return - } + require.NoError(t, err) if assert.Len(t, oids, 1) { assert.True(t, oids[0].Show) } diff --git a/models/user/user.go b/models/user/user.go index 19879fbcc79..293c8769577 100644 --- a/models/user/user.go +++ b/models/user/user.go @@ -14,6 +14,7 @@ import ( "path/filepath" "regexp" "strings" + "sync" "time" "unicode" @@ -213,7 +214,7 @@ func (u *User) GetPlaceholderEmail() string { return fmt.Sprintf("%s@%s", u.LowerName, setting.Service.NoReplyAddress) } -// GetEmail returns an noreply email, if the user has set to keep his +// GetEmail returns a noreply email, if the user has set to keep his // email address private, otherwise the primary email address. func (u *User) GetEmail() string { if u.KeepEmailPrivate { @@ -384,11 +385,12 @@ func (u *User) ValidatePassword(passwd string) bool { } // IsPasswordSet checks if the password is set or left empty +// TODO: It's better to clarify the "password" behavior for different types (individual, bot) func (u *User) IsPasswordSet() bool { - return len(u.Passwd) != 0 + return u.Passwd != "" } -// IsOrganization returns true if user is actually a organization. +// IsOrganization returns true if user is actually an organization. func (u *User) IsOrganization() bool { return u.Type == UserTypeOrganization } @@ -398,13 +400,14 @@ func (u *User) IsIndividual() bool { return u.Type == UserTypeIndividual } -func (u *User) IsUser() bool { - return u.Type == UserTypeIndividual || u.Type == UserTypeBot +// IsTypeBot returns whether the user is of type bot +func (u *User) IsTypeBot() bool { + return u.Type == UserTypeBot } -// IsBot returns whether or not the user is of type bot -func (u *User) IsBot() bool { - return u.Type == UserTypeBot +// IsTokenAccessAllowed returns whether the user is an individual or a bot (which allows for token access) +func (u *User) IsTokenAccessAllowed() bool { + return u.Type == UserTypeIndividual || u.Type == UserTypeBot } // DisplayName returns full name if it's not empty, @@ -417,19 +420,9 @@ func (u *User) DisplayName() string { return u.Name } -var emailToReplacer = strings.NewReplacer( - "\n", "", - "\r", "", - "<", "", - ">", "", - ",", "", - ":", "", - ";", "", -) - // EmailTo returns a string suitable to be put into a e-mail `To:` header. func (u *User) EmailTo() string { - sanitizedDisplayName := emailToReplacer.Replace(u.DisplayName()) + sanitizedDisplayName := globalVars().emailToReplacer.Replace(u.DisplayName()) // should be an edge case but nice to have if sanitizedDisplayName == u.Email { @@ -502,10 +495,10 @@ func (u *User) IsMailable() bool { return u.IsActive } -// IsUserExist checks if given user name exist, -// the user name should be noncased unique. +// IsUserExist checks if given username exist, +// the username should be non-cased unique. // If uid is presented, then check will rule out that one, -// it is used when update a user name in settings page. +// it is used when update a username in settings page. func IsUserExist(ctx context.Context, uid int64, name string) (bool, error) { if len(name) == 0 { return false, nil @@ -515,7 +508,7 @@ func IsUserExist(ctx context.Context, uid int64, name string) (bool, error) { Get(&User{LowerName: strings.ToLower(name)}) } -// Note: As of the beginning of 2022, it is recommended to use at least +// SaltByteLength as of the beginning of 2022, it is recommended to use at least // 64 bits of salt, but NIST is already recommending to use to 128 bits. // (16 bytes = 16 * 8 = 128 bits) const SaltByteLength = 16 @@ -526,28 +519,58 @@ func GetUserSalt() (string, error) { if err != nil { return "", err } - // Returns a 32 bytes long string. + // Returns a 32-byte long string. return hex.EncodeToString(rBytes), nil } -// Note: The set of characters here can safely expand without a breaking change, -// but characters removed from this set can cause user account linking to break -var ( - customCharsReplacement = strings.NewReplacer("Æ", "AE") - removeCharsRE = regexp.MustCompile("['`´]") - transformDiacritics = transform.Chain(norm.NFD, runes.Remove(runes.In(unicode.Mn)), norm.NFC) - replaceCharsHyphenRE = regexp.MustCompile(`[\s~+]`) -) +type globalVarsStruct struct { + customCharsReplacement *strings.Replacer + removeCharsRE *regexp.Regexp + transformDiacritics transform.Transformer + replaceCharsHyphenRE *regexp.Regexp + emailToReplacer *strings.Replacer + emailRegexp *regexp.Regexp + systemUserNewFuncs map[int64]func() *User +} + +var globalVars = sync.OnceValue(func() *globalVarsStruct { + return &globalVarsStruct{ + // Note: The set of characters here can safely expand without a breaking change, + // but characters removed from this set can cause user account linking to break + customCharsReplacement: strings.NewReplacer("Æ", "AE"), + + removeCharsRE: regexp.MustCompile("['`´]"), + transformDiacritics: transform.Chain(norm.NFD, runes.Remove(runes.In(unicode.Mn)), norm.NFC), + replaceCharsHyphenRE: regexp.MustCompile(`[\s~+]`), + + emailToReplacer: strings.NewReplacer( + "\n", "", + "\r", "", + "<", "", + ">", "", + ",", "", + ":", "", + ";", "", + ), + emailRegexp: regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]*@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$"), + + systemUserNewFuncs: map[int64]func() *User{ + GhostUserID: NewGhostUser, + ActionsUserID: NewActionsUser, + }, + } +}) // NormalizeUserName only takes the name part if it is an email address, transforms it diacritics to ASCII characters. // It returns a string with the single-quotes removed, and any other non-supported username characters are replaced with a `-` character func NormalizeUserName(s string) (string, error) { + vars := globalVars() s, _, _ = strings.Cut(s, "@") - strDiacriticsRemoved, n, err := transform.String(transformDiacritics, customCharsReplacement.Replace(s)) + strDiacriticsRemoved, n, err := transform.String(vars.transformDiacritics, vars.customCharsReplacement.Replace(s)) if err != nil { return "", fmt.Errorf("failed to normalize the string of provided username %q at position %d", s, n) } - return replaceCharsHyphenRE.ReplaceAllLiteralString(removeCharsRE.ReplaceAllLiteralString(strDiacriticsRemoved, ""), "-"), nil + return vars.replaceCharsHyphenRE.ReplaceAllLiteralString(vars.removeCharsRE.ReplaceAllLiteralString(strDiacriticsRemoved, ""), "-"), nil } var ( @@ -963,30 +986,28 @@ func GetUserByIDs(ctx context.Context, ids []int64) ([]*User, error) { return users, err } -// GetPossibleUserByID returns the user if id > 0 or return system usrs if id < 0 +// GetPossibleUserByID returns the user if id > 0 or returns system user if id < 0 func GetPossibleUserByID(ctx context.Context, id int64) (*User, error) { - switch id { - case GhostUserID: - return NewGhostUser(), nil - case ActionsUserID: - return NewActionsUser(), nil - case 0: + if id < 0 { + if newFunc, ok := globalVars().systemUserNewFuncs[id]; ok { + return newFunc(), nil + } + return nil, ErrUserNotExist{UID: id} + } else if id == 0 { return nil, ErrUserNotExist{} - default: - return GetUserByID(ctx, id) } + return GetUserByID(ctx, id) } -// GetPossibleUserByIDs returns the users if id > 0 or return system users if id < 0 +// GetPossibleUserByIDs returns the users if id > 0 or returns system users if id < 0 func GetPossibleUserByIDs(ctx context.Context, ids []int64) ([]*User, error) { uniqueIDs := container.SetOf(ids...) users := make([]*User, 0, len(ids)) _ = uniqueIDs.Remove(0) - if uniqueIDs.Remove(GhostUserID) { - users = append(users, NewGhostUser()) - } - if uniqueIDs.Remove(ActionsUserID) { - users = append(users, NewActionsUser()) + for systemUID, newFunc := range globalVars().systemUserNewFuncs { + if uniqueIDs.Remove(systemUID) { + users = append(users, newFunc()) + } } res, err := GetUserByIDs(ctx, uniqueIDs.Values()) if err != nil { @@ -996,7 +1017,7 @@ func GetPossibleUserByIDs(ctx context.Context, ids []int64) ([]*User, error) { return users, nil } -// GetUserByNameCtx returns user by given name. +// GetUserByName returns user by given name. func GetUserByName(ctx context.Context, name string) (*User, error) { if len(name) == 0 { return nil, ErrUserNotExist{Name: name} @@ -1027,8 +1048,8 @@ func GetUserEmailsByNames(ctx context.Context, names []string) []string { return mails } -// GetMaileableUsersByIDs gets users from ids, but only if they can receive mails -func GetMaileableUsersByIDs(ctx context.Context, ids []int64, isMention bool) ([]*User, error) { +// GetMailableUsersByIDs gets users from ids, but only if they can receive mails +func GetMailableUsersByIDs(ctx context.Context, ids []int64, isMention bool) ([]*User, error) { if len(ids) == 0 { return nil, nil } @@ -1053,17 +1074,6 @@ func GetMaileableUsersByIDs(ctx context.Context, ids []int64, isMention bool) ([ Find(&ous) } -// GetUserNamesByIDs returns usernames for all resolved users from a list of Ids. -func GetUserNamesByIDs(ctx context.Context, ids []int64) ([]string, error) { - unames := make([]string, 0, len(ids)) - err := db.GetEngine(ctx).In("id", ids). - Table("user"). - Asc("name"). - Cols("name"). - Find(&unames) - return unames, err -} - // GetUserNameByID returns username for the id func GetUserNameByID(ctx context.Context, id int64) (string, error) { var name string diff --git a/models/user/user_list.go b/models/user/user_list.go new file mode 100644 index 00000000000..c66d59f0d92 --- /dev/null +++ b/models/user/user_list.go @@ -0,0 +1,47 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package user + +import ( + "context" + + "code.gitea.io/gitea/models/db" +) + +func GetUsersMapByIDs(ctx context.Context, userIDs []int64) (map[int64]*User, error) { + userMaps := make(map[int64]*User, len(userIDs)) + left := len(userIDs) + for left > 0 { + limit := db.DefaultMaxInSize + if left < limit { + limit = left + } + err := db.GetEngine(ctx). + In("id", userIDs[:limit]). + Find(&userMaps) + if err != nil { + return nil, err + } + left -= limit + userIDs = userIDs[limit:] + } + return userMaps, nil +} + +func GetPossibleUserFromMap(userID int64, usererMaps map[int64]*User) *User { + switch userID { + case GhostUserID: + return NewGhostUser() + case ActionsUserID: + return NewActionsUser() + case 0: + return nil + default: + user, ok := usererMaps[userID] + if !ok { + return NewGhostUser() + } + return user + } +} diff --git a/models/user/user_system.go b/models/user/user_system.go index 612cdb2caef..6fbfd9e69e5 100644 --- a/models/user/user_system.go +++ b/models/user/user_system.go @@ -10,9 +10,8 @@ import ( ) const ( - GhostUserID = -1 - GhostUserName = "Ghost" - GhostUserLowerName = "ghost" + GhostUserID = -1 + GhostUserName = "Ghost" ) // NewGhostUser creates and returns a fake user for someone has deleted their account. @@ -20,10 +19,14 @@ func NewGhostUser() *User { return &User{ ID: GhostUserID, Name: GhostUserName, - LowerName: GhostUserLowerName, + LowerName: strings.ToLower(GhostUserName), } } +func IsGhostUserName(name string) bool { + return strings.EqualFold(name, GhostUserName) +} + // IsGhost check if user is fake user for a deleted account func (u *User) IsGhost() bool { if u == nil { @@ -32,22 +35,16 @@ func (u *User) IsGhost() bool { return u.ID == GhostUserID && u.Name == GhostUserName } -// NewReplaceUser creates and returns a fake user for external user -func NewReplaceUser(name string) *User { - return &User{ - ID: 0, - Name: name, - LowerName: strings.ToLower(name), - } -} - const ( - ActionsUserID = -2 - ActionsUserName = "gitea-actions" - ActionsFullName = "Gitea Actions" - ActionsEmail = "teabot@gitea.io" + ActionsUserID = -2 + ActionsUserName = "gitea-actions" + ActionsUserEmail = "teabot@gitea.io" ) +func IsGiteaActionsUserName(name string) bool { + return strings.EqualFold(name, ActionsUserName) +} + // NewActionsUser creates and returns a fake user for running the actions. func NewActionsUser() *User { return &User{ @@ -55,16 +52,26 @@ func NewActionsUser() *User { Name: ActionsUserName, LowerName: ActionsUserName, IsActive: true, - FullName: ActionsFullName, - Email: ActionsEmail, + FullName: "Gitea Actions", + Email: ActionsUserEmail, KeepEmailPrivate: true, LoginName: ActionsUserName, - Type: UserTypeIndividual, + Type: UserTypeBot, AllowCreateOrganization: true, Visibility: structs.VisibleTypePublic, } } -func (u *User) IsActions() bool { +func (u *User) IsGiteaActions() bool { return u != nil && u.ID == ActionsUserID } + +func GetSystemUserByName(name string) *User { + if IsGhostUserName(name) { + return NewGhostUser() + } + if IsGiteaActionsUserName(name) { + return NewActionsUser() + } + return nil +} diff --git a/models/user/user_system_test.go b/models/user/user_system_test.go new file mode 100644 index 00000000000..97768b509be --- /dev/null +++ b/models/user/user_system_test.go @@ -0,0 +1,32 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package user + +import ( + "testing" + + "code.gitea.io/gitea/models/db" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestSystemUser(t *testing.T) { + u, err := GetPossibleUserByID(db.DefaultContext, -1) + require.NoError(t, err) + assert.Equal(t, "Ghost", u.Name) + assert.Equal(t, "ghost", u.LowerName) + assert.True(t, u.IsGhost()) + assert.True(t, IsGhostUserName("gHost")) + + u, err = GetPossibleUserByID(db.DefaultContext, -2) + require.NoError(t, err) + assert.Equal(t, "gitea-actions", u.Name) + assert.Equal(t, "gitea-actions", u.LowerName) + assert.True(t, u.IsGiteaActions()) + assert.True(t, IsGiteaActionsUserName("Gitea-actionS")) + + _, err = GetPossibleUserByID(db.DefaultContext, -3) + require.Error(t, err) +} diff --git a/models/user/user_test.go b/models/user/user_test.go index 7ebc64f69e7..51098417e6d 100644 --- a/models/user/user_test.go +++ b/models/user/user_test.go @@ -25,6 +25,21 @@ import ( "github.com/stretchr/testify/assert" ) +func TestIsUsableUsername(t *testing.T) { + assert.NoError(t, user_model.IsUsableUsername("a")) + assert.NoError(t, user_model.IsUsableUsername("foo.wiki")) + assert.NoError(t, user_model.IsUsableUsername("foo.git")) + + assert.Error(t, user_model.IsUsableUsername("a--b")) + assert.Error(t, user_model.IsUsableUsername("-1_.")) + assert.Error(t, user_model.IsUsableUsername(".profile")) + assert.Error(t, user_model.IsUsableUsername("-")) + assert.Error(t, user_model.IsUsableUsername("🌞")) + assert.Error(t, user_model.IsUsableUsername("the..repo")) + assert.Error(t, user_model.IsUsableUsername("foo.RSS")) + assert.Error(t, user_model.IsUsableUsername("foo.PnG")) +} + func TestOAuth2Application_LoadUser(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) app := unittest.AssertExistsAndLoadBean(t, &auth.OAuth2Application{ID: 1}) @@ -318,14 +333,14 @@ func TestGetUserIDsByNames(t *testing.T) { func TestGetMaileableUsersByIDs(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - results, err := user_model.GetMaileableUsersByIDs(db.DefaultContext, []int64{1, 4}, false) + results, err := user_model.GetMailableUsersByIDs(db.DefaultContext, []int64{1, 4}, false) assert.NoError(t, err) assert.Len(t, results, 1) if len(results) > 1 { assert.Equal(t, 1, results[0].ID) } - results, err = user_model.GetMaileableUsersByIDs(db.DefaultContext, []int64{1, 4}, true) + results, err = user_model.GetMailableUsersByIDs(db.DefaultContext, []int64{1, 4}, true) assert.NoError(t, err) assert.Len(t, results, 2) if len(results) > 2 { diff --git a/models/webhook/webhook.go b/models/webhook/webhook.go index 894357e36a1..97ad3730277 100644 --- a/models/webhook/webhook.go +++ b/models/webhook/webhook.go @@ -167,186 +167,39 @@ func (w *Webhook) UpdateEvent() error { return err } -// HasCreateEvent returns true if hook enabled create event. -func (w *Webhook) HasCreateEvent() bool { - return w.SendEverything || - (w.ChooseEvents && w.HookEvents.Create) -} - -// HasDeleteEvent returns true if hook enabled delete event. -func (w *Webhook) HasDeleteEvent() bool { - return w.SendEverything || - (w.ChooseEvents && w.HookEvents.Delete) -} - -// HasForkEvent returns true if hook enabled fork event. -func (w *Webhook) HasForkEvent() bool { - return w.SendEverything || - (w.ChooseEvents && w.HookEvents.Fork) -} - -// HasIssuesEvent returns true if hook enabled issues event. -func (w *Webhook) HasIssuesEvent() bool { - return w.SendEverything || - (w.ChooseEvents && w.HookEvents.Issues) -} - -// HasIssuesAssignEvent returns true if hook enabled issues assign event. -func (w *Webhook) HasIssuesAssignEvent() bool { - return w.SendEverything || - (w.ChooseEvents && w.HookEvents.IssueAssign) -} - -// HasIssuesLabelEvent returns true if hook enabled issues label event. -func (w *Webhook) HasIssuesLabelEvent() bool { - return w.SendEverything || - (w.ChooseEvents && w.HookEvents.IssueLabel) -} - -// HasIssuesMilestoneEvent returns true if hook enabled issues milestone event. -func (w *Webhook) HasIssuesMilestoneEvent() bool { - return w.SendEverything || - (w.ChooseEvents && w.HookEvents.IssueMilestone) -} - -// HasIssueCommentEvent returns true if hook enabled issue_comment event. -func (w *Webhook) HasIssueCommentEvent() bool { - return w.SendEverything || - (w.ChooseEvents && w.HookEvents.IssueComment) -} - -// HasPushEvent returns true if hook enabled push event. -func (w *Webhook) HasPushEvent() bool { - return w.PushOnly || w.SendEverything || - (w.ChooseEvents && w.HookEvents.Push) -} - -// HasPullRequestEvent returns true if hook enabled pull request event. -func (w *Webhook) HasPullRequestEvent() bool { - return w.SendEverything || - (w.ChooseEvents && w.HookEvents.PullRequest) -} - -// HasPullRequestAssignEvent returns true if hook enabled pull request assign event. -func (w *Webhook) HasPullRequestAssignEvent() bool { - return w.SendEverything || - (w.ChooseEvents && w.HookEvents.PullRequestAssign) -} - -// HasPullRequestLabelEvent returns true if hook enabled pull request label event. -func (w *Webhook) HasPullRequestLabelEvent() bool { - return w.SendEverything || - (w.ChooseEvents && w.HookEvents.PullRequestLabel) -} - -// HasPullRequestMilestoneEvent returns true if hook enabled pull request milestone event. -func (w *Webhook) HasPullRequestMilestoneEvent() bool { - return w.SendEverything || - (w.ChooseEvents && w.HookEvents.PullRequestMilestone) -} - -// HasPullRequestCommentEvent returns true if hook enabled pull_request_comment event. -func (w *Webhook) HasPullRequestCommentEvent() bool { - return w.SendEverything || - (w.ChooseEvents && w.HookEvents.PullRequestComment) -} - -// HasPullRequestApprovedEvent returns true if hook enabled pull request review event. -func (w *Webhook) HasPullRequestApprovedEvent() bool { - return w.SendEverything || - (w.ChooseEvents && w.HookEvents.PullRequestReview) -} - -// HasPullRequestRejectedEvent returns true if hook enabled pull request review event. -func (w *Webhook) HasPullRequestRejectedEvent() bool { - return w.SendEverything || - (w.ChooseEvents && w.HookEvents.PullRequestReview) -} - -// HasPullRequestReviewCommentEvent returns true if hook enabled pull request review event. -func (w *Webhook) HasPullRequestReviewCommentEvent() bool { - return w.SendEverything || - (w.ChooseEvents && w.HookEvents.PullRequestReview) -} - -// HasPullRequestSyncEvent returns true if hook enabled pull request sync event. -func (w *Webhook) HasPullRequestSyncEvent() bool { - return w.SendEverything || - (w.ChooseEvents && w.HookEvents.PullRequestSync) -} - -// HasWikiEvent returns true if hook enabled wiki event. -func (w *Webhook) HasWikiEvent() bool { - return w.SendEverything || - (w.ChooseEvents && w.HookEvent.Wiki) -} - -// HasReleaseEvent returns if hook enabled release event. -func (w *Webhook) HasReleaseEvent() bool { - return w.SendEverything || - (w.ChooseEvents && w.HookEvents.Release) -} - -// HasRepositoryEvent returns if hook enabled repository event. -func (w *Webhook) HasRepositoryEvent() bool { - return w.SendEverything || - (w.ChooseEvents && w.HookEvents.Repository) -} - -// HasPackageEvent returns if hook enabled package event. -func (w *Webhook) HasPackageEvent() bool { - return w.SendEverything || - (w.ChooseEvents && w.HookEvents.Package) -} - -// HasPullRequestReviewRequestEvent returns true if hook enabled pull request review request event. -func (w *Webhook) HasPullRequestReviewRequestEvent() bool { - return w.SendEverything || - (w.ChooseEvents && w.HookEvents.PullRequestReviewRequest) -} - -// EventCheckers returns event checkers -func (w *Webhook) EventCheckers() []struct { - Has func() bool - Type webhook_module.HookEventType -} { - return []struct { - Has func() bool - Type webhook_module.HookEventType - }{ - {w.HasCreateEvent, webhook_module.HookEventCreate}, - {w.HasDeleteEvent, webhook_module.HookEventDelete}, - {w.HasForkEvent, webhook_module.HookEventFork}, - {w.HasPushEvent, webhook_module.HookEventPush}, - {w.HasIssuesEvent, webhook_module.HookEventIssues}, - {w.HasIssuesAssignEvent, webhook_module.HookEventIssueAssign}, - {w.HasIssuesLabelEvent, webhook_module.HookEventIssueLabel}, - {w.HasIssuesMilestoneEvent, webhook_module.HookEventIssueMilestone}, - {w.HasIssueCommentEvent, webhook_module.HookEventIssueComment}, - {w.HasPullRequestEvent, webhook_module.HookEventPullRequest}, - {w.HasPullRequestAssignEvent, webhook_module.HookEventPullRequestAssign}, - {w.HasPullRequestLabelEvent, webhook_module.HookEventPullRequestLabel}, - {w.HasPullRequestMilestoneEvent, webhook_module.HookEventPullRequestMilestone}, - {w.HasPullRequestCommentEvent, webhook_module.HookEventPullRequestComment}, - {w.HasPullRequestApprovedEvent, webhook_module.HookEventPullRequestReviewApproved}, - {w.HasPullRequestRejectedEvent, webhook_module.HookEventPullRequestReviewRejected}, - {w.HasPullRequestCommentEvent, webhook_module.HookEventPullRequestReviewComment}, - {w.HasPullRequestSyncEvent, webhook_module.HookEventPullRequestSync}, - {w.HasWikiEvent, webhook_module.HookEventWiki}, - {w.HasRepositoryEvent, webhook_module.HookEventRepository}, - {w.HasReleaseEvent, webhook_module.HookEventRelease}, - {w.HasPackageEvent, webhook_module.HookEventPackage}, - {w.HasPullRequestReviewRequestEvent, webhook_module.HookEventPullRequestReviewRequest}, +func (w *Webhook) HasEvent(evt webhook_module.HookEventType) bool { + if w.SendEverything { + return true } + if w.PushOnly { + return evt == webhook_module.HookEventPush + } + checkEvt := evt + switch evt { + case webhook_module.HookEventPullRequestReviewApproved, webhook_module.HookEventPullRequestReviewRejected, webhook_module.HookEventPullRequestReviewComment: + checkEvt = webhook_module.HookEventPullRequestReview + } + return w.HookEvents[checkEvt] } // EventsArray returns an array of hook events func (w *Webhook) EventsArray() []string { - events := make([]string, 0, 7) + if w.SendEverything { + events := make([]string, 0, len(webhook_module.AllEvents())) + for _, evt := range webhook_module.AllEvents() { + events = append(events, string(evt)) + } + return events + } - for _, c := range w.EventCheckers() { - if c.Has() { - events = append(events, string(c.Type)) + if w.PushOnly { + return []string{string(webhook_module.HookEventPush)} + } + + events := make([]string, 0, len(w.HookEvents)) + for event, enabled := range w.HookEvents { + if enabled { + events = append(events, string(event)) } } return events diff --git a/models/webhook/webhook_system.go b/models/webhook/webhook_system.go index a2a9ee321ae..58d9d4a5c1b 100644 --- a/models/webhook/webhook_system.go +++ b/models/webhook/webhook_system.go @@ -11,6 +11,19 @@ import ( "code.gitea.io/gitea/modules/optional" ) +// GetSystemOrDefaultWebhooks returns webhooks by given argument or all if argument is missing. +func GetSystemOrDefaultWebhooks(ctx context.Context, isSystemWebhook optional.Option[bool]) ([]*Webhook, error) { + webhooks := make([]*Webhook, 0, 5) + if !isSystemWebhook.Has() { + return webhooks, db.GetEngine(ctx).Where("repo_id=? AND owner_id=?", 0, 0). + Find(&webhooks) + } + + return webhooks, db.GetEngine(ctx). + Where("repo_id=? AND owner_id=? AND is_system_webhook=?", 0, 0, isSystemWebhook.Value()). + Find(&webhooks) +} + // GetDefaultWebhooks returns all admin-default webhooks. func GetDefaultWebhooks(ctx context.Context) ([]*Webhook, error) { webhooks := make([]*Webhook, 0, 5) diff --git a/models/webhook/webhook_system_test.go b/models/webhook/webhook_system_test.go new file mode 100644 index 00000000000..96157ed9c9d --- /dev/null +++ b/models/webhook/webhook_system_test.go @@ -0,0 +1,37 @@ +// Copyright 2017 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package webhook + +import ( + "testing" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/unittest" + "code.gitea.io/gitea/modules/optional" + + "github.com/stretchr/testify/assert" +) + +func TestGetSystemOrDefaultWebhooks(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + hooks, err := GetSystemOrDefaultWebhooks(db.DefaultContext, optional.None[bool]()) + assert.NoError(t, err) + if assert.Len(t, hooks, 2) { + assert.Equal(t, int64(5), hooks[0].ID) + assert.Equal(t, int64(6), hooks[1].ID) + } + + hooks, err = GetSystemOrDefaultWebhooks(db.DefaultContext, optional.Some(true)) + assert.NoError(t, err) + if assert.Len(t, hooks, 1) { + assert.Equal(t, int64(5), hooks[0].ID) + } + + hooks, err = GetSystemOrDefaultWebhooks(db.DefaultContext, optional.Some(false)) + assert.NoError(t, err) + if assert.Len(t, hooks, 1) { + assert.Equal(t, int64(6), hooks[0].ID) + } +} diff --git a/models/webhook/webhook_test.go b/models/webhook/webhook_test.go index c6c3f40d46e..ee53d6da924 100644 --- a/models/webhook/webhook_test.go +++ b/models/webhook/webhook_test.go @@ -54,9 +54,9 @@ func TestWebhook_UpdateEvent(t *testing.T) { SendEverything: false, ChooseEvents: false, HookEvents: webhook_module.HookEvents{ - Create: false, - Push: true, - PullRequest: false, + webhook_module.HookEventCreate: false, + webhook_module.HookEventPush: true, + webhook_module.HookEventPullRequest: false, }, } webhook.HookEvent = hookEvent @@ -68,13 +68,13 @@ func TestWebhook_UpdateEvent(t *testing.T) { } func TestWebhook_EventsArray(t *testing.T) { - assert.Equal(t, []string{ + assert.EqualValues(t, []string{ "create", "delete", "fork", "push", "issues", "issue_assign", "issue_label", "issue_milestone", "issue_comment", "pull_request", "pull_request_assign", "pull_request_label", "pull_request_milestone", "pull_request_comment", "pull_request_review_approved", "pull_request_review_rejected", - "pull_request_review_comment", "pull_request_sync", "wiki", "repository", "release", - "package", "pull_request_review_request", + "pull_request_review_comment", "pull_request_sync", "pull_request_review_request", "wiki", "repository", "release", + "package", "status", }, (&Webhook{ HookEvent: &webhook_module.HookEvent{SendEverything: true}, diff --git a/modules/analyze/vendor_test.go b/modules/analyze/vendor_test.go index aafd3c431b7..02a51d4c8fd 100644 --- a/modules/analyze/vendor_test.go +++ b/modules/analyze/vendor_test.go @@ -3,7 +3,11 @@ package analyze -import "testing" +import ( + "testing" + + "github.com/stretchr/testify/assert" +) func TestIsVendor(t *testing.T) { tests := []struct { @@ -33,9 +37,8 @@ func TestIsVendor(t *testing.T) { } for _, tt := range tests { t.Run(tt.path, func(t *testing.T) { - if got := IsVendor(tt.path); got != tt.want { - t.Errorf("IsVendor() = %v, want %v", got, tt.want) - } + got := IsVendor(tt.path) + assert.Equal(t, tt.want, got) }) } } diff --git a/modules/assetfs/layered.go b/modules/assetfs/layered.go index 9678d23ad67..4f3811ba2b2 100644 --- a/modules/assetfs/layered.go +++ b/modules/assetfs/layered.go @@ -103,7 +103,7 @@ func (l *LayeredFS) ReadLayeredFile(elems ...string) ([]byte, string, error) { } func shouldInclude(info fs.FileInfo, fileMode ...bool) bool { - if util.CommonSkip(info.Name()) { + if util.IsCommonHiddenFileName(info.Name()) { return false } if len(fileMode) == 0 { diff --git a/modules/auth/openid/discovery_cache_test.go b/modules/auth/openid/discovery_cache_test.go index 5a7f450937a..7d4b27c5dfe 100644 --- a/modules/auth/openid/discovery_cache_test.go +++ b/modules/auth/openid/discovery_cache_test.go @@ -6,6 +6,9 @@ package openid import ( "testing" "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) type testDiscoveredInfo struct{} @@ -29,21 +32,17 @@ func TestTimedDiscoveryCache(t *testing.T) { dc.Put("foo", &testDiscoveredInfo{}) // openid.opEndpoint: "a", openid.opLocalID: "b", openid.claimedID: "c"}) // Make sure we can retrieve them - if di := dc.Get("foo"); di == nil { - t.Errorf("Expected a result, got nil") - } else if di.OpEndpoint() != "opEndpoint" || di.OpLocalID() != "opLocalID" || di.ClaimedID() != "claimedID" { - t.Errorf("Expected opEndpoint opLocalID claimedID, got %v %v %v", di.OpEndpoint(), di.OpLocalID(), di.ClaimedID()) - } + di := dc.Get("foo") + require.NotNil(t, di) + assert.Equal(t, "opEndpoint", di.OpEndpoint()) + assert.Equal(t, "opLocalID", di.OpLocalID()) + assert.Equal(t, "claimedID", di.ClaimedID()) // Attempt to get a non-existent value - if di := dc.Get("bar"); di != nil { - t.Errorf("Expected nil, got %v", di) - } + assert.Nil(t, dc.Get("bar")) // Sleep one second and try retrieve again time.Sleep(1 * time.Second) - if di := dc.Get("foo"); di != nil { - t.Errorf("Expected a nil, got a result") - } + assert.Nil(t, dc.Get("foo")) } diff --git a/modules/auth/pam/pam_test.go b/modules/auth/pam/pam_test.go index 7265b5d0c15..d4ab058ec78 100644 --- a/modules/auth/pam/pam_test.go +++ b/modules/auth/pam/pam_test.go @@ -15,5 +15,5 @@ func TestPamAuth(t *testing.T) { result, err := Auth("gitea", "user1", "false-pwd") assert.Error(t, err) assert.EqualError(t, err, "Authentication failure") - assert.Len(t, result) + assert.Empty(t, result) } diff --git a/modules/auth/password/pwn/pwn_test.go b/modules/auth/password/pwn/pwn_test.go index b3e7734c3fc..ae03fabc579 100644 --- a/modules/auth/password/pwn/pwn_test.go +++ b/modules/auth/password/pwn/pwn_test.go @@ -4,46 +4,57 @@ package pwn import ( + "errors" + "io" "net/http" + "strings" "testing" - "time" - "github.com/h2non/gock" "github.com/stretchr/testify/assert" ) -var client = New(WithHTTP(&http.Client{ - Timeout: time.Second * 2, -})) +type mockTransport struct{} + +func (mockTransport) RoundTrip(req *http.Request) (*http.Response, error) { + if req.URL.Host != "api.pwnedpasswords.com" { + return nil, errors.New("unsupported host") + } + respMap := map[string]string{ + "/range/5c1d8": "EAF2F254732680E8AC339B84F3266ECCBB5:1\r\nFC446EB88938834178CB9322C1EE273C2A7:2", + "/range/ba189": "FD4CB34F0378BCB15D23F6FFD28F0775C9E:3\r\nFDF342FCD8C3611DAE4D76E8A992A3E4169:4", + "/range/a1733": "C4CE0F1F0062B27B9E2F41AF0C08218017C:1\r\nFC446EB88938834178CB9322C1EE273C2A7:2\r\nFE81480327C992FE62065A827429DD1318B:0", + "/range/5617b": "FD4CB34F0378BCB15D23F6FFD28F0775C9E:3\r\nFDF342FCD8C3611DAE4D76E8A992A3E4169:4\r\nFE81480327C992FE62065A827429DD1318B:0", + "/range/79082": "FDF342FCD8C3611DAE4D76E8A992A3E4169:4\r\nFE81480327C992FE62065A827429DD1318B:0\r\nAFEF386F56EB0B4BE314E07696E5E6E6536:0", + } + if resp, ok := respMap[req.URL.Path]; ok { + return &http.Response{Request: req, Body: io.NopCloser(strings.NewReader(resp))}, nil + } + return nil, errors.New("unsupported path") +} func TestPassword(t *testing.T) { - defer gock.Off() + client := New(WithHTTP(&http.Client{Transport: mockTransport{}})) count, err := client.CheckPassword("", false) assert.ErrorIs(t, err, ErrEmptyPassword, "blank input should return ErrEmptyPassword") assert.Equal(t, -1, count) - gock.New("https://api.pwnedpasswords.com").Get("/range/5c1d8").Times(1).Reply(200).BodyString("EAF2F254732680E8AC339B84F3266ECCBB5:1\r\nFC446EB88938834178CB9322C1EE273C2A7:2") count, err = client.CheckPassword("pwned", false) assert.NoError(t, err) assert.Equal(t, 1, count) - gock.New("https://api.pwnedpasswords.com").Get("/range/ba189").Times(1).Reply(200).BodyString("FD4CB34F0378BCB15D23F6FFD28F0775C9E:3\r\nFDF342FCD8C3611DAE4D76E8A992A3E4169:4") count, err = client.CheckPassword("notpwned", false) assert.NoError(t, err) assert.Equal(t, 0, count) - gock.New("https://api.pwnedpasswords.com").Get("/range/a1733").Times(1).Reply(200).BodyString("C4CE0F1F0062B27B9E2F41AF0C08218017C:1\r\nFC446EB88938834178CB9322C1EE273C2A7:2\r\nFE81480327C992FE62065A827429DD1318B:0") count, err = client.CheckPassword("paddedpwned", true) assert.NoError(t, err) assert.Equal(t, 1, count) - gock.New("https://api.pwnedpasswords.com").Get("/range/5617b").Times(1).Reply(200).BodyString("FD4CB34F0378BCB15D23F6FFD28F0775C9E:3\r\nFDF342FCD8C3611DAE4D76E8A992A3E4169:4\r\nFE81480327C992FE62065A827429DD1318B:0") count, err = client.CheckPassword("paddednotpwned", true) assert.NoError(t, err) assert.Equal(t, 0, count) - gock.New("https://api.pwnedpasswords.com").Get("/range/79082").Times(1).Reply(200).BodyString("FDF342FCD8C3611DAE4D76E8A992A3E4169:4\r\nFE81480327C992FE62065A827429DD1318B:0\r\nAFEF386F56EB0B4BE314E07696E5E6E6536:0") count, err = client.CheckPassword("paddednotpwnedzero", true) assert.NoError(t, err) assert.Equal(t, 0, count) diff --git a/modules/base/tool.go b/modules/base/tool.go index 1d16186bc5d..b6ed8cbf9a5 100644 --- a/modules/base/tool.go +++ b/modules/base/tool.go @@ -18,7 +18,6 @@ import ( "time" "code.gitea.io/gitea/modules/git" - "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" @@ -64,10 +63,7 @@ func VerifyTimeLimitCode(now time.Time, data string, minutes int, code string) b // check code retCode := CreateTimeLimitCode(data, aliveTime, startTimeStr, nil) if subtle.ConstantTimeCompare([]byte(retCode), []byte(code)) != 1 { - retCode = CreateTimeLimitCode(data, aliveTime, startTimeStr, sha1.New()) // TODO: this is only for the support of legacy codes, remove this in/after 1.23 - if subtle.ConstantTimeCompare([]byte(retCode), []byte(code)) != 1 { - return false - } + return false } // check time is expired or not: startTime <= now && now < startTime + minutes @@ -144,13 +140,12 @@ func Int64sToStrings(ints []int64) []string { return strs } -// EntryIcon returns the octicon class for displaying files/directories +// EntryIcon returns the octicon name for displaying files/directories func EntryIcon(entry *git.TreeEntry) string { switch { case entry.IsLink(): te, err := entry.FollowLink() if err != nil { - log.Debug(err.Error()) return "file-symlink-file" } if te.IsDir() { diff --git a/modules/base/tool_test.go b/modules/base/tool_test.go index c821a55c19c..7cebedb073c 100644 --- a/modules/base/tool_test.go +++ b/modules/base/tool_test.go @@ -86,13 +86,10 @@ JWT_SECRET = %s verifyDataCode := func(c string) bool { return VerifyTimeLimitCode(now, "data", 2, c) } - code1 := CreateTimeLimitCode("data", 2, now, sha1.New()) - code2 := CreateTimeLimitCode("data", 2, now, nil) - assert.True(t, verifyDataCode(code1)) - assert.True(t, verifyDataCode(code2)) + code := CreateTimeLimitCode("data", 2, now, nil) + assert.True(t, verifyDataCode(code)) initGeneralSecret("000_QLUd4fYVyxetjxC4eZkrBgWM2SndOOWDNtgUUko") - assert.False(t, verifyDataCode(code1)) - assert.False(t, verifyDataCode(code2)) + assert.False(t, verifyDataCode(code)) }) } @@ -137,5 +134,3 @@ func TestInt64sToStrings(t *testing.T) { Int64sToStrings([]int64{1, 4, 16, 64, 256}), ) } - -// TODO: Test EntryIcon diff --git a/modules/cache/cache.go b/modules/cache/cache.go index b5400b0bd6a..f7828e3cae2 100644 --- a/modules/cache/cache.go +++ b/modules/cache/cache.go @@ -37,10 +37,15 @@ func Init() error { } const ( - testCacheKey = "DefaultCache.TestKey" - SlowCacheThreshold = 100 * time.Microsecond + testCacheKey = "DefaultCache.TestKey" + // SlowCacheThreshold marks cache tests as slow + // set to 30ms per discussion: https://github.com/go-gitea/gitea/issues/33190 + // TODO: Replace with metrics histogram + SlowCacheThreshold = 30 * time.Millisecond ) +// Test performs delete, put and get operations on a predefined key +// returns func Test() (time.Duration, error) { if defaultCache == nil { return 0, fmt.Errorf("default cache not initialized") diff --git a/modules/cache/cache_test.go b/modules/cache/cache_test.go index d0352947a83..5408020306b 100644 --- a/modules/cache/cache_test.go +++ b/modules/cache/cache_test.go @@ -43,7 +43,8 @@ func TestTest(t *testing.T) { elapsed, err := Test() assert.NoError(t, err) // mem cache should take from 300ns up to 1ms on modern hardware ... - assert.Less(t, elapsed, time.Millisecond) + assert.Positive(t, elapsed) + assert.Less(t, elapsed, SlowCacheThreshold) } func TestGetCache(t *testing.T) { diff --git a/modules/emoji/emoji_test.go b/modules/emoji/emoji_test.go index 2526cd121e4..fbf80fe41aa 100644 --- a/modules/emoji/emoji_test.go +++ b/modules/emoji/emoji_test.go @@ -5,7 +5,6 @@ package emoji import ( - "reflect" "testing" "github.com/stretchr/testify/assert" @@ -22,32 +21,18 @@ func TestLookup(t *testing.T) { c := FromAlias(":beer:") d := FromAlias("beer") - if !reflect.DeepEqual(a, b) { - t.Errorf("a and b should equal") - } - if !reflect.DeepEqual(b, c) { - t.Errorf("b and c should equal") - } - if !reflect.DeepEqual(c, d) { - t.Errorf("c and d should equal") - } - if !reflect.DeepEqual(a, d) { - t.Errorf("a and d should equal") - } + assert.Equal(t, a, b) + assert.Equal(t, b, c) + assert.Equal(t, c, d) + assert.Equal(t, a, d) m := FromCode("\U0001f44d") n := FromAlias(":thumbsup:") o := FromAlias("+1") - if !reflect.DeepEqual(m, n) { - t.Errorf("m and n should equal") - } - if !reflect.DeepEqual(n, o) { - t.Errorf("n and o should equal") - } - if !reflect.DeepEqual(m, o) { - t.Errorf("m and o should equal") - } + assert.Equal(t, m, n) + assert.Equal(t, m, o) + assert.Equal(t, n, o) } func TestReplacers(t *testing.T) { @@ -61,9 +46,7 @@ func TestReplacers(t *testing.T) { for i, x := range tests { s := x.f(x.v) - if s != x.exp { - t.Errorf("test %d `%s` expected `%s`, got: `%s`", i, x.v, x.exp, s) - } + assert.Equalf(t, x.exp, s, "test %d `%s` expected `%s`, got: `%s`", i, x.v, x.exp, s) } } diff --git a/modules/eventsource/event_test.go b/modules/eventsource/event_test.go index 4c4272880d6..a1c3e5c7a8a 100644 --- a/modules/eventsource/event_test.go +++ b/modules/eventsource/event_test.go @@ -6,6 +6,9 @@ package eventsource import ( "bytes" "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func Test_wrapNewlines(t *testing.T) { @@ -38,16 +41,10 @@ func Test_wrapNewlines(t *testing.T) { t.Run(tt.name, func(t *testing.T) { w := &bytes.Buffer{} gotSum, err := wrapNewlines(w, []byte(tt.prefix), []byte(tt.value)) - if err != nil { - t.Errorf("wrapNewlines() error = %v", err) - return - } - if gotSum != int64(len(tt.output)) { - t.Errorf("wrapNewlines() = %v, want %v", gotSum, int64(len(tt.output))) - } - if gotW := w.String(); gotW != tt.output { - t.Errorf("wrapNewlines() = %v, want %v", gotW, tt.output) - } + require.NoError(t, err) + + assert.EqualValues(t, len(tt.output), gotSum) + assert.Equal(t, tt.output, w.String()) }) } } diff --git a/modules/git/batch_reader.go b/modules/git/batch_reader.go index 532dbad9894..33e54fe75cb 100644 --- a/modules/git/batch_reader.go +++ b/modules/git/batch_reader.go @@ -242,7 +242,7 @@ func BinToHex(objectFormat ObjectFormat, sha, out []byte) []byte { return out } -// ParseTreeLine reads an entry from a tree in a cat-file --batch stream +// ParseCatFileTreeLine reads an entry from a tree in a cat-file --batch stream // This carefully avoids allocations - except where fnameBuf is too small. // It is recommended therefore to pass in an fnameBuf large enough to avoid almost all allocations // @@ -250,7 +250,7 @@ func BinToHex(objectFormat ObjectFormat, sha, out []byte) []byte { // SP NUL // // We don't attempt to convert the raw HASH to save a lot of time -func ParseTreeLine(objectFormat ObjectFormat, rd *bufio.Reader, modeBuf, fnameBuf, shaBuf []byte) (mode, fname, sha []byte, n int, err error) { +func ParseCatFileTreeLine(objectFormat ObjectFormat, rd *bufio.Reader, modeBuf, fnameBuf, shaBuf []byte) (mode, fname, sha []byte, n int, err error) { var readBytes []byte // Read the Mode & fname @@ -260,7 +260,7 @@ func ParseTreeLine(objectFormat ObjectFormat, rd *bufio.Reader, modeBuf, fnameBu } idx := bytes.IndexByte(readBytes, ' ') if idx < 0 { - log.Debug("missing space in readBytes ParseTreeLine: %s", readBytes) + log.Debug("missing space in readBytes ParseCatFileTreeLine: %s", readBytes) return mode, fname, sha, n, &ErrNotExist{} } diff --git a/modules/git/blob_test.go b/modules/git/blob_test.go index 63374384f6b..d0804350edd 100644 --- a/modules/git/blob_test.go +++ b/modules/git/blob_test.go @@ -17,9 +17,7 @@ func TestBlob_Data(t *testing.T) { output := "file2\n" bareRepo1Path := filepath.Join(testReposDir, "repo1_bare") repo, err := openRepositoryWithDefaultContext(bareRepo1Path) - if !assert.NoError(t, err) { - t.Fatal() - } + require.NoError(t, err) defer repo.Close() testBlob, err := repo.GetBlob("6c493ff740f9380390d5c9ddef4af18697ac9375") diff --git a/modules/git/command.go b/modules/git/command.go index b231c3beea0..602d00f0271 100644 --- a/modules/git/command.go +++ b/modules/git/command.go @@ -18,6 +18,7 @@ import ( "time" "code.gitea.io/gitea/modules/git/internal" //nolint:depguard // only this file can use the internal type CmdArg, other files and packages should use AddXxx functions + "code.gitea.io/gitea/modules/gtprof" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/process" "code.gitea.io/gitea/modules/util" @@ -54,7 +55,7 @@ func logArgSanitize(arg string) string { } else if filepath.IsAbs(arg) { base := filepath.Base(arg) dir := filepath.Dir(arg) - return filepath.Join(filepath.Base(dir), base) + return ".../" + filepath.Join(filepath.Base(dir), base) } return arg } @@ -236,10 +237,16 @@ type RunOpts struct { } func commonBaseEnvs() []string { - // at the moment, do not set "GIT_CONFIG_NOSYSTEM", users may have put some configs like "receive.certNonceSeed" in it envs := []string{ - "HOME=" + HomeDir(), // make Gitea use internal git config only, to prevent conflicts with user's git config - "GIT_NO_REPLACE_OBJECTS=1", // ignore replace references (https://git-scm.com/docs/git-replace) + // Make Gitea use internal git config only, to prevent conflicts with user's git config + // It's better to use GIT_CONFIG_GLOBAL, but it requires git >= 2.32, so we still use HOME at the moment. + "HOME=" + HomeDir(), + // Avoid using system git config, it would cause problems (eg: use macOS osxkeychain to show a modal dialog, auto installing lfs hooks) + // This might be a breaking change in 1.24, because some users said that they have put some configs like "receive.certNonceSeed" in "/etc/gitconfig" + // For these users, they need to migrate the necessary configs to Gitea's git config file manually. + "GIT_CONFIG_NOSYSTEM=1", + // Ignore replace references (https://git-scm.com/docs/git-replace) + "GIT_NO_REPLACE_OBJECTS=1", } // some environment variables should be passed to git command @@ -289,15 +296,20 @@ func (c *Command) run(skip int, opts *RunOpts) error { timeout = defaultCommandExecutionTimeout } - var desc string + cmdLogString := c.LogString() callerInfo := util.CallerFuncName(1 /* util */ + 1 /* this */ + skip /* parent */) if pos := strings.LastIndex(callerInfo, "/"); pos >= 0 { callerInfo = callerInfo[pos+1:] } // these logs are for debugging purposes only, so no guarantee of correctness or stability - desc = fmt.Sprintf("git.Run(by:%s, repo:%s): %s", callerInfo, logArgSanitize(opts.Dir), c.LogString()) + desc := fmt.Sprintf("git.Run(by:%s, repo:%s): %s", callerInfo, logArgSanitize(opts.Dir), cmdLogString) log.Debug("git.Command: %s", desc) + _, span := gtprof.GetTracer().Start(c.parentContext, gtprof.TraceSpanGitRun) + defer span.End() + span.SetAttributeString(gtprof.TraceAttrFuncCaller, callerInfo) + span.SetAttributeString(gtprof.TraceAttrGitCommand, cmdLogString) + var ctx context.Context var cancel context.CancelFunc var finished context.CancelFunc diff --git a/modules/git/command_test.go b/modules/git/command_test.go index 0823afd7f76..e988714db7f 100644 --- a/modules/git/command_test.go +++ b/modules/git/command_test.go @@ -58,5 +58,5 @@ func TestCommandString(t *testing.T) { assert.EqualValues(t, cmd.prog+` a "-m msg" "it's a test" "say \"hello\""`, cmd.LogString()) cmd = NewCommandContextNoGlobals(context.Background(), "url: https://a:b@c/", "/root/dir-a/dir-b") - assert.EqualValues(t, cmd.prog+` "url: https://sanitized-credential@c/" dir-a/dir-b`, cmd.LogString()) + assert.EqualValues(t, cmd.prog+` "url: https://sanitized-credential@c/" .../dir-a/dir-b`, cmd.LogString()) } diff --git a/modules/git/commit.go b/modules/git/commit.go index 0ed268e3469..0a50ba43563 100644 --- a/modules/git/commit.go +++ b/modules/git/commit.go @@ -476,8 +476,12 @@ func (c *Commit) GetRepositoryDefaultPublicGPGKey(forceUpdate bool) (*GPGSetting } func IsStringLikelyCommitID(objFmt ObjectFormat, s string, minLength ...int) bool { - minLen := util.OptionalArg(minLength, objFmt.FullLength()) - if len(s) < minLen || len(s) > objFmt.FullLength() { + maxLen := 64 // sha256 + if objFmt != nil { + maxLen = objFmt.FullLength() + } + minLen := util.OptionalArg(minLength, maxLen) + if len(s) < minLen || len(s) > maxLen { return false } for _, c := range s { diff --git a/modules/git/commit_info.go b/modules/git/commit_info.go index 545081275b7..c046acbb508 100644 --- a/modules/git/commit_info.go +++ b/modules/git/commit_info.go @@ -7,5 +7,5 @@ package git type CommitInfo struct { Entry *TreeEntry Commit *Commit - SubModuleFile *CommitSubModuleFile + SubmoduleFile *CommitSubmoduleFile } diff --git a/modules/git/commit_info_gogit.go b/modules/git/commit_info_gogit.go index 11b44f7c356..314c2df7284 100644 --- a/modules/git/commit_info_gogit.go +++ b/modules/git/commit_info_gogit.go @@ -85,8 +85,8 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath } else if subModule != nil { subModuleURL = subModule.URL } - subModuleFile := NewCommitSubModuleFile(subModuleURL, entry.ID.String()) - commitsInfo[i].SubModuleFile = subModuleFile + subModuleFile := NewCommitSubmoduleFile(subModuleURL, entry.ID.String()) + commitsInfo[i].SubmoduleFile = subModuleFile } } diff --git a/modules/git/commit_info_nogogit.go b/modules/git/commit_info_nogogit.go index 20d586f0ff5..ef2df0b133a 100644 --- a/modules/git/commit_info_nogogit.go +++ b/modules/git/commit_info_nogogit.go @@ -79,8 +79,8 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath } else if subModule != nil { subModuleURL = subModule.URL } - subModuleFile := NewCommitSubModuleFile(subModuleURL, entry.ID.String()) - commitsInfo[i].SubModuleFile = subModuleFile + subModuleFile := NewCommitSubmoduleFile(subModuleURL, entry.ID.String()) + commitsInfo[i].SubmoduleFile = subModuleFile } } diff --git a/modules/git/commit_sha256_test.go b/modules/git/commit_sha256_test.go index 2184a9c47cf..f6ca83c9edb 100644 --- a/modules/git/commit_sha256_test.go +++ b/modules/git/commit_sha256_test.go @@ -11,6 +11,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestCommitsCountSha256(t *testing.T) { @@ -94,9 +95,7 @@ signed commit` commitFromReader, err := CommitFromReader(gitRepo, sha, strings.NewReader(commitString)) assert.NoError(t, err) - if !assert.NotNil(t, commitFromReader) { - return - } + require.NotNil(t, commitFromReader) assert.EqualValues(t, sha, commitFromReader.ID) assert.EqualValues(t, `-----BEGIN PGP SIGNATURE----- diff --git a/modules/git/commit_submodule.go b/modules/git/commit_submodule.go index 6603061da29..031fd4e5d02 100644 --- a/modules/git/commit_submodule.go +++ b/modules/git/commit_submodule.go @@ -3,6 +3,10 @@ package git +type SubmoduleWebLink struct { + RepoWebLink, CommitWebLink string +} + // GetSubModules get all the submodules of current revision git tree func (c *Commit) GetSubModules() (*ObjectCache[*SubModule], error) { if c.submoduleCache != nil { diff --git a/modules/git/commit_submodule_file.go b/modules/git/commit_submodule_file.go index bdec35f6829..729401f7521 100644 --- a/modules/git/commit_submodule_file.go +++ b/modules/git/commit_submodule_file.go @@ -5,107 +5,50 @@ package git import ( - "fmt" - "net" - "net/url" - "path" - "regexp" - "strings" + "context" + + giturl "code.gitea.io/gitea/modules/git/url" ) -var scpSyntax = regexp.MustCompile(`^([a-zA-Z0-9_]+@)?([a-zA-Z0-9._-]+):(.*)$`) - -// CommitSubModuleFile represents a file with submodule type. -type CommitSubModuleFile struct { - refURL string - refID string +// CommitSubmoduleFile represents a file with submodule type. +type CommitSubmoduleFile struct { + refURL string + parsedURL *giturl.RepositoryURL + parsed bool + refID string + repoLink string } -// NewCommitSubModuleFile create a new submodule file -func NewCommitSubModuleFile(refURL, refID string) *CommitSubModuleFile { - return &CommitSubModuleFile{ - refURL: refURL, - refID: refID, - } +// NewCommitSubmoduleFile create a new submodule file +func NewCommitSubmoduleFile(refURL, refID string) *CommitSubmoduleFile { + return &CommitSubmoduleFile{refURL: refURL, refID: refID} } -func getRefURL(refURL, urlPrefix, repoFullName, sshDomain string) string { - if refURL == "" { - return "" +func (sf *CommitSubmoduleFile) RefID() string { + return sf.refID // this function is only used in templates +} + +// SubmoduleWebLink tries to make some web links for a submodule, it also works on "nil" receiver +func (sf *CommitSubmoduleFile) SubmoduleWebLink(ctx context.Context, optCommitID ...string) *SubmoduleWebLink { + if sf == nil { + return nil } - - refURI := strings.TrimSuffix(refURL, ".git") - - prefixURL, _ := url.Parse(urlPrefix) - urlPrefixHostname, _, err := net.SplitHostPort(prefixURL.Host) - if err != nil { - urlPrefixHostname = prefixURL.Host - } - - urlPrefix = strings.TrimSuffix(urlPrefix, "/") - - // FIXME: Need to consider branch - which will require changes in modules/git/commit.go:GetSubModules - // Relative url prefix check (according to git submodule documentation) - if strings.HasPrefix(refURI, "./") || strings.HasPrefix(refURI, "../") { - return urlPrefix + path.Clean(path.Join("/", repoFullName, refURI)) - } - - if !strings.Contains(refURI, "://") { - // scp style syntax which contains *no* port number after the : (and is not parsed by net/url) - // ex: git@try.gitea.io:go-gitea/gitea - match := scpSyntax.FindAllStringSubmatch(refURI, -1) - if len(match) > 0 { - m := match[0] - refHostname := m[2] - pth := m[3] - - if !strings.HasPrefix(pth, "/") { - pth = "/" + pth - } - - if urlPrefixHostname == refHostname || refHostname == sshDomain { - return urlPrefix + path.Clean(path.Join("/", pth)) - } - return "http://" + refHostname + pth + if !sf.parsed { + sf.parsed = true + parsedURL, err := giturl.ParseRepositoryURL(ctx, sf.refURL) + if err != nil { + return nil } + sf.parsedURL = parsedURL + sf.repoLink = giturl.MakeRepositoryWebLink(sf.parsedURL) } - - ref, err := url.Parse(refURI) - if err != nil { - return "" + var commitLink string + if len(optCommitID) == 2 { + commitLink = sf.repoLink + "/compare/" + optCommitID[0] + "..." + optCommitID[1] + } else if len(optCommitID) == 1 { + commitLink = sf.repoLink + "/tree/" + optCommitID[0] + } else { + commitLink = sf.repoLink + "/tree/" + sf.refID } - - refHostname, _, err := net.SplitHostPort(ref.Host) - if err != nil { - refHostname = ref.Host - } - - supportedSchemes := []string{"http", "https", "git", "ssh", "git+ssh"} - - for _, scheme := range supportedSchemes { - if ref.Scheme == scheme { - if ref.Scheme == "http" || ref.Scheme == "https" { - if len(ref.User.Username()) > 0 { - return ref.Scheme + "://" + fmt.Sprintf("%v", ref.User) + "@" + ref.Host + ref.Path - } - return ref.Scheme + "://" + ref.Host + ref.Path - } else if urlPrefixHostname == refHostname || refHostname == sshDomain { - return urlPrefix + path.Clean(path.Join("/", ref.Path)) - } - return "http://" + refHostname + ref.Path - } - } - - return "" -} - -// RefURL guesses and returns reference URL. -// FIXME: template passes AppURL as urlPrefix, it needs to figure out the correct approach (no hard-coded AppURL anymore) -func (sf *CommitSubModuleFile) RefURL(urlPrefix, repoFullName, sshDomain string) string { - return getRefURL(sf.refURL, urlPrefix, repoFullName, sshDomain) -} - -// RefID returns reference ID. -func (sf *CommitSubModuleFile) RefID() string { - return sf.refID + return &SubmoduleWebLink{RepoWebLink: sf.repoLink, CommitWebLink: commitLink} } diff --git a/modules/git/commit_submodule_file_test.go b/modules/git/commit_submodule_file_test.go index 473b996b820..98342aa9e97 100644 --- a/modules/git/commit_submodule_file_test.go +++ b/modules/git/commit_submodule_file_test.go @@ -1,42 +1,30 @@ -// Copyright 2018 The Gitea Authors. All rights reserved. +// Copyright 2024 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT package git import ( + "context" "testing" "github.com/stretchr/testify/assert" ) -func TestCommitSubModuleFileGetRefURL(t *testing.T) { - kases := []struct { - refURL string - prefixURL string - parentPath string - SSHDomain string - expect string - }{ - {"git://github.com/user1/repo1", "/", "user1/repo2", "", "http://github.com/user1/repo1"}, - {"https://localhost/user1/repo1.git", "/", "user1/repo2", "", "https://localhost/user1/repo1"}, - {"http://localhost/user1/repo1.git", "/", "owner/reponame", "", "http://localhost/user1/repo1"}, - {"git@github.com:user1/repo1.git", "/", "owner/reponame", "", "http://github.com/user1/repo1"}, - {"ssh://git@git.zefie.net:2222/zefie/lge_g6_kernel_scripts.git", "/", "zefie/lge_g6_kernel", "", "http://git.zefie.net/zefie/lge_g6_kernel_scripts"}, - {"git@git.zefie.net:2222/zefie/lge_g6_kernel_scripts.git", "/", "zefie/lge_g6_kernel", "", "http://git.zefie.net/2222/zefie/lge_g6_kernel_scripts"}, - {"git@try.gitea.io:go-gitea/gitea", "https://try.gitea.io/", "go-gitea/sdk", "", "https://try.gitea.io/go-gitea/gitea"}, - {"ssh://git@try.gitea.io:9999/go-gitea/gitea", "https://try.gitea.io/", "go-gitea/sdk", "", "https://try.gitea.io/go-gitea/gitea"}, - {"git://git@try.gitea.io:9999/go-gitea/gitea", "https://try.gitea.io/", "go-gitea/sdk", "", "https://try.gitea.io/go-gitea/gitea"}, - {"ssh://git@127.0.0.1:9999/go-gitea/gitea", "https://127.0.0.1:3000/", "go-gitea/sdk", "", "https://127.0.0.1:3000/go-gitea/gitea"}, - {"https://gitea.com:3000/user1/repo1.git", "https://127.0.0.1:3000/", "user/repo2", "", "https://gitea.com:3000/user1/repo1"}, - {"https://example.gitea.com/gitea/user1/repo1.git", "https://example.gitea.com/gitea/", "", "user/repo2", "https://example.gitea.com/gitea/user1/repo1"}, - {"https://username:password@github.com/username/repository.git", "/", "username/repository2", "", "https://username:password@github.com/username/repository"}, - {"somethingbad", "https://127.0.0.1:3000/go-gitea/gitea", "/", "", ""}, - {"git@localhost:user/repo", "https://localhost/", "user2/repo1", "", "https://localhost/user/repo"}, - {"../path/to/repo.git/", "https://localhost/", "user/repo2", "", "https://localhost/user/path/to/repo.git"}, - {"ssh://git@ssh.gitea.io:2222/go-gitea/gitea", "https://try.gitea.io/", "go-gitea/sdk", "ssh.gitea.io", "https://try.gitea.io/go-gitea/gitea"}, - } +func TestCommitSubmoduleLink(t *testing.T) { + sf := NewCommitSubmoduleFile("git@github.com:user/repo.git", "aaaa") - for _, kase := range kases { - assert.EqualValues(t, kase.expect, getRefURL(kase.refURL, kase.prefixURL, kase.parentPath, kase.SSHDomain)) - } + wl := sf.SubmoduleWebLink(context.Background()) + assert.Equal(t, "https://github.com/user/repo", wl.RepoWebLink) + assert.Equal(t, "https://github.com/user/repo/tree/aaaa", wl.CommitWebLink) + + wl = sf.SubmoduleWebLink(context.Background(), "1111") + assert.Equal(t, "https://github.com/user/repo", wl.RepoWebLink) + assert.Equal(t, "https://github.com/user/repo/tree/1111", wl.CommitWebLink) + + wl = sf.SubmoduleWebLink(context.Background(), "1111", "2222") + assert.Equal(t, "https://github.com/user/repo", wl.RepoWebLink) + assert.Equal(t, "https://github.com/user/repo/compare/1111...2222", wl.CommitWebLink) + + wl = (*CommitSubmoduleFile)(nil).SubmoduleWebLink(context.Background()) + assert.Nil(t, wl) } diff --git a/modules/git/commit_test.go b/modules/git/commit_test.go index 6ac65564dc1..9a87dd32d99 100644 --- a/modules/git/commit_test.go +++ b/modules/git/commit_test.go @@ -11,6 +11,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestCommitsCount(t *testing.T) { @@ -91,9 +92,7 @@ empty commit` commitFromReader, err := CommitFromReader(gitRepo, sha, strings.NewReader(commitString)) assert.NoError(t, err) - if !assert.NotNil(t, commitFromReader) { - return - } + require.NotNil(t, commitFromReader) assert.EqualValues(t, sha, commitFromReader.ID) assert.EqualValues(t, `-----BEGIN PGP SIGNATURE----- @@ -159,9 +158,7 @@ ISO-8859-1` commitFromReader, err := CommitFromReader(gitRepo, sha, strings.NewReader(commitString)) assert.NoError(t, err) - if !assert.NotNil(t, commitFromReader) { - return - } + require.NotNil(t, commitFromReader) assert.EqualValues(t, sha, commitFromReader.ID) assert.EqualValues(t, `-----BEGIN PGP SIGNATURE----- @@ -360,5 +357,5 @@ func Test_GetCommitBranchStart(t *testing.T) { startCommitID, err := repo.GetCommitBranchStart(os.Environ(), "branch1", commit.ID.String()) assert.NoError(t, err) assert.NotEmpty(t, startCommitID) - assert.EqualValues(t, "9c9aef8dd84e02bc7ec12641deb4c930a7c30185", startCommitID) + assert.EqualValues(t, "95bb4d39648ee7e325106df01a621c530863a653", startCommitID) } diff --git a/modules/git/diff.go b/modules/git/diff.go index 833f6220f97..da0a2f26ba7 100644 --- a/modules/git/diff.go +++ b/modules/git/diff.go @@ -64,7 +64,10 @@ func GetRepoRawDiffForFile(repo *Repository, startCommit, endCommit string, diff } else if commit.ParentCount() == 0 { cmd.AddArguments("show").AddDynamicArguments(endCommit).AddDashesAndList(files...) } else { - c, _ := commit.Parent(0) + c, err := commit.Parent(0) + if err != nil { + return err + } cmd.AddArguments("diff", "-M").AddDynamicArguments(c.ID.String(), endCommit).AddDashesAndList(files...) } case RawDiffPatch: @@ -74,7 +77,10 @@ func GetRepoRawDiffForFile(repo *Repository, startCommit, endCommit string, diff } else if commit.ParentCount() == 0 { cmd.AddArguments("format-patch", "--no-signature", "--stdout", "--root").AddDynamicArguments(endCommit).AddDashesAndList(files...) } else { - c, _ := commit.Parent(0) + c, err := commit.Parent(0) + if err != nil { + return err + } query := fmt.Sprintf("%s...%s", endCommit, c.ID.String()) cmd.AddArguments("format-patch", "--no-signature", "--stdout").AddDynamicArguments(query).AddDashesAndList(files...) } diff --git a/modules/git/parse.go b/modules/git/parse.go new file mode 100644 index 00000000000..a7f5c58e896 --- /dev/null +++ b/modules/git/parse.go @@ -0,0 +1,68 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package git + +import ( + "bytes" + "fmt" + "strconv" + "strings" + + "code.gitea.io/gitea/modules/optional" +) + +var sepSpace = []byte{' '} + +type LsTreeEntry struct { + ID ObjectID + EntryMode EntryMode + Name string + Size optional.Option[int64] +} + +func parseLsTreeLine(line []byte) (*LsTreeEntry, error) { + // expect line to be of the form: + // \t + // \t + + var err error + posTab := bytes.IndexByte(line, '\t') + if posTab == -1 { + return nil, fmt.Errorf("invalid ls-tree output (no tab): %q", line) + } + + entry := new(LsTreeEntry) + + entryAttrs := line[:posTab] + entryName := line[posTab+1:] + + entryMode, entryAttrs, _ := bytes.Cut(entryAttrs, sepSpace) + _ /* entryType */, entryAttrs, _ = bytes.Cut(entryAttrs, sepSpace) // the type is not used, the mode is enough to determine the type + entryObjectID, entryAttrs, _ := bytes.Cut(entryAttrs, sepSpace) + if len(entryAttrs) > 0 { + entrySize := entryAttrs // the last field is the space-padded-size + size, _ := strconv.ParseInt(strings.TrimSpace(string(entrySize)), 10, 64) + entry.Size = optional.Some(size) + } + + entry.EntryMode, err = ParseEntryMode(string(entryMode)) + if err != nil || entry.EntryMode == EntryModeNoEntry { + return nil, fmt.Errorf("invalid ls-tree output (invalid mode): %q, err: %w", line, err) + } + + entry.ID, err = NewIDFromString(string(entryObjectID)) + if err != nil { + return nil, fmt.Errorf("invalid ls-tree output (invalid object id): %q, err: %w", line, err) + } + + if len(entryName) > 0 && entryName[0] == '"' { + entry.Name, err = strconv.Unquote(string(entryName)) + if err != nil { + return nil, fmt.Errorf("invalid ls-tree output (invalid name): %q, err: %w", line, err) + } + } else { + entry.Name = string(entryName) + } + return entry, nil +} diff --git a/modules/git/parse_nogogit.go b/modules/git/parse_nogogit.go index 546b38be379..676bb3c76c0 100644 --- a/modules/git/parse_nogogit.go +++ b/modules/git/parse_nogogit.go @@ -10,8 +10,6 @@ import ( "bytes" "fmt" "io" - "strconv" - "strings" "code.gitea.io/gitea/modules/log" ) @@ -21,71 +19,30 @@ func ParseTreeEntries(data []byte) ([]*TreeEntry, error) { return parseTreeEntries(data, nil) } -var sepSpace = []byte{' '} - +// parseTreeEntries FIXME this function's design is not right, it should make the caller read all data into memory func parseTreeEntries(data []byte, ptree *Tree) ([]*TreeEntry, error) { - var err error entries := make([]*TreeEntry, 0, bytes.Count(data, []byte{'\n'})+1) for pos := 0; pos < len(data); { - // expect line to be of the form: - // \t - // \t posEnd := bytes.IndexByte(data[pos:], '\n') if posEnd == -1 { posEnd = len(data) } else { posEnd += pos } + line := data[pos:posEnd] - posTab := bytes.IndexByte(line, '\t') - if posTab == -1 { - return nil, fmt.Errorf("invalid ls-tree output (no tab): %q", line) - } - - entry := new(TreeEntry) - entry.ptree = ptree - - entryAttrs := line[:posTab] - entryName := line[posTab+1:] - - entryMode, entryAttrs, _ := bytes.Cut(entryAttrs, sepSpace) - _ /* entryType */, entryAttrs, _ = bytes.Cut(entryAttrs, sepSpace) // the type is not used, the mode is enough to determine the type - entryObjectID, entryAttrs, _ := bytes.Cut(entryAttrs, sepSpace) - if len(entryAttrs) > 0 { - entrySize := entryAttrs // the last field is the space-padded-size - entry.size, _ = strconv.ParseInt(strings.TrimSpace(string(entrySize)), 10, 64) - entry.sized = true - } - - switch string(entryMode) { - case "100644": - entry.entryMode = EntryModeBlob - case "100755": - entry.entryMode = EntryModeExec - case "120000": - entry.entryMode = EntryModeSymlink - case "160000": - entry.entryMode = EntryModeCommit - case "040000", "040755": // git uses 040000 for tree object, but some users may get 040755 for unknown reasons - entry.entryMode = EntryModeTree - default: - return nil, fmt.Errorf("unknown type: %v", string(entryMode)) - } - - entry.ID, err = NewIDFromString(string(entryObjectID)) + lsTreeLine, err := parseLsTreeLine(line) if err != nil { - return nil, fmt.Errorf("invalid ls-tree output (invalid object id): %q, err: %w", line, err) + return nil, err } - - if len(entryName) > 0 && entryName[0] == '"' { - entry.name, err = strconv.Unquote(string(entryName)) - if err != nil { - return nil, fmt.Errorf("invalid ls-tree output (invalid name): %q, err: %w", line, err) - } - } else { - entry.name = string(entryName) + entry := &TreeEntry{ + ptree: ptree, + ID: lsTreeLine.ID, + entryMode: lsTreeLine.EntryMode, + name: lsTreeLine.Name, + size: lsTreeLine.Size.Value(), + sized: lsTreeLine.Size.Has(), } - pos = posEnd + 1 entries = append(entries, entry) } @@ -100,7 +57,7 @@ func catBatchParseTreeEntries(objectFormat ObjectFormat, ptree *Tree, rd *bufio. loop: for sz > 0 { - mode, fname, sha, count, err := ParseTreeLine(objectFormat, rd, modeBuf, fnameBuf, shaBuf) + mode, fname, sha, count, err := ParseCatFileTreeLine(objectFormat, rd, modeBuf, fnameBuf, shaBuf) if err != nil { if err == io.EOF { break loop diff --git a/modules/git/pipeline/lfs_nogogit.go b/modules/git/pipeline/lfs_nogogit.go index b22805c1327..92e35c5a102 100644 --- a/modules/git/pipeline/lfs_nogogit.go +++ b/modules/git/pipeline/lfs_nogogit.go @@ -114,7 +114,7 @@ func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, err case "tree": var n int64 for n < size { - mode, fname, binObjectID, count, err := git.ParseTreeLine(objectID.Type(), batchReader, modeBuf, fnameBuf, workingShaBuf) + mode, fname, binObjectID, count, err := git.ParseCatFileTreeLine(objectID.Type(), batchReader, modeBuf, fnameBuf, workingShaBuf) if err != nil { return nil, err } diff --git a/modules/git/ref.go b/modules/git/ref.go index aab4c5d77d7..f20a175e422 100644 --- a/modules/git/ref.go +++ b/modules/git/ref.go @@ -80,6 +80,10 @@ func RefNameFromTag(shortName string) RefName { return RefName(TagPrefix + shortName) } +func RefNameFromCommit(shortName string) RefName { + return RefName(shortName) +} + func (ref RefName) String() string { return string(ref) } @@ -181,32 +185,38 @@ func (ref RefName) RefGroup() string { return "" } +// RefType is a simple ref type of the reference, it is used for UI and webhooks +type RefType string + +const ( + RefTypeBranch RefType = "branch" + RefTypeTag RefType = "tag" + RefTypeCommit RefType = "commit" +) + // RefType returns the simple ref type of the reference, e.g. branch, tag // It's different from RefGroup, which is using the name of the directory under .git/refs -// Here we using branch but not heads, using tag but not tags -func (ref RefName) RefType() string { - var refType string - if ref.IsBranch() { - refType = "branch" - } else if ref.IsTag() { - refType = "tag" +func (ref RefName) RefType() RefType { + switch { + case ref.IsBranch(): + return RefTypeBranch + case ref.IsTag(): + return RefTypeTag + case IsStringLikelyCommitID(nil, string(ref), 6): + return RefTypeCommit } - return refType + return "" } -// RefURL returns the absolute URL for a ref in a repository -func RefURL(repoURL, ref string) string { - refFullName := RefName(ref) - refName := util.PathEscapeSegments(refFullName.ShortName()) - switch { - case refFullName.IsBranch(): - return repoURL + "/src/branch/" + refName - case refFullName.IsTag(): - return repoURL + "/src/tag/" + refName - case !Sha1ObjectFormat.IsValid(ref): - // assume they mean a branch - return repoURL + "/src/branch/" + refName - default: - return repoURL + "/src/commit/" + refName +// RefWebLinkPath returns a path for the reference that can be used in a web link: +// * "branch/" +// * "tag/" +// * "commit/" +// It returns an empty string if the reference is not a branch, tag or commit. +func (ref RefName) RefWebLinkPath() string { + refType := ref.RefType() + if refType == "" { + return "" } + return string(refType) + "/" + util.PathEscapeSegments(ref.ShortName()) } diff --git a/modules/git/ref_test.go b/modules/git/ref_test.go index 58f679b7d6e..53971915612 100644 --- a/modules/git/ref_test.go +++ b/modules/git/ref_test.go @@ -20,6 +20,8 @@ func TestRefName(t *testing.T) { // Test pull names assert.Equal(t, "1", RefName("refs/pull/1/head").PullName()) + assert.True(t, RefName("refs/pull/1/head").IsPull()) + assert.True(t, RefName("refs/pull/1/merge").IsPull()) assert.Equal(t, "my/pull", RefName("refs/pull/my/pull/head").PullName()) // Test for branch names @@ -30,9 +32,8 @@ func TestRefName(t *testing.T) { assert.Equal(t, "c0ffee", RefName("c0ffee").ShortName()) } -func TestRefURL(t *testing.T) { - repoURL := "/user/repo" - assert.Equal(t, repoURL+"/src/branch/foo", RefURL(repoURL, "refs/heads/foo")) - assert.Equal(t, repoURL+"/src/tag/foo", RefURL(repoURL, "refs/tags/foo")) - assert.Equal(t, repoURL+"/src/commit/c0ffee", RefURL(repoURL, "c0ffee")) +func TestRefWebLinkPath(t *testing.T) { + assert.Equal(t, "branch/foo", RefName("refs/heads/foo").RefWebLinkPath()) + assert.Equal(t, "tag/foo", RefName("refs/tags/foo").RefWebLinkPath()) + assert.Equal(t, "commit/c0ffee", RefName("c0ffee").RefWebLinkPath()) } diff --git a/modules/git/remote.go b/modules/git/remote.go index de8d74eded7..ff8c040eb15 100644 --- a/modules/git/remote.go +++ b/modules/git/remote.go @@ -39,7 +39,7 @@ func GetRemoteURL(ctx context.Context, repoPath, remoteName string) (*giturl.Git if err != nil { return nil, err } - return giturl.Parse(addr) + return giturl.ParseGitURL(addr) } // ErrInvalidCloneAddr represents a "InvalidCloneAddr" kind of error. @@ -79,6 +79,15 @@ func (err *ErrInvalidCloneAddr) Unwrap() error { return util.ErrInvalidArgument } +// IsRemoteNotExistError checks the prefix of the error message to see whether a remote does not exist. +func IsRemoteNotExistError(err error) bool { + // see: https://github.com/go-gitea/gitea/issues/32889#issuecomment-2571848216 + // Should not add space in the end, sometimes git will add a `:` + prefix1 := "exit status 128 - fatal: No such remote" // git < 2.30 + prefix2 := "exit status 2 - error: No such remote" // git >= 2.30 + return strings.HasPrefix(err.Error(), prefix1) || strings.HasPrefix(err.Error(), prefix2) +} + // ParseRemoteAddr checks if given remote address is valid, // and returns composed URL with needed username and password. func ParseRemoteAddr(remoteAddr, authUsername, authPassword string) (string, error) { diff --git a/modules/git/repo_archive.go b/modules/git/repo_archive.go index 1bf1aa41b94..92f3e88f7cd 100644 --- a/modules/git/repo_archive.go +++ b/modules/git/repo_archive.go @@ -8,7 +8,6 @@ import ( "context" "fmt" "io" - "os" "path/filepath" "strings" ) @@ -17,37 +16,35 @@ import ( type ArchiveType int const ( - // ZIP zip archive type - ZIP ArchiveType = iota + 1 - // TARGZ tar gz archive type - TARGZ - // BUNDLE bundle archive type - BUNDLE + ArchiveUnknown ArchiveType = iota + ArchiveZip // 1 + ArchiveTarGz // 2 + ArchiveBundle // 3 ) -// String converts an ArchiveType to string +// String converts an ArchiveType to string: the extension of the archive file without prefix dot func (a ArchiveType) String() string { switch a { - case ZIP: + case ArchiveZip: return "zip" - case TARGZ: + case ArchiveTarGz: return "tar.gz" - case BUNDLE: + case ArchiveBundle: return "bundle" } return "unknown" } -func ToArchiveType(s string) ArchiveType { - switch s { - case "zip": - return ZIP - case "tar.gz": - return TARGZ - case "bundle": - return BUNDLE +func SplitArchiveNameType(s string) (string, ArchiveType) { + switch { + case strings.HasSuffix(s, ".zip"): + return strings.TrimSuffix(s, ".zip"), ArchiveZip + case strings.HasSuffix(s, ".tar.gz"): + return strings.TrimSuffix(s, ".tar.gz"), ArchiveTarGz + case strings.HasSuffix(s, ".bundle"): + return strings.TrimSuffix(s, ".bundle"), ArchiveBundle } - return 0 + return s, ArchiveUnknown } // CreateArchive create archive content to the target path @@ -63,15 +60,11 @@ func (repo *Repository) CreateArchive(ctx context.Context, format ArchiveType, t cmd.AddOptionFormat("--format=%s", format.String()) cmd.AddDynamicArguments(commitID) - // Avoid LFS hooks getting installed because of /etc/gitconfig, which can break pull requests. - env := append(os.Environ(), "GIT_CONFIG_NOSYSTEM=1") - var stderr strings.Builder err := cmd.Run(&RunOpts{ Dir: repo.Path, Stdout: target, Stderr: &stderr, - Env: env, }) if err != nil { return ConcatenateError(err, stderr.String()) diff --git a/modules/git/repo_archive_test.go b/modules/git/repo_archive_test.go new file mode 100644 index 00000000000..ff7e2dfce1b --- /dev/null +++ b/modules/git/repo_archive_test.go @@ -0,0 +1,32 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package git + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestArchiveType(t *testing.T) { + name, archiveType := SplitArchiveNameType("test.tar.gz") + assert.Equal(t, "test", name) + assert.Equal(t, "tar.gz", archiveType.String()) + + name, archiveType = SplitArchiveNameType("a/b/test.zip") + assert.Equal(t, "a/b/test", name) + assert.Equal(t, "zip", archiveType.String()) + + name, archiveType = SplitArchiveNameType("1234.bundle") + assert.Equal(t, "1234", name) + assert.Equal(t, "bundle", archiveType.String()) + + name, archiveType = SplitArchiveNameType("test") + assert.Equal(t, "test", name) + assert.Equal(t, "unknown", archiveType.String()) + + name, archiveType = SplitArchiveNameType("test.xz") + assert.Equal(t, "test.xz", name) + assert.Equal(t, "unknown", archiveType.String()) +} diff --git a/modules/git/repo_branch_gogit.go b/modules/git/repo_branch_gogit.go index dbc4a5fedc9..77aecb21ebd 100644 --- a/modules/git/repo_branch_gogit.go +++ b/modules/git/repo_branch_gogit.go @@ -57,7 +57,7 @@ func (repo *Repository) IsBranchExist(name string) bool { // GetBranches returns branches from the repository, skipping "skip" initial branches and // returning at most "limit" branches, or all branches if "limit" is 0. -// Branches are returned with sort of `-commiterdate` as the nogogit +// Branches are returned with sort of `-committerdate` as the nogogit // implementation. This requires full fetch, sort and then the // skip/limit applies later as gogit returns in undefined order. func (repo *Repository) GetBranchNames(skip, limit int) ([]string, int, error) { diff --git a/modules/git/repo_commit.go b/modules/git/repo_commit.go index 647894bb213..02d8e163e4f 100644 --- a/modules/git/repo_commit.go +++ b/modules/git/repo_commit.go @@ -519,6 +519,7 @@ func (repo *Repository) AddLastCommitCache(cacheKey, fullName, sha string) error return nil } +// GetCommitBranchStart returns the commit where the branch diverged func (repo *Repository) GetCommitBranchStart(env []string, branch, endCommitID string) (string, error) { cmd := NewCommand(repo.Ctx, "log", prettyLogFormat) cmd.AddDynamicArguments(endCommitID) @@ -533,7 +534,8 @@ func (repo *Repository) GetCommitBranchStart(env []string, branch, endCommitID s parts := bytes.Split(bytes.TrimSpace(stdout), []byte{'\n'}) - var startCommitID string + // check the commits one by one until we find a commit contained by another branch + // and we think this commit is the divergence point for _, commitID := range parts { branches, err := repo.getBranches(env, string(commitID), 2) if err != nil { @@ -541,11 +543,9 @@ func (repo *Repository) GetCommitBranchStart(env []string, branch, endCommitID s } for _, b := range branches { if b != branch { - return startCommitID, nil + return string(commitID), nil } } - - startCommitID = string(commitID) } return "", nil diff --git a/modules/git/repo_language_stats_test.go b/modules/git/repo_language_stats_test.go index da3871e909a..1ee5f4c3af2 100644 --- a/modules/git/repo_language_stats_test.go +++ b/modules/git/repo_language_stats_test.go @@ -10,20 +10,18 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestRepository_GetLanguageStats(t *testing.T) { repoPath := filepath.Join(testReposDir, "language_stats_repo") gitRepo, err := openRepositoryWithDefaultContext(repoPath) - if !assert.NoError(t, err) { - t.Fatal() - } + require.NoError(t, err) + defer gitRepo.Close() stats, err := gitRepo.GetLanguageStats("8fee858da5796dfb37704761701bb8e800ad9ef3") - if !assert.NoError(t, err) { - t.Fatal() - } + require.NoError(t, err) assert.EqualValues(t, map[string]int64{ "Python": 134, diff --git a/modules/git/repo_tag_test.go b/modules/git/repo_tag_test.go index 0117cb902d4..f1f081680a3 100644 --- a/modules/git/repo_tag_test.go +++ b/modules/git/repo_tag_test.go @@ -182,7 +182,6 @@ func TestRepository_GetAnnotatedTag(t *testing.T) { // Annotated tag's name should fail tag3, err := bareRepo1.GetAnnotatedTag(aTagName) - assert.Error(t, err) assert.Errorf(t, err, "Length must be 40: %d", len(aTagName)) assert.Nil(t, tag3) diff --git a/modules/git/submodule.go b/modules/git/submodule.go new file mode 100644 index 00000000000..017b644052b --- /dev/null +++ b/modules/git/submodule.go @@ -0,0 +1,66 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package git + +import ( + "bufio" + "context" + "fmt" + "os" + + "code.gitea.io/gitea/modules/log" +) + +type TemplateSubmoduleCommit struct { + Path string + Commit string +} + +// GetTemplateSubmoduleCommits returns a list of submodules paths and their commits from a repository +// This function is only for generating new repos based on existing template, the template couldn't be too large. +func GetTemplateSubmoduleCommits(ctx context.Context, repoPath string) (submoduleCommits []TemplateSubmoduleCommit, _ error) { + stdoutReader, stdoutWriter, err := os.Pipe() + if err != nil { + return nil, err + } + opts := &RunOpts{ + Dir: repoPath, + Stdout: stdoutWriter, + PipelineFunc: func(ctx context.Context, cancel context.CancelFunc) error { + _ = stdoutWriter.Close() + defer stdoutReader.Close() + + scanner := bufio.NewScanner(stdoutReader) + for scanner.Scan() { + entry, err := parseLsTreeLine(scanner.Bytes()) + if err != nil { + cancel() + return err + } + if entry.EntryMode == EntryModeCommit { + submoduleCommits = append(submoduleCommits, TemplateSubmoduleCommit{Path: entry.Name, Commit: entry.ID.String()}) + } + } + return scanner.Err() + }, + } + err = NewCommand(ctx, "ls-tree", "-r", "--", "HEAD").Run(opts) + if err != nil { + return nil, fmt.Errorf("GetTemplateSubmoduleCommits: error running git ls-tree: %v", err) + } + return submoduleCommits, nil +} + +// AddTemplateSubmoduleIndexes Adds the given submodules to the git index. +// It is only for generating new repos based on existing template, requires the .gitmodules file to be already present in the work dir. +func AddTemplateSubmoduleIndexes(ctx context.Context, repoPath string, submodules []TemplateSubmoduleCommit) error { + for _, submodule := range submodules { + cmd := NewCommand(ctx, "update-index", "--add", "--cacheinfo", "160000").AddDynamicArguments(submodule.Commit, submodule.Path) + if stdout, _, err := cmd.RunStdString(&RunOpts{Dir: repoPath}); err != nil { + log.Error("Unable to add %s as submodule to repo %s: stdout %s\nError: %v", submodule.Path, repoPath, stdout, err) + return err + } + } + return nil +} diff --git a/modules/git/submodule_test.go b/modules/git/submodule_test.go new file mode 100644 index 00000000000..d53946a27d4 --- /dev/null +++ b/modules/git/submodule_test.go @@ -0,0 +1,48 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package git + +import ( + "context" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGetTemplateSubmoduleCommits(t *testing.T) { + testRepoPath := filepath.Join(testReposDir, "repo4_submodules") + submodules, err := GetTemplateSubmoduleCommits(DefaultContext, testRepoPath) + require.NoError(t, err) + + assert.Len(t, submodules, 2) + + assert.EqualValues(t, "<°)))><", submodules[0].Path) + assert.EqualValues(t, "d2932de67963f23d43e1c7ecf20173e92ee6c43c", submodules[0].Commit) + + assert.EqualValues(t, "libtest", submodules[1].Path) + assert.EqualValues(t, "1234567890123456789012345678901234567890", submodules[1].Commit) +} + +func TestAddTemplateSubmoduleIndexes(t *testing.T) { + ctx := context.Background() + tmpDir := t.TempDir() + var err error + _, _, err = NewCommand(ctx, "init").RunStdString(&RunOpts{Dir: tmpDir}) + require.NoError(t, err) + _ = os.Mkdir(filepath.Join(tmpDir, "new-dir"), 0o755) + err = AddTemplateSubmoduleIndexes(ctx, tmpDir, []TemplateSubmoduleCommit{{Path: "new-dir", Commit: "1234567890123456789012345678901234567890"}}) + require.NoError(t, err) + _, _, err = NewCommand(ctx, "add", "--all").RunStdString(&RunOpts{Dir: tmpDir}) + require.NoError(t, err) + _, _, err = NewCommand(ctx, "-c", "user.name=a", "-c", "user.email=b", "commit", "-m=test").RunStdString(&RunOpts{Dir: tmpDir}) + require.NoError(t, err) + submodules, err := GetTemplateSubmoduleCommits(DefaultContext, tmpDir) + require.NoError(t, err) + assert.Len(t, submodules, 1) + assert.EqualValues(t, "new-dir", submodules[0].Path) + assert.EqualValues(t, "1234567890123456789012345678901234567890", submodules[0].Commit) +} diff --git a/services/gitdiff/testdata/academic-module/HEAD b/modules/git/tests/repos/repo4_submodules/HEAD similarity index 100% rename from services/gitdiff/testdata/academic-module/HEAD rename to modules/git/tests/repos/repo4_submodules/HEAD diff --git a/modules/git/tests/repos/repo4_submodules/config b/modules/git/tests/repos/repo4_submodules/config new file mode 100644 index 00000000000..07d359d07cf --- /dev/null +++ b/modules/git/tests/repos/repo4_submodules/config @@ -0,0 +1,4 @@ +[core] + repositoryformatversion = 0 + filemode = true + bare = true diff --git a/modules/git/tests/repos/repo4_submodules/objects/97/c3d30df0e6492348292600920a6482feaebb74 b/modules/git/tests/repos/repo4_submodules/objects/97/c3d30df0e6492348292600920a6482feaebb74 new file mode 100644 index 00000000000..7596090b49f Binary files /dev/null and b/modules/git/tests/repos/repo4_submodules/objects/97/c3d30df0e6492348292600920a6482feaebb74 differ diff --git a/modules/git/tests/repos/repo4_submodules/objects/c7/e064ed49b44523cba8a5dfbc37d2ce1bb41d34 b/modules/git/tests/repos/repo4_submodules/objects/c7/e064ed49b44523cba8a5dfbc37d2ce1bb41d34 new file mode 100644 index 00000000000..e3a13c156dc Binary files /dev/null and b/modules/git/tests/repos/repo4_submodules/objects/c7/e064ed49b44523cba8a5dfbc37d2ce1bb41d34 differ diff --git a/modules/git/tests/repos/repo4_submodules/objects/e1/e59caba97193d48862d6809912043871f37437 b/modules/git/tests/repos/repo4_submodules/objects/e1/e59caba97193d48862d6809912043871f37437 new file mode 100644 index 00000000000..a8d6e5c17c8 --- /dev/null +++ b/modules/git/tests/repos/repo4_submodules/objects/e1/e59caba97193d48862d6809912043871f37437 @@ -0,0 +1,2 @@ +x[ +0E*_$M5tifBk Iŕ7k~9ܘܠ.j O "z`#IirF͹$%|4)?t=:K#[$D^ӒyHU/f?G \ No newline at end of file diff --git a/modules/git/tests/repos/repo4_submodules/refs/heads/master b/modules/git/tests/repos/repo4_submodules/refs/heads/master new file mode 100644 index 00000000000..102bc34da8c --- /dev/null +++ b/modules/git/tests/repos/repo4_submodules/refs/heads/master @@ -0,0 +1 @@ +e1e59caba97193d48862d6809912043871f37437 diff --git a/modules/git/tree.go b/modules/git/tree.go index d35dc58d8d0..5a644f6c87a 100644 --- a/modules/git/tree.go +++ b/modules/git/tree.go @@ -17,7 +17,7 @@ func NewTree(repo *Repository, id ObjectID) *Tree { } } -// SubTree get a sub tree by the sub dir path +// SubTree get a subtree by the sub dir path func (t *Tree) SubTree(rpath string) (*Tree, error) { if len(rpath) == 0 { return t, nil @@ -63,7 +63,7 @@ func (repo *Repository) LsTree(ref string, filenames ...string) ([]string, error return filelist, err } -// GetTreePathLatestCommitID returns the latest commit of a tree path +// GetTreePathLatestCommit returns the latest commit of a tree path func (repo *Repository) GetTreePathLatestCommit(refName, treePath string) (*Commit, error) { stdout, _, err := NewCommand(repo.Ctx, "rev-list", "-1"). AddDynamicArguments(refName).AddDashesAndList(treePath). diff --git a/modules/git/tree_blob_nogogit.go b/modules/git/tree_blob_nogogit.go index 92d3d107a74..b7bcf40edd2 100644 --- a/modules/git/tree_blob_nogogit.go +++ b/modules/git/tree_blob_nogogit.go @@ -17,7 +17,6 @@ func (t *Tree) GetTreeEntryByPath(relpath string) (*TreeEntry, error) { ptree: t, ID: t.ID, name: "", - fullName: "", entryMode: EntryModeTree, }, nil } diff --git a/modules/git/tree_entry_mode.go b/modules/git/tree_entry_mode.go index a399118cf85..ec4487549df 100644 --- a/modules/git/tree_entry_mode.go +++ b/modules/git/tree_entry_mode.go @@ -3,7 +3,10 @@ package git -import "strconv" +import ( + "fmt" + "strconv" +) // EntryMode the type of the object in the git tree type EntryMode int @@ -11,6 +14,9 @@ type EntryMode int // There are only a few file modes in Git. They look like unix file modes, but they can only be // one of these. const ( + // EntryModeNoEntry is possible if the file was added or removed in a commit. In the case of + // added the base commit will not have the file in its tree so a mode of 0o000000 is used. + EntryModeNoEntry EntryMode = 0o000000 // EntryModeBlob EntryModeBlob EntryMode = 0o100644 // EntryModeExec @@ -33,3 +39,22 @@ func ToEntryMode(value string) EntryMode { v, _ := strconv.ParseInt(value, 8, 32) return EntryMode(v) } + +func ParseEntryMode(mode string) (EntryMode, error) { + switch mode { + case "000000": + return EntryModeNoEntry, nil + case "100644": + return EntryModeBlob, nil + case "100755": + return EntryModeExec, nil + case "120000": + return EntryModeSymlink, nil + case "160000": + return EntryModeCommit, nil + case "040000", "040755": // git uses 040000 for tree object, but some users may get 040755 for unknown reasons + return EntryModeTree, nil + default: + return 0, fmt.Errorf("unparsable entry mode: %s", mode) + } +} diff --git a/modules/git/tree_entry_nogogit.go b/modules/git/tree_entry_nogogit.go index 1c3bcd197a0..81fb638d56f 100644 --- a/modules/git/tree_entry_nogogit.go +++ b/modules/git/tree_entry_nogogit.go @@ -9,23 +9,17 @@ import "code.gitea.io/gitea/modules/log" // TreeEntry the leaf in the git tree type TreeEntry struct { - ID ObjectID - + ID ObjectID ptree *Tree entryMode EntryMode name string - - size int64 - sized bool - fullName string + size int64 + sized bool } // Name returns the name of the entry func (te *TreeEntry) Name() string { - if te.fullName != "" { - return te.fullName - } return te.name } diff --git a/modules/git/url/url.go b/modules/git/url/url.go index 637685183e5..1c5e8377a6c 100644 --- a/modules/git/url/url.go +++ b/modules/git/url/url.go @@ -4,9 +4,15 @@ package url import ( + "context" "fmt" + "net" stdurl "net/url" "strings" + + "code.gitea.io/gitea/modules/httplib" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" ) // ErrWrongURLFormat represents an error with wrong url format @@ -21,7 +27,7 @@ func (err ErrWrongURLFormat) Error() string { // GitURL represents a git URL type GitURL struct { *stdurl.URL - extraMark int // 0 no extra 1 scp 2 file path with no prefix + extraMark int // 0: standard URL with scheme, 1: scp short syntax (no scheme), 2: file path with no prefix } // String returns the URL's string @@ -38,8 +44,11 @@ func (u *GitURL) String() string { } } -// Parse parse all kinds of git URL -func Parse(remote string) (*GitURL, error) { +// ParseGitURL parse all kinds of git URL: +// * Full URL: http://git@host/path, http://git@host:port/path +// * SCP short syntax: git@host:/path +// * File path: /dir/repo/path +func ParseGitURL(remote string) (*GitURL, error) { if strings.Contains(remote, "://") { u, err := stdurl.Parse(remote) if err != nil { @@ -87,3 +96,86 @@ func Parse(remote string) (*GitURL, error) { extraMark: 2, }, nil } + +type RepositoryURL struct { + GitURL *GitURL + + // if the URL belongs to current Gitea instance, then the below fields have values + OwnerName string + RepoName string + RemainingPath string +} + +// ParseRepositoryURL tries to parse a Git URL and extract the owner/repository name if it belongs to current Gitea instance. +func ParseRepositoryURL(ctx context.Context, repoURL string) (*RepositoryURL, error) { + // possible urls for git: + // https://my.domain/sub-path//[.git] + // git+ssh://user@my.domain//[.git] + // ssh://user@my.domain//[.git] + // user@my.domain:/[.git] + parsed, err := ParseGitURL(repoURL) + if err != nil { + return nil, err + } + + ret := &RepositoryURL{} + ret.GitURL = parsed + + fillPathParts := func(s string) { + s = strings.TrimPrefix(s, "/") + fields := strings.SplitN(s, "/", 3) + if len(fields) >= 2 { + ret.OwnerName = fields[0] + ret.RepoName = strings.TrimSuffix(fields[1], ".git") + if len(fields) == 3 { + ret.RemainingPath = "/" + fields[2] + } + } + } + + if parsed.URL.Scheme == "http" || parsed.URL.Scheme == "https" { + if !httplib.IsCurrentGiteaSiteURL(ctx, repoURL) { + return ret, nil + } + fillPathParts(strings.TrimPrefix(parsed.URL.Path, setting.AppSubURL)) + } else if parsed.URL.Scheme == "ssh" || parsed.URL.Scheme == "git+ssh" { + domainSSH := setting.SSH.Domain + domainCur := httplib.GuessCurrentHostDomain(ctx) + urlDomain, _, _ := net.SplitHostPort(parsed.URL.Host) + urlDomain = util.IfZero(urlDomain, parsed.URL.Host) + if urlDomain == "" { + return ret, nil + } + // check whether URL domain is the App domain + domainMatches := domainSSH == urlDomain + // check whether URL domain is current domain from context + domainMatches = domainMatches || (domainCur != "" && domainCur == urlDomain) + if domainMatches { + fillPathParts(parsed.URL.Path) + } + } + return ret, nil +} + +// MakeRepositoryWebLink generates a web link (http/https) for a git repository (by guessing sometimes) +func MakeRepositoryWebLink(repoURL *RepositoryURL) string { + if repoURL.OwnerName != "" { + return setting.AppSubURL + "/" + repoURL.OwnerName + "/" + repoURL.RepoName + } + + // now, let's guess, for example: + // * git@github.com:owner/submodule.git + // * https://github.com/example/submodule1.git + if repoURL.GitURL.Scheme == "http" || repoURL.GitURL.Scheme == "https" { + return strings.TrimSuffix(repoURL.GitURL.String(), ".git") + } else if repoURL.GitURL.Scheme == "ssh" || repoURL.GitURL.Scheme == "git+ssh" { + hostname, _, _ := net.SplitHostPort(repoURL.GitURL.Host) + hostname = util.IfZero(hostname, repoURL.GitURL.Host) + urlPath := strings.TrimSuffix(repoURL.GitURL.Path, ".git") + urlPath = strings.TrimPrefix(urlPath, "/") + urlFull := fmt.Sprintf("https://%s/%s", hostname, urlPath) + urlFull = strings.TrimSuffix(urlFull, "/") + return urlFull + } + return "" +} diff --git a/modules/git/url/url_test.go b/modules/git/url/url_test.go index da820ed8897..9c020adb4d9 100644 --- a/modules/git/url/url_test.go +++ b/modules/git/url/url_test.go @@ -4,9 +4,15 @@ package url import ( + "context" + "net/http" "net/url" "testing" + "code.gitea.io/gitea/modules/httplib" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" + "github.com/stretchr/testify/assert" ) @@ -157,10 +163,105 @@ func TestParseGitURLs(t *testing.T) { for _, kase := range kases { t.Run(kase.kase, func(t *testing.T) { - u, err := Parse(kase.kase) + u, err := ParseGitURL(kase.kase) assert.NoError(t, err) assert.EqualValues(t, kase.expected.extraMark, u.extraMark) assert.EqualValues(t, *kase.expected, *u) }) } } + +func TestParseRepositoryURL(t *testing.T) { + defer test.MockVariableValue(&setting.AppURL, "https://localhost:3000")() + defer test.MockVariableValue(&setting.SSH.Domain, "try.gitea.io")() + + ctxURL, _ := url.Parse("https://gitea") + ctxReq := &http.Request{URL: ctxURL, Header: http.Header{}} + ctxReq.Host = ctxURL.Host + ctxReq.Header.Add("X-Forwarded-Proto", ctxURL.Scheme) + ctx := context.WithValue(context.Background(), httplib.RequestContextKey, ctxReq) + cases := []struct { + input string + ownerName, repoName, remaining string + }{ + {input: "/user/repo"}, + + {input: "https://localhost:3000/user/repo", ownerName: "user", repoName: "repo"}, + {input: "https://external:3000/user/repo"}, + + {input: "https://localhost:3000/user/repo.git/other", ownerName: "user", repoName: "repo", remaining: "/other"}, + + {input: "https://gitea/user/repo", ownerName: "user", repoName: "repo"}, + {input: "https://gitea:3333/user/repo"}, + + {input: "ssh://try.gitea.io:2222/user/repo", ownerName: "user", repoName: "repo"}, + {input: "ssh://external:2222/user/repo"}, + + {input: "git+ssh://user@try.gitea.io/user/repo.git", ownerName: "user", repoName: "repo"}, + {input: "git+ssh://user@external/user/repo.git"}, + + {input: "root@try.gitea.io:user/repo.git", ownerName: "user", repoName: "repo"}, + {input: "root@gitea:user/repo.git", ownerName: "user", repoName: "repo"}, + {input: "root@external:user/repo.git"}, + } + + for _, c := range cases { + t.Run(c.input, func(t *testing.T) { + ret, _ := ParseRepositoryURL(ctx, c.input) + assert.Equal(t, c.ownerName, ret.OwnerName) + assert.Equal(t, c.repoName, ret.RepoName) + assert.Equal(t, c.remaining, ret.RemainingPath) + }) + } + + t.Run("WithSubpath", func(t *testing.T) { + defer test.MockVariableValue(&setting.AppURL, "https://localhost:3000/subpath")() + defer test.MockVariableValue(&setting.AppSubURL, "/subpath")() + cases = []struct { + input string + ownerName, repoName, remaining string + }{ + {input: "https://localhost:3000/user/repo"}, + {input: "https://localhost:3000/subpath/user/repo.git/other", ownerName: "user", repoName: "repo", remaining: "/other"}, + + {input: "ssh://try.gitea.io:2222/user/repo", ownerName: "user", repoName: "repo"}, + {input: "ssh://external:2222/user/repo"}, + + {input: "git+ssh://user@try.gitea.io/user/repo.git", ownerName: "user", repoName: "repo"}, + {input: "git+ssh://user@external/user/repo.git"}, + + {input: "root@try.gitea.io:user/repo.git", ownerName: "user", repoName: "repo"}, + {input: "root@external:user/repo.git"}, + } + + for _, c := range cases { + t.Run(c.input, func(t *testing.T) { + ret, _ := ParseRepositoryURL(ctx, c.input) + assert.Equal(t, c.ownerName, ret.OwnerName) + assert.Equal(t, c.repoName, ret.RepoName) + assert.Equal(t, c.remaining, ret.RemainingPath) + }) + } + }) +} + +func TestMakeRepositoryBaseLink(t *testing.T) { + defer test.MockVariableValue(&setting.AppURL, "https://localhost:3000/subpath")() + defer test.MockVariableValue(&setting.AppSubURL, "/subpath")() + + u, err := ParseRepositoryURL(context.Background(), "https://localhost:3000/subpath/user/repo.git") + assert.NoError(t, err) + assert.Equal(t, "/subpath/user/repo", MakeRepositoryWebLink(u)) + + u, err = ParseRepositoryURL(context.Background(), "https://github.com/owner/repo.git") + assert.NoError(t, err) + assert.Equal(t, "https://github.com/owner/repo", MakeRepositoryWebLink(u)) + + u, err = ParseRepositoryURL(context.Background(), "git@github.com:owner/repo.git") + assert.NoError(t, err) + assert.Equal(t, "https://github.com/owner/repo", MakeRepositoryWebLink(u)) + + u, err = ParseRepositoryURL(context.Background(), "git+ssh://other:123/owner/repo.git") + assert.NoError(t, err) + assert.Equal(t, "https://other/owner/repo", MakeRepositoryWebLink(u)) +} diff --git a/modules/gitrepo/gitrepo.go b/modules/gitrepo/gitrepo.go index 831b9d7bb78..540b7244892 100644 --- a/modules/gitrepo/gitrepo.go +++ b/modules/gitrepo/gitrepo.go @@ -43,19 +43,20 @@ type contextKey struct { } // RepositoryFromContextOrOpen attempts to get the repository from the context or just opens it +// The caller must call "defer gitRepo.Close()" func RepositoryFromContextOrOpen(ctx context.Context, repo Repository) (*git.Repository, io.Closer, error) { - ds := reqctx.GetRequestDataStore(ctx) - if ds != nil { - gitRepo, err := RepositoryFromRequestContextOrOpen(ctx, ds, repo) + reqCtx := reqctx.FromContext(ctx) + if reqCtx != nil { + gitRepo, err := RepositoryFromRequestContextOrOpen(reqCtx, repo) return gitRepo, util.NopCloser{}, err } gitRepo, err := OpenRepository(ctx, repo) return gitRepo, gitRepo, err } -// RepositoryFromRequestContextOrOpen opens the repository at the given relative path in the provided request context -// The repo will be automatically closed when the request context is done -func RepositoryFromRequestContextOrOpen(ctx context.Context, ds reqctx.RequestDataStore, repo Repository) (*git.Repository, error) { +// RepositoryFromRequestContextOrOpen opens the repository at the given relative path in the provided request context. +// Caller shouldn't close the git repo manually, the git repo will be automatically closed when the request context is done. +func RepositoryFromRequestContextOrOpen(ctx reqctx.RequestContext, repo Repository) (*git.Repository, error) { ck := contextKey{repoPath: repoPath(repo)} if gitRepo, ok := ctx.Value(ck).(*git.Repository); ok { return gitRepo, nil @@ -64,7 +65,7 @@ func RepositoryFromRequestContextOrOpen(ctx context.Context, ds reqctx.RequestDa if err != nil { return nil, err } - ds.AddCloser(gitRepo) - ds.SetContextValue(ck, gitRepo) + ctx.AddCloser(gitRepo) + ctx.SetContextValue(ck, gitRepo) return gitRepo, nil } diff --git a/modules/gtprof/event.go b/modules/gtprof/event.go new file mode 100644 index 00000000000..da4a0faff93 --- /dev/null +++ b/modules/gtprof/event.go @@ -0,0 +1,32 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package gtprof + +type EventConfig struct { + attributes []*TraceAttribute +} + +type EventOption interface { + applyEvent(*EventConfig) +} + +type applyEventFunc func(*EventConfig) + +func (f applyEventFunc) applyEvent(cfg *EventConfig) { + f(cfg) +} + +func WithAttributes(attrs ...*TraceAttribute) EventOption { + return applyEventFunc(func(cfg *EventConfig) { + cfg.attributes = append(cfg.attributes, attrs...) + }) +} + +func eventConfigFromOptions(options ...EventOption) *EventConfig { + cfg := &EventConfig{} + for _, opt := range options { + opt.applyEvent(cfg) + } + return cfg +} diff --git a/modules/gtprof/trace.go b/modules/gtprof/trace.go new file mode 100644 index 00000000000..ad67c226dce --- /dev/null +++ b/modules/gtprof/trace.go @@ -0,0 +1,175 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package gtprof + +import ( + "context" + "fmt" + "sync" + "time" + + "code.gitea.io/gitea/modules/util" +) + +type contextKey struct { + name string +} + +var contextKeySpan = &contextKey{"span"} + +type traceStarter interface { + start(ctx context.Context, traceSpan *TraceSpan, internalSpanIdx int) (context.Context, traceSpanInternal) +} + +type traceSpanInternal interface { + addEvent(name string, cfg *EventConfig) + recordError(err error, cfg *EventConfig) + end() +} + +type TraceSpan struct { + // immutable + parent *TraceSpan + internalSpans []traceSpanInternal + internalContexts []context.Context + + // mutable, must be protected by mutex + mu sync.RWMutex + name string + statusCode uint32 + statusDesc string + startTime time.Time + endTime time.Time + attributes []*TraceAttribute + children []*TraceSpan +} + +type TraceAttribute struct { + Key string + Value TraceValue +} + +type TraceValue struct { + v any +} + +func (t *TraceValue) AsString() string { + return fmt.Sprint(t.v) +} + +func (t *TraceValue) AsInt64() int64 { + v, _ := util.ToInt64(t.v) + return v +} + +func (t *TraceValue) AsFloat64() float64 { + v, _ := util.ToFloat64(t.v) + return v +} + +var globalTraceStarters []traceStarter + +type Tracer struct { + starters []traceStarter +} + +func (s *TraceSpan) SetName(name string) { + s.mu.Lock() + defer s.mu.Unlock() + s.name = name +} + +func (s *TraceSpan) SetStatus(code uint32, desc string) { + s.mu.Lock() + defer s.mu.Unlock() + s.statusCode, s.statusDesc = code, desc +} + +func (s *TraceSpan) AddEvent(name string, options ...EventOption) { + cfg := eventConfigFromOptions(options...) + for _, tsp := range s.internalSpans { + tsp.addEvent(name, cfg) + } +} + +func (s *TraceSpan) RecordError(err error, options ...EventOption) { + cfg := eventConfigFromOptions(options...) + for _, tsp := range s.internalSpans { + tsp.recordError(err, cfg) + } +} + +func (s *TraceSpan) SetAttributeString(key, value string) *TraceSpan { + s.mu.Lock() + defer s.mu.Unlock() + + s.attributes = append(s.attributes, &TraceAttribute{Key: key, Value: TraceValue{v: value}}) + return s +} + +func (t *Tracer) Start(ctx context.Context, spanName string) (context.Context, *TraceSpan) { + starters := t.starters + if starters == nil { + starters = globalTraceStarters + } + ts := &TraceSpan{name: spanName, startTime: time.Now()} + parentSpan := GetContextSpan(ctx) + if parentSpan != nil { + parentSpan.mu.Lock() + parentSpan.children = append(parentSpan.children, ts) + parentSpan.mu.Unlock() + ts.parent = parentSpan + } + + parentCtx := ctx + for internalSpanIdx, tsp := range starters { + var internalSpan traceSpanInternal + if parentSpan != nil { + parentCtx = parentSpan.internalContexts[internalSpanIdx] + } + ctx, internalSpan = tsp.start(parentCtx, ts, internalSpanIdx) + ts.internalContexts = append(ts.internalContexts, ctx) + ts.internalSpans = append(ts.internalSpans, internalSpan) + } + ctx = context.WithValue(ctx, contextKeySpan, ts) + return ctx, ts +} + +type mutableContext interface { + context.Context + SetContextValue(key, value any) + GetContextValue(key any) any +} + +// StartInContext starts a trace span in Gitea's mutable context (usually the web request context). +// Due to the design limitation of Gitea's web framework, it can't use `context.WithValue` to bind a new span into a new context. +// So here we use our "reqctx" framework to achieve the same result: web request context could always see the latest "span". +func (t *Tracer) StartInContext(ctx mutableContext, spanName string) (*TraceSpan, func()) { + curTraceSpan := GetContextSpan(ctx) + _, newTraceSpan := GetTracer().Start(ctx, spanName) + ctx.SetContextValue(contextKeySpan, newTraceSpan) + return newTraceSpan, func() { + newTraceSpan.End() + ctx.SetContextValue(contextKeySpan, curTraceSpan) + } +} + +func (s *TraceSpan) End() { + s.mu.Lock() + s.endTime = time.Now() + s.mu.Unlock() + + for _, tsp := range s.internalSpans { + tsp.end() + } +} + +func GetTracer() *Tracer { + return &Tracer{} +} + +func GetContextSpan(ctx context.Context) *TraceSpan { + ts, _ := ctx.Value(contextKeySpan).(*TraceSpan) + return ts +} diff --git a/modules/gtprof/trace_builtin.go b/modules/gtprof/trace_builtin.go new file mode 100644 index 00000000000..41743a25e4d --- /dev/null +++ b/modules/gtprof/trace_builtin.go @@ -0,0 +1,96 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package gtprof + +import ( + "context" + "fmt" + "strings" + "sync/atomic" + "time" + + "code.gitea.io/gitea/modules/tailmsg" +) + +type traceBuiltinStarter struct{} + +type traceBuiltinSpan struct { + ts *TraceSpan + + internalSpanIdx int +} + +func (t *traceBuiltinSpan) addEvent(name string, cfg *EventConfig) { + // No-op because builtin tracer doesn't need it. + // In the future we might use it to mark the time point between backend logic and network response. +} + +func (t *traceBuiltinSpan) recordError(err error, cfg *EventConfig) { + // No-op because builtin tracer doesn't need it. + // Actually Gitea doesn't handle err this way in most cases +} + +func (t *traceBuiltinSpan) toString(out *strings.Builder, indent int) { + t.ts.mu.RLock() + defer t.ts.mu.RUnlock() + + out.WriteString(strings.Repeat(" ", indent)) + out.WriteString(t.ts.name) + if t.ts.endTime.IsZero() { + out.WriteString(" duration: (not ended)") + } else { + out.WriteString(fmt.Sprintf(" duration=%.4fs", t.ts.endTime.Sub(t.ts.startTime).Seconds())) + } + for _, a := range t.ts.attributes { + out.WriteString(" ") + out.WriteString(a.Key) + out.WriteString("=") + value := a.Value.AsString() + if strings.ContainsAny(value, " \t\r\n") { + quoted := false + for _, c := range "\"'`" { + if quoted = !strings.Contains(value, string(c)); quoted { + value = string(c) + value + string(c) + break + } + } + if !quoted { + value = fmt.Sprintf("%q", value) + } + } + out.WriteString(value) + } + out.WriteString("\n") + for _, c := range t.ts.children { + span := c.internalSpans[t.internalSpanIdx].(*traceBuiltinSpan) + span.toString(out, indent+2) + } +} + +func (t *traceBuiltinSpan) end() { + if t.ts.parent == nil { + // TODO: debug purpose only + // TODO: it should distinguish between http response network lag and actual processing time + threshold := time.Duration(traceBuiltinThreshold.Load()) + if threshold != 0 && t.ts.endTime.Sub(t.ts.startTime) > threshold { + sb := &strings.Builder{} + t.toString(sb, 0) + tailmsg.GetManager().GetTraceRecorder().Record(sb.String()) + } + } +} + +func (t *traceBuiltinStarter) start(ctx context.Context, traceSpan *TraceSpan, internalSpanIdx int) (context.Context, traceSpanInternal) { + return ctx, &traceBuiltinSpan{ts: traceSpan, internalSpanIdx: internalSpanIdx} +} + +func init() { + globalTraceStarters = append(globalTraceStarters, &traceBuiltinStarter{}) +} + +var traceBuiltinThreshold atomic.Int64 + +func EnableBuiltinTracer(threshold time.Duration) { + traceBuiltinThreshold.Store(int64(threshold)) +} diff --git a/modules/gtprof/trace_const.go b/modules/gtprof/trace_const.go new file mode 100644 index 00000000000..af9ce9223fd --- /dev/null +++ b/modules/gtprof/trace_const.go @@ -0,0 +1,19 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package gtprof + +// Some interesting names could be found in https://github.com/open-telemetry/opentelemetry-go/tree/main/semconv + +const ( + TraceSpanHTTP = "http" + TraceSpanGitRun = "git-run" + TraceSpanDatabase = "database" +) + +const ( + TraceAttrFuncCaller = "func.caller" + TraceAttrDbSQL = "db.sql" + TraceAttrGitCommand = "git.command" + TraceAttrHTTPRoute = "http.route" +) diff --git a/modules/gtprof/trace_test.go b/modules/gtprof/trace_test.go new file mode 100644 index 00000000000..7e1743c88d4 --- /dev/null +++ b/modules/gtprof/trace_test.go @@ -0,0 +1,93 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package gtprof + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" +) + +// "vendor span" is a simple demo for a span from a vendor library + +var vendorContextKey any = "vendorContextKey" + +type vendorSpan struct { + name string + children []*vendorSpan +} + +func vendorTraceStart(ctx context.Context, name string) (context.Context, *vendorSpan) { + span := &vendorSpan{name: name} + parentSpan, ok := ctx.Value(vendorContextKey).(*vendorSpan) + if ok { + parentSpan.children = append(parentSpan.children, span) + } + ctx = context.WithValue(ctx, vendorContextKey, span) + return ctx, span +} + +// below "testTrace*" integrate the vendor span into our trace system + +type testTraceSpan struct { + vendorSpan *vendorSpan +} + +func (t *testTraceSpan) addEvent(name string, cfg *EventConfig) {} + +func (t *testTraceSpan) recordError(err error, cfg *EventConfig) {} + +func (t *testTraceSpan) end() {} + +type testTraceStarter struct{} + +func (t *testTraceStarter) start(ctx context.Context, traceSpan *TraceSpan, internalSpanIdx int) (context.Context, traceSpanInternal) { + ctx, span := vendorTraceStart(ctx, traceSpan.name) + return ctx, &testTraceSpan{span} +} + +func TestTraceStarter(t *testing.T) { + globalTraceStarters = []traceStarter{&testTraceStarter{}} + + ctx := context.Background() + ctx, span := GetTracer().Start(ctx, "root") + defer span.End() + + func(ctx context.Context) { + ctx, span := GetTracer().Start(ctx, "span1") + defer span.End() + func(ctx context.Context) { + _, span := GetTracer().Start(ctx, "spanA") + defer span.End() + }(ctx) + func(ctx context.Context) { + _, span := GetTracer().Start(ctx, "spanB") + defer span.End() + }(ctx) + }(ctx) + + func(ctx context.Context) { + _, span := GetTracer().Start(ctx, "span2") + defer span.End() + }(ctx) + + var spanFullNames []string + var collectSpanNames func(parentFullName string, s *vendorSpan) + collectSpanNames = func(parentFullName string, s *vendorSpan) { + fullName := parentFullName + "/" + s.name + spanFullNames = append(spanFullNames, fullName) + for _, c := range s.children { + collectSpanNames(fullName, c) + } + } + collectSpanNames("", span.internalSpans[0].(*testTraceSpan).vendorSpan) + assert.Equal(t, []string{ + "/root", + "/root/span1", + "/root/span1/spanA", + "/root/span1/spanB", + "/root/span2", + }, spanFullNames) +} diff --git a/modules/htmlutil/html.go b/modules/htmlutil/html.go index 9b5f5a92d89..0ab0e71689d 100644 --- a/modules/htmlutil/html.go +++ b/modules/htmlutil/html.go @@ -30,7 +30,7 @@ func ParseSizeAndClass(defaultSize int, defaultClass string, others ...any) (int return size, class } -func HTMLFormat(s string, rawArgs ...any) template.HTML { +func HTMLFormat(s template.HTML, rawArgs ...any) template.HTML { args := slices.Clone(rawArgs) for i, v := range args { switch v := v.(type) { @@ -44,5 +44,5 @@ func HTMLFormat(s string, rawArgs ...any) template.HTML { args[i] = template.HTMLEscapeString(fmt.Sprint(v)) } } - return template.HTML(fmt.Sprintf(s, args...)) + return template.HTML(fmt.Sprintf(string(s), args...)) } diff --git a/modules/httplib/request.go b/modules/httplib/request.go index 880d7ad3cbe..267e276df34 100644 --- a/modules/httplib/request.go +++ b/modules/httplib/request.go @@ -99,10 +99,10 @@ func (r *Request) Param(key, value string) *Request { return r } -// Body adds request raw body. -// it supports string and []byte. +// Body adds request raw body. It supports string, []byte and io.Reader as body. func (r *Request) Body(data any) *Request { switch t := data.(type) { + case nil: // do nothing case string: bf := bytes.NewBufferString(t) r.req.Body = io.NopCloser(bf) @@ -111,6 +111,12 @@ func (r *Request) Body(data any) *Request { bf := bytes.NewBuffer(t) r.req.Body = io.NopCloser(bf) r.req.ContentLength = int64(len(t)) + case io.ReadCloser: + r.req.Body = t + case io.Reader: + r.req.Body = io.NopCloser(t) + default: + panic(fmt.Sprintf("unsupported request body type %T", t)) } return r } @@ -141,7 +147,7 @@ func (r *Request) getResponse() (*http.Response, error) { } } else if r.req.Method == "POST" && r.req.Body == nil && len(paramBody) > 0 { r.Header("Content-Type", "application/x-www-form-urlencoded") - r.Body(paramBody) + r.Body(paramBody) // string } var err error @@ -185,6 +191,7 @@ func (r *Request) getResponse() (*http.Response, error) { } // Response executes request client gets response manually. +// Caller MUST close the response body if no error occurs func (r *Request) Response() (*http.Response, error) { return r.getResponse() } diff --git a/modules/httplib/serve_test.go b/modules/httplib/serve_test.go index c2229dffe96..e53f38b6974 100644 --- a/modules/httplib/serve_test.go +++ b/modules/httplib/serve_test.go @@ -13,6 +13,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestServeContentByReader(t *testing.T) { @@ -71,9 +72,7 @@ func TestServeContentByReadSeeker(t *testing.T) { } seekReader, err := os.OpenFile(tmpFile, os.O_RDONLY, 0o644) - if !assert.NoError(t, err) { - return - } + require.NoError(t, err) defer seekReader.Close() w := httptest.NewRecorder() diff --git a/modules/httplib/url.go b/modules/httplib/url.go index e3bad1e5fba..f543c091906 100644 --- a/modules/httplib/url.go +++ b/modules/httplib/url.go @@ -5,6 +5,7 @@ package httplib import ( "context" + "net" "net/http" "net/url" "strings" @@ -81,6 +82,12 @@ func GuessCurrentHostURL(ctx context.Context) string { return reqScheme + "://" + req.Host } +func GuessCurrentHostDomain(ctx context.Context) string { + _, host, _ := strings.Cut(GuessCurrentHostURL(ctx), "://") + domain, _, _ := net.SplitHostPort(host) + return util.IfZero(domain, host) +} + // MakeAbsoluteURL tries to make a link to an absolute URL: // * If link is empty, it returns the current app URL. // * If link is absolute, it returns the link. @@ -105,7 +112,7 @@ func IsCurrentGiteaSiteURL(ctx context.Context, s string) bool { if cleanedPath == "" || cleanedPath == "." { u.Path = "/" } else { - u.Path += "/" + cleanedPath + "/" + u.Path = "/" + cleanedPath + "/" } } if urlIsRelative(s, u) { diff --git a/modules/indexer/code/indexer.go b/modules/indexer/code/indexer.go index c1ab26569c6..728b37fab6e 100644 --- a/modules/indexer/code/indexer.go +++ b/modules/indexer/code/indexer.go @@ -123,13 +123,12 @@ func Init() { for _, indexerData := range items { log.Trace("IndexerData Process Repo: %d", indexerData.RepoID) if err := index(ctx, indexer, indexerData.RepoID); err != nil { - unhandled = append(unhandled, indexerData) if !setting.IsInTesting { log.Error("Codes indexer handler: index error for repo %v: %v", indexerData.RepoID, err) } } } - return unhandled + return nil // do not re-queue the failed items, otherwise some broken repo will block the queue } indexerQueue = queue.CreateUniqueQueue(ctx, "code_indexer", handler) diff --git a/modules/indexer/code/indexer_test.go b/modules/indexer/code/indexer_test.go index d04088531ac..f358bbe7851 100644 --- a/modules/indexer/code/indexer_test.go +++ b/modules/indexer/code/indexer_test.go @@ -15,6 +15,8 @@ import ( "code.gitea.io/gitea/modules/indexer/code/bleve" "code.gitea.io/gitea/modules/indexer/code/elasticsearch" "code.gitea.io/gitea/modules/indexer/code/internal" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" _ "code.gitea.io/gitea/models" _ "code.gitea.io/gitea/models/actions" @@ -279,7 +281,7 @@ func testIndexer(name string, t *testing.T, indexer internal.Indexer) { func TestBleveIndexAndSearch(t *testing.T) { unittest.PrepareTestEnv(t) - + defer test.MockVariableValue(&setting.Indexer.TypeBleveMaxFuzzniess, 2)() dir := t.TempDir() idx := bleve.NewIndexer(dir) diff --git a/modules/indexer/internal/bleve/util.go b/modules/indexer/internal/bleve/util.go index a0c3dc4ad45..b6daa9e14b4 100644 --- a/modules/indexer/internal/bleve/util.go +++ b/modules/indexer/internal/bleve/util.go @@ -9,6 +9,7 @@ import ( "unicode" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" "github.com/blevesearch/bleve/v2" @@ -54,9 +55,9 @@ func openIndexer(path string, latestVersion int) (bleve.Index, int, error) { return index, 0, nil } -// This method test the GuessFuzzinessByKeyword method. The fuzziness is based on the levenshtein distance and determines how many chars -// may be different on two string and they still be considered equivalent. -// Given a phrasse, its shortest word determines its fuzziness. If a phrase uses CJK (eg: `갃갃갃` `啊啊啊`), the fuzziness is zero. +// GuessFuzzinessByKeyword guesses fuzziness based on the levenshtein distance and determines how many chars +// may be different on two string, and they still be considered equivalent. +// Given a phrase, its shortest word determines its fuzziness. If a phrase uses CJK (eg: `갃갃갃` `啊啊啊`), the fuzziness is zero. func GuessFuzzinessByKeyword(s string) int { tokenizer := unicode_tokenizer.NewUnicodeTokenizer() tokens := tokenizer.Tokenize([]byte(s)) @@ -85,5 +86,5 @@ func guessFuzzinessByKeyword(s string) int { return 0 } } - return min(maxFuzziness, len(s)/4) + return min(min(setting.Indexer.TypeBleveMaxFuzzniess, maxFuzziness), len(s)/4) } diff --git a/modules/indexer/internal/bleve/util_test.go b/modules/indexer/internal/bleve/util_test.go index 8f7844464e7..1a7e4db0f42 100644 --- a/modules/indexer/internal/bleve/util_test.go +++ b/modules/indexer/internal/bleve/util_test.go @@ -7,10 +7,15 @@ import ( "fmt" "testing" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" + "github.com/stretchr/testify/assert" ) func TestBleveGuessFuzzinessByKeyword(t *testing.T) { + defer test.MockVariableValue(&setting.Indexer.TypeBleveMaxFuzzniess, 2)() + scenarios := []struct { Input string Fuzziness int // See util.go for the definition of fuzziness in this particular context @@ -46,7 +51,7 @@ func TestBleveGuessFuzzinessByKeyword(t *testing.T) { } for _, scenario := range scenarios { - t.Run(fmt.Sprintf("ensure fuzziness of '%s' is '%d'", scenario.Input, scenario.Fuzziness), func(t *testing.T) { + t.Run(fmt.Sprintf("Fuziniess:%s=%d", scenario.Input, scenario.Fuzziness), func(t *testing.T) { assert.Equal(t, scenario.Fuzziness, GuessFuzzinessByKeyword(scenario.Input)) }) } diff --git a/modules/indexer/issues/elasticsearch/elasticsearch_test.go b/modules/indexer/issues/elasticsearch/elasticsearch_test.go index ffd85b1aa1d..dc329c07dd9 100644 --- a/modules/indexer/issues/elasticsearch/elasticsearch_test.go +++ b/modules/indexer/issues/elasticsearch/elasticsearch_test.go @@ -11,6 +11,8 @@ import ( "time" "code.gitea.io/gitea/modules/indexer/issues/internal/tests" + + "github.com/stretchr/testify/require" ) func TestElasticsearchIndexer(t *testing.T) { @@ -26,20 +28,10 @@ func TestElasticsearchIndexer(t *testing.T) { } } - ok := false - for i := 0; i < 60; i++ { + require.Eventually(t, func() bool { resp, err := http.Get(url) - if err == nil && resp.StatusCode == http.StatusOK { - ok = true - break - } - t.Logf("Waiting for elasticsearch to be up: %v", err) - time.Sleep(time.Second) - } - if !ok { - t.Fatalf("Failed to wait for elasticsearch to be up") - return - } + return err == nil && resp.StatusCode == http.StatusOK + }, time.Minute, time.Second, "Expected elasticsearch to be up") indexer := NewIndexer(url, fmt.Sprintf("test_elasticsearch_indexer_%d", time.Now().Unix())) defer indexer.Close() diff --git a/modules/indexer/issues/indexer_test.go b/modules/indexer/issues/indexer_test.go index 06a6a46c234..8043d33eebb 100644 --- a/modules/indexer/issues/indexer_test.go +++ b/modules/indexer/issues/indexer_test.go @@ -19,6 +19,7 @@ import ( _ "code.gitea.io/gitea/models/activities" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestMain(m *testing.M) { @@ -26,7 +27,7 @@ func TestMain(m *testing.M) { } func TestDBSearchIssues(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) + require.NoError(t, unittest.PrepareTestDatabase()) setting.Indexer.IssueType = "db" InitIssueIndexer(true) @@ -83,9 +84,7 @@ func searchIssueWithKeyword(t *testing.T) { for _, test := range tests { issueIDs, _, err := SearchIssues(context.TODO(), &test.opts) - if !assert.NoError(t, err) { - return - } + require.NoError(t, err) assert.Equal(t, test.expectedIDs, issueIDs) } } @@ -120,9 +119,7 @@ func searchIssueByIndex(t *testing.T) { for _, test := range tests { issueIDs, _, err := SearchIssues(context.TODO(), &test.opts) - if !assert.NoError(t, err) { - return - } + require.NoError(t, err) assert.Equal(t, test.expectedIDs, issueIDs) } } @@ -166,9 +163,7 @@ func searchIssueInRepo(t *testing.T) { for _, test := range tests { issueIDs, _, err := SearchIssues(context.TODO(), &test.opts) - if !assert.NoError(t, err) { - return - } + require.NoError(t, err) assert.Equal(t, test.expectedIDs, issueIDs) } } @@ -238,9 +233,7 @@ func searchIssueByID(t *testing.T) { for _, test := range tests { issueIDs, _, err := SearchIssues(context.TODO(), &test.opts) - if !assert.NoError(t, err) { - return - } + require.NoError(t, err) assert.Equal(t, test.expectedIDs, issueIDs) } } @@ -265,9 +258,7 @@ func searchIssueIsPull(t *testing.T) { } for _, test := range tests { issueIDs, _, err := SearchIssues(context.TODO(), &test.opts) - if !assert.NoError(t, err) { - return - } + require.NoError(t, err) assert.Equal(t, test.expectedIDs, issueIDs) } } @@ -292,9 +283,7 @@ func searchIssueIsClosed(t *testing.T) { } for _, test := range tests { issueIDs, _, err := SearchIssues(context.TODO(), &test.opts) - if !assert.NoError(t, err) { - return - } + require.NoError(t, err) assert.Equal(t, test.expectedIDs, issueIDs) } } @@ -319,9 +308,7 @@ func searchIssueIsArchived(t *testing.T) { } for _, test := range tests { issueIDs, _, err := SearchIssues(context.TODO(), &test.opts) - if !assert.NoError(t, err) { - return - } + require.NoError(t, err) assert.Equal(t, test.expectedIDs, issueIDs) } } @@ -346,9 +333,7 @@ func searchIssueByMilestoneID(t *testing.T) { } for _, test := range tests { issueIDs, _, err := SearchIssues(context.TODO(), &test.opts) - if !assert.NoError(t, err) { - return - } + require.NoError(t, err) assert.Equal(t, test.expectedIDs, issueIDs) } } @@ -379,9 +364,7 @@ func searchIssueByLabelID(t *testing.T) { } for _, test := range tests { issueIDs, _, err := SearchIssues(context.TODO(), &test.opts) - if !assert.NoError(t, err) { - return - } + require.NoError(t, err) assert.Equal(t, test.expectedIDs, issueIDs) } } @@ -400,9 +383,7 @@ func searchIssueByTime(t *testing.T) { } for _, test := range tests { issueIDs, _, err := SearchIssues(context.TODO(), &test.opts) - if !assert.NoError(t, err) { - return - } + require.NoError(t, err) assert.Equal(t, test.expectedIDs, issueIDs) } } @@ -421,9 +402,7 @@ func searchIssueWithOrder(t *testing.T) { } for _, test := range tests { issueIDs, _, err := SearchIssues(context.TODO(), &test.opts) - if !assert.NoError(t, err) { - return - } + require.NoError(t, err) assert.Equal(t, test.expectedIDs, issueIDs) } } @@ -454,9 +433,7 @@ func searchIssueInProject(t *testing.T) { } for _, test := range tests { issueIDs, _, err := SearchIssues(context.TODO(), &test.opts) - if !assert.NoError(t, err) { - return - } + require.NoError(t, err) assert.Equal(t, test.expectedIDs, issueIDs) } } @@ -479,9 +456,7 @@ func searchIssueWithPaginator(t *testing.T) { } for _, test := range tests { issueIDs, total, err := SearchIssues(context.TODO(), &test.opts) - if !assert.NoError(t, err) { - return - } + require.NoError(t, err) assert.Equal(t, test.expectedIDs, issueIDs) assert.Equal(t, test.expectedTotal, total) } diff --git a/modules/indexer/issues/meilisearch/meilisearch_test.go b/modules/indexer/issues/meilisearch/meilisearch_test.go index 4666df136a0..a3a332554ac 100644 --- a/modules/indexer/issues/meilisearch/meilisearch_test.go +++ b/modules/indexer/issues/meilisearch/meilisearch_test.go @@ -15,6 +15,7 @@ import ( "github.com/meilisearch/meilisearch-go" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestMeilisearchIndexer(t *testing.T) { @@ -32,20 +33,10 @@ func TestMeilisearchIndexer(t *testing.T) { key = os.Getenv("TEST_MEILISEARCH_KEY") } - ok := false - for i := 0; i < 60; i++ { + require.Eventually(t, func() bool { resp, err := http.Get(url) - if err == nil && resp.StatusCode == http.StatusOK { - ok = true - break - } - t.Logf("Waiting for meilisearch to be up: %v", err) - time.Sleep(time.Second) - } - if !ok { - t.Fatalf("Failed to wait for meilisearch to be up") - return - } + return err == nil && resp.StatusCode == http.StatusOK + }, time.Minute, time.Second, "Expected meilisearch to be up") indexer := NewIndexer(url, key, fmt.Sprintf("test_meilisearch_indexer_%d", time.Now().Unix())) defer indexer.Close() diff --git a/modules/indexer/issues/util.go b/modules/indexer/issues/util.go index deb19adc49d..19d835a1d80 100644 --- a/modules/indexer/issues/util.go +++ b/modules/indexer/issues/util.go @@ -92,6 +92,11 @@ func getIssueIndexerData(ctx context.Context, issueID int64) (*internal.IndexerD projectID = issue.Project.ID } + projectColumnID, err := issue.ProjectColumnID(ctx) + if err != nil { + return nil, false, err + } + return &internal.IndexerData{ ID: issue.ID, RepoID: issue.RepoID, @@ -106,7 +111,7 @@ func getIssueIndexerData(ctx context.Context, issueID int64) (*internal.IndexerD NoLabel: len(labels) == 0, MilestoneID: issue.MilestoneID, ProjectID: projectID, - ProjectColumnID: issue.ProjectColumnID(ctx), + ProjectColumnID: projectColumnID, PosterID: issue.PosterID, AssigneeID: issue.AssigneeID, MentionIDs: mentionIDs, diff --git a/modules/issue/template/template_test.go b/modules/issue/template/template_test.go index 689a285b479..575e23def93 100644 --- a/modules/issue/template/template_test.go +++ b/modules/issue/template/template_test.go @@ -957,9 +957,8 @@ func Test_minQuotes(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := minQuotes(tt.args.value); got != tt.want { - t.Errorf("minQuotes() = %v, want %v", got, tt.want) - } + got := minQuotes(tt.args.value) + assert.Equal(t, tt.want, got) }) } } diff --git a/modules/lfs/http_client.go b/modules/lfs/http_client.go index 3acd23b8f73..0a27fb0c867 100644 --- a/modules/lfs/http_client.go +++ b/modules/lfs/http_client.go @@ -72,10 +72,14 @@ func (c *HTTPClient) batch(ctx context.Context, operation string, objects []Poin url := fmt.Sprintf("%s/objects/batch", c.endpoint) + // Original: In some lfs server implementations, they require the ref attribute. #32838 // `ref` is an "optional object describing the server ref that the objects belong to" - // but some (incorrect) lfs servers require it, so maybe adding an empty ref here doesn't break the correct ones. + // but some (incorrect) lfs servers like aliyun require it, so maybe adding an empty ref here doesn't break the correct ones. // https://github.com/git-lfs/git-lfs/blob/a32a02b44bf8a511aa14f047627c49e1a7fd5021/docs/api/batch.md?plain=1#L37 - request := &BatchRequest{operation, c.transferNames(), &Reference{}, objects} + // + // UPDATE: it can't use "empty ref" here because it breaks others like https://github.com/go-gitea/gitea/issues/33453 + request := &BatchRequest{operation, c.transferNames(), nil, objects} + payload := new(bytes.Buffer) err := json.NewEncoder(payload).Encode(request) if err != nil { diff --git a/modules/lfstransfer/backend/backend.go b/modules/lfstransfer/backend/backend.go index 2b1fe49fdab..540932b9302 100644 --- a/modules/lfstransfer/backend/backend.go +++ b/modules/lfstransfer/backend/backend.go @@ -4,7 +4,6 @@ package backend import ( - "bytes" "context" "encoding/base64" "fmt" @@ -29,7 +28,7 @@ var Capabilities = []string{ "locking", } -var _ transfer.Backend = &GiteaBackend{} +var _ transfer.Backend = (*GiteaBackend)(nil) // GiteaBackend is an adapter between git-lfs-transfer library and Gitea's internal LFS API type GiteaBackend struct { @@ -78,17 +77,17 @@ func (g *GiteaBackend) Batch(_ string, pointers []transfer.BatchItem, args trans headerAccept: mimeGitLFS, headerContentType: mimeGitLFS, } - req := newInternalRequest(g.ctx, url, http.MethodPost, headers, bodyBytes) + req := newInternalRequestLFS(g.ctx, url, http.MethodPost, headers, bodyBytes) resp, err := req.Response() if err != nil { g.logger.Log("http request error", err) return nil, err } + defer resp.Body.Close() if resp.StatusCode != http.StatusOK { g.logger.Log("http statuscode error", resp.StatusCode, statusCodeToErr(resp.StatusCode)) return nil, statusCodeToErr(resp.StatusCode) } - defer resp.Body.Close() respBytes, err := io.ReadAll(resp.Body) if err != nil { g.logger.Log("http read error", err) @@ -158,8 +157,7 @@ func (g *GiteaBackend) Batch(_ string, pointers []transfer.BatchItem, args trans return pointers, nil } -// Download implements transfer.Backend. The returned reader must be closed by the -// caller. +// Download implements transfer.Backend. The returned reader must be closed by the caller. func (g *GiteaBackend) Download(oid string, args transfer.Args) (io.ReadCloser, int64, error) { idMapStr, exists := args[argID] if !exists { @@ -187,25 +185,25 @@ func (g *GiteaBackend) Download(oid string, args transfer.Args) (io.ReadCloser, headerGiteaInternalAuth: g.internalAuth, headerAccept: mimeOctetStream, } - req := newInternalRequest(g.ctx, url, http.MethodGet, headers, nil) + req := newInternalRequestLFS(g.ctx, url, http.MethodGet, headers, nil) resp, err := req.Response() if err != nil { - return nil, 0, err + return nil, 0, fmt.Errorf("failed to get response: %w", err) } + // no need to close the body here by "defer resp.Body.Close()", see below if resp.StatusCode != http.StatusOK { return nil, 0, statusCodeToErr(resp.StatusCode) } - defer resp.Body.Close() - respBytes, err := io.ReadAll(resp.Body) + + respSize, err := strconv.ParseInt(resp.Header.Get("X-Gitea-LFS-Content-Length"), 10, 64) if err != nil { - return nil, 0, err + return nil, 0, fmt.Errorf("failed to parse content length: %w", err) } - respSize := int64(len(respBytes)) - respBuf := io.NopCloser(bytes.NewBuffer(respBytes)) - return respBuf, respSize, nil + // transfer.Backend will check io.Closer interface and close this Body reader + return resp.Body, respSize, nil } -// StartUpload implements transfer.Backend. +// Upload implements transfer.Backend. func (g *GiteaBackend) Upload(oid string, size int64, r io.Reader, args transfer.Args) error { idMapStr, exists := args[argID] if !exists { @@ -234,15 +232,14 @@ func (g *GiteaBackend) Upload(oid string, size int64, r io.Reader, args transfer headerContentType: mimeOctetStream, headerContentLength: strconv.FormatInt(size, 10), } - reqBytes, err := io.ReadAll(r) - if err != nil { - return err - } - req := newInternalRequest(g.ctx, url, http.MethodPut, headers, reqBytes) + + req := newInternalRequestLFS(g.ctx, url, http.MethodPut, headers, nil) + req.Body(r) resp, err := req.Response() if err != nil { return err } + defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return statusCodeToErr(resp.StatusCode) } @@ -284,11 +281,12 @@ func (g *GiteaBackend) Verify(oid string, size int64, args transfer.Args) (trans headerAccept: mimeGitLFS, headerContentType: mimeGitLFS, } - req := newInternalRequest(g.ctx, url, http.MethodPost, headers, bodyBytes) + req := newInternalRequestLFS(g.ctx, url, http.MethodPost, headers, bodyBytes) resp, err := req.Response() if err != nil { return transfer.NewStatus(transfer.StatusInternalServerError), err } + defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return transfer.NewStatus(uint32(resp.StatusCode), http.StatusText(resp.StatusCode)), statusCodeToErr(resp.StatusCode) } diff --git a/modules/lfstransfer/backend/lock.go b/modules/lfstransfer/backend/lock.go index f094cce1db6..4b456586114 100644 --- a/modules/lfstransfer/backend/lock.go +++ b/modules/lfstransfer/backend/lock.go @@ -50,7 +50,7 @@ func (g *giteaLockBackend) Create(path, refname string) (transfer.Lock, error) { headerAccept: mimeGitLFS, headerContentType: mimeGitLFS, } - req := newInternalRequest(g.ctx, url, http.MethodPost, headers, bodyBytes) + req := newInternalRequestLFS(g.ctx, url, http.MethodPost, headers, bodyBytes) resp, err := req.Response() if err != nil { g.logger.Log("http request error", err) @@ -102,7 +102,7 @@ func (g *giteaLockBackend) Unlock(lock transfer.Lock) error { headerAccept: mimeGitLFS, headerContentType: mimeGitLFS, } - req := newInternalRequest(g.ctx, url, http.MethodPost, headers, bodyBytes) + req := newInternalRequestLFS(g.ctx, url, http.MethodPost, headers, bodyBytes) resp, err := req.Response() if err != nil { g.logger.Log("http request error", err) @@ -185,7 +185,7 @@ func (g *giteaLockBackend) queryLocks(v url.Values) ([]transfer.Lock, string, er headerAccept: mimeGitLFS, headerContentType: mimeGitLFS, } - req := newInternalRequest(g.ctx, url, http.MethodGet, headers, nil) + req := newInternalRequestLFS(g.ctx, url, http.MethodGet, headers, nil) resp, err := req.Response() if err != nil { g.logger.Log("http request error", err) diff --git a/modules/lfstransfer/backend/util.go b/modules/lfstransfer/backend/util.go index cffefef375c..f322d542575 100644 --- a/modules/lfstransfer/backend/util.go +++ b/modules/lfstransfer/backend/util.go @@ -5,15 +5,12 @@ package backend import ( "context" - "crypto/tls" "fmt" - "net" + "io" "net/http" - "time" "code.gitea.io/gitea/modules/httplib" - "code.gitea.io/gitea/modules/proxyprotocol" - "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/private" "github.com/charmbracelet/git-lfs-transfer/transfer" ) @@ -89,53 +86,19 @@ func statusCodeToErr(code int) error { } } -func newInternalRequest(ctx context.Context, url, method string, headers map[string]string, body []byte) *httplib.Request { - req := httplib.NewRequest(url, method). - SetContext(ctx). - SetTimeout(10*time.Second, 60*time.Second). - SetTLSClientConfig(&tls.Config{ - InsecureSkipVerify: true, - }) - - if setting.Protocol == setting.HTTPUnix { - req.SetTransport(&http.Transport{ - DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) { - var d net.Dialer - conn, err := d.DialContext(ctx, "unix", setting.HTTPAddr) - if err != nil { - return conn, err - } - if setting.LocalUseProxyProtocol { - if err = proxyprotocol.WriteLocalHeader(conn); err != nil { - _ = conn.Close() - return nil, err - } - } - return conn, err - }, - }) - } else if setting.LocalUseProxyProtocol { - req.SetTransport(&http.Transport{ - DialContext: func(ctx context.Context, network, address string) (net.Conn, error) { - var d net.Dialer - conn, err := d.DialContext(ctx, network, address) - if err != nil { - return conn, err - } - if err = proxyprotocol.WriteLocalHeader(conn); err != nil { - _ = conn.Close() - return nil, err - } - return conn, err - }, - }) - } - +func newInternalRequestLFS(ctx context.Context, url, method string, headers map[string]string, body any) *httplib.Request { + req := private.NewInternalRequest(ctx, url, method) for k, v := range headers { req.Header(k, v) } - - req.Body(body) - + switch body := body.(type) { + case nil: // do nothing + case []byte: + req.Body(body) // []byte + case io.Reader: + req.Body(body) // io.Reader or io.ReadCloser + default: + panic(fmt.Sprintf("unsupported request body type %T", body)) + } return req } diff --git a/modules/markup/asciicast/asciicast.go b/modules/markup/asciicast/asciicast.go index 1d0d631650c..d86d61d7c4c 100644 --- a/modules/markup/asciicast/asciicast.go +++ b/modules/markup/asciicast/asciicast.go @@ -46,7 +46,7 @@ func (Renderer) Render(ctx *markup.RenderContext, _ io.Reader, output io.Writer) setting.AppSubURL, url.PathEscape(ctx.RenderOptions.Metas["user"]), url.PathEscape(ctx.RenderOptions.Metas["repo"]), - ctx.RenderOptions.Metas["BranchNameSubURL"], + ctx.RenderOptions.Metas["RefTypeNameSubURL"], url.PathEscape(ctx.RenderOptions.RelativePath), ) return ctx.RenderInternal.FormatWithSafeAttrs(output, `
`, playerClassName, playerSrcAttr, rawURL) diff --git a/modules/markup/html.go b/modules/markup/html.go index bb12febf277..3aaf669c632 100644 --- a/modules/markup/html.go +++ b/modules/markup/html.go @@ -47,7 +47,7 @@ var globalVars = sync.OnceValue(func() *globalVarsType { // NOTE: All below regex matching do not perform any extra validation. // Thus a link is produced even if the linked entity does not exist. // While fast, this is also incorrect and lead to false positives. - // TODO: fix invalid linking issue + // TODO: fix invalid linking issue (update: stale TODO, what issues? maybe no TODO anymore) // valid chars in encoded path and parameter: [-+~_%.a-zA-Z0-9/] diff --git a/modules/markup/internal/renderinternal.go b/modules/markup/internal/renderinternal.go index 4d58f160a92..7a3e37b120f 100644 --- a/modules/markup/internal/renderinternal.go +++ b/modules/markup/internal/renderinternal.go @@ -76,7 +76,7 @@ func (r *RenderInternal) ProtectSafeAttrs(content template.HTML) template.HTML { return template.HTML(reAttrClass().ReplaceAllString(string(content), `$1 data-attr-class="`+r.secureIDPrefix+`$2"$3`)) } -func (r *RenderInternal) FormatWithSafeAttrs(w io.Writer, fmt string, a ...any) error { +func (r *RenderInternal) FormatWithSafeAttrs(w io.Writer, fmt template.HTML, a ...any) error { _, err := w.Write([]byte(r.ProtectSafeAttrs(htmlutil.HTMLFormat(fmt, a...)))) return err } diff --git a/modules/markup/markdown/math/block_renderer.go b/modules/markup/markdown/math/block_renderer.go index c29f0618821..412e4d0dee6 100644 --- a/modules/markup/markdown/math/block_renderer.go +++ b/modules/markup/markdown/math/block_renderer.go @@ -4,6 +4,8 @@ package math import ( + "html/template" + "code.gitea.io/gitea/modules/markup/internal" giteaUtil "code.gitea.io/gitea/modules/util" @@ -50,7 +52,7 @@ func (r *BlockRenderer) renderBlock(w util.BufWriter, source []byte, node gast.N n := node.(*Block) if entering { code := giteaUtil.Iif(n.Inline, "", `
`) + ``
-		_ = r.renderInternal.FormatWithSafeAttrs(w, code)
+		_ = r.renderInternal.FormatWithSafeAttrs(w, template.HTML(code))
 		r.writeLines(w, source, n)
 	} else {
 		_, _ = w.WriteString(`` + giteaUtil.Iif(n.Inline, "", `
`) + "\n") diff --git a/modules/markup/markdown/renderconfig_test.go b/modules/markup/markdown/renderconfig_test.go index c53acdc77a7..13346570fa6 100644 --- a/modules/markup/markdown/renderconfig_test.go +++ b/modules/markup/markdown/renderconfig_test.go @@ -7,6 +7,8 @@ import ( "strings" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "gopkg.in/yaml.v3" ) @@ -140,23 +142,13 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) { Icon: "table", Lang: "", } - if err := yaml.Unmarshal([]byte(strings.ReplaceAll(tt.args, "\t", " ")), got); err != nil { - t.Errorf("RenderConfig.UnmarshalYAML() error = %v\n%q", err, tt.args) - return - } + err := yaml.Unmarshal([]byte(strings.ReplaceAll(tt.args, "\t", " ")), got) + require.NoError(t, err) - if got.Meta != tt.expected.Meta { - t.Errorf("Meta Expected %s Got %s", tt.expected.Meta, got.Meta) - } - if got.Icon != tt.expected.Icon { - t.Errorf("Icon Expected %s Got %s", tt.expected.Icon, got.Icon) - } - if got.Lang != tt.expected.Lang { - t.Errorf("Lang Expected %s Got %s", tt.expected.Lang, got.Lang) - } - if got.TOC != tt.expected.TOC { - t.Errorf("TOC Expected %q Got %q", tt.expected.TOC, got.TOC) - } + assert.Equal(t, tt.expected.Meta, got.Meta) + assert.Equal(t, tt.expected.Icon, got.Icon) + assert.Equal(t, tt.expected.Lang, got.Lang) + assert.Equal(t, tt.expected.TOC, got.TOC) }) } } diff --git a/modules/markup/orgmode/orgmode.go b/modules/markup/orgmode/orgmode.go index c6cc3340000..70d02c13216 100644 --- a/modules/markup/orgmode/orgmode.go +++ b/modules/markup/orgmode/orgmode.go @@ -147,7 +147,7 @@ func (r *orgWriter) resolveLink(kind, link string) string { func (r *orgWriter) WriteRegularLink(l org.RegularLink) { link := r.resolveLink(l.Kind(), l.URL) - printHTML := func(html string, a ...any) { + printHTML := func(html template.HTML, a ...any) { _, _ = fmt.Fprint(r, htmlutil.HTMLFormat(html, a...)) } // Inspired by https://github.com/niklasfasching/go-org/blob/6eb20dbda93cb88c3503f7508dc78cbbc639378f/org/html_writer.go#L406-L427 diff --git a/modules/markup/orgmode/orgmode_test.go b/modules/markup/orgmode/orgmode_test.go index e3cc05b4f02..de39bafebea 100644 --- a/modules/markup/orgmode/orgmode_test.go +++ b/modules/markup/orgmode/orgmode_test.go @@ -103,8 +103,8 @@ func HelloWorld() { } #+end_src `, `
-
// HelloWorld prints "Hello World"
-func HelloWorld() {
+
// HelloWorld prints "Hello World"
+func HelloWorld() {
 	fmt.Println("Hello World")
 }
`) diff --git a/modules/markup/render.go b/modules/markup/render.go index b239e59687b..37a2a86687d 100644 --- a/modules/markup/render.go +++ b/modules/markup/render.go @@ -44,7 +44,7 @@ type RenderOptions struct { MarkupType string // user&repo, format&style®exp (for external issue pattern), teams&org (for mention) - // BranchNameSubURL (for iframe&asciicast) + // RefTypeNameSubURL (for iframe&asciicast) // markupAllowShortIssuePattern // markdownLineBreakStyle (comment, document) Metas map[string]string @@ -170,7 +170,7 @@ sandbox="allow-scripts" setting.AppSubURL, url.PathEscape(ctx.RenderOptions.Metas["user"]), url.PathEscape(ctx.RenderOptions.Metas["repo"]), - ctx.RenderOptions.Metas["BranchNameSubURL"], + ctx.RenderOptions.Metas["RefTypeNameSubURL"], url.PathEscape(ctx.RenderOptions.RelativePath), )) return err diff --git a/modules/markup/sanitizer_default.go b/modules/markup/sanitizer_default.go index 5eeafe940a4..14161eb5337 100644 --- a/modules/markup/sanitizer_default.go +++ b/modules/markup/sanitizer_default.go @@ -48,7 +48,7 @@ func (st *Sanitizer) createDefaultPolicy() *bluemonday.Policy { policy.AllowAttrs("class").Matching(regexp.MustCompile(`^(unchecked|checked|indeterminate)$`)).OnElements("li") // Allow 'color' and 'background-color' properties for the style attribute on text elements. - policy.AllowStyles("color", "background-color").OnElements("span", "p") + policy.AllowStyles("color", "background-color").OnElements("div", "span", "p", "tr", "th", "td") policy.AllowAttrs("src", "autoplay", "controls").OnElements("video") diff --git a/modules/migration/downloader.go b/modules/migration/downloader.go index 08dbbc29a99..669222dea2a 100644 --- a/modules/migration/downloader.go +++ b/modules/migration/downloader.go @@ -12,18 +12,17 @@ import ( // Downloader downloads the site repo information type Downloader interface { - SetContext(context.Context) - GetRepoInfo() (*Repository, error) - GetTopics() ([]string, error) - GetMilestones() ([]*Milestone, error) - GetReleases() ([]*Release, error) - GetLabels() ([]*Label, error) - GetIssues(page, perPage int) ([]*Issue, bool, error) - GetComments(commentable Commentable) ([]*Comment, bool, error) - GetAllComments(page, perPage int) ([]*Comment, bool, error) + GetRepoInfo(ctx context.Context) (*Repository, error) + GetTopics(ctx context.Context) ([]string, error) + GetMilestones(ctx context.Context) ([]*Milestone, error) + GetReleases(ctx context.Context) ([]*Release, error) + GetLabels(ctx context.Context) ([]*Label, error) + GetIssues(ctx context.Context, page, perPage int) ([]*Issue, bool, error) + GetComments(ctx context.Context, commentable Commentable) ([]*Comment, bool, error) + GetAllComments(ctx context.Context, page, perPage int) ([]*Comment, bool, error) SupportGetRepoComments() bool - GetPullRequests(page, perPage int) ([]*PullRequest, bool, error) - GetReviews(reviewable Reviewable) ([]*Review, error) + GetPullRequests(ctx context.Context, page, perPage int) ([]*PullRequest, bool, error) + GetReviews(ctx context.Context, reviewable Reviewable) ([]*Review, error) FormatCloneURL(opts MigrateOptions, remoteAddr string) (string, error) } diff --git a/modules/migration/null_downloader.go b/modules/migration/null_downloader.go index e5b69331df7..e488f6914f2 100644 --- a/modules/migration/null_downloader.go +++ b/modules/migration/null_downloader.go @@ -13,56 +13,53 @@ type NullDownloader struct{} var _ Downloader = &NullDownloader{} -// SetContext set context -func (n NullDownloader) SetContext(_ context.Context) {} - // GetRepoInfo returns a repository information -func (n NullDownloader) GetRepoInfo() (*Repository, error) { +func (n NullDownloader) GetRepoInfo(_ context.Context) (*Repository, error) { return nil, ErrNotSupported{Entity: "RepoInfo"} } // GetTopics return repository topics -func (n NullDownloader) GetTopics() ([]string, error) { +func (n NullDownloader) GetTopics(_ context.Context) ([]string, error) { return nil, ErrNotSupported{Entity: "Topics"} } // GetMilestones returns milestones -func (n NullDownloader) GetMilestones() ([]*Milestone, error) { +func (n NullDownloader) GetMilestones(_ context.Context) ([]*Milestone, error) { return nil, ErrNotSupported{Entity: "Milestones"} } // GetReleases returns releases -func (n NullDownloader) GetReleases() ([]*Release, error) { +func (n NullDownloader) GetReleases(_ context.Context) ([]*Release, error) { return nil, ErrNotSupported{Entity: "Releases"} } // GetLabels returns labels -func (n NullDownloader) GetLabels() ([]*Label, error) { +func (n NullDownloader) GetLabels(_ context.Context) ([]*Label, error) { return nil, ErrNotSupported{Entity: "Labels"} } // GetIssues returns issues according start and limit -func (n NullDownloader) GetIssues(page, perPage int) ([]*Issue, bool, error) { +func (n NullDownloader) GetIssues(_ context.Context, page, perPage int) ([]*Issue, bool, error) { return nil, false, ErrNotSupported{Entity: "Issues"} } // GetComments returns comments of an issue or PR -func (n NullDownloader) GetComments(commentable Commentable) ([]*Comment, bool, error) { +func (n NullDownloader) GetComments(_ context.Context, commentable Commentable) ([]*Comment, bool, error) { return nil, false, ErrNotSupported{Entity: "Comments"} } // GetAllComments returns paginated comments -func (n NullDownloader) GetAllComments(page, perPage int) ([]*Comment, bool, error) { +func (n NullDownloader) GetAllComments(_ context.Context, page, perPage int) ([]*Comment, bool, error) { return nil, false, ErrNotSupported{Entity: "AllComments"} } // GetPullRequests returns pull requests according page and perPage -func (n NullDownloader) GetPullRequests(page, perPage int) ([]*PullRequest, bool, error) { +func (n NullDownloader) GetPullRequests(_ context.Context, page, perPage int) ([]*PullRequest, bool, error) { return nil, false, ErrNotSupported{Entity: "PullRequests"} } // GetReviews returns pull requests review -func (n NullDownloader) GetReviews(reviewable Reviewable) ([]*Review, error) { +func (n NullDownloader) GetReviews(_ context.Context, reviewable Reviewable) ([]*Review, error) { return nil, ErrNotSupported{Entity: "Reviews"} } diff --git a/modules/migration/retry_downloader.go b/modules/migration/retry_downloader.go index 1cacf5f3758..2926c40df7e 100644 --- a/modules/migration/retry_downloader.go +++ b/modules/migration/retry_downloader.go @@ -49,21 +49,15 @@ func (d *RetryDownloader) retry(work func() error) error { return err } -// SetContext set context -func (d *RetryDownloader) SetContext(ctx context.Context) { - d.ctx = ctx - d.Downloader.SetContext(ctx) -} - // GetRepoInfo returns a repository information with retry -func (d *RetryDownloader) GetRepoInfo() (*Repository, error) { +func (d *RetryDownloader) GetRepoInfo(ctx context.Context) (*Repository, error) { var ( repo *Repository err error ) err = d.retry(func() error { - repo, err = d.Downloader.GetRepoInfo() + repo, err = d.Downloader.GetRepoInfo(ctx) return err }) @@ -71,14 +65,14 @@ func (d *RetryDownloader) GetRepoInfo() (*Repository, error) { } // GetTopics returns a repository's topics with retry -func (d *RetryDownloader) GetTopics() ([]string, error) { +func (d *RetryDownloader) GetTopics(ctx context.Context) ([]string, error) { var ( topics []string err error ) err = d.retry(func() error { - topics, err = d.Downloader.GetTopics() + topics, err = d.Downloader.GetTopics(ctx) return err }) @@ -86,14 +80,14 @@ func (d *RetryDownloader) GetTopics() ([]string, error) { } // GetMilestones returns a repository's milestones with retry -func (d *RetryDownloader) GetMilestones() ([]*Milestone, error) { +func (d *RetryDownloader) GetMilestones(ctx context.Context) ([]*Milestone, error) { var ( milestones []*Milestone err error ) err = d.retry(func() error { - milestones, err = d.Downloader.GetMilestones() + milestones, err = d.Downloader.GetMilestones(ctx) return err }) @@ -101,14 +95,14 @@ func (d *RetryDownloader) GetMilestones() ([]*Milestone, error) { } // GetReleases returns a repository's releases with retry -func (d *RetryDownloader) GetReleases() ([]*Release, error) { +func (d *RetryDownloader) GetReleases(ctx context.Context) ([]*Release, error) { var ( releases []*Release err error ) err = d.retry(func() error { - releases, err = d.Downloader.GetReleases() + releases, err = d.Downloader.GetReleases(ctx) return err }) @@ -116,14 +110,14 @@ func (d *RetryDownloader) GetReleases() ([]*Release, error) { } // GetLabels returns a repository's labels with retry -func (d *RetryDownloader) GetLabels() ([]*Label, error) { +func (d *RetryDownloader) GetLabels(ctx context.Context) ([]*Label, error) { var ( labels []*Label err error ) err = d.retry(func() error { - labels, err = d.Downloader.GetLabels() + labels, err = d.Downloader.GetLabels(ctx) return err }) @@ -131,7 +125,7 @@ func (d *RetryDownloader) GetLabels() ([]*Label, error) { } // GetIssues returns a repository's issues with retry -func (d *RetryDownloader) GetIssues(page, perPage int) ([]*Issue, bool, error) { +func (d *RetryDownloader) GetIssues(ctx context.Context, page, perPage int) ([]*Issue, bool, error) { var ( issues []*Issue isEnd bool @@ -139,7 +133,7 @@ func (d *RetryDownloader) GetIssues(page, perPage int) ([]*Issue, bool, error) { ) err = d.retry(func() error { - issues, isEnd, err = d.Downloader.GetIssues(page, perPage) + issues, isEnd, err = d.Downloader.GetIssues(ctx, page, perPage) return err }) @@ -147,7 +141,7 @@ func (d *RetryDownloader) GetIssues(page, perPage int) ([]*Issue, bool, error) { } // GetComments returns a repository's comments with retry -func (d *RetryDownloader) GetComments(commentable Commentable) ([]*Comment, bool, error) { +func (d *RetryDownloader) GetComments(ctx context.Context, commentable Commentable) ([]*Comment, bool, error) { var ( comments []*Comment isEnd bool @@ -155,7 +149,7 @@ func (d *RetryDownloader) GetComments(commentable Commentable) ([]*Comment, bool ) err = d.retry(func() error { - comments, isEnd, err = d.Downloader.GetComments(commentable) + comments, isEnd, err = d.Downloader.GetComments(ctx, commentable) return err }) @@ -163,7 +157,7 @@ func (d *RetryDownloader) GetComments(commentable Commentable) ([]*Comment, bool } // GetPullRequests returns a repository's pull requests with retry -func (d *RetryDownloader) GetPullRequests(page, perPage int) ([]*PullRequest, bool, error) { +func (d *RetryDownloader) GetPullRequests(ctx context.Context, page, perPage int) ([]*PullRequest, bool, error) { var ( prs []*PullRequest err error @@ -171,7 +165,7 @@ func (d *RetryDownloader) GetPullRequests(page, perPage int) ([]*PullRequest, bo ) err = d.retry(func() error { - prs, isEnd, err = d.Downloader.GetPullRequests(page, perPage) + prs, isEnd, err = d.Downloader.GetPullRequests(ctx, page, perPage) return err }) @@ -179,14 +173,13 @@ func (d *RetryDownloader) GetPullRequests(page, perPage int) ([]*PullRequest, bo } // GetReviews returns pull requests reviews -func (d *RetryDownloader) GetReviews(reviewable Reviewable) ([]*Review, error) { +func (d *RetryDownloader) GetReviews(ctx context.Context, reviewable Reviewable) ([]*Review, error) { var ( reviews []*Review err error ) - err = d.retry(func() error { - reviews, err = d.Downloader.GetReviews(reviewable) + reviews, err = d.Downloader.GetReviews(ctx, reviewable) return err }) diff --git a/modules/migration/uploader.go b/modules/migration/uploader.go index ff642aa4faf..65752e248e7 100644 --- a/modules/migration/uploader.go +++ b/modules/migration/uploader.go @@ -4,20 +4,22 @@ package migration +import "context" + // Uploader uploads all the information of one repository type Uploader interface { MaxBatchInsertSize(tp string) int - CreateRepo(repo *Repository, opts MigrateOptions) error - CreateTopics(topic ...string) error - CreateMilestones(milestones ...*Milestone) error - CreateReleases(releases ...*Release) error - SyncTags() error - CreateLabels(labels ...*Label) error - CreateIssues(issues ...*Issue) error - CreateComments(comments ...*Comment) error - CreatePullRequests(prs ...*PullRequest) error - CreateReviews(reviews ...*Review) error + CreateRepo(ctx context.Context, repo *Repository, opts MigrateOptions) error + CreateTopics(ctx context.Context, topic ...string) error + CreateMilestones(ctx context.Context, milestones ...*Milestone) error + CreateReleases(ctx context.Context, releases ...*Release) error + SyncTags(ctx context.Context) error + CreateLabels(ctx context.Context, labels ...*Label) error + CreateIssues(ctx context.Context, issues ...*Issue) error + CreateComments(ctx context.Context, comments ...*Comment) error + CreatePullRequests(ctx context.Context, prs ...*PullRequest) error + CreateReviews(ctx context.Context, reviews ...*Review) error Rollback() error - Finish() error + Finish(ctx context.Context) error Close() } diff --git a/modules/nosql/redis_test.go b/modules/nosql/redis_test.go index 43652e314c0..93276ca7931 100644 --- a/modules/nosql/redis_test.go +++ b/modules/nosql/redis_test.go @@ -5,6 +5,9 @@ package nosql import ( "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestToRedisURI(t *testing.T) { @@ -26,9 +29,9 @@ func TestToRedisURI(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := ToRedisURI(tt.connection); got == nil || got.String() != tt.want { - t.Errorf(`ToRedisURI(%q) = %s, want %s`, tt.connection, got.String(), tt.want) - } + got := ToRedisURI(tt.connection) + require.NotNil(t, got) + assert.Equal(t, tt.want, got.String()) }) } } diff --git a/modules/packages/npm/creator.go b/modules/packages/npm/creator.go index 7d3d7cd6b55..8ba4dbfba71 100644 --- a/modules/packages/npm/creator.go +++ b/modules/packages/npm/creator.go @@ -81,6 +81,7 @@ type PackageMetadataVersion struct { BundleDependencies []string `json:"bundleDependencies,omitempty"` DevDependencies map[string]string `json:"devDependencies,omitempty"` PeerDependencies map[string]string `json:"peerDependencies,omitempty"` + PeerDependenciesMeta map[string]any `json:"peerDependenciesMeta,omitempty"` Bin map[string]string `json:"bin,omitempty"` OptionalDependencies map[string]string `json:"optionalDependencies,omitempty"` Readme string `json:"readme,omitempty"` @@ -222,6 +223,7 @@ func ParsePackage(r io.Reader) (*Package, error) { BundleDependencies: meta.BundleDependencies, DevelopmentDependencies: meta.DevDependencies, PeerDependencies: meta.PeerDependencies, + PeerDependenciesMeta: meta.PeerDependenciesMeta, OptionalDependencies: meta.OptionalDependencies, Bin: meta.Bin, Readme: meta.Readme, diff --git a/modules/packages/npm/metadata.go b/modules/packages/npm/metadata.go index 6bb77f302bb..d1d02633878 100644 --- a/modules/packages/npm/metadata.go +++ b/modules/packages/npm/metadata.go @@ -19,6 +19,7 @@ type Metadata struct { BundleDependencies []string `json:"bundleDependencies,omitempty"` DevelopmentDependencies map[string]string `json:"development_dependencies,omitempty"` PeerDependencies map[string]string `json:"peer_dependencies,omitempty"` + PeerDependenciesMeta map[string]any `json:"peer_dependencies_meta,omitempty"` OptionalDependencies map[string]string `json:"optional_dependencies,omitempty"` Bin map[string]string `json:"bin,omitempty"` Readme string `json:"readme,omitempty"` diff --git a/modules/private/actions.go b/modules/private/actions.go index 311a2836500..e68f2f85b03 100644 --- a/modules/private/actions.go +++ b/modules/private/actions.go @@ -17,7 +17,7 @@ type GenerateTokenRequest struct { func GenerateActionsRunnerToken(ctx context.Context, scope string) (*ResponseText, ResponseExtra) { reqURL := setting.LocalURL + "api/internal/actions/generate_actions_runner_token" - req := newInternalRequest(ctx, reqURL, "POST", GenerateTokenRequest{ + req := newInternalRequestAPI(ctx, reqURL, "POST", GenerateTokenRequest{ Scope: scope, }) diff --git a/modules/private/hook.go b/modules/private/hook.go index 745c200619f..87d6549f9c5 100644 --- a/modules/private/hook.go +++ b/modules/private/hook.go @@ -85,7 +85,7 @@ type HookProcReceiveRefResult struct { // HookPreReceive check whether the provided commits are allowed func HookPreReceive(ctx context.Context, ownerName, repoName string, opts HookOptions) ResponseExtra { reqURL := setting.LocalURL + fmt.Sprintf("api/internal/hook/pre-receive/%s/%s", url.PathEscape(ownerName), url.PathEscape(repoName)) - req := newInternalRequest(ctx, reqURL, "POST", opts) + req := newInternalRequestAPI(ctx, reqURL, "POST", opts) req.SetReadWriteTimeout(time.Duration(60+len(opts.OldCommitIDs)) * time.Second) _, extra := requestJSONResp(req, &ResponseText{}) return extra @@ -94,7 +94,7 @@ func HookPreReceive(ctx context.Context, ownerName, repoName string, opts HookOp // HookPostReceive updates services and users func HookPostReceive(ctx context.Context, ownerName, repoName string, opts HookOptions) (*HookPostReceiveResult, ResponseExtra) { reqURL := setting.LocalURL + fmt.Sprintf("api/internal/hook/post-receive/%s/%s", url.PathEscape(ownerName), url.PathEscape(repoName)) - req := newInternalRequest(ctx, reqURL, "POST", opts) + req := newInternalRequestAPI(ctx, reqURL, "POST", opts) req.SetReadWriteTimeout(time.Duration(60+len(opts.OldCommitIDs)) * time.Second) return requestJSONResp(req, &HookPostReceiveResult{}) } @@ -103,7 +103,7 @@ func HookPostReceive(ctx context.Context, ownerName, repoName string, opts HookO func HookProcReceive(ctx context.Context, ownerName, repoName string, opts HookOptions) (*HookProcReceiveResult, ResponseExtra) { reqURL := setting.LocalURL + fmt.Sprintf("api/internal/hook/proc-receive/%s/%s", url.PathEscape(ownerName), url.PathEscape(repoName)) - req := newInternalRequest(ctx, reqURL, "POST", opts) + req := newInternalRequestAPI(ctx, reqURL, "POST", opts) req.SetReadWriteTimeout(time.Duration(60+len(opts.OldCommitIDs)) * time.Second) return requestJSONResp(req, &HookProcReceiveResult{}) } @@ -115,7 +115,7 @@ func SetDefaultBranch(ctx context.Context, ownerName, repoName, branch string) R url.PathEscape(repoName), url.PathEscape(branch), ) - req := newInternalRequest(ctx, reqURL, "POST") + req := newInternalRequestAPI(ctx, reqURL, "POST") _, extra := requestJSONResp(req, &ResponseText{}) return extra } @@ -123,7 +123,7 @@ func SetDefaultBranch(ctx context.Context, ownerName, repoName, branch string) R // SSHLog sends ssh error log response func SSHLog(ctx context.Context, isErr bool, msg string) error { reqURL := setting.LocalURL + "api/internal/ssh/log" - req := newInternalRequest(ctx, reqURL, "POST", &SSHLogOption{IsError: isErr, Message: msg}) + req := newInternalRequestAPI(ctx, reqURL, "POST", &SSHLogOption{IsError: isErr, Message: msg}) _, extra := requestJSONResp(req, &ResponseText{}) return extra.Error } diff --git a/modules/private/internal.go b/modules/private/internal.go index c7e7773524d..3bd4eb06b19 100644 --- a/modules/private/internal.go +++ b/modules/private/internal.go @@ -34,7 +34,7 @@ func getClientIP() string { return strings.Fields(sshConnEnv)[0] } -func newInternalRequest(ctx context.Context, url, method string, body ...any) *httplib.Request { +func NewInternalRequest(ctx context.Context, url, method string) *httplib.Request { if setting.InternalToken == "" { log.Fatal(`The INTERNAL_TOKEN setting is missing from the configuration file: %q. Ensure you are running in the correct environment or set the correct configuration file with -c.`, setting.CustomConf) @@ -82,13 +82,17 @@ Ensure you are running in the correct environment or set the correct configurati }, }) } + return req +} +func newInternalRequestAPI(ctx context.Context, url, method string, body ...any) *httplib.Request { + req := NewInternalRequest(ctx, url, method) if len(body) == 1 { req.Header("Content-Type", "application/json") jsonBytes, _ := json.Marshal(body[0]) req.Body(jsonBytes) } else if len(body) > 1 { - log.Fatal("Too many arguments for newInternalRequest") + log.Fatal("Too many arguments for newInternalRequestAPI") } req.SetTimeout(10*time.Second, 60*time.Second) diff --git a/modules/private/key.go b/modules/private/key.go index dcd17148564..114683b3439 100644 --- a/modules/private/key.go +++ b/modules/private/key.go @@ -14,7 +14,7 @@ import ( func UpdatePublicKeyInRepo(ctx context.Context, keyID, repoID int64) error { // Ask for running deliver hook and test pull request tasks. reqURL := setting.LocalURL + fmt.Sprintf("api/internal/ssh/%d/update/%d", keyID, repoID) - req := newInternalRequest(ctx, reqURL, "POST") + req := newInternalRequestAPI(ctx, reqURL, "POST") _, extra := requestJSONResp(req, &ResponseText{}) return extra.Error } @@ -24,7 +24,7 @@ func UpdatePublicKeyInRepo(ctx context.Context, keyID, repoID int64) error { func AuthorizedPublicKeyByContent(ctx context.Context, content string) (*ResponseText, ResponseExtra) { // Ask for running deliver hook and test pull request tasks. reqURL := setting.LocalURL + "api/internal/ssh/authorized_keys" - req := newInternalRequest(ctx, reqURL, "POST") + req := newInternalRequestAPI(ctx, reqURL, "POST") req.Param("content", content) return requestJSONResp(req, &ResponseText{}) } diff --git a/modules/private/mail.go b/modules/private/mail.go index 08de5b7e287..3904e37beab 100644 --- a/modules/private/mail.go +++ b/modules/private/mail.go @@ -23,7 +23,7 @@ type Email struct { func SendEmail(ctx context.Context, subject, message string, to []string) (*ResponseText, ResponseExtra) { reqURL := setting.LocalURL + "api/internal/mail/send" - req := newInternalRequest(ctx, reqURL, "POST", Email{ + req := newInternalRequestAPI(ctx, reqURL, "POST", Email{ Subject: subject, Message: message, To: to, diff --git a/modules/private/manager.go b/modules/private/manager.go index 6055e553bd0..e3d5ad57e06 100644 --- a/modules/private/manager.go +++ b/modules/private/manager.go @@ -18,21 +18,21 @@ import ( // Shutdown calls the internal shutdown function func Shutdown(ctx context.Context) ResponseExtra { reqURL := setting.LocalURL + "api/internal/manager/shutdown" - req := newInternalRequest(ctx, reqURL, "POST") + req := newInternalRequestAPI(ctx, reqURL, "POST") return requestJSONClientMsg(req, "Shutting down") } // Restart calls the internal restart function func Restart(ctx context.Context) ResponseExtra { reqURL := setting.LocalURL + "api/internal/manager/restart" - req := newInternalRequest(ctx, reqURL, "POST") + req := newInternalRequestAPI(ctx, reqURL, "POST") return requestJSONClientMsg(req, "Restarting") } // ReloadTemplates calls the internal reload-templates function func ReloadTemplates(ctx context.Context) ResponseExtra { reqURL := setting.LocalURL + "api/internal/manager/reload-templates" - req := newInternalRequest(ctx, reqURL, "POST") + req := newInternalRequestAPI(ctx, reqURL, "POST") return requestJSONClientMsg(req, "Reloaded") } @@ -45,7 +45,7 @@ type FlushOptions struct { // FlushQueues calls the internal flush-queues function func FlushQueues(ctx context.Context, timeout time.Duration, nonBlocking bool) ResponseExtra { reqURL := setting.LocalURL + "api/internal/manager/flush-queues" - req := newInternalRequest(ctx, reqURL, "POST", FlushOptions{Timeout: timeout, NonBlocking: nonBlocking}) + req := newInternalRequestAPI(ctx, reqURL, "POST", FlushOptions{Timeout: timeout, NonBlocking: nonBlocking}) if timeout > 0 { req.SetReadWriteTimeout(timeout + 10*time.Second) } @@ -55,28 +55,28 @@ func FlushQueues(ctx context.Context, timeout time.Duration, nonBlocking bool) R // PauseLogging pauses logging func PauseLogging(ctx context.Context) ResponseExtra { reqURL := setting.LocalURL + "api/internal/manager/pause-logging" - req := newInternalRequest(ctx, reqURL, "POST") + req := newInternalRequestAPI(ctx, reqURL, "POST") return requestJSONClientMsg(req, "Logging Paused") } // ResumeLogging resumes logging func ResumeLogging(ctx context.Context) ResponseExtra { reqURL := setting.LocalURL + "api/internal/manager/resume-logging" - req := newInternalRequest(ctx, reqURL, "POST") + req := newInternalRequestAPI(ctx, reqURL, "POST") return requestJSONClientMsg(req, "Logging Restarted") } // ReleaseReopenLogging releases and reopens logging files func ReleaseReopenLogging(ctx context.Context) ResponseExtra { reqURL := setting.LocalURL + "api/internal/manager/release-and-reopen-logging" - req := newInternalRequest(ctx, reqURL, "POST") + req := newInternalRequestAPI(ctx, reqURL, "POST") return requestJSONClientMsg(req, "Logging Restarted") } // SetLogSQL sets database logging func SetLogSQL(ctx context.Context, on bool) ResponseExtra { reqURL := setting.LocalURL + "api/internal/manager/set-log-sql?on=" + strconv.FormatBool(on) - req := newInternalRequest(ctx, reqURL, "POST") + req := newInternalRequestAPI(ctx, reqURL, "POST") return requestJSONClientMsg(req, "Log SQL setting set") } @@ -91,7 +91,7 @@ type LoggerOptions struct { // AddLogger adds a logger func AddLogger(ctx context.Context, logger, writer, mode string, config map[string]any) ResponseExtra { reqURL := setting.LocalURL + "api/internal/manager/add-logger" - req := newInternalRequest(ctx, reqURL, "POST", LoggerOptions{ + req := newInternalRequestAPI(ctx, reqURL, "POST", LoggerOptions{ Logger: logger, Writer: writer, Mode: mode, @@ -103,7 +103,7 @@ func AddLogger(ctx context.Context, logger, writer, mode string, config map[stri // RemoveLogger removes a logger func RemoveLogger(ctx context.Context, logger, writer string) ResponseExtra { reqURL := setting.LocalURL + fmt.Sprintf("api/internal/manager/remove-logger/%s/%s", url.PathEscape(logger), url.PathEscape(writer)) - req := newInternalRequest(ctx, reqURL, "POST") + req := newInternalRequestAPI(ctx, reqURL, "POST") return requestJSONClientMsg(req, "Removed") } @@ -111,7 +111,7 @@ func RemoveLogger(ctx context.Context, logger, writer string) ResponseExtra { func Processes(ctx context.Context, out io.Writer, flat, noSystem, stacktraces, json bool, cancel string) ResponseExtra { reqURL := setting.LocalURL + fmt.Sprintf("api/internal/manager/processes?flat=%t&no-system=%t&stacktraces=%t&json=%t&cancel-pid=%s", flat, noSystem, stacktraces, json, url.QueryEscape(cancel)) - req := newInternalRequest(ctx, reqURL, "GET") + req := newInternalRequestAPI(ctx, reqURL, "GET") callback := func(resp *http.Response, extra *ResponseExtra) { _, extra.Error = io.Copy(out, resp.Body) } diff --git a/modules/private/restore_repo.go b/modules/private/restore_repo.go index 496209d3cbd..9c3a0081427 100644 --- a/modules/private/restore_repo.go +++ b/modules/private/restore_repo.go @@ -24,7 +24,7 @@ type RestoreParams struct { func RestoreRepo(ctx context.Context, repoDir, ownerName, repoName string, units []string, validation bool) ResponseExtra { reqURL := setting.LocalURL + "api/internal/restore_repo" - req := newInternalRequest(ctx, reqURL, "POST", RestoreParams{ + req := newInternalRequestAPI(ctx, reqURL, "POST", RestoreParams{ RepoDir: repoDir, OwnerName: ownerName, RepoName: repoName, diff --git a/modules/private/serv.go b/modules/private/serv.go index 480a4469549..2ccc6c11291 100644 --- a/modules/private/serv.go +++ b/modules/private/serv.go @@ -23,7 +23,7 @@ type KeyAndOwner struct { // ServNoCommand returns information about the provided key func ServNoCommand(ctx context.Context, keyID int64) (*asymkey_model.PublicKey, *user_model.User, error) { reqURL := setting.LocalURL + fmt.Sprintf("api/internal/serv/none/%d", keyID) - req := newInternalRequest(ctx, reqURL, "GET") + req := newInternalRequestAPI(ctx, reqURL, "GET") keyAndOwner, extra := requestJSONResp(req, &KeyAndOwner{}) if extra.HasError() { return nil, nil, extra.Error @@ -58,6 +58,6 @@ func ServCommand(ctx context.Context, keyID int64, ownerName, repoName string, m reqURL += fmt.Sprintf("&verb=%s", url.QueryEscape(verb)) } } - req := newInternalRequest(ctx, reqURL, "GET") + req := newInternalRequestAPI(ctx, reqURL, "GET") return requestJSONResp(req, &ServCommandResults{}) } diff --git a/modules/queue/base_levelqueue_test.go b/modules/queue/base_levelqueue_test.go index b881802ca2b..05d82085606 100644 --- a/modules/queue/base_levelqueue_test.go +++ b/modules/queue/base_levelqueue_test.go @@ -11,6 +11,7 @@ import ( "gitea.com/lunny/levelqueue" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/syndtr/goleveldb/leveldb" ) @@ -29,9 +30,7 @@ func TestCorruptedLevelQueue(t *testing.T) { // sometimes the levelqueue could be in a corrupted state, this test is to make sure it can recover from it dbDir := t.TempDir() + "/levelqueue-test" db, err := leveldb.OpenFile(dbDir, nil) - if !assert.NoError(t, err) { - return - } + require.NoError(t, err) defer db.Close() assert.NoError(t, db.Put([]byte("other-key"), []byte("other-value"), nil)) diff --git a/modules/queue/base_redis_test.go b/modules/queue/base_redis_test.go index 19fbccbc8fe..6478988d7fd 100644 --- a/modules/queue/base_redis_test.go +++ b/modules/queue/base_redis_test.go @@ -14,6 +14,7 @@ import ( "code.gitea.io/gitea/modules/setting" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func waitRedisReady(conn string, dur time.Duration) (ready bool) { @@ -61,9 +62,7 @@ func TestBaseRedis(t *testing.T) { return } assert.NoError(t, redisServer.Start()) - if !assert.True(t, waitRedisReady("redis://127.0.0.1:6379/0", 5*time.Second), "start redis-server") { - return - } + require.True(t, waitRedisReady("redis://127.0.0.1:6379/0", 5*time.Second), "start redis-server") } testQueueBasic(t, newBaseRedisSimple, toBaseConfig("baseRedis", setting.QueueSettings{Length: 10}), false) diff --git a/modules/repository/init.go b/modules/repository/init.go index 5f500c5233f..24602ae0901 100644 --- a/modules/repository/init.go +++ b/modules/repository/init.go @@ -81,7 +81,7 @@ func LoadRepoConfig() error { if isDir, err := util.IsDir(customPath); err != nil { return fmt.Errorf("failed to check custom %s dir: %w", t, err) } else if isDir { - if typeFiles[i].custom, err = util.StatDir(customPath); err != nil { + if typeFiles[i].custom, err = util.ListDirRecursively(customPath, &util.ListDirOptions{SkipCommonHiddenNames: true}); err != nil { return fmt.Errorf("failed to list custom %s files: %w", t, err) } } diff --git a/modules/reqctx/datastore.go b/modules/reqctx/datastore.go index 66361a45874..94232450f33 100644 --- a/modules/reqctx/datastore.go +++ b/modules/reqctx/datastore.go @@ -88,6 +88,21 @@ func (r *requestDataStore) cleanUp() { } } +type RequestContext interface { + context.Context + RequestDataStore +} + +func FromContext(ctx context.Context) RequestContext { + // here we must use the current ctx and the underlying store + // the current ctx guarantees that the ctx deadline/cancellation/values are respected + // the underlying store guarantees that the request-specific data is available + if store := GetRequestDataStore(ctx); store != nil { + return &requestContext{Context: ctx, RequestDataStore: store} + } + return nil +} + func GetRequestDataStore(ctx context.Context) RequestDataStore { if req, ok := ctx.Value(RequestDataStoreKey).(*requestDataStore); ok { return req @@ -97,11 +112,11 @@ func GetRequestDataStore(ctx context.Context) RequestDataStore { type requestContext struct { context.Context - dataStore *requestDataStore + RequestDataStore } func (c *requestContext) Value(key any) any { - if v := c.dataStore.GetContextValue(key); v != nil { + if v := c.GetContextValue(key); v != nil { return v } return c.Context.Value(key) @@ -109,9 +124,10 @@ func (c *requestContext) Value(key any) any { func NewRequestContext(parentCtx context.Context, profDesc string) (_ context.Context, finished func()) { ctx, _, processFinished := process.GetManager().AddTypedContext(parentCtx, profDesc, process.RequestProcessType, true) - reqCtx := &requestContext{Context: ctx, dataStore: &requestDataStore{values: make(map[any]any)}} + store := &requestDataStore{values: make(map[any]any)} + reqCtx := &requestContext{Context: ctx, RequestDataStore: store} return reqCtx, func() { - reqCtx.dataStore.cleanUp() + store.cleanUp() processFinished() } } @@ -119,5 +135,5 @@ func NewRequestContext(parentCtx context.Context, profDesc string) (_ context.Co // NewRequestContextForTest creates a new RequestContext for testing purposes // It doesn't add the context to the process manager, nor do cleanup func NewRequestContextForTest(parentCtx context.Context) context.Context { - return &requestContext{Context: parentCtx, dataStore: &requestDataStore{values: make(map[any]any)}} + return &requestContext{Context: parentCtx, RequestDataStore: &requestDataStore{values: make(map[any]any)}} } diff --git a/modules/setting/config_env.go b/modules/setting/config_env.go index dfcb7db3c86..5d94a9641f7 100644 --- a/modules/setting/config_env.go +++ b/modules/setting/config_env.go @@ -166,3 +166,25 @@ func EnvironmentToConfig(cfg ConfigProvider, envs []string) (changed bool) { } return changed } + +// InitGiteaEnvVars initializes the environment variables for gitea +func InitGiteaEnvVars() { + // Ideally Gitea should only accept the environment variables which it clearly knows instead of unsetting the ones it doesn't want, + // but the ideal behavior would be a breaking change, and it seems not bringing enough benefits to end users, + // so at the moment we could still keep "unsetting the unnecessary environments" + + // HOME is managed by Gitea, Gitea's git should use "HOME/.gitconfig". + // But git would try "XDG_CONFIG_HOME/git/config" first if "HOME/.gitconfig" does not exist, + // then our git.InitFull would still write to "XDG_CONFIG_HOME/git/config" if XDG_CONFIG_HOME is set. + _ = os.Unsetenv("XDG_CONFIG_HOME") +} + +func InitGiteaEnvVarsForTesting() { + InitGiteaEnvVars() + _ = os.Unsetenv("GIT_AUTHOR_NAME") + _ = os.Unsetenv("GIT_AUTHOR_EMAIL") + _ = os.Unsetenv("GIT_AUTHOR_DATE") + _ = os.Unsetenv("GIT_COMMITTER_NAME") + _ = os.Unsetenv("GIT_COMMITTER_EMAIL") + _ = os.Unsetenv("GIT_COMMITTER_DATE") +} diff --git a/modules/setting/indexer.go b/modules/setting/indexer.go index 34b4eac15b9..e34baae012b 100644 --- a/modules/setting/indexer.go +++ b/modules/setting/indexer.go @@ -31,6 +31,8 @@ var Indexer = struct { IncludePatterns []*GlobMatcher ExcludePatterns []*GlobMatcher ExcludeVendored bool + + TypeBleveMaxFuzzniess int }{ IssueType: "bleve", IssuePath: "indexers/issues.bleve", @@ -88,6 +90,7 @@ func loadIndexerFrom(rootCfg ConfigProvider) { Indexer.ExcludeVendored = sec.Key("REPO_INDEXER_EXCLUDE_VENDORED").MustBool(true) Indexer.MaxIndexerFileSize = sec.Key("MAX_FILE_SIZE").MustInt64(1024 * 1024) Indexer.StartupTimeout = sec.Key("STARTUP_TIMEOUT").MustDuration(30 * time.Second) + Indexer.TypeBleveMaxFuzzniess = sec.Key("TYPE_BLEVE_MAX_FUZZINESS").MustInt(0) } // IndexerGlobFromString parses a comma separated list of patterns and returns a glob.Glob slice suited for repo indexing diff --git a/modules/setting/security.go b/modules/setting/security.go index 3d12fcf8d9f..2f798b75c7e 100644 --- a/modules/setting/security.go +++ b/modules/setting/security.go @@ -13,8 +13,9 @@ import ( "code.gitea.io/gitea/modules/log" ) +// Security settings + var ( - // Security settings InstallLock bool SecretKey string InternalToken string // internal access token @@ -27,7 +28,7 @@ var ( ReverseProxyTrustedProxies []string MinPasswordLength int ImportLocalPaths bool - DisableGitHooks bool + DisableGitHooks = true DisableWebhooks bool OnlyAllowPushIfGiteaEnvironmentSet bool PasswordComplexity []string diff --git a/modules/setting/service.go b/modules/setting/service.go index 526ad64eb40..8c1843eeb75 100644 --- a/modules/setting/service.go +++ b/modules/setting/service.go @@ -46,6 +46,7 @@ var Service = struct { RequireSignInView bool EnableNotifyMail bool EnableBasicAuth bool + EnablePasskeyAuth bool EnableReverseProxyAuth bool EnableReverseProxyAuthAPI bool EnableReverseProxyAutoRegister bool @@ -161,6 +162,7 @@ func loadServiceFrom(rootCfg ConfigProvider) { Service.RequireSignInView = sec.Key("REQUIRE_SIGNIN_VIEW").MustBool() Service.EnableBasicAuth = sec.Key("ENABLE_BASIC_AUTHENTICATION").MustBool(true) Service.EnablePasswordSignInForm = sec.Key("ENABLE_PASSWORD_SIGNIN_FORM").MustBool(true) + Service.EnablePasskeyAuth = sec.Key("ENABLE_PASSKEY_AUTHENTICATION").MustBool(true) Service.EnableReverseProxyAuth = sec.Key("ENABLE_REVERSE_PROXY_AUTHENTICATION").MustBool() Service.EnableReverseProxyAuthAPI = sec.Key("ENABLE_REVERSE_PROXY_AUTHENTICATION_API").MustBool() Service.EnableReverseProxyAutoRegister = sec.Key("ENABLE_REVERSE_PROXY_AUTO_REGISTRATION").MustBool() diff --git a/modules/storage/storage.go b/modules/storage/storage.go index 750ecdfe0db..b0529941e7d 100644 --- a/modules/storage/storage.go +++ b/modules/storage/storage.go @@ -93,7 +93,7 @@ func Clean(storage ObjectStorage) error { } // SaveFrom saves data to the ObjectStorage with path p from the callback -func SaveFrom(objStorage ObjectStorage, p string, callback func(w io.Writer) error) error { +func SaveFrom(objStorage ObjectStorage, path string, callback func(w io.Writer) error) error { pr, pw := io.Pipe() defer pr.Close() go func() { @@ -103,7 +103,7 @@ func SaveFrom(objStorage ObjectStorage, p string, callback func(w io.Writer) err } }() - _, err := objStorage.Save(p, pr, -1) + _, err := objStorage.Save(path, pr, -1) return err } diff --git a/modules/structs/commit_status_test.go b/modules/structs/commit_status_test.go index f06808534c4..88e09aadc15 100644 --- a/modules/structs/commit_status_test.go +++ b/modules/structs/commit_status_test.go @@ -5,6 +5,8 @@ package structs import ( "testing" + + "github.com/stretchr/testify/assert" ) func TestNoBetterThan(t *testing.T) { @@ -166,9 +168,7 @@ func TestNoBetterThan(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := tt.args.css.NoBetterThan(tt.args.css2) - if result != tt.want { - t.Errorf("NoBetterThan() = %v, want %v", result, tt.want) - } + assert.Equal(t, tt.want, result) }) } } diff --git a/modules/structs/hook.go b/modules/structs/hook.go index ce5742e5c7c..cef2dbd7129 100644 --- a/modules/structs/hook.go +++ b/modules/structs/hook.go @@ -116,14 +116,7 @@ var ( _ Payloader = &PackagePayload{} ) -// _________ __ -// \_ ___ \_______ ____ _____ _/ |_ ____ -// / \ \/\_ __ \_/ __ \\__ \\ __\/ __ \ -// \ \____| | \/\ ___/ / __ \| | \ ___/ -// \______ /|__| \___ >____ /__| \___ > -// \/ \/ \/ \/ - -// CreatePayload FIXME +// CreatePayload represents a payload information of create event. type CreatePayload struct { Sha string `json:"sha"` Ref string `json:"ref"` @@ -157,13 +150,6 @@ func ParseCreateHook(raw []byte) (*CreatePayload, error) { return hook, nil } -// ________ .__ __ -// \______ \ ____ | | _____/ |_ ____ -// | | \_/ __ \| | _/ __ \ __\/ __ \ -// | ` \ ___/| |_\ ___/| | \ ___/ -// /_______ /\___ >____/\___ >__| \___ > -// \/ \/ \/ \/ - // PusherType define the type to push type PusherType string @@ -186,13 +172,6 @@ func (p *DeletePayload) JSONPayload() ([]byte, error) { return json.MarshalIndent(p, "", " ") } -// ___________ __ -// \_ _____/__________| | __ -// | __)/ _ \_ __ \ |/ / -// | \( <_> ) | \/ < -// \___ / \____/|__| |__|_ \ -// \/ \/ - // ForkPayload represents fork payload type ForkPayload struct { Forkee *Repository `json:"forkee"` @@ -232,13 +211,6 @@ func (p *IssueCommentPayload) JSONPayload() ([]byte, error) { return json.MarshalIndent(p, "", " ") } -// __________ .__ -// \______ \ ____ | | ____ _____ ______ ____ -// | _// __ \| | _/ __ \\__ \ / ___// __ \ -// | | \ ___/| |_\ ___/ / __ \_\___ \\ ___/ -// |____|_ /\___ >____/\___ >____ /____ >\___ > -// \/ \/ \/ \/ \/ \/ - // HookReleaseAction defines hook release action type type HookReleaseAction string @@ -302,13 +274,6 @@ func (p *PushPayload) Branch() string { return strings.ReplaceAll(p.Ref, "refs/heads/", "") } -// .___ -// | | ______ ________ __ ____ -// | |/ ___// ___/ | \_/ __ \ -// | |\___ \ \___ \| | /\ ___/ -// |___/____ >____ >____/ \___ > -// \/ \/ \/ - // HookIssueAction FIXME type HookIssueAction string @@ -371,13 +336,6 @@ type ChangesPayload struct { Ref *ChangesFromPayload `json:"ref,omitempty"` } -// __________ .__ .__ __________ __ -// \______ \__ __| | | | \______ \ ____ ________ __ ____ _______/ |_ -// | ___/ | \ | | | | _// __ \/ ____/ | \_/ __ \ / ___/\ __\ -// | | | | / |_| |__ | | \ ___< <_| | | /\ ___/ \___ \ | | -// |____| |____/|____/____/ |____|_ /\___ >__ |____/ \___ >____ > |__| -// \/ \/ |__| \/ \/ - // PullRequestPayload represents a payload information of pull request event. type PullRequestPayload struct { Action HookIssueAction `json:"action"` @@ -402,13 +360,6 @@ type ReviewPayload struct { Content string `json:"content"` } -// __ __.__ __ .__ -// / \ / \__| | _|__| -// \ \/\/ / | |/ / | -// \ /| | <| | -// \__/\ / |__|__|_ \__| -// \/ \/ - // HookWikiAction an action that happens to a wiki page type HookWikiAction string @@ -435,13 +386,6 @@ func (p *WikiPayload) JSONPayload() ([]byte, error) { return json.MarshalIndent(p, "", " ") } -//__________ .__ __ -//\______ \ ____ ______ ____ _____|__|/ |_ ___________ ___.__. -// | _// __ \\____ \ / _ \/ ___/ \ __\/ _ \_ __ < | | -// | | \ ___/| |_> > <_> )___ \| || | ( <_> ) | \/\___ | -// |____|_ /\___ > __/ \____/____ >__||__| \____/|__| / ____| -// \/ \/|__| \/ \/ - // HookRepoAction an action that happens to a repo type HookRepoAction string @@ -480,7 +424,7 @@ type PackagePayload struct { Action HookPackageAction `json:"action"` Repository *Repository `json:"repository"` Package *Package `json:"package"` - Organization *User `json:"organization"` + Organization *Organization `json:"organization"` Sender *User `json:"sender"` } diff --git a/modules/structs/org.go b/modules/structs/org.go index c0a545ac1c3..f93b3b6493d 100644 --- a/modules/structs/org.go +++ b/modules/structs/org.go @@ -57,3 +57,12 @@ type EditOrgOption struct { Visibility string `json:"visibility" binding:"In(,public,limited,private)"` RepoAdminChangeTeamAccess *bool `json:"repo_admin_change_team_access"` } + +// RenameOrgOption options when renaming an organization +type RenameOrgOption struct { + // New username for this org. This name cannot be in use yet by any other user. + // + // required: true + // unique: true + NewName string `json:"new_name" binding:"Required"` +} diff --git a/modules/structs/repo_branch.go b/modules/structs/repo_branch.go index a9aa1d330a1..55c98d60b91 100644 --- a/modules/structs/repo_branch.go +++ b/modules/structs/repo_branch.go @@ -133,3 +133,11 @@ type EditBranchProtectionOption struct { type UpdateBranchProtectionPriories struct { IDs []int64 `json:"ids"` } + +type MergeUpstreamRequest struct { + Branch string `json:"branch"` +} + +type MergeUpstreamResponse struct { + MergeStyle string `json:"merge_type"` +} diff --git a/modules/tailmsg/talimsg.go b/modules/tailmsg/talimsg.go new file mode 100644 index 00000000000..aafc98e2d2d --- /dev/null +++ b/modules/tailmsg/talimsg.go @@ -0,0 +1,73 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package tailmsg + +import ( + "sync" + "time" +) + +type MsgRecord struct { + Time time.Time + Content string +} + +type MsgRecorder interface { + Record(content string) + GetRecords() []*MsgRecord +} + +type memoryMsgRecorder struct { + mu sync.RWMutex + msgs []*MsgRecord + limit int +} + +// TODO: use redis for a clustered environment + +func (m *memoryMsgRecorder) Record(content string) { + m.mu.Lock() + defer m.mu.Unlock() + m.msgs = append(m.msgs, &MsgRecord{ + Time: time.Now(), + Content: content, + }) + if len(m.msgs) > m.limit { + m.msgs = m.msgs[len(m.msgs)-m.limit:] + } +} + +func (m *memoryMsgRecorder) GetRecords() []*MsgRecord { + m.mu.RLock() + defer m.mu.RUnlock() + ret := make([]*MsgRecord, len(m.msgs)) + copy(ret, m.msgs) + return ret +} + +func NewMsgRecorder(limit int) MsgRecorder { + return &memoryMsgRecorder{ + limit: limit, + } +} + +type Manager struct { + traceRecorder MsgRecorder + logRecorder MsgRecorder +} + +func (m *Manager) GetTraceRecorder() MsgRecorder { + return m.traceRecorder +} + +func (m *Manager) GetLogRecorder() MsgRecorder { + return m.logRecorder +} + +var GetManager = sync.OnceValue(func() *Manager { + return &Manager{ + traceRecorder: NewMsgRecorder(100), + logRecorder: NewMsgRecorder(1000), + } +}) diff --git a/modules/templates/helper.go b/modules/templates/helper.go index b673450f5a9..c0b0ddc97dd 100644 --- a/modules/templates/helper.go +++ b/modules/templates/helper.go @@ -38,7 +38,7 @@ func NewFuncMap() template.FuncMap { "Iif": iif, "Eval": evalTokens, "SafeHTML": safeHTML, - "HTMLFormat": htmlutil.HTMLFormat, + "HTMLFormat": htmlFormat, "HTMLEscape": htmlEscape, "QueryEscape": queryEscape, "QueryBuild": QueryBuild, @@ -69,7 +69,7 @@ func NewFuncMap() template.FuncMap { // time / number / format "FileSize": base.FileSize, "CountFmt": countFmt, - "Sec2Time": util.SecToTime, + "Sec2Hour": util.SecToHours, "TimeEstimateString": timeEstimateString, @@ -131,15 +131,9 @@ func NewFuncMap() template.FuncMap { "EnableTimetracking": func() bool { return setting.Service.EnableTimetracking }, - "DisableGitHooks": func() bool { - return setting.DisableGitHooks - }, "DisableWebhooks": func() bool { return setting.DisableWebhooks }, - "DisableImportLocal": func() bool { - return !setting.ImportLocalPaths - }, "UserThemeName": userThemeName, "NotificationSettings": func() map[string]any { return map[string]any{ @@ -213,6 +207,20 @@ func htmlEscape(s any) template.HTML { panic(fmt.Sprintf("unexpected type %T", s)) } +func htmlFormat(s any, args ...any) template.HTML { + if len(args) == 0 { + // to prevent developers from calling "HTMLFormat $userInput" by mistake which will lead to XSS + panic("missing arguments for HTMLFormat") + } + switch v := s.(type) { + case string: + return htmlutil.HTMLFormat(template.HTML(v), args...) + case template.HTML: + return htmlutil.HTMLFormat(v, args...) + } + panic(fmt.Sprintf("unexpected type %T", s)) +} + func jsEscapeSafe(s string) template.HTML { return template.HTML(template.JSEscapeString(s)) } @@ -264,22 +272,42 @@ func userThemeName(user *user_model.User) string { return setting.UI.DefaultTheme } +func isQueryParamEmpty(v any) bool { + return v == nil || v == false || v == 0 || v == int64(0) || v == "" +} + // QueryBuild builds a query string from a list of key-value pairs. -// It omits the nil and empty strings, but it doesn't omit other zero values, -// because the zero value of number types may have a meaning. +// It omits the nil, false, zero int/int64 and empty string values, +// because they are default empty values for "ctx.FormXxx" calls. +// If 0 or false need to be included, use string values: "0" and "false". +// Build rules: +// * Even parameters: always build as query string: a=b&c=d +// * Odd parameters: +// * * {"/anything", param-pairs...} => "/?param-paris" +// * * {"anything?old-params", new-param-pairs...} => "anything?old-params&new-param-paris" +// * * Otherwise: {"old¶ms", new-param-pairs...} => "old¶ms&new-param-paris" +// * * Other behaviors are undefined yet. func QueryBuild(a ...any) template.URL { - var s string + var reqPath, s string + hasTrailingSep := false if len(a)%2 == 1 { if v, ok := a[0].(string); ok { - if v == "" || (v[0] != '?' && v[0] != '&') { - panic("QueryBuild: invalid argument") - } s = v } else if v, ok := a[0].(template.URL); ok { s = string(v) } else { panic("QueryBuild: invalid argument") } + hasTrailingSep = s != "&" && strings.HasSuffix(s, "&") + if strings.HasPrefix(s, "/") || strings.Contains(s, "?") { + if s1, s2, ok := strings.Cut(s, "?"); ok { + reqPath = s1 + "?" + s = s2 + } else { + reqPath += s + "?" + s = "" + } + } } for i := len(a) % 2; i < len(a); i += 2 { k, ok := a[i].(string) @@ -290,19 +318,16 @@ func QueryBuild(a ...any) template.URL { if va, ok := a[i+1].(string); ok { v = va } else if a[i+1] != nil { - v = fmt.Sprint(a[i+1]) + if !isQueryParamEmpty(a[i+1]) { + v = fmt.Sprint(a[i+1]) + } } // pos1 to pos2 is the "k=v&" part, "&" is optional pos1 := strings.Index(s, "&"+k+"=") if pos1 != -1 { pos1++ - } else { - pos1 = strings.Index(s, "?"+k+"=") - if pos1 != -1 { - pos1++ - } else if strings.HasPrefix(s, k+"=") { - pos1 = 0 - } + } else if strings.HasPrefix(s, k+"=") { + pos1 = 0 } pos2 := len(s) if pos1 == -1 { @@ -315,7 +340,7 @@ func QueryBuild(a ...any) template.URL { } if v != "" { sep := "" - hasPrefixSep := pos1 == 0 || (pos1 <= len(s) && (s[pos1-1] == '?' || s[pos1-1] == '&')) + hasPrefixSep := pos1 == 0 || (pos1 <= len(s) && s[pos1-1] == '&') if !hasPrefixSep { sep = "&" } @@ -324,9 +349,22 @@ func QueryBuild(a ...any) template.URL { s = s[:pos1] + s[pos2:] } } - if s != "" && s != "&" && s[len(s)-1] == '&' { + if s != "" && s[len(s)-1] == '&' && !hasTrailingSep { s = s[:len(s)-1] } + if reqPath != "" { + if s == "" { + s = reqPath + if s != "?" { + s = s[:len(s)-1] + } + } else { + if s[0] == '&' { + s = s[1:] + } + s = reqPath + s + } + } return template.URL(s) } diff --git a/modules/templates/helper_test.go b/modules/templates/helper_test.go index a530d484bc4..5d7bc93622b 100644 --- a/modules/templates/helper_test.go +++ b/modules/templates/helper_test.go @@ -8,7 +8,6 @@ import ( "strings" "testing" - "code.gitea.io/gitea/modules/htmlutil" "code.gitea.io/gitea/modules/util" "github.com/stretchr/testify/assert" @@ -88,7 +87,7 @@ func TestTemplateIif(t *testing.T) { func TestTemplateEscape(t *testing.T) { execTmpl := func(code string) string { tmpl := template.New("test") - tmpl.Funcs(template.FuncMap{"QueryBuild": QueryBuild, "HTMLFormat": htmlutil.HTMLFormat}) + tmpl.Funcs(template.FuncMap{"QueryBuild": QueryBuild, "HTMLFormat": htmlFormat}) template.Must(tmpl.Parse(code)) w := &strings.Builder{} assert.NoError(t, tmpl.Execute(w, nil)) @@ -118,3 +117,58 @@ func TestTemplateEscape(t *testing.T) { assert.Equal(t, `<>`, actual) }) } + +func TestQueryBuild(t *testing.T) { + t.Run("construct", func(t *testing.T) { + assert.Equal(t, "", string(QueryBuild())) + assert.Equal(t, "", string(QueryBuild("a", nil, "b", false, "c", 0, "d", ""))) + assert.Equal(t, "a=1&b=true", string(QueryBuild("a", 1, "b", "true"))) + + // path with query parameters + assert.Equal(t, "/?k=1", string(QueryBuild("/", "k", 1))) + assert.Equal(t, "/", string(QueryBuild("/?k=a", "k", 0))) + + // no path but question mark with query parameters + assert.Equal(t, "?k=1", string(QueryBuild("?", "k", 1))) + assert.Equal(t, "?", string(QueryBuild("?", "k", 0))) + assert.Equal(t, "path?k=1", string(QueryBuild("path?", "k", 1))) + assert.Equal(t, "path", string(QueryBuild("path?", "k", 0))) + + // only query parameters + assert.Equal(t, "&k=1", string(QueryBuild("&", "k", 1))) + assert.Equal(t, "", string(QueryBuild("&", "k", 0))) + assert.Equal(t, "", string(QueryBuild("&k=a", "k", 0))) + assert.Equal(t, "", string(QueryBuild("k=a&", "k", 0))) + assert.Equal(t, "a=1&b=2", string(QueryBuild("a=1", "b", 2))) + assert.Equal(t, "&a=1&b=2", string(QueryBuild("&a=1", "b", 2))) + assert.Equal(t, "a=1&b=2&", string(QueryBuild("a=1&", "b", 2))) + }) + + t.Run("replace", func(t *testing.T) { + assert.Equal(t, "a=1&c=d&e=f", string(QueryBuild("a=b&c=d&e=f", "a", 1))) + assert.Equal(t, "a=b&c=1&e=f", string(QueryBuild("a=b&c=d&e=f", "c", 1))) + assert.Equal(t, "a=b&c=d&e=1", string(QueryBuild("a=b&c=d&e=f", "e", 1))) + assert.Equal(t, "a=b&c=d&e=f&k=1", string(QueryBuild("a=b&c=d&e=f", "k", 1))) + }) + + t.Run("replace-&", func(t *testing.T) { + assert.Equal(t, "&a=1&c=d&e=f", string(QueryBuild("&a=b&c=d&e=f", "a", 1))) + assert.Equal(t, "&a=b&c=1&e=f", string(QueryBuild("&a=b&c=d&e=f", "c", 1))) + assert.Equal(t, "&a=b&c=d&e=1", string(QueryBuild("&a=b&c=d&e=f", "e", 1))) + assert.Equal(t, "&a=b&c=d&e=f&k=1", string(QueryBuild("&a=b&c=d&e=f", "k", 1))) + }) + + t.Run("delete", func(t *testing.T) { + assert.Equal(t, "c=d&e=f", string(QueryBuild("a=b&c=d&e=f", "a", ""))) + assert.Equal(t, "a=b&e=f", string(QueryBuild("a=b&c=d&e=f", "c", ""))) + assert.Equal(t, "a=b&c=d", string(QueryBuild("a=b&c=d&e=f", "e", ""))) + assert.Equal(t, "a=b&c=d&e=f", string(QueryBuild("a=b&c=d&e=f", "k", ""))) + }) + + t.Run("delete-&", func(t *testing.T) { + assert.Equal(t, "&c=d&e=f", string(QueryBuild("&a=b&c=d&e=f", "a", ""))) + assert.Equal(t, "&a=b&e=f", string(QueryBuild("&a=b&c=d&e=f", "c", ""))) + assert.Equal(t, "&a=b&c=d", string(QueryBuild("&a=b&c=d&e=f", "e", ""))) + assert.Equal(t, "&a=b&c=d&e=f", string(QueryBuild("&a=b&c=d&e=f", "k", ""))) + }) +} diff --git a/modules/templates/util_misc.go b/modules/templates/util_misc.go index d645fa013e8..2d42bc76b54 100644 --- a/modules/templates/util_misc.go +++ b/modules/templates/util_misc.go @@ -150,7 +150,7 @@ func mirrorRemoteAddress(ctx context.Context, m *repo_model.Repository, remoteNa return ret } - u, err := giturl.Parse(remoteURL) + u, err := giturl.ParseGitURL(remoteURL) if err != nil { log.Error("giturl.Parse %v", err) return ret diff --git a/modules/user/user_test.go b/modules/user/user_test.go index 372a675d342..d6b3911ca6e 100644 --- a/modules/user/user_test.go +++ b/modules/user/user_test.go @@ -8,6 +8,9 @@ import ( "runtime" "strings" "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func getWhoamiOutput() (string, error) { @@ -20,24 +23,19 @@ func getWhoamiOutput() (string, error) { func TestCurrentUsername(t *testing.T) { user := CurrentUsername() - if len(user) == 0 { - t.Errorf("expected non-empty user, got: %s", user) - } + require.NotEmpty(t, user) + // Windows whoami is weird, so just skip remaining tests if runtime.GOOS == "windows" { t.Skip("skipped test because of weird whoami on Windows") } whoami, err := getWhoamiOutput() - if err != nil { - t.Errorf("failed to run whoami to test current user: %f", err) - } + require.NoError(t, err) + user = CurrentUsername() - if user != whoami { - t.Errorf("expected %s as user, got: %s", whoami, user) - } + assert.Equal(t, whoami, user) + t.Setenv("USER", "spoofed") user = CurrentUsername() - if user != whoami { - t.Errorf("expected %s as user, got: %s", whoami, user) - } + assert.Equal(t, whoami, user) } diff --git a/modules/util/path.go b/modules/util/path.go index 1272f5af2ee..d9f17bd124b 100644 --- a/modules/util/path.go +++ b/modules/util/path.go @@ -140,81 +140,51 @@ func IsExist(path string) (bool, error) { return false, err } -func statDir(dirPath, recPath string, includeDir, isDirOnly, followSymlinks bool) ([]string, error) { - dir, err := os.Open(dirPath) +func listDirRecursively(result *[]string, fsDir, recordParentPath string, opts *ListDirOptions) error { + dir, err := os.Open(fsDir) if err != nil { - return nil, err + return err } defer dir.Close() fis, err := dir.Readdir(0) if err != nil { - return nil, err + return err } - statList := make([]string, 0) for _, fi := range fis { - if CommonSkip(fi.Name()) { + if opts.SkipCommonHiddenNames && IsCommonHiddenFileName(fi.Name()) { continue } - - relPath := path.Join(recPath, fi.Name()) - curPath := path.Join(dirPath, fi.Name()) + relPath := path.Join(recordParentPath, fi.Name()) + curPath := filepath.Join(fsDir, fi.Name()) if fi.IsDir() { - if includeDir { - statList = append(statList, relPath+"/") + if opts.IncludeDir { + *result = append(*result, relPath+"/") } - s, err := statDir(curPath, relPath, includeDir, isDirOnly, followSymlinks) - if err != nil { - return nil, err - } - statList = append(statList, s...) - } else if !isDirOnly { - statList = append(statList, relPath) - } else if followSymlinks && fi.Mode()&os.ModeSymlink != 0 { - link, err := os.Readlink(curPath) - if err != nil { - return nil, err - } - - isDir, err := IsDir(link) - if err != nil { - return nil, err - } - if isDir { - if includeDir { - statList = append(statList, relPath+"/") - } - s, err := statDir(curPath, relPath, includeDir, isDirOnly, followSymlinks) - if err != nil { - return nil, err - } - statList = append(statList, s...) + if err = listDirRecursively(result, curPath, relPath, opts); err != nil { + return err } + } else { + *result = append(*result, relPath) } } - return statList, nil + return nil } -// StatDir gathers information of given directory by depth-first. -// It returns slice of file list and includes subdirectories if enabled; -// it returns error and nil slice when error occurs in underlying functions, -// or given path is not a directory or does not exist. -// -// Slice does not include given path itself. -// If subdirectories is enabled, they will have suffix '/'. -func StatDir(rootPath string, includeDir ...bool) ([]string, error) { - if isDir, err := IsDir(rootPath); err != nil { - return nil, err - } else if !isDir { - return nil, errors.New("not a directory or does not exist: " + rootPath) - } +type ListDirOptions struct { + IncludeDir bool // subdirectories are also included with suffix slash + SkipCommonHiddenNames bool +} - isIncludeDir := false - if len(includeDir) != 0 { - isIncludeDir = includeDir[0] +// ListDirRecursively gathers information of given directory by depth-first. +// The paths are always in "dir/slash/file" format (not "\\" even in Windows) +// Slice does not include given path itself. +func ListDirRecursively(rootDir string, opts *ListDirOptions) (res []string, err error) { + if err = listDirRecursively(&res, rootDir, "", opts); err != nil { + return nil, err } - return statDir(rootPath, "", isIncludeDir, false, false) + return res, nil } func isOSWindows() bool { @@ -265,8 +235,8 @@ func HomeDir() (home string, err error) { return home, nil } -// CommonSkip will check a provided name to see if it represents file or directory that should not be watched -func CommonSkip(name string) bool { +// IsCommonHiddenFileName will check a provided name to see if it represents file or directory that should not be watched +func IsCommonHiddenFileName(name string) bool { if name == "" { return true } @@ -275,9 +245,9 @@ func CommonSkip(name string) bool { case '.': return true case 't', 'T': - return name[1:] == "humbs.db" + return name[1:] == "humbs.db" // macOS case 'd', 'D': - return name[1:] == "esktop.ini" + return name[1:] == "esktop.ini" // Windows } return false diff --git a/modules/util/path_test.go b/modules/util/path_test.go index 6a38bf4ace8..79c37e55f7a 100644 --- a/modules/util/path_test.go +++ b/modules/util/path_test.go @@ -5,10 +5,12 @@ package util import ( "net/url" + "os" "runtime" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestFileURLToPath(t *testing.T) { @@ -210,3 +212,21 @@ func TestCleanPath(t *testing.T) { assert.Equal(t, c.expected, FilePathJoinAbs(c.elems[0], c.elems[1:]...), "case: %v", c.elems) } } + +func TestListDirRecursively(t *testing.T) { + tmpDir := t.TempDir() + _ = os.WriteFile(tmpDir+"/.config", nil, 0o644) + _ = os.Mkdir(tmpDir+"/d1", 0o755) + _ = os.WriteFile(tmpDir+"/d1/f-d1", nil, 0o644) + _ = os.Mkdir(tmpDir+"/d1/s1", 0o755) + _ = os.WriteFile(tmpDir+"/d1/s1/f-d1s1", nil, 0o644) + _ = os.Mkdir(tmpDir+"/d2", 0o755) + + res, err := ListDirRecursively(tmpDir, &ListDirOptions{IncludeDir: true}) + require.NoError(t, err) + assert.ElementsMatch(t, []string{".config", "d1/", "d1/f-d1", "d1/s1/", "d1/s1/f-d1s1", "d2/"}, res) + + res, err = ListDirRecursively(tmpDir, &ListDirOptions{SkipCommonHiddenNames: true}) + require.NoError(t, err) + assert.ElementsMatch(t, []string{"d1/f-d1", "d1/s1/f-d1s1"}, res) +} diff --git a/modules/util/sec_to_time.go b/modules/util/sec_to_time.go index ad0fb1a68b4..646f33c82a5 100644 --- a/modules/util/sec_to_time.go +++ b/modules/util/sec_to_time.go @@ -8,61 +8,23 @@ import ( "strings" ) -// SecToTime converts an amount of seconds to a human-readable string. E.g. -// 66s -> 1 minute 6 seconds -// 52410s -> 14 hours 33 minutes -// 563418 -> 6 days 12 hours -// 1563418 -> 2 weeks 4 days -// 3937125s -> 1 month 2 weeks -// 45677465s -> 1 year 6 months -func SecToTime(durationVal any) string { - duration, _ := ToInt64(durationVal) +// SecToHours converts an amount of seconds to a human-readable hours string. +// This is stable for planning and managing timesheets. +// Here it only supports hours and minutes, because a work day could contain 6 or 7 or 8 hours. +// If the duration is less than 1 minute, it will be shown as seconds. +func SecToHours(durationVal any) string { + seconds, _ := ToInt64(durationVal) + hours := seconds / 3600 + minutes := (seconds / 60) % 60 formattedTime := "" - - // The following four variables are calculated by taking - // into account the previously calculated variables, this avoids - // pitfalls when using remainders. As that could lead to incorrect - // results when the calculated number equals the quotient number. - remainingDays := duration / (60 * 60 * 24) - years := remainingDays / 365 - remainingDays -= years * 365 - months := remainingDays * 12 / 365 - remainingDays -= months * 365 / 12 - weeks := remainingDays / 7 - remainingDays -= weeks * 7 - days := remainingDays - - // The following three variables are calculated without depending - // on the previous calculated variables. - hours := (duration / 3600) % 24 - minutes := (duration / 60) % 60 - seconds := duration % 60 - - // Extract only the relevant information of the time - // If the time is greater than a year, it makes no sense to display seconds. - switch { - case years > 0: - formattedTime = formatTime(years, "year", formattedTime) - formattedTime = formatTime(months, "month", formattedTime) - case months > 0: - formattedTime = formatTime(months, "month", formattedTime) - formattedTime = formatTime(weeks, "week", formattedTime) - case weeks > 0: - formattedTime = formatTime(weeks, "week", formattedTime) - formattedTime = formatTime(days, "day", formattedTime) - case days > 0: - formattedTime = formatTime(days, "day", formattedTime) - formattedTime = formatTime(hours, "hour", formattedTime) - case hours > 0: - formattedTime = formatTime(hours, "hour", formattedTime) - formattedTime = formatTime(minutes, "minute", formattedTime) - default: - formattedTime = formatTime(minutes, "minute", formattedTime) - formattedTime = formatTime(seconds, "second", formattedTime) - } + formattedTime = formatTime(hours, "hour", formattedTime) + formattedTime = formatTime(minutes, "minute", formattedTime) // The formatTime() function always appends a space at the end. This will be trimmed + if formattedTime == "" && seconds > 0 { + formattedTime = formatTime(seconds, "second", "") + } return strings.TrimRight(formattedTime, " ") } @@ -76,6 +38,5 @@ func formatTime(value int64, name, formattedTime string) string { } else if value > 1 { formattedTime = fmt.Sprintf("%s%d %ss ", formattedTime, value, name) } - return formattedTime } diff --git a/modules/util/sec_to_time_test.go b/modules/util/sec_to_time_test.go index 4d1213a52c0..b67926bbcff 100644 --- a/modules/util/sec_to_time_test.go +++ b/modules/util/sec_to_time_test.go @@ -9,22 +9,20 @@ import ( "github.com/stretchr/testify/assert" ) -func TestSecToTime(t *testing.T) { +func TestSecToHours(t *testing.T) { second := int64(1) minute := 60 * second hour := 60 * minute day := 24 * hour - year := 365 * day - assert.Equal(t, "1 minute 6 seconds", SecToTime(minute+6*second)) - assert.Equal(t, "1 hour", SecToTime(hour)) - assert.Equal(t, "1 hour", SecToTime(hour+second)) - assert.Equal(t, "14 hours 33 minutes", SecToTime(14*hour+33*minute+30*second)) - assert.Equal(t, "6 days 12 hours", SecToTime(6*day+12*hour+30*minute+18*second)) - assert.Equal(t, "2 weeks 4 days", SecToTime((2*7+4)*day+2*hour+16*minute+58*second)) - assert.Equal(t, "4 weeks", SecToTime(4*7*day)) - assert.Equal(t, "4 weeks 1 day", SecToTime((4*7+1)*day)) - assert.Equal(t, "1 month 2 weeks", SecToTime((6*7+3)*day+13*hour+38*minute+45*second)) - assert.Equal(t, "11 months", SecToTime(year-25*day)) - assert.Equal(t, "1 year 5 months", SecToTime(year+163*day+10*hour+11*minute+5*second)) + assert.Equal(t, "1 minute", SecToHours(minute+6*second)) + assert.Equal(t, "1 hour", SecToHours(hour)) + assert.Equal(t, "1 hour", SecToHours(hour+second)) + assert.Equal(t, "14 hours 33 minutes", SecToHours(14*hour+33*minute+30*second)) + assert.Equal(t, "156 hours 30 minutes", SecToHours(6*day+12*hour+30*minute+18*second)) + assert.Equal(t, "98 hours 16 minutes", SecToHours(4*day+2*hour+16*minute+58*second)) + assert.Equal(t, "672 hours", SecToHours(4*7*day)) + assert.Equal(t, "1 second", SecToHours(1)) + assert.Equal(t, "2 seconds", SecToHours(2)) + assert.Equal(t, "", SecToHours(nil)) // old behavior, empty means no output } diff --git a/modules/util/shellquote_test.go b/modules/util/shellquote_test.go index 969998c5926..4ef5ce69809 100644 --- a/modules/util/shellquote_test.go +++ b/modules/util/shellquote_test.go @@ -3,7 +3,11 @@ package util -import "testing" +import ( + "testing" + + "github.com/stretchr/testify/assert" +) func TestShellEscape(t *testing.T) { tests := []struct { @@ -83,9 +87,7 @@ func TestShellEscape(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := ShellEscape(tt.toEscape); got != tt.want { - t.Errorf("ShellEscape(%q):\nGot: %s\nWanted: %s", tt.toEscape, got, tt.want) - } + assert.Equal(t, tt.want, ShellEscape(tt.toEscape)) }) } } diff --git a/modules/validation/glob_pattern_test.go b/modules/validation/glob_pattern_test.go index 1bf622e61d6..7f3e609acfa 100644 --- a/modules/validation/glob_pattern_test.go +++ b/modules/validation/glob_pattern_test.go @@ -19,40 +19,40 @@ func getGlobPatternErrorString(pattern string) string { return "" } -var globValidationTestCases = []validationTestCase{ - { - description: "Empty glob pattern", - data: TestForm{ - GlobPattern: "", - }, - expectedErrors: binding.Errors{}, - }, - { - description: "Valid glob", - data: TestForm{ - GlobPattern: "{master,release*}", - }, - expectedErrors: binding.Errors{}, - }, - - { - description: "Invalid glob", - data: TestForm{ - GlobPattern: "[a-", - }, - expectedErrors: binding.Errors{ - binding.Error{ - FieldNames: []string{"GlobPattern"}, - Classification: ErrGlobPattern, - Message: getGlobPatternErrorString("[a-"), - }, - }, - }, -} - func Test_GlobPatternValidation(t *testing.T) { AddBindingRules() + globValidationTestCases := []validationTestCase{ + { + description: "Empty glob pattern", + data: TestForm{ + GlobPattern: "", + }, + expectedErrors: binding.Errors{}, + }, + { + description: "Valid glob", + data: TestForm{ + GlobPattern: "{master,release*}", + }, + expectedErrors: binding.Errors{}, + }, + + { + description: "Invalid glob", + data: TestForm{ + GlobPattern: "[a-", + }, + expectedErrors: binding.Errors{ + binding.Error{ + FieldNames: []string{"GlobPattern"}, + Classification: ErrGlobPattern, + Message: getGlobPatternErrorString("[a-"), + }, + }, + }, + } + for _, testCase := range globValidationTestCases { t.Run(testCase.description, func(t *testing.T) { performValidationTest(t, testCase) diff --git a/modules/validation/helpers.go b/modules/validation/helpers.go index f6e00f3887a..9f6cf5201a2 100644 --- a/modules/validation/helpers.go +++ b/modules/validation/helpers.go @@ -8,13 +8,26 @@ import ( "net/url" "regexp" "strings" + "sync" "code.gitea.io/gitea/modules/setting" "github.com/gobwas/glob" ) -var externalTrackerRegex = regexp.MustCompile(`({?)(?:user|repo|index)+?(}?)`) +type globalVarsStruct struct { + externalTrackerRegex *regexp.Regexp + validUsernamePattern *regexp.Regexp + invalidUsernamePattern *regexp.Regexp +} + +var globalVars = sync.OnceValue(func() *globalVarsStruct { + return &globalVarsStruct{ + externalTrackerRegex: regexp.MustCompile(`({?)(?:user|repo|index)+?(}?)`), + validUsernamePattern: regexp.MustCompile(`^[\da-zA-Z][-.\w]*$`), + invalidUsernamePattern: regexp.MustCompile(`[-._]{2,}|[-._]$`), // No consecutive or trailing non-alphanumeric chars + } +}) func isLoopbackIP(ip string) bool { return net.ParseIP(ip).IsLoopback() @@ -105,9 +118,9 @@ func IsValidExternalTrackerURLFormat(uri string) bool { if !IsValidExternalURL(uri) { return false } - + vars := globalVars() // check for typoed variables like /{index/ or /[repo} - for _, match := range externalTrackerRegex.FindAllStringSubmatch(uri, -1) { + for _, match := range vars.externalTrackerRegex.FindAllStringSubmatch(uri, -1) { if (match[1] == "{" || match[2] == "}") && (match[1] != "{" || match[2] != "}") { return false } @@ -116,14 +129,10 @@ func IsValidExternalTrackerURLFormat(uri string) bool { return true } -var ( - validUsernamePattern = regexp.MustCompile(`^[\da-zA-Z][-.\w]*$`) - invalidUsernamePattern = regexp.MustCompile(`[-._]{2,}|[-._]$`) // No consecutive or trailing non-alphanumeric chars -) - // IsValidUsername checks if username is valid func IsValidUsername(name string) bool { // It is difficult to find a single pattern that is both readable and effective, // but it's easier to use positive and negative checks. - return validUsernamePattern.MatchString(name) && !invalidUsernamePattern.MatchString(name) + vars := globalVars() + return vars.validUsernamePattern.MatchString(name) && !vars.invalidUsernamePattern.MatchString(name) } diff --git a/modules/validation/refname_test.go b/modules/validation/refname_test.go index 3af7387c474..93703761cf9 100644 --- a/modules/validation/refname_test.go +++ b/modules/validation/refname_test.go @@ -9,253 +9,252 @@ import ( "gitea.com/go-chi/binding" ) -var gitRefNameValidationTestCases = []validationTestCase{ - { - description: "Reference name contains only characters", - data: TestForm{ - BranchName: "test", - }, - expectedErrors: binding.Errors{}, - }, - { - description: "Reference name contains single slash", - data: TestForm{ - BranchName: "feature/test", - }, - expectedErrors: binding.Errors{}, - }, - { - description: "Reference name has allowed special characters", - data: TestForm{ - BranchName: "debian/1%1.6.0-2", - }, - expectedErrors: binding.Errors{}, - }, - { - description: "Reference name contains backslash", - data: TestForm{ - BranchName: "feature\\test", - }, - expectedErrors: binding.Errors{ - binding.Error{ - FieldNames: []string{"BranchName"}, - Classification: ErrGitRefName, - Message: "GitRefName", - }, - }, - }, - { - description: "Reference name starts with dot", - data: TestForm{ - BranchName: ".test", - }, - expectedErrors: binding.Errors{ - binding.Error{ - FieldNames: []string{"BranchName"}, - Classification: ErrGitRefName, - Message: "GitRefName", - }, - }, - }, - { - description: "Reference name ends with dot", - data: TestForm{ - BranchName: "test.", - }, - expectedErrors: binding.Errors{ - binding.Error{ - FieldNames: []string{"BranchName"}, - Classification: ErrGitRefName, - Message: "GitRefName", - }, - }, - }, - { - description: "Reference name starts with slash", - data: TestForm{ - BranchName: "/test", - }, - expectedErrors: binding.Errors{ - binding.Error{ - FieldNames: []string{"BranchName"}, - Classification: ErrGitRefName, - Message: "GitRefName", - }, - }, - }, - { - description: "Reference name ends with slash", - data: TestForm{ - BranchName: "test/", - }, - expectedErrors: binding.Errors{ - binding.Error{ - FieldNames: []string{"BranchName"}, - Classification: ErrGitRefName, - Message: "GitRefName", - }, - }, - }, - { - description: "Reference name ends with .lock", - data: TestForm{ - BranchName: "test.lock", - }, - expectedErrors: binding.Errors{ - binding.Error{ - FieldNames: []string{"BranchName"}, - Classification: ErrGitRefName, - Message: "GitRefName", - }, - }, - }, - { - description: "Reference name contains multiple consecutive dots", - data: TestForm{ - BranchName: "te..st", - }, - expectedErrors: binding.Errors{ - binding.Error{ - FieldNames: []string{"BranchName"}, - Classification: ErrGitRefName, - Message: "GitRefName", - }, - }, - }, - { - description: "Reference name contains multiple consecutive slashes", - data: TestForm{ - BranchName: "te//st", - }, - expectedErrors: binding.Errors{ - binding.Error{ - FieldNames: []string{"BranchName"}, - Classification: ErrGitRefName, - Message: "GitRefName", - }, - }, - }, - { - description: "Reference name is single @", - data: TestForm{ - BranchName: "@", - }, - expectedErrors: binding.Errors{ - binding.Error{ - FieldNames: []string{"BranchName"}, - Classification: ErrGitRefName, - Message: "GitRefName", - }, - }, - }, - { - description: "Reference name has @{", - data: TestForm{ - BranchName: "branch@{", - }, - expectedErrors: binding.Errors{ - binding.Error{ - FieldNames: []string{"BranchName"}, - Classification: ErrGitRefName, - Message: "GitRefName", - }, - }, - }, - { - description: "Reference name has unallowed special character ~", - data: TestForm{ - BranchName: "~debian/1%1.6.0-2", - }, - expectedErrors: binding.Errors{ - binding.Error{ - FieldNames: []string{"BranchName"}, - Classification: ErrGitRefName, - Message: "GitRefName", - }, - }, - }, - { - description: "Reference name has unallowed special character *", - data: TestForm{ - BranchName: "*debian/1%1.6.0-2", - }, - expectedErrors: binding.Errors{ - binding.Error{ - FieldNames: []string{"BranchName"}, - Classification: ErrGitRefName, - Message: "GitRefName", - }, - }, - }, - { - description: "Reference name has unallowed special character ?", - data: TestForm{ - BranchName: "?debian/1%1.6.0-2", - }, - expectedErrors: binding.Errors{ - binding.Error{ - FieldNames: []string{"BranchName"}, - Classification: ErrGitRefName, - Message: "GitRefName", - }, - }, - }, - { - description: "Reference name has unallowed special character ^", - data: TestForm{ - BranchName: "^debian/1%1.6.0-2", - }, - expectedErrors: binding.Errors{ - binding.Error{ - FieldNames: []string{"BranchName"}, - Classification: ErrGitRefName, - Message: "GitRefName", - }, - }, - }, - { - description: "Reference name has unallowed special character :", - data: TestForm{ - BranchName: "debian:jessie", - }, - expectedErrors: binding.Errors{ - binding.Error{ - FieldNames: []string{"BranchName"}, - Classification: ErrGitRefName, - Message: "GitRefName", - }, - }, - }, - { - description: "Reference name has unallowed special character (whitespace)", - data: TestForm{ - BranchName: "debian jessie", - }, - expectedErrors: binding.Errors{ - binding.Error{ - FieldNames: []string{"BranchName"}, - Classification: ErrGitRefName, - Message: "GitRefName", - }, - }, - }, - { - description: "Reference name has unallowed special character [", - data: TestForm{ - BranchName: "debian[jessie", - }, - expectedErrors: binding.Errors{ - binding.Error{ - FieldNames: []string{"BranchName"}, - Classification: ErrGitRefName, - Message: "GitRefName", - }, - }, - }, -} - func Test_GitRefNameValidation(t *testing.T) { AddBindingRules() + gitRefNameValidationTestCases := []validationTestCase{ + { + description: "Reference name contains only characters", + data: TestForm{ + BranchName: "test", + }, + expectedErrors: binding.Errors{}, + }, + { + description: "Reference name contains single slash", + data: TestForm{ + BranchName: "feature/test", + }, + expectedErrors: binding.Errors{}, + }, + { + description: "Reference name has allowed special characters", + data: TestForm{ + BranchName: "debian/1%1.6.0-2", + }, + expectedErrors: binding.Errors{}, + }, + { + description: "Reference name contains backslash", + data: TestForm{ + BranchName: "feature\\test", + }, + expectedErrors: binding.Errors{ + binding.Error{ + FieldNames: []string{"BranchName"}, + Classification: ErrGitRefName, + Message: "GitRefName", + }, + }, + }, + { + description: "Reference name starts with dot", + data: TestForm{ + BranchName: ".test", + }, + expectedErrors: binding.Errors{ + binding.Error{ + FieldNames: []string{"BranchName"}, + Classification: ErrGitRefName, + Message: "GitRefName", + }, + }, + }, + { + description: "Reference name ends with dot", + data: TestForm{ + BranchName: "test.", + }, + expectedErrors: binding.Errors{ + binding.Error{ + FieldNames: []string{"BranchName"}, + Classification: ErrGitRefName, + Message: "GitRefName", + }, + }, + }, + { + description: "Reference name starts with slash", + data: TestForm{ + BranchName: "/test", + }, + expectedErrors: binding.Errors{ + binding.Error{ + FieldNames: []string{"BranchName"}, + Classification: ErrGitRefName, + Message: "GitRefName", + }, + }, + }, + { + description: "Reference name ends with slash", + data: TestForm{ + BranchName: "test/", + }, + expectedErrors: binding.Errors{ + binding.Error{ + FieldNames: []string{"BranchName"}, + Classification: ErrGitRefName, + Message: "GitRefName", + }, + }, + }, + { + description: "Reference name ends with .lock", + data: TestForm{ + BranchName: "test.lock", + }, + expectedErrors: binding.Errors{ + binding.Error{ + FieldNames: []string{"BranchName"}, + Classification: ErrGitRefName, + Message: "GitRefName", + }, + }, + }, + { + description: "Reference name contains multiple consecutive dots", + data: TestForm{ + BranchName: "te..st", + }, + expectedErrors: binding.Errors{ + binding.Error{ + FieldNames: []string{"BranchName"}, + Classification: ErrGitRefName, + Message: "GitRefName", + }, + }, + }, + { + description: "Reference name contains multiple consecutive slashes", + data: TestForm{ + BranchName: "te//st", + }, + expectedErrors: binding.Errors{ + binding.Error{ + FieldNames: []string{"BranchName"}, + Classification: ErrGitRefName, + Message: "GitRefName", + }, + }, + }, + { + description: "Reference name is single @", + data: TestForm{ + BranchName: "@", + }, + expectedErrors: binding.Errors{ + binding.Error{ + FieldNames: []string{"BranchName"}, + Classification: ErrGitRefName, + Message: "GitRefName", + }, + }, + }, + { + description: "Reference name has @{", + data: TestForm{ + BranchName: "branch@{", + }, + expectedErrors: binding.Errors{ + binding.Error{ + FieldNames: []string{"BranchName"}, + Classification: ErrGitRefName, + Message: "GitRefName", + }, + }, + }, + { + description: "Reference name has unallowed special character ~", + data: TestForm{ + BranchName: "~debian/1%1.6.0-2", + }, + expectedErrors: binding.Errors{ + binding.Error{ + FieldNames: []string{"BranchName"}, + Classification: ErrGitRefName, + Message: "GitRefName", + }, + }, + }, + { + description: "Reference name has unallowed special character *", + data: TestForm{ + BranchName: "*debian/1%1.6.0-2", + }, + expectedErrors: binding.Errors{ + binding.Error{ + FieldNames: []string{"BranchName"}, + Classification: ErrGitRefName, + Message: "GitRefName", + }, + }, + }, + { + description: "Reference name has unallowed special character ?", + data: TestForm{ + BranchName: "?debian/1%1.6.0-2", + }, + expectedErrors: binding.Errors{ + binding.Error{ + FieldNames: []string{"BranchName"}, + Classification: ErrGitRefName, + Message: "GitRefName", + }, + }, + }, + { + description: "Reference name has unallowed special character ^", + data: TestForm{ + BranchName: "^debian/1%1.6.0-2", + }, + expectedErrors: binding.Errors{ + binding.Error{ + FieldNames: []string{"BranchName"}, + Classification: ErrGitRefName, + Message: "GitRefName", + }, + }, + }, + { + description: "Reference name has unallowed special character :", + data: TestForm{ + BranchName: "debian:jessie", + }, + expectedErrors: binding.Errors{ + binding.Error{ + FieldNames: []string{"BranchName"}, + Classification: ErrGitRefName, + Message: "GitRefName", + }, + }, + }, + { + description: "Reference name has unallowed special character (whitespace)", + data: TestForm{ + BranchName: "debian jessie", + }, + expectedErrors: binding.Errors{ + binding.Error{ + FieldNames: []string{"BranchName"}, + Classification: ErrGitRefName, + Message: "GitRefName", + }, + }, + }, + { + description: "Reference name has unallowed special character [", + data: TestForm{ + BranchName: "debian[jessie", + }, + expectedErrors: binding.Errors{ + binding.Error{ + FieldNames: []string{"BranchName"}, + Classification: ErrGitRefName, + Message: "GitRefName", + }, + }, + }, + } for _, testCase := range gitRefNameValidationTestCases { t.Run(testCase.description, func(t *testing.T) { diff --git a/modules/validation/regex_pattern_test.go b/modules/validation/regex_pattern_test.go index efcb2767340..80790a23b16 100644 --- a/modules/validation/regex_pattern_test.go +++ b/modules/validation/regex_pattern_test.go @@ -17,40 +17,40 @@ func getRegexPatternErrorString(pattern string) string { return "" } -var regexValidationTestCases = []validationTestCase{ - { - description: "Empty regex pattern", - data: TestForm{ - RegexPattern: "", - }, - expectedErrors: binding.Errors{}, - }, - { - description: "Valid regex", - data: TestForm{ - RegexPattern: `(\d{1,3})+`, - }, - expectedErrors: binding.Errors{}, - }, - - { - description: "Invalid regex", - data: TestForm{ - RegexPattern: "[a-", - }, - expectedErrors: binding.Errors{ - binding.Error{ - FieldNames: []string{"RegexPattern"}, - Classification: ErrRegexPattern, - Message: getRegexPatternErrorString("[a-"), - }, - }, - }, -} - func Test_RegexPatternValidation(t *testing.T) { AddBindingRules() + regexValidationTestCases := []validationTestCase{ + { + description: "Empty regex pattern", + data: TestForm{ + RegexPattern: "", + }, + expectedErrors: binding.Errors{}, + }, + { + description: "Valid regex", + data: TestForm{ + RegexPattern: `(\d{1,3})+`, + }, + expectedErrors: binding.Errors{}, + }, + + { + description: "Invalid regex", + data: TestForm{ + RegexPattern: "[a-", + }, + expectedErrors: binding.Errors{ + binding.Error{ + FieldNames: []string{"RegexPattern"}, + Classification: ErrRegexPattern, + Message: getRegexPatternErrorString("[a-"), + }, + }, + }, + } + for _, testCase := range regexValidationTestCases { t.Run(testCase.description, func(t *testing.T) { performValidationTest(t, testCase) diff --git a/modules/validation/validurl_test.go b/modules/validation/validurl_test.go index 39f7fa5d65a..ce4898b2716 100644 --- a/modules/validation/validurl_test.go +++ b/modules/validation/validurl_test.go @@ -9,99 +9,99 @@ import ( "gitea.com/go-chi/binding" ) -var urlValidationTestCases = []validationTestCase{ - { - description: "Empty URL", - data: TestForm{ - URL: "", - }, - expectedErrors: binding.Errors{}, - }, - { - description: "URL without port", - data: TestForm{ - URL: "http://test.lan/", - }, - expectedErrors: binding.Errors{}, - }, - { - description: "URL with port", - data: TestForm{ - URL: "http://test.lan:3000/", - }, - expectedErrors: binding.Errors{}, - }, - { - description: "URL with IPv6 address without port", - data: TestForm{ - URL: "http://[::1]/", - }, - expectedErrors: binding.Errors{}, - }, - { - description: "URL with IPv6 address with port", - data: TestForm{ - URL: "http://[::1]:3000/", - }, - expectedErrors: binding.Errors{}, - }, - { - description: "Invalid URL", - data: TestForm{ - URL: "http//test.lan/", - }, - expectedErrors: binding.Errors{ - binding.Error{ - FieldNames: []string{"URL"}, - Classification: binding.ERR_URL, - Message: "Url", - }, - }, - }, - { - description: "Invalid schema", - data: TestForm{ - URL: "ftp://test.lan/", - }, - expectedErrors: binding.Errors{ - binding.Error{ - FieldNames: []string{"URL"}, - Classification: binding.ERR_URL, - Message: "Url", - }, - }, - }, - { - description: "Invalid port", - data: TestForm{ - URL: "http://test.lan:3x4/", - }, - expectedErrors: binding.Errors{ - binding.Error{ - FieldNames: []string{"URL"}, - Classification: binding.ERR_URL, - Message: "Url", - }, - }, - }, - { - description: "Invalid port with IPv6 address", - data: TestForm{ - URL: "http://[::1]:3x4/", - }, - expectedErrors: binding.Errors{ - binding.Error{ - FieldNames: []string{"URL"}, - Classification: binding.ERR_URL, - Message: "Url", - }, - }, - }, -} - func Test_ValidURLValidation(t *testing.T) { AddBindingRules() + urlValidationTestCases := []validationTestCase{ + { + description: "Empty URL", + data: TestForm{ + URL: "", + }, + expectedErrors: binding.Errors{}, + }, + { + description: "URL without port", + data: TestForm{ + URL: "http://test.lan/", + }, + expectedErrors: binding.Errors{}, + }, + { + description: "URL with port", + data: TestForm{ + URL: "http://test.lan:3000/", + }, + expectedErrors: binding.Errors{}, + }, + { + description: "URL with IPv6 address without port", + data: TestForm{ + URL: "http://[::1]/", + }, + expectedErrors: binding.Errors{}, + }, + { + description: "URL with IPv6 address with port", + data: TestForm{ + URL: "http://[::1]:3000/", + }, + expectedErrors: binding.Errors{}, + }, + { + description: "Invalid URL", + data: TestForm{ + URL: "http//test.lan/", + }, + expectedErrors: binding.Errors{ + binding.Error{ + FieldNames: []string{"URL"}, + Classification: binding.ERR_URL, + Message: "Url", + }, + }, + }, + { + description: "Invalid schema", + data: TestForm{ + URL: "ftp://test.lan/", + }, + expectedErrors: binding.Errors{ + binding.Error{ + FieldNames: []string{"URL"}, + Classification: binding.ERR_URL, + Message: "Url", + }, + }, + }, + { + description: "Invalid port", + data: TestForm{ + URL: "http://test.lan:3x4/", + }, + expectedErrors: binding.Errors{ + binding.Error{ + FieldNames: []string{"URL"}, + Classification: binding.ERR_URL, + Message: "Url", + }, + }, + }, + { + description: "Invalid port with IPv6 address", + data: TestForm{ + URL: "http://[::1]:3x4/", + }, + expectedErrors: binding.Errors{ + binding.Error{ + FieldNames: []string{"URL"}, + Classification: binding.ERR_URL, + Message: "Url", + }, + }, + }, + } + for _, testCase := range urlValidationTestCases { t.Run(testCase.description, func(t *testing.T) { performValidationTest(t, testCase) diff --git a/modules/validation/validurllist_test.go b/modules/validation/validurllist_test.go index c6f862a962b..cccc570a1a8 100644 --- a/modules/validation/validurllist_test.go +++ b/modules/validation/validurllist_test.go @@ -9,146 +9,146 @@ import ( "gitea.com/go-chi/binding" ) -// This is a copy of all the URL tests cases, plus additional ones to -// account for multiple URLs -var urlListValidationTestCases = []validationTestCase{ - { - description: "Empty URL", - data: TestForm{ - URLs: "", - }, - expectedErrors: binding.Errors{}, - }, - { - description: "URL without port", - data: TestForm{ - URLs: "http://test.lan/", - }, - expectedErrors: binding.Errors{}, - }, - { - description: "URL with port", - data: TestForm{ - URLs: "http://test.lan:3000/", - }, - expectedErrors: binding.Errors{}, - }, - { - description: "URL with IPv6 address without port", - data: TestForm{ - URLs: "http://[::1]/", - }, - expectedErrors: binding.Errors{}, - }, - { - description: "URL with IPv6 address with port", - data: TestForm{ - URLs: "http://[::1]:3000/", - }, - expectedErrors: binding.Errors{}, - }, - { - description: "Invalid URL", - data: TestForm{ - URLs: "http//test.lan/", - }, - expectedErrors: binding.Errors{ - binding.Error{ - FieldNames: []string{"URLs"}, - Classification: binding.ERR_URL, - Message: "http//test.lan/", - }, - }, - }, - { - description: "Invalid schema", - data: TestForm{ - URLs: "ftp://test.lan/", - }, - expectedErrors: binding.Errors{ - binding.Error{ - FieldNames: []string{"URLs"}, - Classification: binding.ERR_URL, - Message: "ftp://test.lan/", - }, - }, - }, - { - description: "Invalid port", - data: TestForm{ - URLs: "http://test.lan:3x4/", - }, - expectedErrors: binding.Errors{ - binding.Error{ - FieldNames: []string{"URLs"}, - Classification: binding.ERR_URL, - Message: "http://test.lan:3x4/", - }, - }, - }, - { - description: "Invalid port with IPv6 address", - data: TestForm{ - URLs: "http://[::1]:3x4/", - }, - expectedErrors: binding.Errors{ - binding.Error{ - FieldNames: []string{"URLs"}, - Classification: binding.ERR_URL, - Message: "http://[::1]:3x4/", - }, - }, - }, - { - description: "Multi URLs", - data: TestForm{ - URLs: "http://test.lan:3000/\nhttp://test.local/", - }, - expectedErrors: binding.Errors{}, - }, - { - description: "Multi URLs with newline", - data: TestForm{ - URLs: "http://test.lan:3000/\nhttp://test.local/\n", - }, - expectedErrors: binding.Errors{}, - }, - { - description: "List with invalid entry", - data: TestForm{ - URLs: "http://test.lan:3000/\nhttp://[::1]:3x4/", - }, - expectedErrors: binding.Errors{ - binding.Error{ - FieldNames: []string{"URLs"}, - Classification: binding.ERR_URL, - Message: "http://[::1]:3x4/", - }, - }, - }, - { - description: "List with two invalid entries", - data: TestForm{ - URLs: "ftp://test.lan:3000/\nhttp://[::1]:3x4/\n", - }, - expectedErrors: binding.Errors{ - binding.Error{ - FieldNames: []string{"URLs"}, - Classification: binding.ERR_URL, - Message: "ftp://test.lan:3000/", - }, - binding.Error{ - FieldNames: []string{"URLs"}, - Classification: binding.ERR_URL, - Message: "http://[::1]:3x4/", - }, - }, - }, -} - func Test_ValidURLListValidation(t *testing.T) { AddBindingRules() + // This is a copy of all the URL tests cases, plus additional ones to + // account for multiple URLs + urlListValidationTestCases := []validationTestCase{ + { + description: "Empty URL", + data: TestForm{ + URLs: "", + }, + expectedErrors: binding.Errors{}, + }, + { + description: "URL without port", + data: TestForm{ + URLs: "http://test.lan/", + }, + expectedErrors: binding.Errors{}, + }, + { + description: "URL with port", + data: TestForm{ + URLs: "http://test.lan:3000/", + }, + expectedErrors: binding.Errors{}, + }, + { + description: "URL with IPv6 address without port", + data: TestForm{ + URLs: "http://[::1]/", + }, + expectedErrors: binding.Errors{}, + }, + { + description: "URL with IPv6 address with port", + data: TestForm{ + URLs: "http://[::1]:3000/", + }, + expectedErrors: binding.Errors{}, + }, + { + description: "Invalid URL", + data: TestForm{ + URLs: "http//test.lan/", + }, + expectedErrors: binding.Errors{ + binding.Error{ + FieldNames: []string{"URLs"}, + Classification: binding.ERR_URL, + Message: "http//test.lan/", + }, + }, + }, + { + description: "Invalid schema", + data: TestForm{ + URLs: "ftp://test.lan/", + }, + expectedErrors: binding.Errors{ + binding.Error{ + FieldNames: []string{"URLs"}, + Classification: binding.ERR_URL, + Message: "ftp://test.lan/", + }, + }, + }, + { + description: "Invalid port", + data: TestForm{ + URLs: "http://test.lan:3x4/", + }, + expectedErrors: binding.Errors{ + binding.Error{ + FieldNames: []string{"URLs"}, + Classification: binding.ERR_URL, + Message: "http://test.lan:3x4/", + }, + }, + }, + { + description: "Invalid port with IPv6 address", + data: TestForm{ + URLs: "http://[::1]:3x4/", + }, + expectedErrors: binding.Errors{ + binding.Error{ + FieldNames: []string{"URLs"}, + Classification: binding.ERR_URL, + Message: "http://[::1]:3x4/", + }, + }, + }, + { + description: "Multi URLs", + data: TestForm{ + URLs: "http://test.lan:3000/\nhttp://test.local/", + }, + expectedErrors: binding.Errors{}, + }, + { + description: "Multi URLs with newline", + data: TestForm{ + URLs: "http://test.lan:3000/\nhttp://test.local/\n", + }, + expectedErrors: binding.Errors{}, + }, + { + description: "List with invalid entry", + data: TestForm{ + URLs: "http://test.lan:3000/\nhttp://[::1]:3x4/", + }, + expectedErrors: binding.Errors{ + binding.Error{ + FieldNames: []string{"URLs"}, + Classification: binding.ERR_URL, + Message: "http://[::1]:3x4/", + }, + }, + }, + { + description: "List with two invalid entries", + data: TestForm{ + URLs: "ftp://test.lan:3000/\nhttp://[::1]:3x4/\n", + }, + expectedErrors: binding.Errors{ + binding.Error{ + FieldNames: []string{"URLs"}, + Classification: binding.ERR_URL, + Message: "ftp://test.lan:3000/", + }, + binding.Error{ + FieldNames: []string{"URLs"}, + Classification: binding.ERR_URL, + Message: "http://[::1]:3x4/", + }, + }, + }, + } + for _, testCase := range urlListValidationTestCases { t.Run(testCase.description, func(t *testing.T) { performValidationTest(t, testCase) diff --git a/modules/web/handler.go b/modules/web/handler.go index 9a3e4a7f176..42a649714d9 100644 --- a/modules/web/handler.go +++ b/modules/web/handler.go @@ -121,7 +121,7 @@ func wrapHandlerProvider[T http.Handler](hp func(next http.Handler) T, funcInfo return func(next http.Handler) http.Handler { h := hp(next) // this handle could be dynamically generated, so we can't use it for debug info return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { - routing.UpdateFuncInfo(req.Context(), funcInfo) + defer routing.RecordFuncInfo(req.Context(), funcInfo)() h.ServeHTTP(resp, req) }) } @@ -157,7 +157,7 @@ func toHandlerProvider(handler any) func(next http.Handler) http.Handler { return // it's doing pre-check, just return } - routing.UpdateFuncInfo(req.Context(), funcInfo) + defer routing.RecordFuncInfo(req.Context(), funcInfo)() ret := fn.Call(argsIn) // handle the return value (no-op at the moment) diff --git a/modules/web/middleware/binding.go b/modules/web/middleware/binding.go index 43e1bbc70e3..03e188f5098 100644 --- a/modules/web/middleware/binding.go +++ b/modules/web/middleware/binding.go @@ -78,7 +78,7 @@ func GetInclude(field reflect.StructField) string { return getRuleBody(field, "Include(") } -// Validate validate TODO: +// Validate validate func Validate(errs binding.Errors, data map[string]any, f Form, l translation.Locale) binding.Errors { if errs.Len() == 0 { return errs diff --git a/modules/web/middleware/flash.go b/modules/web/middleware/flash.go index 0caaa8c0364..0e848c79029 100644 --- a/modules/web/middleware/flash.go +++ b/modules/web/middleware/flash.go @@ -6,6 +6,7 @@ package middleware import ( "fmt" "html/template" + "net/http" "net/url" "code.gitea.io/gitea/modules/reqctx" @@ -65,3 +66,27 @@ func (f *Flash) Success(msg any, current ...bool) { f.SuccessMsg = flashMsgStringOrHTML(msg) f.set("success", f.SuccessMsg, current...) } + +func ParseCookieFlashMessage(val string) *Flash { + if vals, _ := url.ParseQuery(val); len(vals) > 0 { + return &Flash{ + Values: vals, + ErrorMsg: vals.Get("error"), + SuccessMsg: vals.Get("success"), + InfoMsg: vals.Get("info"), + WarningMsg: vals.Get("warning"), + } + } + return nil +} + +func GetSiteCookieFlashMessage(dataStore reqctx.RequestDataStore, req *http.Request, cookieName string) (string, *Flash) { + // Get the last flash message from cookie + lastFlashCookie := GetSiteCookie(req, cookieName) + lastFlashMsg := ParseCookieFlashMessage(lastFlashCookie) + if lastFlashMsg != nil { + lastFlashMsg.DataStore = dataStore + return lastFlashCookie, lastFlashMsg + } + return lastFlashCookie, nil +} diff --git a/modules/web/middleware/request.go b/modules/web/middleware/request.go deleted file mode 100644 index 0bb155df703..00000000000 --- a/modules/web/middleware/request.go +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright 2020 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package middleware - -import ( - "net/http" - "strings" -) - -// IsAPIPath returns true if the specified URL is an API path -func IsAPIPath(req *http.Request) bool { - return strings.HasPrefix(req.URL.Path, "/api/") -} diff --git a/modules/web/router_path.go b/modules/web/router_path.go index 39082c07245..b59948581a9 100644 --- a/modules/web/router_path.go +++ b/modules/web/router_path.go @@ -89,11 +89,23 @@ func (p *routerPathMatcher) matchPath(chiCtx *chi.Context, path string) bool { return true } +func isValidMethod(name string) bool { + switch name { + case http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete, http.MethodHead, http.MethodOptions, http.MethodConnect, http.MethodTrace: + return true + } + return false +} + func newRouterPathMatcher(methods, pattern string, h ...any) *routerPathMatcher { middlewares, handlerFunc := wrapMiddlewareAndHandler(nil, h) p := &routerPathMatcher{methods: make(container.Set[string]), middlewares: middlewares, handlerFunc: handlerFunc} for _, method := range strings.Split(methods, ",") { - p.methods.Add(strings.TrimSpace(method)) + method = strings.TrimSpace(method) + if !isValidMethod(method) { + panic(fmt.Sprintf("invalid HTTP method: %s", method)) + } + p.methods.Add(method) } re := []byte{'^'} lastEnd := 0 diff --git a/modules/web/router_test.go b/modules/web/router_test.go index bdcf623b951..582980a27ae 100644 --- a/modules/web/router_test.go +++ b/modules/web/router_test.go @@ -183,6 +183,11 @@ func TestRouter(t *testing.T) { pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1/d2/fn", "dir": "d1/d2", "file": "fn"}, handlerMark: "match-path", }) + testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/d1%2fd2/fn", resultStruct{ + method: "GET", + pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1%2fd2/fn", "dir": "d1%2fd2", "file": "fn"}, + handlerMark: "match-path", + }) testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/d1/d2/000", resultStruct{ method: "GET", pathParams: map[string]string{"reponame": "the-repo", "username": "the-user", "*": "d1/d2/000"}, diff --git a/modules/web/routing/context.go b/modules/web/routing/context.go index c5e85a415b1..d3eb98f83db 100644 --- a/modules/web/routing/context.go +++ b/modules/web/routing/context.go @@ -6,22 +6,29 @@ package routing import ( "context" "net/http" + + "code.gitea.io/gitea/modules/gtprof" + "code.gitea.io/gitea/modules/reqctx" ) type contextKeyType struct{} var contextKey contextKeyType -// UpdateFuncInfo updates a context's func info -func UpdateFuncInfo(ctx context.Context, funcInfo *FuncInfo) { - record, ok := ctx.Value(contextKey).(*requestRecord) - if !ok { - return +// RecordFuncInfo records a func info into context +func RecordFuncInfo(ctx context.Context, funcInfo *FuncInfo) (end func()) { + end = func() {} + if reqCtx := reqctx.FromContext(ctx); reqCtx != nil { + var traceSpan *gtprof.TraceSpan + traceSpan, end = gtprof.GetTracer().StartInContext(reqCtx, "http.func") + traceSpan.SetAttributeString("func", funcInfo.shortName) } - - record.lock.Lock() - record.funcInfo = funcInfo - record.lock.Unlock() + if record, ok := ctx.Value(contextKey).(*requestRecord); ok { + record.lock.Lock() + record.funcInfo = funcInfo + record.lock.Unlock() + } + return end } // MarkLongPolling marks the request is a long-polling request, and the logger may output different message for it diff --git a/modules/web/routing/funcinfo_test.go b/modules/web/routing/funcinfo_test.go index 2ab5960373c..974af58931c 100644 --- a/modules/web/routing/funcinfo_test.go +++ b/modules/web/routing/funcinfo_test.go @@ -6,6 +6,8 @@ package routing import ( "fmt" "testing" + + "github.com/stretchr/testify/assert" ) func Test_shortenFilename(t *testing.T) { @@ -37,9 +39,8 @@ func Test_shortenFilename(t *testing.T) { } for _, tt := range tests { t.Run(fmt.Sprintf("shortenFilename('%s')", tt.filename), func(t *testing.T) { - if gotShort := shortenFilename(tt.filename, tt.fallback); gotShort != tt.expected { - t.Errorf("shortenFilename('%s'), expect '%s', but get '%s'", tt.filename, tt.expected, gotShort) - } + gotShort := shortenFilename(tt.filename, tt.fallback) + assert.Equal(t, tt.expected, gotShort) }) } } @@ -72,9 +73,8 @@ func Test_trimAnonymousFunctionSuffix(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := trimAnonymousFunctionSuffix(tt.name); got != tt.want { - t.Errorf("trimAnonymousFunctionSuffix() = %v, want %v", got, tt.want) - } + got := trimAnonymousFunctionSuffix(tt.name) + assert.Equal(t, tt.want, got) }) } } diff --git a/modules/webhook/events.go b/modules/webhook/events.go new file mode 100644 index 00000000000..f4dfff0294a --- /dev/null +++ b/modules/webhook/events.go @@ -0,0 +1,20 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package webhook + +type HookEvents map[HookEventType]bool + +func (he HookEvents) Get(evt HookEventType) bool { + return he[evt] +} + +// HookEvent represents events that will delivery hook. +type HookEvent struct { + PushOnly bool `json:"push_only"` + SendEverything bool `json:"send_everything"` + ChooseEvents bool `json:"choose_events"` + BranchFilter string `json:"branch_filter"` + + HookEvents `json:"events"` +} diff --git a/modules/webhook/structs.go b/modules/webhook/structs.go deleted file mode 100644 index 927a91a74c5..00000000000 --- a/modules/webhook/structs.go +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright 2022 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package webhook - -// HookEvents is a set of web hook events -type HookEvents struct { - Create bool `json:"create"` - Delete bool `json:"delete"` - Fork bool `json:"fork"` - Issues bool `json:"issues"` - IssueAssign bool `json:"issue_assign"` - IssueLabel bool `json:"issue_label"` - IssueMilestone bool `json:"issue_milestone"` - IssueComment bool `json:"issue_comment"` - Push bool `json:"push"` - PullRequest bool `json:"pull_request"` - PullRequestAssign bool `json:"pull_request_assign"` - PullRequestLabel bool `json:"pull_request_label"` - PullRequestMilestone bool `json:"pull_request_milestone"` - PullRequestComment bool `json:"pull_request_comment"` - PullRequestReview bool `json:"pull_request_review"` - PullRequestSync bool `json:"pull_request_sync"` - PullRequestReviewRequest bool `json:"pull_request_review_request"` - Wiki bool `json:"wiki"` - Repository bool `json:"repository"` - Release bool `json:"release"` - Package bool `json:"package"` -} - -// HookEvent represents events that will delivery hook. -type HookEvent struct { - PushOnly bool `json:"push_only"` - SendEverything bool `json:"send_everything"` - ChooseEvents bool `json:"choose_events"` - BranchFilter string `json:"branch_filter"` - - HookEvents `json:"events"` -} diff --git a/modules/webhook/type.go b/modules/webhook/type.go index fbec8892722..b244bb0cff4 100644 --- a/modules/webhook/type.go +++ b/modules/webhook/type.go @@ -31,21 +31,47 @@ const ( HookEventRepository HookEventType = "repository" HookEventRelease HookEventType = "release" HookEventPackage HookEventType = "package" - HookEventSchedule HookEventType = "schedule" HookEventStatus HookEventType = "status" + // once a new event added here, please also added to AllEvents() function + + // FIXME: This event should be a group of pull_request_review_xxx events + HookEventPullRequestReview HookEventType = "pull_request_review" + // Actions event only + HookEventSchedule HookEventType = "schedule" ) +func AllEvents() []HookEventType { + return []HookEventType{ + HookEventCreate, + HookEventDelete, + HookEventFork, + HookEventPush, + HookEventIssues, + HookEventIssueAssign, + HookEventIssueLabel, + HookEventIssueMilestone, + HookEventIssueComment, + HookEventPullRequest, + HookEventPullRequestAssign, + HookEventPullRequestLabel, + HookEventPullRequestMilestone, + HookEventPullRequestComment, + HookEventPullRequestReviewApproved, + HookEventPullRequestReviewRejected, + HookEventPullRequestReviewComment, + HookEventPullRequestSync, + HookEventPullRequestReviewRequest, + HookEventWiki, + HookEventRepository, + HookEventRelease, + HookEventPackage, + HookEventStatus, + } +} + // Event returns the HookEventType as an event string func (h HookEventType) Event() string { switch h { - case HookEventCreate: - return "create" - case HookEventDelete: - return "delete" - case HookEventFork: - return "fork" - case HookEventPush: - return "push" case HookEventIssues, HookEventIssueAssign, HookEventIssueLabel, HookEventIssueMilestone: return "issues" case HookEventPullRequest, HookEventPullRequestAssign, HookEventPullRequestLabel, HookEventPullRequestMilestone, @@ -59,14 +85,13 @@ func (h HookEventType) Event() string { return "pull_request_rejected" case HookEventPullRequestReviewComment: return "pull_request_comment" - case HookEventWiki: - return "wiki" - case HookEventRepository: - return "repository" - case HookEventRelease: - return "release" + default: + return string(h) } - return "" +} + +func (h HookEventType) IsPullRequest() bool { + return h.Event() == "pull_request" } // HookType is the type of a webhook diff --git a/options/gitignore/Flutter b/options/gitignore/Flutter new file mode 100644 index 00000000000..39b8814aecf --- /dev/null +++ b/options/gitignore/Flutter @@ -0,0 +1,119 @@ +# Miscellaneous +*.class +*.lock +*.log +*.pyc +*.swp +.buildlog/ +.history + + + +# Flutter repo-specific +/bin/cache/ +/bin/internal/bootstrap.bat +/bin/internal/bootstrap.sh +/bin/mingit/ +/dev/benchmarks/mega_gallery/ +/dev/bots/.recipe_deps +/dev/bots/android_tools/ +/dev/devicelab/ABresults*.json +/dev/docs/doc/ +/dev/docs/flutter.docs.zip +/dev/docs/lib/ +/dev/docs/pubspec.yaml +/dev/integration_tests/**/xcuserdata +/dev/integration_tests/**/Pods +/packages/flutter/coverage/ +version +analysis_benchmark.json + +# packages file containing multi-root paths +.packages.generated + +# Flutter/Dart/Pub related +**/doc/api/ +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +**/generated_plugin_registrant.dart +.packages +.pub-preload-cache/ +.pub/ +build/ +flutter_*.png +linked_*.ds +unlinked.ds +unlinked_spec.ds + +# Android related +**/android/**/gradle-wrapper.jar +.gradle/ +**/android/captures/ +**/android/gradlew +**/android/gradlew.bat +**/android/local.properties +**/android/**/GeneratedPluginRegistrant.java +**/android/key.properties +*.jks + +# iOS/XCode related +**/ios/**/*.mode1v3 +**/ios/**/*.mode2v3 +**/ios/**/*.moved-aside +**/ios/**/*.pbxuser +**/ios/**/*.perspectivev3 +**/ios/**/*sync/ +**/ios/**/.sconsign.dblite +**/ios/**/.tags* +**/ios/**/.vagrant/ +**/ios/**/DerivedData/ +**/ios/**/Icon? +**/ios/**/Pods/ +**/ios/**/.symlinks/ +**/ios/**/profile +**/ios/**/xcuserdata +**/ios/.generated/ +**/ios/Flutter/.last_build_id +**/ios/Flutter/App.framework +**/ios/Flutter/Flutter.framework +**/ios/Flutter/Flutter.podspec +**/ios/Flutter/Generated.xcconfig +**/ios/Flutter/ephemeral +**/ios/Flutter/app.flx +**/ios/Flutter/app.zip +**/ios/Flutter/flutter_assets/ +**/ios/Flutter/flutter_export_environment.sh +**/ios/ServiceDefinitions.json +**/ios/Runner/GeneratedPluginRegistrant.* + +# macOS +**/Flutter/ephemeral/ +**/Pods/ +**/macos/Flutter/GeneratedPluginRegistrant.swift +**/macos/Flutter/ephemeral +**/xcuserdata/ + +# Windows +**/windows/flutter/generated_plugin_registrant.cc +**/windows/flutter/generated_plugin_registrant.h +**/windows/flutter/generated_plugins.cmake + +# Linux +**/linux/flutter/generated_plugin_registrant.cc +**/linux/flutter/generated_plugin_registrant.h +**/linux/flutter/generated_plugins.cmake + +# Coverage +coverage/ + +# Symbols +app.*.symbols + +# Exceptions to above rules. +!**/ios/**/default.mode1v3 +!**/ios/**/default.mode2v3 +!**/ios/**/default.pbxuser +!**/ios/**/default.perspectivev3 +!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages +!/dev/ci/**/Gemfile.lock \ No newline at end of file diff --git a/options/gitignore/Nix b/options/gitignore/Nix index 1fd04ef1f65..912e6700f4b 100644 --- a/options/gitignore/Nix +++ b/options/gitignore/Nix @@ -1,3 +1,6 @@ # Ignore build outputs from performing a nix-build or `nix build` command result result-* + +# Ignore automatically generated direnv output +.direnv diff --git a/options/gitignore/Node b/options/gitignore/Node index c6bba591381..1170717c147 100644 --- a/options/gitignore/Node +++ b/options/gitignore/Node @@ -104,6 +104,12 @@ dist .temp .cache +# vitepress build output +**/.vitepress/dist + +# vitepress cache directory +**/.vitepress/cache + # Docusaurus cache and generated files .docusaurus diff --git a/options/gitignore/NotesAndCoreConfiguration b/options/gitignore/NotesAndCoreConfiguration new file mode 100644 index 00000000000..4eff01dae13 --- /dev/null +++ b/options/gitignore/NotesAndCoreConfiguration @@ -0,0 +1,16 @@ +# Excludes Obsidian workspace cache and plugins. All notes and core obsidian +# configuration files are tracked by Git. + +# The current application UI state (DOM layout, recently-opened files, etc.) is +# stored in these files (separate for desktop and mobile) so you can resume +# your session seamlessly after a restart. If you want to track UI state, use +# the Workspaces core plugin instead of relying on these files. +.obsidian/workspace.json +.obsidian/workspace-mobile.json + +# Obsidian plugins are stored under .obsidian/plugins/$plugin_name. They +# contain metadata (manifest.json), application code (main.js), stylesheets +# (styles.css), and user-configuration data (data.json). +# We want to exclude all plugin-related files, so we can exclude everything +# under this directory. +.obsidian/plugins/**/* diff --git a/options/gitignore/NotesAndExtendedConfiguration b/options/gitignore/NotesAndExtendedConfiguration new file mode 100644 index 00000000000..3e0804f2998 --- /dev/null +++ b/options/gitignore/NotesAndExtendedConfiguration @@ -0,0 +1,38 @@ +# Excludes Obsidian workspace cache and plugin code, but retains plugin +# configuration. All notes and user-controlled configuration files are tracked +# by Git. +# +# !!! WARNING !!! +# +# Community plugins may store sensitive secrets in their data.json files. By +# including these files, those secrets may be tracked in your Git repository. +# +# To ignore configurations for specific plugins, add a line like this after the +# contents of this file (order is important): +# .obsidian/plugins/{{plugin_name}}/data.json +# +# Alternatively, ensure that you are treating your entire Git repository as +# sensitive data, since it may contain secrets, or may have contained them in +# past commits. Understand your threat profile, and make the decision +# appropriate for yourself. If in doubt, err on the side of not including +# plugin configuration. Use one of the alternative gitignore files instead: +# * NotesOnly.gitignore +# * NotesAndCoreConfiguration.gitignore + +# The current application UI state (DOM layout, recently-opened files, etc.) is +# stored in these files (separate for desktop and mobile) so you can resume +# your session seamlessly after a restart. If you want to track UI state, use +# the Workspaces core plugin instead of relying on these files. +.obsidian/workspace.json +.obsidian/workspace-mobile.json + +# Obsidian plugins are stored under .obsidian/plugins/$plugin_name. They +# contain metadata (manifest.json), application code (main.js), stylesheets +# (styles.css), and user-configuration data (data.json). +# We only want to track data.json, so we: +# 1. exclude everything under the plugins directory recursively, +# 2. unignore the plugin directories themselves, which then allows us to +# 3. unignore the data.json files +.obsidian/plugins/**/* +!.obsidian/plugins/*/ +!.obsidian/plugins/*/data.json diff --git a/options/gitignore/NotesOnly b/options/gitignore/NotesOnly new file mode 100644 index 00000000000..2b3b76ee0ec --- /dev/null +++ b/options/gitignore/NotesOnly @@ -0,0 +1,4 @@ +# Excludes all Obsidian-related configuration. All notes are tracked by Git. + +# All Obsidian configuration and runtime state is stored here +.obsidian/**/* diff --git a/options/gitignore/Python b/options/gitignore/Python index 15201acc113..0a197900e25 100644 --- a/options/gitignore/Python +++ b/options/gitignore/Python @@ -167,5 +167,8 @@ cython_debug/ # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ +# Ruff stuff: +.ruff_cache/ + # PyPI configuration file .pypirc diff --git a/options/gitignore/Rust b/options/gitignore/Rust index d01bd1a990b..0104787a738 100644 --- a/options/gitignore/Rust +++ b/options/gitignore/Rust @@ -3,10 +3,6 @@ debug/ target/ -# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries -# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html -Cargo.lock - # These are backup files generated by rustfmt **/*.rs.bk diff --git a/options/locale/locale_cs-CZ.ini b/options/locale/locale_cs-CZ.ini index 57b209aaf97..8d43077f508 100644 --- a/options/locale/locale_cs-CZ.ini +++ b/options/locale/locale_cs-CZ.ini @@ -54,6 +54,7 @@ webauthn_reload=Znovu načíst repository=Repozitář organization=Organizace mirror=Zrcadlo +issue_milestone=Milník new_repo=Nový repozitář new_migrate=Nová migrace new_mirror=Nové zrcadlo @@ -145,6 +146,7 @@ confirm_delete_selected=Potvrdit odstranění všech vybraných položek? name=Název value=Hodnota +readme=Readme filter=Filtr filter.clear=Vymazat filtr @@ -243,6 +245,7 @@ license_desc=Vše je na dokumentaci, než budete měnit jakákoliv nastavení. require_db_desc=Gitea requires MySQL, PostgreSQL, MSSQL, SQLite3 or TiDB (MySQL protocol). @@ -459,6 +462,7 @@ authorize_application=Autorizovat aplikaci authorize_redirect_notice=Budete přesměrováni na %s, pokud autorizujete tuto aplikaci. authorize_application_created_by=Tuto aplikaci vytvořil %s. authorize_application_description=Pokud povolíte přístup, bude moci přistupovat a zapisovat do všech vašich informací o účtu včetně soukromých repozitářů a organizací. +authorize_application_with_scopes=S rozsahy působnosti: %s authorize_title=Autorizovat „%s“ pro přístup k vašemu účtu? authorization_failed=Autorizace selhala authorization_failed_desc=Autorizace selhala, protože jsme detekovali neplatný požadavek. Kontaktujte prosím správce aplikace, kterou jste se pokoušeli autorizovat. @@ -765,6 +769,7 @@ uploaded_avatar_not_a_image=Nahraný soubor není obrázek. uploaded_avatar_is_too_big=Nahraný soubor (%d KiB) přesahuje maximální velikost (%d KiB). update_avatar_success=Vaše avatar byl aktualizován. update_user_avatar_success=Uživatelův avatar byl aktualizován. +cropper_prompt=Před uložením můžete obrázek upravit. Upravený obrázek bude uložen jako PNG. change_password=Aktualizovat heslo old_password=Stávající heslo @@ -1012,7 +1017,9 @@ new_repo_helper=Repozitář obsahuje všechny projektové soubory, včetně hist owner=Vlastník owner_helper=Některé organizace se nemusejí v seznamu zobrazit kvůli maximálnímu dosaženému počtu repozitářů. repo_name=Název repozitáře -repo_name_helper=Dobrý název repozitáře většinou používá krátká, zapamatovatelná a unikátní klíčová slova. +repo_name_profile_public_hint=.profile je speciální repozitář, který můžete použít k přidání souboru README.md do svého veřejného profilu organizace, který je viditelný pro každého. Ujistěte se, že je veřejný, a pro začátek jej inicializujte pomocí README v adresáři profilu. +repo_name_profile_private_hint=.profile-private je speciální repozitář, který můžete použít k přidání souboru README.md do profilu člena organizace, který je viditelný pouze pro členy organizace. Ujistěte se, že je soukromý, a pro začátek jej inicializujte pomocí README v adresáři profilu. +repo_name_helper=Dobrá jména repozitářů používají krátká, zapamatovatelná a unikátní klíčová slova. Repozitář s názvem „.profile“ nebo „.profile-private“ lze použít k přidání README.md pro uživatelský/organizační profil. repo_size=Velikost repozitáře template=Šablona template_select=Vyberte šablonu. @@ -1031,6 +1038,8 @@ fork_to_different_account=Rozštěpit na jiný účet fork_visibility_helper=Viditelnost rozštěpeného repozitáře nemůže být změněna. fork_branch=Větev, která má být klonována pro fork all_branches=Všechny větve +view_all_branches=Zobrazit všechny větve +view_all_tags=Zobrazit všechny značky fork_no_valid_owners=Tento repozitář nemůže být rozštěpen, protože neexistují žádní platní vlastníci. fork.blocked_user=Nelze rozštěpit repozitář, protože jste blokováni majitelem repozitáře. use_template=Použít tuto šablonu @@ -1042,6 +1051,8 @@ generate_repo=Generovat repozitář generate_from=Generovat z repo_desc=Popis repo_desc_helper=Zadejte krátký popis (volitelné) +repo_no_desc=Nebyl uveden žádný popis +repo_lang=Jazyky repo_gitignore_helper=Vyberte šablony .gitignore. repo_gitignore_helper_desc=Vyberte soubory, které nechcete sledovat ze seznamu šablon pro běžné jazyky. Typické artefakty generované nástroji pro sestavení každého jazyka jsou ve výchozím stavu součástí .gitignore. issue_labels=Štítky úkolů @@ -1103,10 +1114,9 @@ delete_preexisting_success=Smazány nepřijaté soubory v %s blame_prior=Zobrazit blame před touto změnou blame.ignore_revs=Ignorování revizí v .git-blame-ignorerevs. Klikněte zde pro obejití a zobrazení normálního pohledu blame. blame.ignore_revs.failed=Nepodařilo se ignorovat revize v .git-blame-ignore-revs. +user_search_tooltip=Zobrazí maximálně 30 uživatelů -tree_path_not_found_commit=Cesta %[1]s v commitu %[2]s neexistuje -tree_path_not_found_branch=Cesta %[1]s ve větvi %[2]s neexistuje -tree_path_not_found_tag=Cesta %[1]s ve značce %[2]s neexistuje +tree_path_not_found=Cesta %[1]s neexistuje v %[2]s transfer.accept=Přijmout převod transfer.accept_desc=Převést do „%s“ @@ -1224,6 +1234,7 @@ create_new_repo_command=Vytvořit nový repozitář na příkazové řádce push_exist_repo=Nahrání existujícího repozitáře z příkazové řádky empty_message=Tento repozitář nemá žádný obsah. broken_message=Data gitu, která jsou základem tohoto repozitáře, nelze číst. Kontaktujte správce této instance nebo smažte tento repositář. +no_branch=Tento repozitář nemá žádné větve. code=Zdrojový kód code.desc=Přístup ke zdrojovým kódům, souborům, commitům a větvím. @@ -1243,6 +1254,7 @@ labels=Štítky org_labels_desc=Štítky na úrovni organizace, které mohou být použity se všemi repozitáři v rámci této organizace org_labels_desc_manage=spravovat +milestone=Milník milestones=Milníky commits=Commity commit=Commit @@ -1521,6 +1533,8 @@ issues.filter_assignee=Zpracovatel issues.filter_assginee_no_select=Všichni zpracovatelé issues.filter_assginee_no_assignee=Bez zpracovatele issues.filter_poster=Autor +issues.filter_user_placeholder=Hledat uživatele +issues.filter_user_no_select=Všichni uživatelé issues.filter_type=Typ issues.filter_type.all_issues=Všechny úkoly issues.filter_type.assigned_to_you=Přiřazené vám @@ -1637,7 +1651,7 @@ issues.attachment.open_tab=`Klikněte pro zobrazení „%s“ v nové záložce` issues.attachment.download=`Klikněte pro stažení „%s“` issues.subscribe=Odebírat issues.unsubscribe=Zrušit odběr -issues.unpin_issue=Odepnout úkol +issues.unpin=Odepnout issues.max_pinned=Nemůžete připnout další úkoly issues.pin_comment=připnuto %s issues.unpin_comment=odepnul/a tento %s @@ -1665,12 +1679,25 @@ issues.delete.title=Smazat tento úkol? issues.delete.text=Opravdu chcete tento úkol smazat? (Tím se trvale odstraní veškerý obsah. Pokud jej hodláte archivovat, zvažte raději jeho uzavření.) issues.tracker=Sledování času +issues.timetracker_timer_start=Spustit časovač +issues.timetracker_timer_stop=Zastavit časovač +issues.timetracker_timer_discard=Zahodit časovač +issues.timetracker_timer_manually_add=Přidat čas +issues.time_estimate_set=Nastavit odhadovaný čas +issues.time_estimate_display=Odhad: %s +issues.change_time_estimate_at=změnil/a odhad času na %[1]s %[2]s +issues.remove_time_estimate_at=odstranil/a odhad času %s +issues.time_estimate_invalid=Formát odhadu času je neplatný +issues.start_tracking_history=započal/a práci %s issues.tracker_auto_close=Časovač se automaticky zastaví po zavření tohoto úkolu issues.tracking_already_started=`Již jste spustili sledování času na jiném úkolu!` +issues.stop_tracking_history=pracoval/a %[1]s %[2]s issues.cancel_tracking_history=`zrušil/a sledování času %s` issues.del_time=Odstranit tento časový záznam +issues.add_time_history=přidal/a strávený čas %[1]s %[2]s issues.del_time_history=`odstranil/a strávený čas %s` +issues.add_time_manually=Přidat čas ručně issues.add_time_hours=Hodiny issues.add_time_minutes=Minuty issues.add_time_sum_to_small=Čas nebyl zadán. @@ -1923,6 +1950,11 @@ pulls.delete.title=Odstranit tento pull request? pulls.delete.text=Opravdu chcete tento pull request smazat? (Tím se trvale odstraní veškerý obsah. Pokud jej hodláte archivovat, zvažte raději jeho uzavření.) pulls.recently_pushed_new_branches=Nahráli jste větev %[1]s %[2]s +pulls.upstream_diverging_prompt_behind_1=Tato větev je %[1]d commit pozadu za %[2]s +pulls.upstream_diverging_prompt_behind_n=Tato větev je %[1]d commitů pozadu za %[2]s +pulls.upstream_diverging_prompt_base_newer=Hlavní větev %s má nové změny +pulls.upstream_diverging_merge=Synchornizovat rozštěpení +pulls.upstream_diverging_merge_confirm=Chcete sloučit „%[1]s“ do „%[2]s“? pull.deleted_branch=(odstraněno):%s pull.agit_documentation=Prohlédněte si dokumentaci o AGit @@ -2128,7 +2160,7 @@ settings.advanced_settings=Pokročilá nastavení settings.wiki_desc=Povolit Wiki repozitáře settings.use_internal_wiki=Používat vestavěnou Wiki settings.default_wiki_branch_name=Výchozí název větve Wiki -settings.default_wiki_everyone_access=Výchozí přístupová práva pro přihlášené uživatele: +settings.default_permission_everyone_access=Výchozí přístupová práva pro všechny přihlášené uživatele: settings.failed_to_change_default_wiki_branch=Změna výchozí větve wiki se nezdařila. settings.use_external_wiki=Používat externí Wiki settings.external_wiki_url=URL externí Wiki @@ -2596,6 +2628,9 @@ diff.image.overlay=Překrytí diff.has_escaped=Tento řádek má skryté znaky Unicode diff.show_file_tree=Zobrazit souborový strom diff.hide_file_tree=Skrýt souborový strom +diff.submodule_added=Submodul %[1]s přidán v %[2]s +diff.submodule_deleted=Submodul %[1]s odebrán z %[2]s +diff.submodule_updated=Submodul %[1]s aktualizován: %[2]s releases.desc=Sledování verzí projektu a souborů ke stažení. release.releases=Vydání @@ -2605,6 +2640,7 @@ release.new_release=Nové vydání release.draft=Koncept release.prerelease=Předběžná verze release.stable=Stabilní +release.latest=Nejnovější release.compare=Porovnat release.edit=upravit release.ahead.commits=%d revizí @@ -2679,6 +2715,8 @@ 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. +branch.rename_default_or_protected_branch_error=Pouze administrátoři mohou přejmenovat výchozí nebo chráněné větve. +branch.rename_protected_branch_failed=Tato větev je chráněna pravidly ochrany založenými na zástupném vzoru. tag.create_tag=Vytvořit značku %s tag.create_tag_operation=Vytvořit značku @@ -2833,6 +2871,11 @@ teams.invite.title=Byli jste pozváni do týmu %s v organizaci teams.invite.by=Pozvání od %s teams.invite.description=Pro připojení k týmu klikněte na tlačítko níže. +view_as_role=Zobrazit jako: %s +view_as_public_hint=Prohlížíte README jako veřejný uživatel. +view_as_member_hint=Prohlížíte README jako člen této organizace. + + [admin] maintenance=Údržba dashboard=Přehled @@ -3049,7 +3092,7 @@ packages.size=Velikost packages.published=Publikováno defaulthooks=Výchozí webové háčky -defaulthooks.desc=Webové háčky automaticky vytvářejí HTTP POST dotazy na server při určitých Gitea událostech. Webové háčky definované zde jsou výchozí a budou zkopírovány do všech nových repozitářů. Přečtěte si více v průvodci webovými háčky. +defaulthooks.desc=Webové háčky automaticky vytvářejí HTTP POST dotazy na server při určitých Gitea událostech. Webové háčky definované zde jsou výchozí a budou zkopírovány do všech nových repozitářů. Přečtěte si více v průvodci webovými háčky. defaulthooks.add_webhook=Přidat výchozí webový háček defaulthooks.update_webhook=Aktualizovat výchozí webový háček @@ -3327,6 +3370,8 @@ monitor.previous=Předešlý čas spuštění monitor.execute_times=Vykonání monitor.process=Spuštěné procesy monitor.stacktrace=Výpisy zásobníku +monitor.trace=Trasovat +monitor.performance_logs=Výkonnostní logy monitor.processes_count=%d procesů monitor.download_diagnosis_report=Stáhnout diagnosttickou zprávu monitor.desc=Popis @@ -3335,7 +3380,6 @@ monitor.execute_time=Doba provádění monitor.last_execution_result=Výsledek monitor.process.cancel=Zrušit proces monitor.process.cancel_desc=Zrušení procesu může způsobit ztrátu dat -monitor.process.cancel_notices=Zrušit: %s? monitor.process.children=Potomek monitor.queues=Fronty @@ -3502,6 +3546,7 @@ versions=Verze versions.view_all=Zobrazit všechny dependency.id=ID dependency.version=Verze +search_in_external_registry=Hledat v %s alpine.registry=Nastavte tento registr přidáním URL do /etc/apk/repositories: alpine.registry.key=Stáhněte si veřejný RSA klíč registru do složky /etc/apk/keys/ pro ověření podpisu indexu: alpine.registry.info=Vyberte $branch a $repository ze seznamu níže. @@ -3510,6 +3555,8 @@ alpine.repository=Informace o repozitáři alpine.repository.branches=Větve alpine.repository.repositories=Repozitáře alpine.repository.architectures=Architektury +arch.registry=Přidejte server se souvisejícím repozitářem a architekturou do /etc/pacman.conf: +arch.install=Synchronizovat balíček s pacman: arch.repository=Informace o repozitáři arch.repository.repositories=Repozitáře arch.repository.architectures=Architektury @@ -3529,7 +3576,8 @@ conda.install=Pro instalaci balíčku pomocí Conda spusťte následující př container.details.type=Typ obrazu container.details.platform=Platforma container.pull=Stáhněte obraz z příkazové řádky: -container.digest=Výběr: +container.images=Obrázky +container.digest=Výběr container.multi_arch=OS/architektura container.layers=Vrstvy obrazů container.labels=Štítky @@ -3692,6 +3740,7 @@ runners.status.active=Aktivní runners.status.offline=Offline runners.version=Verze runners.reset_registration_token=Resetovat registrační token +runners.reset_registration_token_confirm=Chcete zneplatnit stávající token a vygenerovat nový? runners.reset_registration_token_success=Registrační token runneru byl úspěšně obnoven runs.all_workflows=Všechny pracovní postupy @@ -3724,6 +3773,7 @@ workflow.not_found=Pracovní postup „%s“ nebyl nalezen. workflow.run_success=Pracovní postup „%s“ proběhl úspěšně. workflow.from_ref=Použít pracovní postup od workflow.has_workflow_dispatch=Tento pracovní postup má spouštěč události workflow_dispatch. +workflow.has_no_workflow_dispatch=Pracovní postup „%s“ memá workflow_dispatch spouštěč. need_approval_desc=Potřebujete schválení pro spuštění pracovních postupů pro rozštěpený pull request. @@ -3743,6 +3793,8 @@ variables.creation.success=Proměnná „%s“ byla přidána. variables.update.failed=Úprava proměnné se nezdařila. variables.update.success=Proměnná byla upravena. +logs.always_auto_scroll=Vždy automaticky posouvat logy +logs.always_expand_running=Vždy rozšířit běžící logy [projects] deleted.display_name=Odstraněný projekt diff --git a/options/locale/locale_de-DE.ini b/options/locale/locale_de-DE.ini index 5f2f16bfdf3..32f3a4bbcc9 100644 --- a/options/locale/locale_de-DE.ini +++ b/options/locale/locale_de-DE.ini @@ -54,6 +54,7 @@ webauthn_reload=Neu laden repository=Repository organization=Organisation mirror=Mirror +issue_milestone=Meilenstein new_repo=Neues Repository new_migrate=Neue Migration new_mirror=Neuer Mirror @@ -1015,7 +1016,6 @@ new_repo_helper=Ein Repository enthält alle Projektdateien, einschließlich des owner=Besitzer owner_helper=Einige Organisationen könnten in der Dropdown-Liste nicht angezeigt werden, da die Anzahl an Repositories begrenzt ist. repo_name=Repository-Name -repo_name_helper=Ein guter Repository-Name besteht normalerweise aus kurzen, unvergesslichen und einzigartigen Schlagwörtern. repo_size=Repository-Größe template=Template template_select=Vorlage auswählen @@ -1112,9 +1112,6 @@ blame.ignore_revs=Revisionen in .git-blame-ignore-revs werden i blame.ignore_revs.failed=Fehler beim Ignorieren der Revisionen in .git-blame-ignore-revs. user_search_tooltip=Zeigt maximal 30 Benutzer -tree_path_not_found_commit=Pfad %[1]s existiert nicht in Commit%[2]s -tree_path_not_found_branch=Pfad %[1]s existiert nicht in Branch %[2]s -tree_path_not_found_tag=Pfad %[1]s existiert nicht in Tag %[2]s transfer.accept=Übertragung Akzeptieren transfer.accept_desc=`Übertragung nach "%s"` @@ -1251,6 +1248,7 @@ labels=Label org_labels_desc=Labels der Organisationsebene, die mit allen Repositories in dieser Organisation verwendet werden können org_labels_desc_manage=verwalten +milestone=Meilenstein milestones=Meilensteine commits=Commits commit=Commit @@ -1647,7 +1645,7 @@ issues.attachment.open_tab=`Klicken, um „%s“ in einem neuen Tab zu öffnen` issues.attachment.download=`Klicken, um „%s“ herunterzuladen` issues.subscribe=Abonnieren issues.unsubscribe=Abbestellen -issues.unpin_issue=Issue abheften +issues.unpin=Loslösen issues.max_pinned=Du kannst keine weiteren Issues anheften issues.pin_comment=hat das %s angeheftet issues.unpin_comment=hat das %s abgeheftet @@ -1682,16 +1680,13 @@ issues.timetracker_timer_manually_add=Zeit hinzufügen issues.time_estimate_set=Geschätzte Zeit festlegen issues.time_estimate_display=Schätzung: %s -issues.change_time_estimate_at=Zeitschätzung geändert zu %s %s issues.remove_time_estimate_at=Zeitschätzung %s entfernt issues.time_estimate_invalid=Format der Zeitschätzung ist ungültig issues.start_tracking_history=hat die Zeiterfassung %s gestartet issues.tracker_auto_close=Der Timer wird automatisch gestoppt, wenn dieser Issue geschlossen wird issues.tracking_already_started=`Du hast die Zeiterfassung bereits in diesem Issue gestartet!` -issues.stop_tracking_history=hat für %s gearbeitet %s issues.cancel_tracking_history=`hat die Zeiterfassung %s abgebrochen` issues.del_time=Diese Zeiterfassung löschen -issues.add_time_history=hat %s gearbeitete Zeit hinzugefügt %s issues.del_time_history=`hat %s gearbeitete Zeit gelöscht` issues.add_time_manually=Zeit manuell hinzufügen issues.add_time_hours=Stunden @@ -2155,7 +2150,6 @@ settings.advanced_settings=Erweiterte Einstellungen settings.wiki_desc=Repository-Wiki aktivieren settings.use_internal_wiki=Eingebautes Wiki verwenden settings.default_wiki_branch_name=Standardbezeichnung für Wiki-Branch -settings.default_wiki_everyone_access=Standard-Zugriffsberechtigung für angemeldete Benutzer: settings.failed_to_change_default_wiki_branch=Das Ändern des Standard-Wiki-Branches ist fehlgeschlagen. settings.use_external_wiki=Externes Wiki verwenden settings.external_wiki_url=Externe Wiki-URL @@ -2861,6 +2855,8 @@ teams.invite.title=Du wurdest eingeladen, dem Team %s in der Or teams.invite.by=Von %s eingeladen teams.invite.description=Bitte klicke auf die folgende Schaltfläche, um dem Team beizutreten. + + [admin] maintenance=Wartung dashboard=Dashboard @@ -3180,7 +3176,7 @@ auths.tip.oauth2_provider=OAuth2-Anbieter auths.tip.bitbucket=Registriere einen neuen OAuth-Consumer unter %s und füge die Berechtigung 'Account' - 'Read' hinzu auths.tip.nextcloud=Registriere über das "Settings -> Security -> OAuth 2.0 client"-Menü einen neuen "OAuth consumer" auf der Nextcloud-Instanz auths.tip.dropbox=Erstelle eine neue App auf %s -auths.tip.facebook=Erstelle eine neue Anwendung auf %s und füge das Produkt "Facebook Login“ hinzu +auths.tip.facebook=Erstelle eine neue Anwendung auf %s und füge das Produkt „Facebook Login“ hinzu auths.tip.github=Erstelle unter %s eine neue OAuth-Anwendung auths.tip.gitlab_new=Erstelle eine neue Anwendung unter %s auths.tip.google_plus=Du erhältst die OAuth2-Client-Zugangsdaten in der Google-API-Konsole unter %s @@ -3363,7 +3359,6 @@ monitor.execute_time=Ausführungszeit monitor.last_execution_result=Ergebnis monitor.process.cancel=Prozess abbrechen monitor.process.cancel_desc=Abbrechen eines Prozesses kann Datenverlust verursachen -monitor.process.cancel_notices=Abbrechen: %s? monitor.process.children=Subprozesse monitor.queues=Warteschlangen @@ -3559,7 +3554,6 @@ conda.install=Um das Paket mit Conda zu installieren, führe den folgenden Befeh container.details.type=Container-Image Typ container.details.platform=Plattform container.pull=Downloade das Container-Image aus der Kommandozeile: -container.digest=Digest: container.multi_arch=Betriebsystem / Architektur container.layers=Container-Image Ebenen container.labels=Labels diff --git a/options/locale/locale_el-GR.ini b/options/locale/locale_el-GR.ini index fa9c41d5de0..cab63200165 100644 --- a/options/locale/locale_el-GR.ini +++ b/options/locale/locale_el-GR.ini @@ -53,6 +53,7 @@ webauthn_reload=Ανανέωση repository=Αποθετήριο organization=Οργανισμός mirror=Αντίγραφο +issue_milestone=Ορόσημο new_repo=Νέο Αποθετήριο new_migrate=Νέα Μεταφορά new_mirror=Νέο Είδωλο @@ -908,7 +909,6 @@ new_repo_helper=Ένα αποθετήριο περιέχει όλα τα αρχ owner=Ιδιοκτήτης owner_helper=Ορισμένοι οργανισμοί ενδέχεται να μην εμφανίζονται στο αναπτυσσόμενο μενού λόγω του μέγιστου αριθμού αποθετηρίων. repo_name=Όνομα αποθετηρίου -repo_name_helper=Τα καλά ονόματα αποθετηρίων χρησιμοποιούν σύντομες, αξέχαστες και μοναδικές λέξεις-κλειδιά. repo_size=Μέγεθος Αποθετηρίου template=Πρότυπο template_select=Επιλέξτε πρότυπο. @@ -993,9 +993,6 @@ blame_prior=Προβολή ευθύνης πριν από αυτή την αλλ blame.ignore_revs=Αγνόηση των αναθεωρήσεων στο .git-blame-ignore-revs. Πατήστε εδώ για να το παρακάμψετε και να δείτε την κανονική προβολή ευθυνών. blame.ignore_revs.failed=Αποτυχία αγνόησης των αναθεωρήσεων στο .git-blame-ignore-revs. -tree_path_not_found_commit=Η διαδρομή %[1]s δεν υπάρχει στην υποβολή %[2]s -tree_path_not_found_branch=Η διαδρομή %[1]s δεν υπάρχει στον κλάδο %[2]s -tree_path_not_found_tag=Η διαδρομή %[1]s δεν υπάρχει στην ετικέτα %[2]s transfer.accept=Αποδοχή Μεταφοράς transfer.reject=Απόρριψη Μεταφοράς @@ -1123,6 +1120,7 @@ labels=Σήματα org_labels_desc=Τα σήματα στο επίπεδο οργανισμού, που μπορούν να χρησιμοποιηθούν με όλα τα αποθετήρια κάτω από αυτόν τον οργανισμό org_labels_desc_manage=διαχείριση +milestone=Ορόσημο milestones=Ορόσημα commits=Υποβολές commit=Υποβολή @@ -1496,7 +1494,7 @@ issues.attachment.open_tab=`Κάντε κλικ για να δείτε το "%s" issues.attachment.download=`Κάντε κλικ για να λάβετε το "%s"` issues.subscribe=Εγγραφή issues.unsubscribe=Διαγραφή -issues.unpin_issue=Άφεση Ζητήματος +issues.unpin=Άφεση issues.max_pinned=Δεν μπορείτε να διατηρήσετε περισσότερα ζητήματα issues.pin_comment=διατήρησε αυτό %s issues.unpin_comment=άφησε αυτό %s @@ -2593,6 +2591,8 @@ teams.invite.title=Έχετε προσκληθεί να συμμετάσχετε teams.invite.by=Προσκλήθηκε από %s teams.invite.description=Παρακαλώ κάντε κλικ στον παρακάτω σύνδεσμο για συμμετοχή στην ομάδα. + + [admin] dashboard=Πίνακας Ελέγχου identity_access=Ταυτότητα & Πρόσβαση @@ -3239,7 +3239,6 @@ conda.install=Για να εγκαταστήσετε το πακέτο χρησ container.details.type=Τύπος Εικόνας container.details.platform=Πλατφόρμα container.pull=Κατεβάστε την εικόνα από τη γραμμή εντολών: -container.digest=Σύνοψη: container.multi_arch=ΛΣ / Αρχιτεκτονική container.layers=Στρώματα Εικόνας container.labels=Ετικέτες diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index baa45ea7668..7fcdff9d1f9 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -54,6 +54,7 @@ webauthn_reload = Reload repository = Repository organization = Organization mirror = Mirror +issue_milestone = Milestone new_repo = New Repository new_migrate = New Migration new_mirror = New Mirror @@ -244,6 +245,7 @@ license_desc = Go get [install] install = Installation +installing_desc = Installing now, please wait... title = Initial Configuration docker_helper = If you run Gitea inside Docker, please read the documentation before changing any settings. require_db_desc = Gitea requires MySQL, PostgreSQL, MSSQL, SQLite3 or TiDB (MySQL protocol). @@ -1015,7 +1017,9 @@ new_repo_helper = A repository contains all project files, including revision hi owner = Owner owner_helper = Some organizations may not show up in the dropdown due to a maximum repository count limit. repo_name = Repository Name -repo_name_helper = Good repository names use short, memorable and unique keywords. +repo_name_profile_public_hint= .profile is a special repository that you can use to add README.md to your public organization profile, visible to anyone. Make sure it’s public and initialize it with a README in the profile directory to get started. +repo_name_profile_private_hint = .profile-private is a special repository that you can use to add a README.md to your organization member profile, visible only to organization members. Make sure it’s private and initialize it with a README in the profile directory to get started. +repo_name_helper = Good repository names use short, memorable and unique keywords. A repository named ".profile" or ".profile-private" could be used to add a README.md for the user/organization profile. repo_size = Repository Size template = Template template_select = Select a template. @@ -1112,9 +1116,7 @@ blame.ignore_revs = Ignoring revisions in .git-blame-ignore-revs.git-blame-ignore-revs. user_search_tooltip = Shows a maximum of 30 users -tree_path_not_found_commit = Path %[1]s doesn't exist in commit %[2]s -tree_path_not_found_branch = Path %[1]s doesn't exist in branch %[2]s -tree_path_not_found_tag = Path %[1]s doesn't exist in tag %[2]s +tree_path_not_found = Path %[1]s doesn't exist in %[2]s transfer.accept = Accept Transfer transfer.accept_desc = Transfer to "%s" @@ -1232,6 +1234,7 @@ create_new_repo_command = Creating a new repository on the command line push_exist_repo = Pushing an existing repository from the command line empty_message = This repository does not contain any content. broken_message = The Git data underlying this repository cannot be read. Contact the administrator of this instance or delete this repository. +no_branch = This repository doesn’t have any branches. code = Code code.desc = Access source code, files, commits and branches. @@ -1251,6 +1254,7 @@ labels = Labels org_labels_desc = Organization level labels that can be used with all repositories under this organization org_labels_desc_manage = manage +milestone = Milestone milestones = Milestones commits = Commits commit = Commit @@ -1343,6 +1347,8 @@ editor.new_branch_name_desc = New branch name… editor.cancel = Cancel editor.filename_cannot_be_empty = The filename cannot be empty. editor.filename_is_invalid = The filename is invalid: "%s". +editor.commit_email = Commit email +editor.invalid_commit_email = The email for the commit is invalid. editor.branch_does_not_exist = Branch "%s" does not exist in this repository. editor.branch_already_exists = Branch "%s" already exists in this repository. editor.directory_is_a_file = Directory name "%s" is already used as a filename in this repository. @@ -1659,7 +1665,7 @@ issues.attachment.open_tab = `Click to see "%s" in a new tab` issues.attachment.download = `Click to download "%s"` issues.subscribe = Subscribe issues.unsubscribe = Unsubscribe -issues.unpin_issue = Unpin Issue +issues.unpin = Unpin issues.max_pinned = "You can't pin more issues" issues.pin_comment = "pinned this %s" issues.unpin_comment = "unpinned this %s" @@ -1694,16 +1700,16 @@ issues.timetracker_timer_manually_add = Add Time issues.time_estimate_set = Set estimated time issues.time_estimate_display = Estimate: %s -issues.change_time_estimate_at = changed time estimate to %s %s +issues.change_time_estimate_at = changed time estimate to %[1]s %[2]s issues.remove_time_estimate_at = removed time estimate %s issues.time_estimate_invalid = Time estimate format is invalid issues.start_tracking_history = started working %s issues.tracker_auto_close = Timer will be stopped automatically when this issue gets closed issues.tracking_already_started = `You have already started time tracking on another issue!` -issues.stop_tracking_history = worked for %s %s +issues.stop_tracking_history = worked for %[1]s %[2]s issues.cancel_tracking_history = `canceled time tracking %s` issues.del_time = Delete this time log -issues.add_time_history = added spent time %s %s +issues.add_time_history = added spent time %[1]s %[2]s issues.del_time_history= `deleted spent time %s` issues.add_time_manually = Manually Add Time issues.add_time_hours = Hours @@ -1962,6 +1968,7 @@ pulls.upstream_diverging_prompt_behind_1 = This branch is %[1]d commit behind %[ pulls.upstream_diverging_prompt_behind_n = This branch is %[1]d commits behind %[2]s pulls.upstream_diverging_prompt_base_newer = The base branch %s has new changes pulls.upstream_diverging_merge = Sync fork +pulls.upstream_diverging_merge_confirm = Would you like to merge "%[1]s" onto "%[2]s"? pull.deleted_branch = (deleted):%s pull.agit_documentation = Review documentation about AGit @@ -2167,7 +2174,7 @@ settings.advanced_settings = Advanced Settings settings.wiki_desc = Enable Repository Wiki settings.use_internal_wiki = Use Built-In Wiki settings.default_wiki_branch_name = Default Wiki Branch Name -settings.default_wiki_everyone_access = Default Access Permission for signed-in users: +settings.default_permission_everyone_access = Default access permission for all signed-in users: settings.failed_to_change_default_wiki_branch = Failed to change the default wiki branch. settings.use_external_wiki = Use External Wiki settings.external_wiki_url = External Wiki URL @@ -2335,6 +2342,8 @@ settings.event_fork = Fork settings.event_fork_desc = Repository forked. settings.event_wiki = Wiki settings.event_wiki_desc = Wiki page created, renamed, edited or deleted. +settings.event_statuses = Statuses +settings.event_statuses_desc = Commit Status updated from the API. settings.event_release = Release settings.event_release_desc = Release published, updated or deleted in a repository. settings.event_push = Push @@ -2635,6 +2644,9 @@ diff.image.overlay = Overlay diff.has_escaped = This line has hidden Unicode characters diff.show_file_tree = Show file tree diff.hide_file_tree = Hide file tree +diff.submodule_added = Submodule %[1]s added at %[2]s +diff.submodule_deleted = Submodule %[1]s deleted from %[2]s +diff.submodule_updated = Submodule %[1]s updated: %[2]s releases.desc = Track project versions and downloads. release.releases = Releases @@ -2720,6 +2732,8 @@ branch.create_branch_operation = Create branch branch.new_branch = Create new branch branch.new_branch_from = Create new branch from "%s" branch.renamed = Branch %s was renamed to %s. +branch.rename_default_or_protected_branch_error = Only admins can rename default or protected branches. +branch.rename_protected_branch_failed = This branch is protected by glob-based protection rules. tag.create_tag = Create tag %s tag.create_tag_operation = Create tag @@ -2874,6 +2888,19 @@ teams.invite.title = You have been invited to join team %s in o teams.invite.by = Invited by %s teams.invite.description = Please click the button below to join the team. +view_as_role = View as: %s +view_as_public_hint = You are viewing the README as a public user. +view_as_member_hint = You are viewing the README as a member of this organization. + +worktime = Worktime +worktime.date_range_start = Start date +worktime.date_range_end = End date +worktime.query = Query +worktime.time = Time +worktime.by_repositories = By repositories +worktime.by_milestones = By milestones +worktime.by_members = By members + [admin] maintenance = Maintenance dashboard = Dashboard @@ -3368,6 +3395,8 @@ monitor.previous = Previous Time monitor.execute_times = Executions monitor.process = Running Processes monitor.stacktrace = Stacktrace +monitor.trace = Trace +monitor.performance_logs = Performance Logs monitor.processes_count = %d Processes monitor.download_diagnosis_report = Download diagnosis report monitor.desc = Description @@ -3376,7 +3405,6 @@ monitor.execute_time = Execution Time monitor.last_execution_result = Result monitor.process.cancel = Cancel process monitor.process.cancel_desc = Cancelling a process may cause data loss -monitor.process.cancel_notices = Cancel: %s? monitor.process.children = Children monitor.queues = Queues @@ -3543,6 +3571,7 @@ versions = Versions versions.view_all = View all dependency.id = ID dependency.version = Version +search_in_external_registry = Search in %s alpine.registry = Setup this registry by adding the url in your /etc/apk/repositories file: alpine.registry.key = Download the registry public RSA key into the /etc/apk/keys/ folder to verify the index signature: alpine.registry.info = Choose $branch and $repository from the list below. @@ -3572,7 +3601,8 @@ conda.install = To install the package using Conda, run the following command: container.details.type = Image Type container.details.platform = Platform container.pull = Pull the image from the command line: -container.digest = Digest: +container.images = Images +container.digest = Digest container.multi_arch = OS / Arch container.layers = Image Layers container.labels = Labels @@ -3768,6 +3798,7 @@ workflow.not_found = Workflow '%s' not found. workflow.run_success = Workflow '%s' run successfully. workflow.from_ref = Use workflow from workflow.has_workflow_dispatch = This workflow has a workflow_dispatch event trigger. +workflow.has_no_workflow_dispatch = Workflow '%s' has no workflow_dispatch event trigger. need_approval_desc = Need approval to run workflows for fork pull request. diff --git a/options/locale/locale_es-ES.ini b/options/locale/locale_es-ES.ini index d7d1cadd089..359f490356a 100644 --- a/options/locale/locale_es-ES.ini +++ b/options/locale/locale_es-ES.ini @@ -52,6 +52,7 @@ webauthn_reload=Recargar repository=Repositorio organization=Organización mirror=Réplica +issue_milestone=Hito new_repo=Nuevo repositorio new_migrate=Nueva migración new_mirror=Nueva réplica @@ -898,7 +899,6 @@ visibility.private_tooltip=Visible sólo para los miembros de organizaciones a l owner=Propietario owner_helper=Algunas organizaciones pueden no aparecer en el menú desplegable debido a un límite máximo de recuento de repositorios. repo_name=Nombre del repositorio -repo_name_helper=Un buen nombre de repositorio está compuesto por palabras clave cortas, memorables y únicas. repo_size=Tamaño del repositorio template=Plantilla template_select=Seleccionar una plantilla. @@ -983,9 +983,6 @@ blame_prior=Ver la culpa antes de este cambio blame.ignore_revs=Ignorando revisiones en .git-blame-ignore-revs. Haga clic aquí para saltar y para a la vista normal. blame.ignore_revs.failed=No se pudieron ignorar las revisiones en .git-blame-ignore-revs. -tree_path_not_found_commit=La ruta %[1]s no existe en el commit %[2]s -tree_path_not_found_branch=La ruta %[1]s no existe en la rama %[2]s -tree_path_not_found_tag=La ruta %[1]s no existe en la etiqueta %[2]s transfer.accept=Aceptar transferencia transfer.reject=Rechazar transferencia @@ -1113,6 +1110,7 @@ labels=Etiquetas org_labels_desc=Etiquetas de nivel de la organización que pueden ser utilizadas con todos los repositorios bajo esta organización org_labels_desc_manage=gestionar +milestone=Hito milestones=Hitos commits=Commits commit=Commit @@ -1486,7 +1484,7 @@ issues.attachment.open_tab='Haga clic para ver "%s" en una pestaña nueva' issues.attachment.download=`Haga clic para descargar "%s"` issues.subscribe=Suscribir issues.unsubscribe=Desuscribirse -issues.unpin_issue=Desanclar incidencia +issues.unpin=Desanclar issues.max_pinned=No puedes anclar más incidencias issues.pin_comment=anclado este %s issues.unpin_comment=desanclado este %s @@ -2574,6 +2572,8 @@ teams.invite.title=Has sido invitado a unirte al equipo %s en l teams.invite.by=Invitado por %s teams.invite.description=Por favor, haga clic en el botón de abajo para unirse al equipo. + + [admin] dashboard=Panel de control identity_access=Identidad y acceso @@ -3218,7 +3218,6 @@ conda.install=Para instalar el paquete usando Conda, ejecute el siguiente comand container.details.type=Tipo de imagen container.details.platform=Plataforma container.pull=Arrastra la imagen desde la línea de comandos: -container.digest=Resumen: container.multi_arch=SO / Arquitectura container.layers=Capas de imagen container.labels=Etiquetas diff --git a/options/locale/locale_fa-IR.ini b/options/locale/locale_fa-IR.ini index 6dcd35560a6..a84fcd9bd7a 100644 --- a/options/locale/locale_fa-IR.ini +++ b/options/locale/locale_fa-IR.ini @@ -704,7 +704,6 @@ visibility.private=خصوصی owner=مالک owner_helper=بخاطر بیشینه تعداد مخزن، ممکن است برخی از سازمان‌ها در لیست کشویی دیده نشود. repo_name=نام مخزن -repo_name_helper=نام خوب مخزن معمولا از کلمات کلیدی کوتاه و به یاد ماندنی و منحصر به فرد تشکیل شده است. repo_size=اندازه مخزن template=قالب / الگو template_select=انتخاب یک قالب/ الگو. @@ -1993,6 +1992,8 @@ teams.all_repositories_read_permission_desc=این تیم دسترسی teams.all_repositories_write_permission_desc=این تیم دسترسی نوشتن مخازن همه را می بخشد: اعضا می توانند مخازن را مشاهده و درج کنند. teams.all_repositories_admin_permission_desc=این تیم دسترسی مدیر به مخازن همه را می بخشد: اعضا می توانند مخازن را بخواند، همکار و مخزن اضافه کنند. + + [admin] dashboard=پیشخوان users=حساب کاربران diff --git a/options/locale/locale_fi-FI.ini b/options/locale/locale_fi-FI.ini index b4f3869db41..1c78f049f9b 100644 --- a/options/locale/locale_fi-FI.ini +++ b/options/locale/locale_fi-FI.ini @@ -49,6 +49,7 @@ webauthn_reload=Päivitä repository=Repo organization=Organisaatio mirror=Peili +issue_milestone=Merkkipaalu new_repo=Uusi repo new_migrate=Uusi migraatio new_mirror=Uusi peilaus @@ -635,7 +636,6 @@ visibility.private=Yksityinen owner=Omistaja owner_helper=Jotkin organisaatiot eivät välttämättä näy pudotusvalikossa, koska repojen maksimimäärää on rajoitettu. repo_name=Repon nimi -repo_name_helper=Hyvä repon nimi on lyhyt, mieleenpainuva ja yksilöllinen. repo_size=Repon koko template=Malli template_select=Valitse malli. @@ -721,6 +721,7 @@ projects=Projektit packages=Paketit labels=Tunnisteet +milestone=Merkkipaalu milestones=Merkkipaalut commits=Commitit commit=Commit @@ -1361,6 +1362,8 @@ teams.repositories=Tiimin repot teams.members.none=Ei jäseniä tässä tiimissä. teams.all_repositories=Kaikki repot + + [admin] dashboard=Kojelauta users=Käyttäjätilit diff --git a/options/locale/locale_fr-FR.ini b/options/locale/locale_fr-FR.ini index b024be4d882..45cf43956ac 100644 --- a/options/locale/locale_fr-FR.ini +++ b/options/locale/locale_fr-FR.ini @@ -46,7 +46,7 @@ webauthn_unsupported_browser=Votre navigateur ne prend actuellement pas en charg webauthn_error_unknown=Une erreur indéterminée s'est produite. Veuillez réessayer. webauthn_error_insecure=`WebAuthn ne prend en charge que les connexions sécurisées. Pour les tests via HTTP, vous pouvez utiliser l'origine "localhost" ou "127.0.0.1"` webauthn_error_unable_to_process=Le serveur n'a pas pu traiter votre demande. -webauthn_error_duplicated=La clé de sécurité n'est pas autorisée pour cette demande. Veuillez vous assurer que la clé n'est pas déjà enregistrée. +webauthn_error_duplicated=La clé de sécurité n’est pas autorisée pour cette demande. Veuillez vous assurer que la clé n’est pas déjà enregistrée. webauthn_error_empty=Vous devez définir un nom pour cette clé. webauthn_error_timeout=Le délai d'attente imparti a été atteint avant que votre clé ne puisse être lue. Veuillez recharger la page pour réessayer. webauthn_reload=Recharger @@ -54,6 +54,7 @@ webauthn_reload=Recharger repository=Dépôt organization=Organisation mirror=Miroir +issue_milestone=Jalon new_repo=Nouveau dépôt new_migrate=Nouvelle migration new_mirror=Nouveau miroir @@ -244,6 +245,7 @@ license_desc=Venez récupérer documentation avant de modifier les paramètres. require_db_desc=Gitea nécessite MySQL, PostgreSQL, MSSQL, SQLite3 ou TiDB (avec le protocole MySQL). @@ -468,7 +470,7 @@ sspi_auth_failed=Échec de l'authentification SSPI password_pwned=Le mot de passe que vous avez choisi fait partit des mots de passe ayant fuité sur internet. Veuillez réessayer avec un mot de passe différent et considérez remplacer ce mot de passe si vous l’utilisez ailleurs. password_pwned_err=Impossible d'envoyer la demande à HaveIBeenPwned last_admin=Vous ne pouvez pas supprimer ce compte car au moins un administrateur est requis. -signin_passkey=Se connecter avec une clé d’identification (passkey) +signin_passkey=Se connecter avec une clé d’accès (passkey) back_to_sign_in=Revenir à la page de connexion [mail] @@ -817,7 +819,7 @@ manage_ssh_keys=Gérer les clés SSH manage_ssh_principals=Gérer les certificats principaux SSH manage_gpg_keys=Gérer les clés GPG add_key=Ajouter une clé -ssh_desc=Ces clefs SSH publiques sont associées à votre compte. Les clefs privées correspondantes permettent l'accès complet à vos repos. +ssh_desc=Ces clés SSH publiques sont associées à votre compte. Les clés privées correspondantes permettent l’accès complet à vos dépôts. principal_desc=Ces Principaux de certificats SSH sont associés à votre compte et permettent un accès complet à vos dépôts. gpg_desc=Ces clés GPG sont associées à votre compte. Conservez-les en lieu sûr, car elles permettent de vérifier vos révisions. ssh_helper=Besoin d'aide ? Consultez le guide de GitHub pour créer vos propres clés SSH ou résoudre les problèmes courants que vous pourriez rencontrer en utilisant SSH. @@ -968,7 +970,7 @@ passcode_invalid=Le mot de passe est invalide. Réessayez. twofa_enrolled=L’authentification à deux facteurs a été activée pour votre compte. Gardez votre clé de secours (%s) en lieu sûr, car il ne vous sera montré qu'une seule fois. twofa_failed_get_secret=Impossible d'obtenir le secret. -webauthn_desc=Les clefs de sécurité sont des dispositifs matériels contenant des clefs cryptographiques. Elles peuvent être utilisées pour l’authentification à deux facteurs. La clef de sécurité doit supporter le standard WebAuthn Authenticator. +webauthn_desc=Les clés de sécurité sont des dispositifs matériels contenant des clés cryptographiques. Elles peuvent être utilisées pour l’authentification à deux facteurs. La clé de sécurité doit supporter le standard WebAuthn Authenticator. webauthn_register_key=Ajouter une clé de sécurité webauthn_nickname=Pseudonyme webauthn_delete_key=Retirer la clé de sécurité @@ -1015,7 +1017,9 @@ new_repo_helper=Un dépôt contient tous les fichiers d’un projet, ainsi que l owner=Propriétaire owner_helper=Certaines organisations peuvent ne pas apparaître dans la liste déroulante en raison d'une limite maximale du nombre de dépôts. repo_name=Nom du dépôt -repo_name_helper=Idéalement, le nom d'un dépôt devrait être court, mémorisable et unique. +repo_name_profile_public_hint=.profile est un dépôt spécial que vous pouvez utiliser pour ajouter un README.md à votre profil public d’organisation, visible à tout le monde. Assurez-vous qu’il soit public et initialisez-le avec un README dans le répertoire de profil pour commencer. +repo_name_profile_private_hint=.profile-private est un dépôt spécial que vous pouvez utiliser pour ajouter un README.md à votre profil d’organisation, visible uniquement aux membres de l’organisation. Assurez-vous qu’il soit privé et initialisez-le avec un README dans le répertoire de profil pour commencer. +repo_name_helper=Idéalement, le nom d’un dépôt devrait être court, mémorisable et unique. Vous pouvez personnaliser votre profil ou celui de votre organisation en créant un dépôt nommé « .profile » ou « .profile-private » et contenant un README.md. repo_size=Taille du dépôt template=Modèle template_select=Répliquer un modèle @@ -1112,9 +1116,7 @@ blame.ignore_revs=Les révisions dans .git-blame-ignore-revs so blame.ignore_revs.failed=Impossible d'ignorer les révisions dans .git-blame-ignore-revs. user_search_tooltip=Affiche un maximum de 30 utilisateurs -tree_path_not_found_commit=Le chemin %[1]s n’existe pas dans la révision %[2]s. -tree_path_not_found_branch=Le chemin %[1]s n’existe pas dans la branche %[2]s. -tree_path_not_found_tag=Le chemin %[1]s n’existe pas dans l’étiquette %[2]s. +tree_path_not_found=Le chemin %[1]s n’existe pas dans %[2]s transfer.accept=Accepter le transfert transfer.accept_desc=Transférer à « %s » @@ -1171,7 +1173,7 @@ migrate_items_releases=Publications migrate_repo=Migrer le dépôt migrate.clone_address=Migrer/Cloner depuis une URL migrate.clone_address_desc=L'URL HTTP(S) ou Git "clone" d'un dépôt existant -migrate.github_token_desc=Vous pouvez mettre un ou plusieurs jetons séparés par des virgules ici pour rendre la migration plus rapide en raison de la limite de débit de l'API GitHub. ATTENTION : Abuser de cette fonctionnalité peut enfreindre la politique du fournisseur de services et entraîner un blocage de compte. +migrate.github_token_desc=Vous pouvez mettre un ou plusieurs jetons séparés par des virgules ici pour rendre la migration plus rapide et contourner la limite de débit de l’API GitHub. ATTENTION : Abuser de cette fonctionnalité peut enfreindre la politique du fournisseur de service et entraîner un blocage de votre compte. migrate.clone_local_path=ou un chemin serveur local migrate.permission_denied=Vous n'êtes pas autorisé à importer des dépôts locaux. migrate.permission_denied_blocked=Vous ne pouvez pas importer depuis des hôtes interdits, veuillez demander à l'administrateur de vérifier les paramètres ALLOWED_DOMAINS/ALLOW_LOCALNETWORKS/BLOCKED_DOMAINS. @@ -1232,6 +1234,7 @@ create_new_repo_command=Création d'un nouveau dépôt en ligne de commande push_exist_repo=Soumission d'un dépôt existant par ligne de commande empty_message=Ce dépôt n'a pas de contenu. broken_message=Les données git de ce dépôt ne peuvent pas être lues. Contactez l'administrateur de cette instance ou supprimez ce dépôt. +no_branch=Ce dépôt n’a aucune branche. code=Code code.desc=Accéder au code source, fichiers, révisions et branches. @@ -1251,6 +1254,7 @@ labels=Labels org_labels_desc=Les labels d'une organisation peuvent être utilisés avec tous les dépôts de cette organisation. org_labels_desc_manage=gérer +milestone=Jalon milestones=Jalons commits=Révisions commit=Révision @@ -1343,6 +1347,8 @@ editor.new_branch_name_desc=Nouveau nom de la branche… editor.cancel=Annuler editor.filename_cannot_be_empty=Le nom de fichier ne peut être vide. editor.filename_is_invalid=Le nom du fichier est invalide : "%s". +editor.commit_email=Courriel de la révision +editor.invalid_commit_email=Le courriel pour la révision n’est pas valide. editor.branch_does_not_exist=La branche "%s" n'existe pas dans ce dépôt. editor.branch_already_exists=La branche "%s" existe déjà dans ce dépôt. editor.directory_is_a_file=Le nom de dossier "%s" est déjà utilisé comme nom de fichier dans ce dépôt. @@ -1497,7 +1503,7 @@ issues.remove_labels=a supprimé les labels %s %s. issues.add_remove_labels=a ajouté le label %s et supprimé %s %s. issues.add_milestone_at=`a ajouté ça au jalon %s %s.` issues.add_project_at=`a ajouté ça au projet %s %s.` -issues.move_to_column_of_project=`a déplacé ça vers %s dans %s sur %s` +issues.move_to_column_of_project=`a déplacé ça vers %s dans %s %s.` issues.change_milestone_at=`a remplacé le jalon %s par %s %s.` issues.change_project_at=`a remplacé le projet %s par %s %s.` issues.remove_milestone_at=`a supprimé ça du jalon %s %s.` @@ -1560,12 +1566,12 @@ issues.action_assignee=Assigné à issues.action_assignee_no_select=Pas d'assignataire issues.action_check=Cocher/Décocher issues.action_check_all=Cocher/Décocher tous les éléments -issues.opened_by=créé %[1]s par %[3]s -pulls.merged_by=par %[3]s fusionné %[1]s. -pulls.merged_by_fake=par %[2]s fusionné %[1]s. -issues.closed_by=de %[3]s, clôt %[1]s -issues.opened_by_fake=%[1]s ouvert par %[2]s -issues.closed_by_fake=de %[2]s, clôt %[1]s +issues.opened_by=ouvert(e) par %[3]s %[1]s +pulls.merged_by=par %[3]s a été fusionnée %[1]s +pulls.merged_by_fake=par %[2]s a été fusionnée %[1]s +issues.closed_by=par %[3]s a été fermé(e) %[1]s +issues.opened_by_fake=ouvert(e) par %[2]s %[1]s +issues.closed_by_fake=par %[2]s a été fermé(e) %[1]s issues.previous=Précédent issues.next=Suivant issues.open_title=Ouvert @@ -1647,7 +1653,7 @@ issues.attachment.open_tab=`Cliquez ici pour voir « %s » dans un nouvel ongl issues.attachment.download=`Cliquez pour télécharger « %s ».` issues.subscribe=S’abonner issues.unsubscribe=Se désabonner -issues.unpin_issue=Désépingler le ticket +issues.unpin=Désépingler issues.max_pinned=Vous ne pouvez pas épingler plus de tickets issues.pin_comment=a épinglé ça %s. issues.unpin_comment=a désépinglé ça %s. @@ -1682,16 +1688,16 @@ issues.timetracker_timer_manually_add=Pointer du temps issues.time_estimate_set=Définir le temps estimé issues.time_estimate_display=Estimation : %s -issues.change_time_estimate_at=a changé le temps estimé à %s %s +issues.change_time_estimate_at=a changé le temps estimé à %[1]s %[2]s issues.remove_time_estimate_at=a supprimé le temps estimé %s issues.time_estimate_invalid=Le format du temps estimé est invalide issues.start_tracking_history=`a commencé son travail %s.` issues.tracker_auto_close=Le minuteur sera automatiquement arrêté quand le ticket sera fermé. issues.tracking_already_started=`Vous avez déjà un minuteur en cours sur un autre ticket !` -issues.stop_tracking_history=`a fini de travailler sur %s %s.` +issues.stop_tracking_history=a travaillé sur %[1]s %[2]s issues.cancel_tracking_history=`a abandonné son minuteur %s.` issues.del_time=Supprimer ce minuteur du journal -issues.add_time_history=`a pointé du temps de travail %s.` +issues.add_time_history=a pointé du temps de travail sur %[1]s, %[2]s issues.del_time_history=`a supprimé son temps de travail %s.` issues.add_time_manually=Temps pointé manuellement issues.add_time_hours=Heures @@ -1733,8 +1739,8 @@ issues.dependency.added_dependency=`a créé une dépendance %s.` issues.dependency.removed_dependency=`a supprimé une dépendance %s.` issues.dependency.pr_closing_blockedby=La fermeture de cette demande d’ajout est bloquée par les tickets suivants issues.dependency.issue_closing_blockedby=La fermeture de ce ticket est bloquée par les tickets suivants -issues.dependency.issue_close_blocks=Cette demande d'ajout empêche la clôture des tickets suivants -issues.dependency.pr_close_blocks=Cette demande d'ajout empêche la clôture des tickets suivants +issues.dependency.issue_close_blocks=Ce ticket empêche la clôture des tickets suivants +issues.dependency.pr_close_blocks=Cette demande d’ajout empêche la clôture des tickets suivants issues.dependency.issue_close_blocked=Vous devez fermer tous les tickets qui bloquent ce ticket avant de pouvoir le fermer. issues.dependency.issue_batch_close_blocked=Impossible de fermer tous les tickets que vous avez choisis, car le ticket #%d a toujours des dépendances ouvertes. issues.dependency.pr_close_blocked=Vous devez fermer tous les tickets qui bloquent cette demande d'ajout avant de pouvoir la fusionner. @@ -1950,6 +1956,7 @@ pulls.upstream_diverging_prompt_behind_1=Cette branche est en retard de %d révi pulls.upstream_diverging_prompt_behind_n=Cette branche est en retard de %d révisions sur %s pulls.upstream_diverging_prompt_base_newer=La branche de base %s a de nouveaux changements pulls.upstream_diverging_merge=Synchroniser la bifurcation +pulls.upstream_diverging_merge_confirm=Voulez-vous fusionner la branche par défaut du dépôt de base sur la branche %s de ce dépôt ? pull.deleted_branch=(supprimé) : %s pull.agit_documentation=Voir la documentation sur AGit @@ -2155,7 +2162,7 @@ settings.advanced_settings=Paramètres avancés settings.wiki_desc=Activer le wiki du dépôt settings.use_internal_wiki=Utiliser le wiki interne settings.default_wiki_branch_name=Nom de la branche du Wiki par défaut -settings.default_wiki_everyone_access=Autorisation d’accès par défaut pour les utilisateurs connectés : +settings.default_permission_everyone_access=Autorisation d’accès par défaut pour tous les utilisateurs connectés : settings.failed_to_change_default_wiki_branch=Impossible de modifier la branche du wiki par défaut. settings.use_external_wiki=Utiliser un wiki externe settings.external_wiki_url=URL Wiki externe @@ -2396,17 +2403,17 @@ settings.packagist_api_token=Jeton API settings.packagist_package_url=URL du paquet Packagist settings.deploy_keys=Clés de déploiement settings.add_deploy_key=Ajouter une clé de déploiement -settings.deploy_key_desc=Les clefs de déploiement ont un accès en lecture seule au dépôt. +settings.deploy_key_desc=Les clés de déploiement ont un accès en lecture seule au dépôt. settings.is_writable=Activer l'accès en écriture settings.is_writable_info=Autoriser cette clé de déploiement à soumettre sur le dépôt. -settings.no_deploy_keys=Il n'y a pas encore de clefs de déploiement. +settings.no_deploy_keys=Il n’y a pas encore de clés de déploiement. settings.title=Titre settings.deploy_key_content=Contenu -settings.key_been_used=Une clef de déploiement identique est déjà en cours d'utilisation. -settings.key_name_used=Une clef de déploiement du même nom existe déjà. -settings.add_key_success=La clé de déploiement "%s" a été ajoutée. -settings.deploy_key_deletion=Supprimer une clef de déploiement -settings.deploy_key_deletion_desc=La suppression d'une clef de déploiement révoque son accès à ce dépôt. Continuer ? +settings.key_been_used=Une clé de déploiement identique est déjà en cours d’utilisation. +settings.key_name_used=Une clé de déploiement du même nom existe déjà. +settings.add_key_success=La clé de déploiement « %s » a été ajoutée. +settings.deploy_key_deletion=Supprimer une clé de déploiement +settings.deploy_key_deletion_desc=La suppression d’une clé de déploiement révoque son accès à ce dépôt. Continuer ? settings.deploy_key_deletion_success=La clé de déploiement a été supprimée. settings.branches=Branches settings.protected_branch=Protection de branche @@ -2623,6 +2630,9 @@ diff.image.overlay=Superposition diff.has_escaped=Cette ligne contient des caractères Unicode cachés diff.show_file_tree=Afficher l’arborescence des fichiers diff.hide_file_tree=Masquer l’arborescence des fichiers +diff.submodule_added=Sous-module %[1]s ajouté à %[2]s +diff.submodule_deleted=Sous-module %[1]s supprimé de %[2]s +diff.submodule_updated=Sous-module %[1]s mis-à-jour : %[2]s releases.desc=Suivi des publications et des téléchargements. release.releases=Publications @@ -2707,6 +2717,8 @@ branch.create_branch_operation=Créer une branche branch.new_branch=Créer une nouvelle branche branch.new_branch_from=`Créer une nouvelle branche à partir de "%s"` branch.renamed=La branche %s à été renommée en %s. +branch.rename_default_or_protected_branch_error=Seuls les administrateurs peuvent renommer les branches par défaut ou protégées. +branch.rename_protected_branch_failed=Cette branche est protégée par des règles de protection basées sur des globs. tag.create_tag=Créer l'étiquette %s tag.create_tag_operation=Créer une étiquette @@ -2861,6 +2873,11 @@ teams.invite.title=Vous avez été invité à rejoindre l'équipe %s%s ? monitor.process.children=Enfant monitor.queues=Files d'attente @@ -3530,6 +3548,7 @@ versions=Versions versions.view_all=Voir tout dependency.id=ID dependency.version=Version +search_in_external_registry=Rechercher dans %s alpine.registry=Configurez ce registre en ajoutant l’URL dans votre fichier /etc/apk/repositories : alpine.registry.key=Téléchargez la clé RSA publique du registre dans le dossier /etc/apk/keys/ pour vérifier la signature de l'index : alpine.registry.info=Choisissez $branch et $repository dans la liste ci-dessous. @@ -3559,7 +3578,8 @@ conda.install=Pour installer le paquet en utilisant Conda, exécutez la commande container.details.type=Type d'image container.details.platform=Plateforme container.pull=Tirez l'image depuis un terminal : -container.digest=Empreinte : +container.images=Images +container.digest=Empreinte container.multi_arch=SE / Arch container.layers=Calques d'image container.labels=Labels @@ -3722,6 +3742,7 @@ runners.status.active=Actif runners.status.offline=Hors-ligne runners.version=Version runners.reset_registration_token=Réinitialiser le jeton d'enregistrement +runners.reset_registration_token_confirm=Voulez-vous révoquer le jeton actuel et en générer un nouveau ? runners.reset_registration_token_success=Le jeton d’inscription de l’exécuteur a été réinitialisé avec succès runs.all_workflows=Tous les flux de travail @@ -3754,6 +3775,7 @@ workflow.not_found=Flux de travail « %s » introuvable. workflow.run_success=Le flux de travail « %s » s’est correctement exécuté. workflow.from_ref=Utiliser le flux de travail depuis workflow.has_workflow_dispatch=Ce flux de travail a un déclencheur d’événement workflow_dispatch. +workflow.has_no_workflow_dispatch=Le flux de travail %s n’a pas de déclencheur d’événement workflow_dispatch. need_approval_desc=Besoin d’approbation pour exécuter des flux de travail pour une demande d’ajout de bifurcation. @@ -3773,6 +3795,8 @@ variables.creation.success=La variable « %s » a été ajoutée. variables.update.failed=Impossible d’éditer la variable. variables.update.success=La variable a bien été modifiée. +logs.always_auto_scroll=Toujours faire défiler les journaux automatiquement +logs.always_expand_running=Toujours développer les journaux en cours [projects] deleted.display_name=Projet supprimé diff --git a/options/locale/locale_ga-IE.ini b/options/locale/locale_ga-IE.ini index 7bf6941e35e..b92714b5999 100644 --- a/options/locale/locale_ga-IE.ini +++ b/options/locale/locale_ga-IE.ini @@ -54,6 +54,7 @@ webauthn_reload=Athlódáil repository=Stór organization=Eagraíocht mirror=Scáthán +issue_milestone=Cloch Mhíle new_repo=Stór Nua new_migrate=Imirce Nua new_mirror=Scáthán Nua @@ -244,6 +245,7 @@ license_desc=Téigh go bhfaighidh doiciméadúchán roimh aon socruithe a athrú. require_db_desc=Éilíonn Gitea MySQL, PostgreSQL, MSSQL, SQLite3 nó TiDB (prótacal MySQL). @@ -573,9 +575,9 @@ alpha_dash_dot_error=` níor cheart go mbeadh ach alfa-uimhriúil, dash ('-'), c git_ref_name_error=` caithfidh gur ainm tagartha Git dea-chruthaithe é.` size_error=` ní mór méid %s.` min_size_error=` ní mór go mbeadh carachtar %s ar a laghad ann.` -max_size_error=` caithfidh `%s carachtar ar a mhéad a bheith ann.` +max_size_error=caithfidh %s carachtar ar a mhéad a bheith ann. email_error=`ní seoladh ríomhphoist bailí é.` -url_error=`ní URL bailí é `"%s". ` +url_error=`ní URL bailí é "%s".` include_error=` ní mór fotheaghrán a bheith ann "%s".` glob_pattern_error=` tá patrún glob neamhbhailí: %s.` regex_pattern_error=`tá patrún regex neamhbhailí: %s.` @@ -1015,7 +1017,9 @@ new_repo_helper=Tá gach comhad tionscadail i stór, lena n-áirítear stair ath owner=Úinéir owner_helper=B'fhéidir nach dtaispeánfar roinnt eagraíochtaí sa anuas mar gheall ar theorainn uasta comhaireamh stórais. repo_name=Ainm Stórais -repo_name_helper=Úsáideann dea-ainmneacha stórtha eochairfhocail ghearr, i gcuimhne agus uathúla. +repo_name_profile_public_hint=Is stóras speisialta é .profile is féidir leat a úsáid chun README.md a chur le do phróifíl eagraíochta poiblí, le feiceáil ag aon duine. Cinntigh go bhfuil sé poiblí agus tosaigh é le README san eolaire próifíle le tosú. +repo_name_profile_private_hint=Is stóras speisialta é .profile-private is féidir leat a úsáid chun README.md a chur le do phróifíl bhall eagraíochta, nach féidir a fheiceáil ach ag baill eagraíochta. Cinntigh go bhfuil sé príobháideach agus tosaigh le README sa eolaire próifíle chun tús a chur leis. +repo_name_helper=Úsáideann ainmneacha maith stóras focail eochair gairide, áithnid agus uathúla. D'fhéadfaí stóras darbh ainm '.profile' nó '.profile-private' a úsáid chun README.md a chur leis an bpróifíl úsáideora/eagraíochta. repo_size=Méid an Stóras template=Teimpléad template_select=Roghnaigh teimpléad. @@ -1112,9 +1116,7 @@ blame.ignore_revs=Ag déanamh neamhairde de leasuithe i .git-blame- blame.ignore_revs.failed=Theip ar neamhaird a dhéanamh ar leasuithe i .git-blame-ignore-revs. user_search_tooltip=Taispeáint uasmhéid de 30 úsáideoir -tree_path_not_found_commit=Níl cosán %[1]s ann i dtiomantas %[2]s -tree_path_not_found_branch=Níl cosán %[1]s ann i mbrainse %[2]s -tree_path_not_found_tag=Níl cosán %[1]s ann i gclib %[2]s +tree_path_not_found=Níl cosán %[1]s ann i %[2]s transfer.accept=Glac le hAistriú transfer.accept_desc=Aistriú chuig “%s” @@ -1232,6 +1234,7 @@ create_new_repo_command=Stóras nua a chruthú ar an líne ordaithe push_exist_repo=Stóras atá ann cheana a bhrú ón líne ordaithe empty_message=Níl aon ábhar sa stóras seo. broken_message=Ní féidir na sonraí Git atá mar bhunús leis an stóras seo a léamh. Déan teagmháil le riarthóir an chás seo nó scrios an stóras seo. +no_branch=Níl aon bhrainsí ag an stóras seo. code=Cód code.desc=Rochtain ar chód foinse, comhaid, gealltanais agus brainsí. @@ -1251,6 +1254,7 @@ labels=Lipéid org_labels_desc=Lipéid ar leibhéal eagraíochta is féidir a úsáid le gach stóras faoin eagraíocht seo org_labels_desc_manage=bainistigh +milestone=Cloch Mhíle milestones=Clocha míle commits=Tiomáintí commit=Tiomantas @@ -1276,7 +1280,7 @@ ambiguous_runes_header=`Tá carachtair Unicode débhríoch sa chomhad seo ` ambiguous_runes_description=`Tá carachtair Unicode sa chomhad seo a d'fhéadfadh a bheith mearbhall le carachtair eile. Má cheapann tú go bhfuil sé seo d'aon ghnó, is féidir leat neamhaird a dhéanamh go sábháilte don rabhadh seo Úsáid an cnaipe Escape chun iad a nochtadh. ` invisible_runes_line=`Tá carachtair unicode dofheicthe ag an líne seo ` ambiguous_runes_line=`Tá carachtair unicode débhríoch ag an líne seo ` -ambiguous_character=Is féidir `%[1]c [U+%04[1]X] a mheascadh le %[2]c [U+%04[2]X]` +ambiguous_character=`Is féidir %[1]c [U+%04[1]X] a mheascadh le %[2]c [U+%04[2]X]` escape_control_characters=Éalú unescape_control_characters=Dí-Éalú @@ -1343,10 +1347,12 @@ editor.new_branch_name_desc=Ainm brainse nua… editor.cancel=Cealaigh editor.filename_cannot_be_empty=Ní féidir ainm an chomhaid a bheith folamh. editor.filename_is_invalid=Tá ainm an chomhaid neamhbhailí: "%s". +editor.commit_email=Tiomantas ríomhphost +editor.invalid_commit_email=Tá an ríomhphost don ghealltanas neamhbhailí. editor.branch_does_not_exist=Níl brainse "%s" ann sa stóras seo. editor.branch_already_exists=Tá brainse "%s" ann cheana féin sa stóras seo. editor.directory_is_a_file=Úsáidtear ainm eolaire "%s" cheana féin mar ainm comhaid sa stóras seo. -editor.file_is_a_symlink=Is nasc siombalach é `"%s". Ní féidir naisc shiombalacha a chur in eagar san eagarthóir gréasáin` +editor.file_is_a_symlink=Is nasc siombalach é "%s". Ní féidir naisc shiombalacha a chur in eagar san eagarthóir gréasáin editor.filename_is_a_directory=Úsáidtear ainm comhaid "%s" cheana féin mar ainm eolaire sa stóras seo. editor.file_editing_no_longer_exists=Níl an comhad atá á chur in eagar, "%s", ann sa stóras seo a thuilleadh. editor.file_deleting_no_longer_exists=Níl an comhad atá á scriosadh, "%s", ann sa stóras seo a thuilleadh. @@ -1644,10 +1650,10 @@ issues.label.filter_sort.by_size=Méid is lú issues.label.filter_sort.reverse_by_size=Méid is mó issues.num_participants=%d Rannpháirtithe issues.attachment.open_tab=`Cliceáil chun "%s" a fheiceáil i gcluaisín nua` -issues.attachment.download=`Cliceáil chun "%s" a íoslódáil +issues.attachment.download=`Cliceáil chun "%s" a íoslódáil` issues.subscribe=Liostáil issues.unsubscribe=Díliostáil -issues.unpin_issue=Bain pionna an t-eagrán +issues.unpin=Díphoráil issues.max_pinned=Ní féidir leat níos mó saincheisteanna a phionadh issues.pin_comment=phionnáil an %s seo issues.unpin_comment=bain pionna an %s seo @@ -1682,16 +1688,16 @@ issues.timetracker_timer_manually_add=Cuir Am leis issues.time_estimate_set=Socraigh am measta issues.time_estimate_display=Meastachán: %s -issues.change_time_estimate_at=d'athraigh an meastachán ama go %s %s +issues.change_time_estimate_at=d'athraigh an meastachán ama go %[1]s %[2]s issues.remove_time_estimate_at=baineadh meastachán ama %s issues.time_estimate_invalid=Tá formáid meastachán ama neamhbhailí issues.start_tracking_history=thosaigh ag obair %s issues.tracker_auto_close=Stopfar ama go huathoibríoch nuair a dhúnfar an tsaincheist seo issues.tracking_already_started=`Tá tús curtha agat cheana féin ag rianú ama ar eagrán eile!` -issues.stop_tracking_history=d'oibrigh do %s %s +issues.stop_tracking_history=d'oibrigh do %[1]s %[2]s issues.cancel_tracking_history=`rianú ama curtha ar ceal %s` issues.del_time=Scrios an log ama seo -issues.add_time_history=cuireadh am caite %s %s leis +issues.add_time_history=cuireadh am caite %[1]s %[2]s leis issues.del_time_history=`an t-am caite scriosta %s` issues.add_time_manually=Cuir Am leis de Láimh issues.add_time_hours=Uaireanta @@ -1950,6 +1956,7 @@ pulls.upstream_diverging_prompt_behind_1=Tá an brainse seo %[1]d tiomantas taob pulls.upstream_diverging_prompt_behind_n=Tá an brainse seo %[1]d geallta taobh thiar de %[2]s pulls.upstream_diverging_prompt_base_newer=Tá athruithe nua ar an mbunbhrainse %s pulls.upstream_diverging_merge=Forc sionc +pulls.upstream_diverging_merge_confirm=Ar mhaith leat "%[1]s" a chumasc le "%[2]s"? pull.deleted_branch=(scriosta): %s pull.agit_documentation=Déan athbhreithniú ar dhoiciméid faoi AGit @@ -2155,7 +2162,7 @@ settings.advanced_settings=Ardsocruithe settings.wiki_desc=Cumasaigh Stór Vicí settings.use_internal_wiki=Úsáid Vicí Insuite settings.default_wiki_branch_name=Ainm Brainse Réamhshocraithe Vicí -settings.default_wiki_everyone_access=Cead Rochtana Réamhshocraithe d'úsáideoirí sínithe isteach: +settings.default_permission_everyone_access=Cead rochtana réamhshocraithe do gach úsáideoir sínithe isteach: settings.failed_to_change_default_wiki_branch=Theip ar an brainse réamhshocraithe vicí a athrú. settings.use_external_wiki=Úsáid Vicí Seachtrach settings.external_wiki_url=URL Vicí Seachtrach @@ -2323,6 +2330,8 @@ settings.event_fork=Forc settings.event_fork_desc=Forcadh stóras. settings.event_wiki=Vicí settings.event_wiki_desc=Leathanach Vicí cruthaithe, athainmnithe, curtha in eagar nó scriosta. +settings.event_statuses=Stádais +settings.event_statuses_desc=Nuashonraíodh Stádas Commit ón API. settings.event_release=Scaoileadh settings.event_release_desc=Scaoileadh foilsithe, nuashonraithe nó scriosta i stóras. settings.event_push=Brúigh @@ -2623,6 +2632,9 @@ diff.image.overlay=Forleagan diff.has_escaped=Tá carachtair Unicode i bhfolach ag an líne seo diff.show_file_tree=Taispeáin crann comhad diff.hide_file_tree=Folaigh crann comhad +diff.submodule_added=Fomhodúl %[1]s curtha leis ag %[2]s +diff.submodule_deleted=Scriosadh fomhodúl %[1]s ó %[2]s +diff.submodule_updated=Nuashonraíodh fomhodúl %[1]s: %[2]s releases.desc=Rian leaganacha tionscadal agus íoslódálacha. release.releases=Eisiúintí @@ -2707,6 +2719,8 @@ branch.create_branch_operation=Cruthaigh brainse branch.new_branch=Cruthaigh brainse nua branch.new_branch_from=`Cruthaigh brainse nua ó "%s"` branch.renamed=Ainmníodh brainse %s go %s. +branch.rename_default_or_protected_branch_error=Ní féidir ach le riarthóirí brainsí réamhshocraithe nó cosanta a athainmniú. +branch.rename_protected_branch_failed=Tá an brainse seo faoi chosaint ag rialacha cosanta domhanda. tag.create_tag=Cruthaigh clib %s tag.create_tag_operation=Cruthaigh clib @@ -2861,6 +2875,19 @@ teams.invite.title=Tugadh cuireadh duit dul isteach i bhfoireann %s%s? monitor.process.children=Leanaí monitor.queues=Scuaineanna @@ -3530,6 +3558,7 @@ versions=Leaganacha versions.view_all=Féach ar gach dependency.id=ID dependency.version=Leagan +search_in_external_registry=Cuardaigh i %s alpine.registry=Socraigh an chlár seo tríd an url a chur i do chomhad /etc/apk/repositories: alpine.registry.key=Íoslódáil eochair RSA poiblí na clárlainne isteach san fhillteán /etc/apk/keys/ chun an síniú innéacs a fhíorú: alpine.registry.info=Roghnaigh $branch agus $repository ón liosta thíos. @@ -3559,7 +3588,8 @@ conda.install=Chun an pacáiste a shuiteáil ag úsáid Conda, reáchtáil an t- container.details.type=Cineál Íomhá container.details.platform=Ardán container.pull=Tarraing an íomhá ón líne ordaithe: -container.digest=Díleáigh: +container.images=Íomhánna +container.digest=Díleáigh container.multi_arch=Córas Oibriúcháin / Ailtireacht container.layers=Sraitheanna Íomhá container.labels=Lipéid @@ -3755,6 +3785,7 @@ workflow.not_found=Níor aimsíodh sreabhadh oibre '%s'. workflow.run_success=Ritheann sreabhadh oibre '%s' go rathúil. workflow.from_ref=Úsáid sreabhadh oibre ó workflow.has_workflow_dispatch=Tá comhoibriú ag an gcur i bhfeidhm seo le himeacht workflow_dispatch. +workflow.has_no_workflow_dispatch=Níl aon truicear teagmhais workflow_dispatch ag sreabhadh oibre '%s'. need_approval_desc=Teastaíonn faomhadh chun sreafaí oibre a rith le haghaidh iarratas tarraingt forc. diff --git a/options/locale/locale_hu-HU.ini b/options/locale/locale_hu-HU.ini index 8e11f07d33a..579a0305673 100644 --- a/options/locale/locale_hu-HU.ini +++ b/options/locale/locale_hu-HU.ini @@ -563,7 +563,6 @@ visibility.private=Privát [repo] owner=Tulajdonos repo_name=Tároló neve -repo_name_helper=A jó tárolónév általában rövid, megjegyezhető és egyedi kulcsszavakból tevődik össze. repo_size=Repozitórium mérete template=Sablon template_select=Válasszon sablont. @@ -1229,6 +1228,8 @@ teams.members.none=Ennek a csapatnak nincsenek tagjai. teams.specific_repositories=Meghatározott tárolók teams.all_repositories=Minden tároló + + [admin] dashboard=Műszerfal users=Felhasználói fiókok diff --git a/options/locale/locale_id-ID.ini b/options/locale/locale_id-ID.ini index 7c8271b98b4..1fb1efbd165 100644 --- a/options/locale/locale_id-ID.ini +++ b/options/locale/locale_id-ID.ini @@ -585,7 +585,6 @@ visibility.private=Pribadi [repo] owner=Pemilik repo_name=Nama Repositori -repo_name_helper=Nama repositori yang baik menggunakan kata kunci yang pendek, unik, dan bisa diingat. repo_size=Ukuran Repositori template=Templat template_select=Pilih template. @@ -819,6 +818,7 @@ issues.attachment.open_tab=`Klik untuk melihat "%s" di tab baru` issues.attachment.download=`Klik untuk mengunduh "%s"` issues.subscribe=Berlangganan issues.unsubscribe=Berhenti berlangganan +issues.unpin=Lepas sematan issues.delete=Hapus @@ -1083,6 +1083,8 @@ teams.add_team_member=Tambahkan Anggota Tim teams.delete_team_success=Tim sudah di hapus. teams.repositories=Tim repositori + + [admin] dashboard=Dasbor organizations=Organisasi diff --git a/options/locale/locale_is-IS.ini b/options/locale/locale_is-IS.ini index d3824f83a21..c8bcf9819e5 100644 --- a/options/locale/locale_is-IS.ini +++ b/options/locale/locale_is-IS.ini @@ -49,6 +49,7 @@ webauthn_reload=Endurhlaða repository=Hugbúnaðarsafn organization=Stofnun mirror=Speglun +issue_milestone=Tímamót new_repo=Nýtt Hugbúnaðarsafn new_migrate=Nýr Flutningur new_mirror=Ný Speglun @@ -652,6 +653,7 @@ projects=Verkefni packages=Pakkar labels=Skýringar +milestone=Tímamót milestones=Tímamót commits=Framlög commit=Framlag @@ -1136,6 +1138,8 @@ teams.settings=Stillingar teams.update_settings=Uppfæra Stillingar teams.all_repositories=Öll hugbúnaðarsöfn + + [admin] repositories=Hugbúnaðarsöfn config=Stilling diff --git a/options/locale/locale_it-IT.ini b/options/locale/locale_it-IT.ini index 1268f156735..f37598eec38 100644 --- a/options/locale/locale_it-IT.ini +++ b/options/locale/locale_it-IT.ini @@ -50,6 +50,7 @@ webauthn_reload=Ricarica repository=Repository organization=Organizzazione mirror=Mirror +issue_milestone=Traguardo new_repo=Nuovo Repository new_migrate=Nuova Migrazione new_mirror=Nuovo Mirror @@ -755,7 +756,6 @@ visibility.private=Privato owner=Proprietario owner_helper=Alcune organizzazioni potrebbero non essere visualizzate nel menu a discesa a causa di un limite massimo al numero di repository. repo_name=Nome Repository -repo_name_helper=Un buon nome per un repository è costituito da parole chiave corte, facili da ricordare e uniche. repo_size=Dimensione repository template=Modello template_select=Seleziona un modello. @@ -943,6 +943,7 @@ labels=Etichette org_labels_desc=Etichette a livello di organizzazione che possono essere utilizzate con tutti i repository sotto questa organizzazione org_labels_desc_manage=gestisci +milestone=Traguardo milestones=Traguardi commits=Commit commit=Commit @@ -2154,6 +2155,8 @@ teams.all_repositories_read_permission_desc=Questo team concede permessi teams.all_repositories_write_permission_desc=Questo team concede permessi di scrittura accesso a tutte le repository: i membri possono leggere e pushare le repository. teams.all_repositories_admin_permission_desc=Questo team concede a Amministratore l'accesso a tutte le repository: i membri possono leggere, pushare e aggiungere collaboratori alle repository. + + [admin] dashboard=Pannello di Controllo users=Account utenti diff --git a/options/locale/locale_ja-JP.ini b/options/locale/locale_ja-JP.ini index efcf34806a4..df0c2b95249 100644 --- a/options/locale/locale_ja-JP.ini +++ b/options/locale/locale_ja-JP.ini @@ -54,6 +54,7 @@ webauthn_reload=リロード repository=リポジトリ organization=組織 mirror=ミラー +issue_milestone=マイルストーン new_repo=新しいリポジトリ new_migrate=新しい移行 new_mirror=新しいミラー @@ -244,6 +245,7 @@ license_desc=Go get %[ [install] install=インストール +installing_desc=インストール中です、お待ちください... title=初期設定 docker_helper=GiteaをDocker内で実行する場合は、設定を変更する前にドキュメントを読んでください。 require_db_desc=Giteaには、MySQL、PostgreSQL、MSSQL、SQLite3、またはTiDB(MySQL プロトコル) が必要です。 @@ -1015,7 +1017,9 @@ new_repo_helper=リポジトリには、プロジェクトのすべてのファ owner=オーナー owner_helper=リポジトリ数の上限により、一部の組織はドロップダウンに表示されない場合があります。 repo_name=リポジトリ名 -repo_name_helper=リポジトリ名は、短く、覚えやすく、他と重複しないキーワードを使用しましょう。 +repo_name_profile_public_hint=.profile は特別なリポジトリで、これを使用して、あなたの組織の公開プロフィール(誰でも閲覧可能)に README.md を追加することができます。 利用を開始するには、必ず公開リポジトリとし、プロフィールディレクトリにREADMEを追加して初期化してください。 +repo_name_profile_private_hint=.profile-private は特別なリポジトリで、これを使用して、あなたの組織のメンバー向けプロフィール(組織メンバーのみ閲覧可能)に README.md を追加することができます。 利用を開始するには、必ずプライベートリポジトリとし、プロフィールディレクトリにREADMEを追加して初期化してください。 +repo_name_helper=リポジトリ名は、短く、覚えやすく、他と重複しないキーワードを使用しましょう。 リポジトリ名を ".profile" または ".profile-private" にして README.md を追加すると、ユーザーや組織のプロフィールとなります。 repo_size=リポジトリサイズ template=テンプレート template_select=テンプレートを選択してください。 @@ -1034,6 +1038,8 @@ fork_to_different_account=別のアカウントにフォークする fork_visibility_helper=フォークしたリポジトリの公開/非公開は変更できません。 fork_branch=フォークにクローンされるブランチ all_branches=すべてのブランチ +view_all_branches=すべてのブランチを表示 +view_all_tags=すべてのタグを表示 fork_no_valid_owners=このリポジトリには有効なオーナーがいないため、フォークできません。 fork.blocked_user=リポジトリのオーナーがあなたをブロックしているため、リポジトリをフォークできません。 use_template=このテンプレートを使用 @@ -1108,10 +1114,9 @@ delete_preexisting_success=%s の未登録ファイルを削除しました blame_prior=この変更より前のBlameを表示 blame.ignore_revs=.git-blame-ignore-revs で指定されたリビジョンは除外しています。 これを迂回して通常のBlame表示を見るには ここをクリック。 blame.ignore_revs.failed=.git-blame-ignore-revs によるリビジョンの無視は失敗しました。 +user_search_tooltip=最大30人までのユーザーを表示 -tree_path_not_found_commit=パス %[1]s はコミット %[2]s に存在しません -tree_path_not_found_branch=パス %[1]s はブランチ %[2]s に存在しません -tree_path_not_found_tag=パス %[1]s はタグ %[2]s に存在しません +tree_path_not_found=パス %[1]s は %[2]s に存在しません transfer.accept=移転を承認 transfer.accept_desc=`"%s" に移転` @@ -1229,6 +1234,7 @@ create_new_repo_command=コマンドラインから新しいリポジトリを push_exist_repo=コマンドラインから既存のリポジトリをプッシュ empty_message=このリポジトリの中には何もありません。 broken_message=このリポジトリの基礎となる Git のデータを読み取れません。このインスタンスの管理者に相談するか、このリポジトリを削除してください。 +no_branch=このリポジトリにはブランチがありません。 code=コード code.desc=ソースコード、ファイル、コミット、ブランチにアクセス。 @@ -1248,6 +1254,7 @@ labels=ラベル org_labels_desc=組織で定義されているラベル (組織のすべてのリポジトリで使用可能なもの) org_labels_desc_manage=編集 +milestone=マイルストーン milestones=マイルストーン commits=コミット commit=コミット @@ -1526,6 +1533,8 @@ issues.filter_assignee=担当者 issues.filter_assginee_no_select=すべての担当者 issues.filter_assginee_no_assignee=担当者なし issues.filter_poster=作成者 +issues.filter_user_placeholder=ユーザーを検索 +issues.filter_user_no_select=すべてのユーザー issues.filter_type=タイプ issues.filter_type.all_issues=すべてのイシュー issues.filter_type.assigned_to_you=自分が担当 @@ -1642,7 +1651,7 @@ issues.attachment.open_tab=`クリックして新しいタブで "%s" を見る` issues.attachment.download=`クリックして "%s" をダウンロード` issues.subscribe=購読する issues.unsubscribe=購読を解除 -issues.unpin_issue=イシューのピン留めを解除 +issues.unpin=ピン留め解除 issues.max_pinned=これ以上イシューをピン留めできません issues.pin_comment=がピン留め %s issues.unpin_comment=がピン留めを解除 %s @@ -1677,16 +1686,16 @@ issues.timetracker_timer_manually_add=時間を追加 issues.time_estimate_set=見積時間を設定 issues.time_estimate_display=見積時間: %s -issues.change_time_estimate_at=が見積時間を %s に変更 %s +issues.change_time_estimate_at=が見積時間を %[1]s に変更 %[2]s issues.remove_time_estimate_at=が見積時間を削除 %s issues.time_estimate_invalid=見積時間のフォーマットが不正です issues.start_tracking_history=が作業を開始 %s issues.tracker_auto_close=タイマーは、このイシューがクローズされると自動的に終了します issues.tracking_already_started=`別のイシューで既にタイムトラッキングを開始しています!` -issues.stop_tracking_history=が %s の作業を終了 %s +issues.stop_tracking_history=が %[1]s の作業を終了 %[2]s issues.cancel_tracking_history=`がタイムトラッキングを中止 %s` issues.del_time=このタイムログを削除 -issues.add_time_history=が作業時間 %s を追加 %s +issues.add_time_history=が作業時間 %[1]s を追加 %[2]s issues.del_time_history=`が作業時間を削除 %s` issues.add_time_manually=時間の手入力 issues.add_time_hours=時間 @@ -1941,8 +1950,11 @@ pulls.delete.title=このプルリクエストを削除しますか? pulls.delete.text=本当にこのプルリクエストを削除しますか? (これはすべてのコンテンツを完全に削除します。 保存しておきたい場合は、代わりにクローズすることを検討してください) pulls.recently_pushed_new_branches=%[2]s 、あなたはブランチ %[1]s にプッシュしました +pulls.upstream_diverging_prompt_behind_1=このブランチは %[2]s よりも %[1]d コミット遅れています +pulls.upstream_diverging_prompt_behind_n=このブランチは %[2]s よりも %[1]d コミット遅れています pulls.upstream_diverging_prompt_base_newer=ベースブランチ %s に新しい変更があります pulls.upstream_diverging_merge=フォークを同期 +pulls.upstream_diverging_merge_confirm=`"%[2]s" に "%[1]s" をマージしてよろしいですか?` pull.deleted_branch=(削除済み):%s pull.agit_documentation=AGitに関するドキュメントを確認する @@ -2148,7 +2160,7 @@ settings.advanced_settings=拡張設定 settings.wiki_desc=Wikiを有効にする settings.use_internal_wiki=ビルトインのWikiを使用する settings.default_wiki_branch_name=デフォルトのWikiブランチ名 -settings.default_wiki_everyone_access=サインインユーザーのデフォルトのアクセス権限: +settings.default_permission_everyone_access=すべてのサインインユーザーにデフォルトで許可するアクセス権限: settings.failed_to_change_default_wiki_branch=デフォルトのWikiブランチを変更できませんでした。 settings.use_external_wiki=外部のWikiを使用する settings.external_wiki_url=外部WikiのURL @@ -2616,6 +2628,9 @@ diff.image.overlay=オーバーレイ diff.has_escaped=この行には不可視Unicode文字があります diff.show_file_tree=ファイルツリーを表示 diff.hide_file_tree=ファイルツリーを隠す +diff.submodule_added=サブモジュール %[1]s が %[2]s で追加されました +diff.submodule_deleted=サブモジュール %[1]s が %[2]s から削除されました +diff.submodule_updated=サブモジュール %[1]s が更新されました: %[2]s releases.desc=プロジェクトバージョンとダウンロードの追跡。 release.releases=リリース @@ -2625,6 +2640,7 @@ release.new_release=新しいリリース release.draft=下書き release.prerelease=プレリリース release.stable=安定版 +release.latest=最新 release.compare=比較 release.edit=編集 release.ahead.commits=%d件のコミット @@ -2699,6 +2715,8 @@ branch.create_branch_operation=ブランチを作成 branch.new_branch=新しいブランチの作成 branch.new_branch_from=`"%s" から新しいブランチを作成` branch.renamed=ブランチ %s は %s にリネームされました。 +branch.rename_default_or_protected_branch_error=デフォルトブランチや保護ブランチのリネームが可能なのは管理者だけです。 +branch.rename_protected_branch_failed=このブランチはglobベースの保護ルールに従って保護されています。 tag.create_tag=タグ %s を作成 tag.create_tag_operation=タグの作成 @@ -2853,6 +2871,11 @@ teams.invite.title=あなたは組織 %[2]s 内のチーム %s? monitor.process.children=子プロセス monitor.queues=キュー @@ -3522,6 +3546,7 @@ versions=バージョン versions.view_all=すべて表示 dependency.id=ID dependency.version=バージョン +search_in_external_registry=%s で検索 alpine.registry=あなたの /etc/apk/repositories ファイルにURLを追加して、このレジストリをセットアップします: alpine.registry.key=インデックス署名の検証のため、レジストリのRSA公開鍵を /etc/apk/keys/ フォルダにダウンロードします: alpine.registry.info=$branch と $repository は下にあるリストから選んでください。 @@ -3530,6 +3555,8 @@ alpine.repository=リポジトリ情報 alpine.repository.branches=Branches alpine.repository.repositories=Repositories alpine.repository.architectures=Architectures +arch.registry=/etc/pacman.conf にリポジトリとアーキテクチャを含めてサーバーを追加します: +arch.install=pacmanでパッケージを同期します: arch.repository=リポジトリ情報 arch.repository.repositories=リポジトリ arch.repository.architectures=Architectures @@ -3549,7 +3576,8 @@ conda.install=Conda を使用してパッケージをインストールするに container.details.type=イメージタイプ container.details.platform=プラットフォーム container.pull=コマンドラインでイメージを取得します: -container.digest=ダイジェスト: +container.images=イメージ +container.digest=ダイジェスト container.multi_arch=OS / アーキテクチャ container.layers=イメージレイヤー container.labels=ラベル @@ -3712,6 +3740,7 @@ runners.status.active=稼働中 runners.status.offline=オフライン runners.version=バージョン runners.reset_registration_token=登録トークンをリセット +runners.reset_registration_token_confirm=現在のトークンを無効にして、新しいトークンを生成しますか? runners.reset_registration_token_success=ランナー登録トークンをリセットしました runs.all_workflows=すべてのワークフロー @@ -3744,6 +3773,7 @@ workflow.not_found=ワークフロー '%s' が見つかりません。 workflow.run_success=ワークフロー '%s' は正常に実行されました。 workflow.from_ref=使用するワークフローの取得元 workflow.has_workflow_dispatch=このワークフローには workflow_dispatch イベントトリガーがあります。 +workflow.has_no_workflow_dispatch=ワークフロー '%s' には workflow_dispatch イベントトリガーがありません。 need_approval_desc=フォークプルリクエストのワークフローを実行するには承認が必要です。 @@ -3763,6 +3793,8 @@ variables.creation.success=変数 "%s" を追加しました。 variables.update.failed=変数を更新できませんでした。 variables.update.success=変数を更新しました。 +logs.always_auto_scroll=常にログを自動スクロール +logs.always_expand_running=常に実行中のログを展開 [projects] deleted.display_name=削除されたプロジェクト diff --git a/options/locale/locale_ko-KR.ini b/options/locale/locale_ko-KR.ini index 361ab23e4cd..05a6d213359 100644 --- a/options/locale/locale_ko-KR.ini +++ b/options/locale/locale_ko-KR.ini @@ -111,7 +111,7 @@ license=오픈 소스 [install] install=설치 title=초기 설정 -docker_helper="Gitea를 Docker에서 실행하려면 설정 전에 이 문서를 읽어보세요." +docker_helper=Gitea를 Docker에서 실행하려면 설정 전에 이 문서를 읽어보세요. db_title=데이터베이스 설정 db_type=데이터베이스 유형 host=호스트 @@ -436,8 +436,8 @@ manage_gpg_keys=GPG 키 관리 add_key=키 추가 ssh_desc=이러한 SSH 공용 키는 귀하의 계정과 연결되어 있습니다. 해당 개인 키는 당신의 저장소에 대한 전체 액세스를 가능하게 합니다. gpg_desc=이러한 GPG 공개키는 당신의 계정과 연결되어있습니다. 커밋이 검증될 수 있도록 당신의 개인 키를 안전하게 유지하십시오. -ssh_helper="도움이 필요하세요? GitHub의 설명서를 참조하시기 바랍니다: SSH 키 생성하기 또는 SSH를 사용할 때 일반적인 문제" -gpg_helper="도움이 필요하세요? GitHub의 설명서를 참조하시기 바랍니다: GPG키에 대하여." +ssh_helper=도움이 필요하세요? GitHub의 설명서를 참조하시기 바랍니다: SSH 키 생성하기 또는 SSH를 사용할 때 일반적인 문제 +gpg_helper=도움이 필요하세요? GitHub의 설명서를 참조하시기 바랍니다: GPG키에 대하여. add_new_key=SSH 키 추가 add_new_gpg_key=GPG 키 추가 gpg_key_id_used=같은 ID의 GPG 공개키가 이미 존재합니다. @@ -531,7 +531,6 @@ visibility.private=비공개 [repo] owner=소유자 repo_name=저장소 이름 -repo_name_helper=좋은 저장소 이름은 보통 짧고 기억하기 좋은 특별한 키워드로 이루어 집니다. repo_size=저장소 용량 template=템플릿 template_select=템플릿 고르기 @@ -539,7 +538,7 @@ template_helper=템플릿으로 저장소 만들기 visibility=가시성 visibility_helper_forced=사이트 관리자가 새 레포지토리에 대해 비공개로만 생성되도록 하였습니다. visibility_fork_helper=(변경사항을 적용하는 경우 모든 포크가 영향을 받게 됩니다.) -clone_helper="클론하는데에 도움이 필요하면 Help에 방문하세요." +clone_helper=클론하는데에 도움이 필요하면 Help에 방문하세요. fork_repo=저장소 포크 fork_from=원본 프로젝트 : fork_visibility_helper=포크된 저장소의 가시성은 변경하실 수 없습니다. @@ -641,7 +640,7 @@ editor.or=혹은 editor.cancel_lower=취소 editor.commit_changes=변경 내용을 커밋 editor.commit_message_desc=선택적 확장 설명을 추가... -editor.commit_directly_to_this_branch="%s 브랜치에서 직접 커밋해주세요." +editor.commit_directly_to_this_branch=%s 브랜치에서 직접 커밋해주세요. editor.create_new_branch=이 커밋에 대한 새로운 브랜치를 만들고 끌어오기 요청을 시작합니다. editor.new_branch_name_desc=새로운 브랜치 명... editor.cancel=취소 @@ -668,7 +667,7 @@ ext_issues.desc=외부 이슈 트래커 연결. projects.description_placeholder=설명 projects.title=제목 projects.new=새 프로젝트 -projects.template.desc="템플릿" +projects.template.desc=템플릿 projects.column.edit_title=이름 projects.column.new_title=이름 @@ -732,7 +731,7 @@ issues.action_milestone=마일스톤 issues.action_milestone_no_select=마일스톤 없음 issues.action_assignee=담당자 issues.action_assignee_no_select=담당자 없음 -issues.opened_by=" %[3]s가 %[1]s을 오픈" +issues.opened_by= %[3]s가 %[1]s을 오픈 issues.previous=이전 issues.next=다음 issues.open_title=오픈 @@ -748,7 +747,7 @@ issues.create_comment=코멘트 issues.commit_ref_at=` 커밋 %[2]s에서 이 이슈 언급` issues.role.owner=소유자 issues.role.member=멤버 -issues.sign_in_require_desc="로그인하여 이 대화에 참여" +issues.sign_in_require_desc=로그인하여 이 대화에 참여 issues.edit=수정 issues.cancel=취소 issues.save=저장 @@ -781,9 +780,9 @@ issues.time_spent_total=총 경과된 시간 issues.time_spent_from_all_authors=`총 경과된 시간: %s` issues.due_date=마감일 -issues.invalid_due_date_format="마감일은 반드시 'yyyy-mm-dd' 형식이어야 합니다." -issues.error_modifying_due_date="마감일 수정을 실패하였습니다." -issues.error_removing_due_date="마감일 삭제를 실패하였습니다." +issues.invalid_due_date_format=마감일은 반드시 'yyyy-mm-dd' 형식이어야 합니다. +issues.error_modifying_due_date=마감일 수정을 실패하였습니다. +issues.error_removing_due_date=마감일 삭제를 실패하였습니다. issues.due_date_form=yyyy-mm-dd issues.due_date_form_add=마감일 추가 issues.due_date_form_edit=편집 @@ -791,8 +790,8 @@ issues.due_date_form_remove=삭제 issues.due_date_not_set=마감일이 설정되지 않았습니다. issues.due_date_added=마감일 %s 를 추가 %s issues.due_date_remove=%s %s 마감일이 삭제되었습니다. -issues.due_date_overdue="기한 초과" -issues.due_date_invalid="기한이 올바르지 않거나 범위를 벗어났습니다. 'yyyy-mm-dd'형식을 사용해주십시오." +issues.due_date_overdue=기한 초과 +issues.due_date_invalid=기한이 올바르지 않거나 범위를 벗어났습니다. 'yyyy-mm-dd'형식을 사용해주십시오. issues.dependency.title=의존성 issues.dependency.add=의존성 추가... issues.dependency.cancel=취소 @@ -810,7 +809,7 @@ issues.dependency.add_error_dep_exists=의존성이 이미 존재합니다. issues.dependency.add_error_dep_not_same_repo=두 이슈는 같은 레포지토리 안에 있어야 합니다. issues.review.self.approval=자신의 풀 리퀘스트를 승인할 수 없습니다. issues.review.self.rejection=자신의 풀 리퀘스트에 대한 변경을 요청할 수 없습니다. -issues.review.approve="이 변경사항을 승인하였습니다. %s" +issues.review.approve=이 변경사항을 승인하였습니다. %s issues.review.pending=보류 issues.review.review=검토 issues.review.reviewers=리뷰어 @@ -825,7 +824,7 @@ pulls.compare_base=병합하기 pulls.compare_compare=다음으로부터 풀 pulls.filter_branch=Filter Branch pulls.create=풀 리퀘스트 생성 -pulls.title_desc="%[2]s 에서 %[3]s 로 %[1]d commits 를 머지하려 합니다" +pulls.title_desc=%[2]s 에서 %[3]s 로 %[1]d commits 를 머지하려 합니다 pulls.merged_title_desc=%[2]s 에서 %[3]s 로 %[1]d commits 를 머지했습니다 %[4]s pulls.tab_conversation=대화 pulls.tab_commits=커밋 @@ -856,7 +855,7 @@ milestones.title=타이틀 milestones.desc=설명 milestones.due_date=기한 (선택 사항) milestones.clear=지우기 -milestones.invalid_due_date_format="마감일은 반드시 'yyyy-mm-dd' 형식이어야 합니다." +milestones.invalid_due_date_format=마감일은 반드시 'yyyy-mm-dd' 형식이어야 합니다. milestones.edit=마일스톤 편집 milestones.cancel=취소 milestones.modify=마일스톤 갱신 @@ -1191,6 +1190,8 @@ teams.repositories=팀 저장소 teams.add_duplicate_users=사용자가 이미 팀 멤버입니다. teams.members.none=이 팀에 멤버가 없습니다. + + [admin] dashboard=대시보드 users=사용자 계정 diff --git a/options/locale/locale_lv-LV.ini b/options/locale/locale_lv-LV.ini index 4aa069b6639..6fc6e53800a 100644 --- a/options/locale/locale_lv-LV.ini +++ b/options/locale/locale_lv-LV.ini @@ -54,6 +54,7 @@ webauthn_reload=Pārlādēt repository=Repozitorijs organization=Organizācija mirror=Spogulis +issue_milestone=Atskaites punktus new_repo=Jauns repozitorijs new_migrate=Jauna migrācija new_mirror=Jauns spogulis @@ -913,7 +914,6 @@ new_repo_helper=Repozitorijs satur visus projekta failus, tajā skaitā izmaiņu owner=Īpašnieks owner_helper=Ņemot vērā maksimālā repozitoriju skaita ierobežojumu, ne visas organizācijas var tikt parādītas sarakstā. repo_name=Repozitorija nosaukums -repo_name_helper=Labi repozitorija nosaukumi ir īsi, unikāli un tādi, ko viegli atcerēties. repo_size=Repozitorija izmērs template=Sagatave template_select=Izvēlieties sagatavi. @@ -998,9 +998,6 @@ blame_prior=Aplūkot vainīgo par izmaiņām pirms šīs revīzijas blame.ignore_revs=Neņem vērā izmaiņas no .git-blame-ignore-revs. Nospiediet šeit, lai to apietu un redzētu visu izmaiņu skatu. blame.ignore_revs.failed=Neizdevās neņemt vērā izmaiņas no .git-blam-ignore-revs. -tree_path_not_found_commit=Revīzijā %[2]s neeksistē ceļš %[1]s -tree_path_not_found_branch=Atzarā %[2]s nepastāv ceļš %[1]s -tree_path_not_found_tag=Tagā %[2]s nepastāv ceļš %[1]s transfer.accept=Apstiprināt īpašnieka maiņu transfer.reject=Noraidīt īpašnieka maiņu @@ -1129,6 +1126,7 @@ labels=Iezīmes org_labels_desc=Organizācijas līmeņa iezīmes var tikt izmantotas visiem repozitorijiem šajā organizācijā org_labels_desc_manage=pārvaldīt +milestone=Atskaites punktus milestones=Atskaites punkti commits=Revīzijas commit=Revīzija @@ -1502,7 +1500,7 @@ issues.attachment.open_tab=`Noklikšķiniet, lai apskatītos "%s" jaunā logā` issues.attachment.download=`Noklikšķiniet, lai lejupielādētu "%s"` issues.subscribe=Abonēt issues.unsubscribe=Atrakstīties -issues.unpin_issue=Atspraust problēmu +issues.unpin=Atspraust issues.max_pinned=Nevar piespraust vairāk problēmas issues.pin_comment=piesprauda šo %s issues.unpin_comment=atsprauda šo %s @@ -2596,6 +2594,8 @@ teams.invite.title=Tu esi uzaicināts pievienoties organizācijas %[2]s< teams.invite.by=Uzaicināja %s teams.invite.description=Nospiediet pogu zemāk, lai pievienotos komandai. + + [admin] dashboard=Infopanelis self_check=Pašpārbaude @@ -3242,7 +3242,6 @@ conda.install=Lai instalētu Conda pakotni, izpildiet sekojošu komandu: container.details.type=Attēla formāts container.details.platform=Platforma container.pull=Atgādājiet šo attēlu no komandrindas: -container.digest=Īssavilkums: container.multi_arch=OS / arhitektūra container.layers=Attēla slāņi container.labels=Iezīmes diff --git a/options/locale/locale_nl-NL.ini b/options/locale/locale_nl-NL.ini index fe41c4529aa..9f61d34bc07 100644 --- a/options/locale/locale_nl-NL.ini +++ b/options/locale/locale_nl-NL.ini @@ -50,6 +50,7 @@ webauthn_reload=Vernieuwen repository=Repository organization=Organisatie mirror=Kopie +issue_milestone=Mijlpaal new_repo=Nieuwe repository new_migrate=Nieuwe migratie new_mirror=Nieuwe kopie @@ -753,7 +754,6 @@ visibility.private=Privé owner=Eigenaar owner_helper=Sommige organisaties kunnen niet worden weergegeven in de dropdown vanwege een limiet op het maximale aantal repositories. repo_name=Naam van repository -repo_name_helper=Goede repository-namen zijn kort, makkelijk te onthouden en uniek. repo_size=Repositorygrootte template=Sjabloon template_select=Selecteer een sjabloon. @@ -941,6 +941,7 @@ labels=Labels org_labels_desc=Organisatielabel dat gebruikt kan worden met alle repositories onder deze organisatie org_labels_desc_manage=beheren +milestone=Mijlpaal milestones=Mijlpalen commits=Commits commit=Commit @@ -2055,6 +2056,8 @@ teams.all_repositories=Alle repositories teams.all_repositories_helper=Team heeft toegang tot alle repositories. Door dit te selecteren worden alle bestaande repositories aan het team toegevoegd. teams.all_repositories_read_permission_desc=Dit team heeft Lees toegang tot alle repositories: leden kunnen repositories bekijken en klonen. + + [admin] dashboard=Overzicht users=Gebruikersacount diff --git a/options/locale/locale_pl-PL.ini b/options/locale/locale_pl-PL.ini index 13c05eebe07..958310a085c 100644 --- a/options/locale/locale_pl-PL.ini +++ b/options/locale/locale_pl-PL.ini @@ -711,7 +711,6 @@ visibility.private=Prywatny owner=Właściciel owner_helper=Niektóre organizacje mogą nie pojawiać się w liście ze względu na limit maksymalnej liczby repozytoriów. repo_name=Nazwa repozytorium -repo_name_helper=Dobra nazwa repozytorium jest utworzona z krótkich, łatwych do zapamiętania i unikalnych słów kluczowych. repo_size=Rozmiar repozytorium template=Szablon template_select=Wybierz szablon. @@ -1934,6 +1933,8 @@ teams.all_repositories_read_permission_desc=Ten zespół nadaje uprawnienie Zapisu do wszystkich repozytoriów: jego członkowie mogą odczytywać i przesyłać do repozytoriów. teams.all_repositories_admin_permission_desc=Ten zespół nadaje uprawnienia Administratora do wszystkich repozytoriów: jego członkowie mogą odczytywać, przesyłać oraz dodawać innych współtwórców do repozytoriów. + + [admin] dashboard=Pulpit users=Konta użytkownika @@ -2310,7 +2311,6 @@ monitor.start=Czas rozpoczęcia monitor.execute_time=Czas wykonania monitor.process.cancel=Anuluj proces monitor.process.cancel_desc=Anulowanie procesu może spowodować utratę danych -monitor.process.cancel_notices=Anuluj: %s? monitor.queues=Kolejki monitor.queue=Kolejka: %s diff --git a/options/locale/locale_pt-BR.ini b/options/locale/locale_pt-BR.ini index 9a8b6aeb62c..9df7e5e9567 100644 --- a/options/locale/locale_pt-BR.ini +++ b/options/locale/locale_pt-BR.ini @@ -52,6 +52,7 @@ webauthn_reload=Recarregar repository=Repositório organization=Organização mirror=Espelhamento +issue_milestone=Marco new_repo=Novo repositório new_migrate=Nova migração new_mirror=Novo espelhamento @@ -908,7 +909,6 @@ new_repo_helper=Um repositório contém todos os arquivos do projeto, inclusive owner=Proprietário owner_helper=Algumas organizações podem não aparecer no menu devido a um limite de contagem dos repositórios. repo_name=Nome do repositório -repo_name_helper=Um bom nome de repositório é composto por palavras curtas, memorizáveis e únicas. repo_size=Tamanho do repositório template=Modelo template_select=Selecione um modelo. @@ -1120,6 +1120,7 @@ labels=Etiquetas org_labels_desc=Rótulos de nível de organização que podem ser usados em todos os repositórios sob esta organização org_labels_desc_manage=gerenciar +milestone=Marco milestones=Marcos commits=Commits commit=Commit @@ -1136,9 +1137,9 @@ file_view_raw=Ver original file_permalink=Link permanente file_too_large=O arquivo é muito grande para ser mostrado. invisible_runes_header=`Este arquivo contém caracteres Unicode invisíveis` -invisible_runes_description=`Este arquivo contém caracteres Unicode invisíveis que são indistinguíveis para humanos, mas que podem ser processados de forma diferente por um computador. Se você acha que isso é intencional, pode ignorar esse aviso com segurança. Use o botão Escapar para revelá-los +invisible_runes_description=`Este arquivo contém caracteres Unicode invisíveis que são indistinguíveis para humanos, mas que podem ser processados de forma diferente por um computador. Se você acha que isso é intencional, pode ignorar esse aviso com segurança. Use o botão Escapar para revelá-los` ambiguous_runes_header=`Este arquivo contém caracteres Unicode ambíguos` -ambiguous_runes_description=`Este arquivo contém caracteres Unicode que podem ser confundidos com outros caracteres. Se você acha que isso é intencional, pode ignorar esse aviso com segurança. Use o botão Escapar para revelá-los +ambiguous_runes_description=`Este arquivo contém caracteres Unicode que podem ser confundidos com outros caracteres. Se você acha que isso é intencional, pode ignorar esse aviso com segurança. Use o botão Escapar para revelá-los` invisible_runes_line=`Esta linha tem caracteres unicode invisíveis` ambiguous_runes_line=`Esta linha tem caracteres unicode ambíguos` @@ -1492,7 +1493,7 @@ issues.attachment.open_tab=`Clique para ver "%s" em uma nova aba` issues.attachment.download=`Clique para baixar "%s"` issues.subscribe=Inscrever-se issues.unsubscribe=Desinscrever -issues.unpin_issue=Desfixar issue +issues.unpin=Desfixar issues.max_pinned=Você não pode fixar mais issues issues.pin_comment=fixou isto %s issues.unpin_comment=desafixou isto %s @@ -2551,6 +2552,8 @@ teams.invite.title=Você foi convidado para fazer parte da equipe %sdocumentação antes de alterar quaisquer configurações. require_db_desc=Gitea requer MySQL, PostgreSQL, MSSQL, SQLite3 ou TiDB (protocolo MySQL). @@ -260,7 +262,7 @@ path=Localização sqlite_helper=Localização do ficheiro da base de dados em SQLite3.
Insira um caminho absoluto se corre o Gitea como um serviço. reinstall_error=Está a tentar instalar numa base de dados do Gitea já existente reinstall_confirm_message=Reinstalar com uma base de dados do Gitea já existente pode causar múltiplos problemas. Na maioria dos casos deve usar o seu "app.ini" existente para correr o Gitea. Se souber o que está a fazer, confirme o seguinte: -reinstall_confirm_check_1=Os dados encriptados pela chave secreta (SECRET_KEY) no ficheiro app.ini poderão ser perdidos: utilizadores poderão não ser capazes de iniciar a sessão com autenticação em dois passos (2FA) ou com chaves de utilização única (OTP) e as réplicas poderão deixar de funcionar em condições. Ao marcar esta opção estará a confirmar que o ficheiro app.ini vigente contém a SECRET_KEY certa. +reinstall_confirm_check_1=Os dados encriptados pela chave secreta (SECRET_KEY) no ficheiro app.ini poderão ser perdidos: utilizadores poderão não ser capazes de iniciar a sessão com autenticação em dois passos (2FA) ou com chaves de utilização única (OTP) e as réplicas poderão deixar de funcionar em boas condições. Ao marcar esta opção estará a confirmar que o ficheiro app.ini vigente contém a SECRET_KEY certa. reinstall_confirm_check_2=Os repositórios e as configurações poderão ter de voltar a ser sincronizados. Ao marcar esta opção estará a confirmar que vai voltar a sincronizar manualmente os automatismos para os repositórios e o ficheiro authorized_keys. Estará também a confirmar que vai assegurar que as configurações do repositório e das réplicas estão em condições. reinstall_confirm_check_3=Você confirma que tem a certeza absoluta de que este Gitea está a correr com a localização certa do ficheiro app.ini e que tem a certeza de que tem de voltar a instalar. Você confirma que tomou conhecimento dos riscos acima descritos. err_empty_db_path=A localização da base de dados SQLite3 não pode estar vazia. @@ -494,7 +496,7 @@ register_notify.text_3=Se esta conta foi criada para si, defina a s reset_password=Recupere a sua conta reset_password.title=%s, você pediu para recuperar a sua conta -reset_password.text=Por favor clique na seguinte ligação para recuperar a sua conta em %s: +reset_password.text=Para recuperar a sua conta, clique na ligação seguinte (válida por %s): register_success=Inscrição bem sucedida @@ -1015,7 +1017,9 @@ new_repo_helper=Um repositório contém todos os ficheiros do trabalho, incluind owner=Proprietário(a) owner_helper=Algumas organizações podem não aparecer na lista suspensa devido a um limite máximo de contagem de repositórios. repo_name=Nome do repositório -repo_name_helper=Um bom nome de repositório utiliza palavras curtas, memoráveis e únicas. +repo_name_profile_public_hint=.profile é um repositório especial que pode usar para adicionar README.md ao seu perfil público da organização, visível para qualquer pessoa. Certifique-se que é público e inicialize-o com um README na pasta do perfil para começar. +repo_name_profile_private_hint=.profile-private é um repositório especial que pode usar para adicionar um README.md ao seu perfil de membro da organização, visível apenas para membros da organização. Certifique-se que é privado e inicialize-o com um README na pasta de perfil para começar. +repo_name_helper=Bons nomes de repositórios usam palavras-chave curtas, memorizáveis e únicas. Um repositório chamado ".profile" ou ".profile-private" pode ser usado para adicionar um README.md ao perfil do utilizador ou da organização. repo_size=Tamanho do repositório template=Modelo template_select=Escolha um modelo. @@ -1048,7 +1052,7 @@ generate_from=Gerar a partir de repo_desc=Descrição repo_desc_helper=Insira uma descrição curta (opcional) repo_no_desc=Descrição não fornecida -repo_lang=Idiomas +repo_lang=Linguagens repo_gitignore_helper=Escolher modelos .gitignore. repo_gitignore_helper_desc=Escolha os ficheiros que não são para rastrear, a partir de uma lista de modelos de linguagens comuns. Serão incluídos no ficheiro .gitignore, logo à partida, artefactos típicos gerados pelas ferramentas de construção de cada uma das linguagens. issue_labels=Rótulos para as questões @@ -1112,9 +1116,7 @@ blame.ignore_revs=Ignorando as revisões em .git-blame-ignore-revs< blame.ignore_revs.failed=Falhou ao ignorar as revisões em .git-blame-ignore-revs. user_search_tooltip=Mostra um máximo de 30 utilizadores -tree_path_not_found_commit=A localização %[1]s não existe no cometimento %[2]s -tree_path_not_found_branch=A localização %[1]s não existe no ramo %[2]s -tree_path_not_found_tag=A localização %[1]s não existe na etiqueta %[2]s +tree_path_not_found=A localização %[1]s não existe em %[2]s transfer.accept=Aceitar transferência transfer.accept_desc=`Transferir para "%s"` @@ -1232,6 +1234,7 @@ create_new_repo_command=Criando um novo repositório na linha de comandos push_exist_repo=Enviando, pela linha de comandos, um repositório existente empty_message=Este repositório não contém qualquer conteúdo. broken_message=Os dados Git subjacentes a este repositório não podem ser lidos. Contacte o administrador desta instância ou elimine este repositório. +no_branch=Este repositório não tem quaisquer ramos. code=Código code.desc=Aceder ao código fonte, ficheiros, cometimentos e ramos. @@ -1251,6 +1254,7 @@ labels=Rótulos org_labels_desc=Rótulos ao nível da organização que podem ser usados em todos os repositórios desta organização org_labels_desc_manage=gerir +milestone=Etapa milestones=Etapas commits=Cometimentos commit=Cometimento @@ -1343,6 +1347,8 @@ editor.new_branch_name_desc=Nome do novo ramo… editor.cancel=Cancelar editor.filename_cannot_be_empty=O nome do ficheiro não pode estar em branco. editor.filename_is_invalid=O nome do ficheiro é inválido: "%s". +editor.commit_email=Email do cometimento +editor.invalid_commit_email=O email do comentimento é inválido. editor.branch_does_not_exist=O ramo "%s" não existe neste repositório. editor.branch_already_exists=O ramo "%s" já existe neste repositório. editor.directory_is_a_file=O nome da pasta "%s" já é usado como um nome de ficheiro neste repositório. @@ -1647,7 +1653,7 @@ issues.attachment.open_tab=`Clique para ver "%s" num separador novo` issues.attachment.download=`Clique para descarregar "%s"` issues.subscribe=Subscrever issues.unsubscribe=Anular subscrição -issues.unpin_issue=Desafixar questão +issues.unpin=Desafixar issues.max_pinned=Já não pode fixar mais questões issues.pin_comment=fixou isto %s issues.unpin_comment=desafixou isto %s @@ -1682,16 +1688,16 @@ issues.timetracker_timer_manually_add=Adicionar tempo issues.time_estimate_set=Definir tempo estimado issues.time_estimate_display=Estimativa: %s -issues.change_time_estimate_at=alterou a estimativa de tempo para %s %s +issues.change_time_estimate_at=alterou a estimativa de tempo para %[1]s %[2]s issues.remove_time_estimate_at=removeu a estimativa de tempo %s issues.time_estimate_invalid=O formato da estimativa de tempo é inválido issues.start_tracking_history=começou a trabalhar %s issues.tracker_auto_close=O cronómetro será parado automaticamente quando esta questão for fechada issues.tracking_already_started=`Você já iniciou a contagem de tempo noutra questão!` -issues.stop_tracking_history=trabalhou durante %s %s +issues.stop_tracking_history=trabalhou durante %[1]s %[2]s issues.cancel_tracking_history=`cancelou a contagem de tempo %s` issues.del_time=Eliminar este registo de tempo -issues.add_time_history=adicionou %s de tempo gasto %s +issues.add_time_history=adicionou %[1]s de tempo gasto %[2]s issues.del_time_history=`eliminou o tempo gasto nesta questão %s` issues.add_time_manually=Adicionar tempo manualmente issues.add_time_hours=Horas @@ -1950,6 +1956,7 @@ pulls.upstream_diverging_prompt_behind_1=Este ramo está %[1]d cometimento atrá pulls.upstream_diverging_prompt_behind_n=Este ramo está %[1]d cometimentos atrás de %[2]s pulls.upstream_diverging_prompt_base_newer=O ramo base %s tem novas modificações pulls.upstream_diverging_merge=Sincronizar derivação +pulls.upstream_diverging_merge_confirm=Gostaria de integrar "%[1]s" em "%[2]s"? pull.deleted_branch=(eliminado):%s pull.agit_documentation=Rever a documentação sobre o AGit @@ -2155,7 +2162,7 @@ settings.advanced_settings=Configurações avançadas settings.wiki_desc=Habilitar wiki do repositório settings.use_internal_wiki=Usar o wiki integrado settings.default_wiki_branch_name=Nome do ramo predefinido do wiki -settings.default_wiki_everyone_access=Permissão de acesso predefinida para utilizadores registados: +settings.default_permission_everyone_access=Permissão de acesso predefinida para todos os utilizadores registados: settings.failed_to_change_default_wiki_branch=Falhou ao mudar o nome do ramo predefinido do wiki. settings.use_external_wiki=Usar um wiki externo settings.external_wiki_url=URL do wiki externo @@ -2323,6 +2330,8 @@ settings.event_fork=Derivar settings.event_fork_desc=Feita a derivação do repositório. settings.event_wiki=Wiki settings.event_wiki_desc=Página do wiki criada, renomeada, editada ou eliminada. +settings.event_statuses=Estados +settings.event_statuses_desc=Estado do cometimento modificado através da API. settings.event_release=Lançamento settings.event_release_desc=Lançamento publicado, modificado ou eliminado num repositório. settings.event_push=Enviar @@ -2623,6 +2632,9 @@ diff.image.overlay=Sobrepor diff.has_escaped=Esta linha tem caracteres unicode escondidos diff.show_file_tree=Mostrar árvore de ficheiros diff.hide_file_tree=Esconder árvore de ficheiros +diff.submodule_added=Submódulo %[1]s adicionado em %[2]s +diff.submodule_deleted=Submódulo %[1]s eliminado de %[2]s +diff.submodule_updated=Submódulo %[1]s modificado: %[2]s releases.desc=Acompanhe as versões e as descargas do repositório. release.releases=Lançamentos @@ -2707,6 +2719,8 @@ branch.create_branch_operation=Criar ramo branch.new_branch=Criar um novo ramo branch.new_branch_from=`Criar um novo ramo a partir do ramo "%s"` branch.renamed=O ramo %s foi renomeado para %s. +branch.rename_default_or_protected_branch_error=Só os administradores é que podem renomear o ramo principal ou ramos protegidos. +branch.rename_protected_branch_failed=Este ramo está protegido por regras de salvaguarda baseadas em padrões glob. tag.create_tag=Criar etiqueta %s tag.create_tag_operation=Criar etiqueta @@ -2861,6 +2875,19 @@ teams.invite.title=Foi-lhe feito um convite para se juntar à equipa %s< teams.invite.by=Convidado(a) por %s teams.invite.description=Clique no botão abaixo para se juntar à equipa. +view_as_role=Ver como: %s +view_as_public_hint=Está a ver o README como um utilizador público. +view_as_member_hint=Está a ver o README como um membro desta organização. + +worktime=Tempo de trabalho +worktime.date_range_start=Data do início +worktime.date_range_end=Data do fim +worktime.query=Consulta +worktime.time=Tempo +worktime.by_repositories=Por repositórios +worktime.by_milestones=Por etapas +worktime.by_members=Por membros + [admin] maintenance=Manutenção dashboard=Painel de controlo @@ -3355,6 +3382,8 @@ monitor.previous=Execução anterior monitor.execute_times=Execuções monitor.process=Processos em execução monitor.stacktrace=Vestígios da pilha +monitor.trace=Rastreio +monitor.performance_logs=Registos de desempenho monitor.processes_count=%d processos monitor.download_diagnosis_report=Descarregar relatório de diagnóstico monitor.desc=Descrição @@ -3363,7 +3392,6 @@ monitor.execute_time=Tempo de execução monitor.last_execution_result=Resultado monitor.process.cancel=Cancelar processo monitor.process.cancel_desc=Cancelar um processo pode resultar na perda de dados -monitor.process.cancel_notices=Cancelar: %s? monitor.process.children=Descendentes monitor.queues=Filas @@ -3530,6 +3558,7 @@ versions=Versões versions.view_all=Ver todas dependency.id=ID dependency.version=Versão +search_in_external_registry=Procurar em %s alpine.registry=Configure este registo adicionando o URL no seu ficheiro /etc/apk/repositories: alpine.registry.key=Descarregue a chave RSA pública do registo para dentro da pasta /etc/apk/keys/ para verificar a assinatura do índice: alpine.registry.info=Escolha $branch e $repository da lista abaixo. @@ -3559,7 +3588,8 @@ conda.install=Para instalar o pacote usando o Conda, execute o seguinte comando: container.details.type=Tipo de imagem container.details.platform=Plataforma container.pull=Puxar a imagem usando a linha de comandos: -container.digest=Resumo: +container.images=Imagens +container.digest=Resumo container.multi_arch=S.O. / Arquit. container.layers=Camadas de imagem container.labels=Rótulos @@ -3755,6 +3785,7 @@ workflow.not_found=A sequência de trabalho '%s' não foi encontrada. workflow.run_success=A sequência de trabalho '%s' foi executada com sucesso. workflow.from_ref=Usar sequência de trabalho de workflow.has_workflow_dispatch=Esta sequência de trabalho tem um despoletador de eventos workflow_dispatch. +workflow.has_no_workflow_dispatch=A sequência de trabalho '%s' não tem nenhum despoletador de eventos workflow_dispatch. need_approval_desc=É necessária aprovação para executar sequências de trabalho para a derivação do pedido de integração. diff --git a/options/locale/locale_ru-RU.ini b/options/locale/locale_ru-RU.ini index dc72dd7621b..f2b4e284053 100644 --- a/options/locale/locale_ru-RU.ini +++ b/options/locale/locale_ru-RU.ini @@ -52,6 +52,7 @@ webauthn_reload=Обновить repository=Репозиторий organization=Организация mirror=Зеркало +issue_milestone=Этап new_repo=Новый репозиторий new_migrate=Новая миграция new_mirror=Новое зеркало @@ -899,7 +900,6 @@ visibility.private_tooltip=Виден только членам организа owner=Владелец owner_helper=Некоторые организации могут не отображаться в раскрывающемся списке из-за максимального ограничения количества репозиториев. repo_name=Название репозитория -repo_name_helper=Лучшие названия репозиториев состоят из коротких, легко запоминаемых и уникальных ключевых слов. repo_size=Размер репозитория template=Шаблон template_select=Выбрать шаблон. @@ -979,8 +979,6 @@ delete_preexisting_content=Удалить файлы из %s delete_preexisting_success=Удалены непринятые файлы в %s blame_prior=Показать авторство предшествующих изменений -tree_path_not_found_commit=Путь %[1]s не существует в коммите %[2]s -tree_path_not_found_branch=Путь %[1]s не существует в ветке %[2]s transfer.accept=Принять трансфер transfer.reject=Отказаться от перемещения @@ -1103,6 +1101,7 @@ labels=Метки org_labels_desc=Метки уровня организации, которые можно использовать с всеми репозиториями в этой организации org_labels_desc_manage=управлять +milestone=Этап milestones=Этапы commits=коммитов commit=коммит @@ -1472,7 +1471,7 @@ issues.attachment.open_tab=`Нажмите, чтобы увидеть «%s» в issues.attachment.download=`Нажмите, чтобы скачать «%s»` issues.subscribe=Подписаться issues.unsubscribe=Отказаться от подписки -issues.unpin_issue=Открепить задачу +issues.unpin=Открепить issues.max_pinned=Нельзя закрепить больше задач issues.pin_comment=закрепил(а) эту задачу %s issues.unpin_comment=открепил(а) эту задачу %s @@ -2542,6 +2541,8 @@ teams.invite.title=Вас пригласили присоединиться к teams.invite.by=Приглашен(а) %s teams.invite.description=Нажмите на кнопку ниже, чтобы присоединиться к команде. + + [admin] dashboard=Панель identity_access=Идентификация и доступ @@ -3178,7 +3179,6 @@ conda.install=Чтобы установить пакет с помощью Conda container.details.type=Тип образа container.details.platform=Платформа container.pull=Загрузите образ из командной строки: -container.digest=Отпечаток: container.multi_arch=ОС / архитектура container.layers=Слои образа container.labels=Метки diff --git a/options/locale/locale_si-LK.ini b/options/locale/locale_si-LK.ini index f722d160eb5..2a0b9394d5b 100644 --- a/options/locale/locale_si-LK.ini +++ b/options/locale/locale_si-LK.ini @@ -693,7 +693,6 @@ visibility.private=පෞද්ගලික owner=හිමිකරු owner_helper=උපරිම නිධි ගණන් සීමාවක් හේතුවෙන් සමහර සංවිධාන පහත වැටීමේ දී පෙන්විය නොහැක. repo_name=කෝෂ්ඨයේ නම -repo_name_helper=හොඳ ගබඩාවක් නම් කෙටි, අමතක නොවන සහ අද්විතීය මූල පද භාවිතා කරයි. repo_size=කෝෂ්ඨයේ ප්‍රමාණය template=සැකිල්ල template_select=අච්චුවක් තෝරන්න. @@ -1955,6 +1954,8 @@ teams.all_repositories_read_permission_desc=මෙම කණ්ඩායම ප teams.all_repositories_write_permission_desc=මෙම කණ්ඩායම ප්රදානය කරයි වෙත ප්රවේශය ලියන්න සියලු ගබඩාවන්ට: සාමාජිකයින්ට කියවීමට සහ ගබඩාවන්ට තල්ලු කළ හැකිය. teams.all_repositories_admin_permission_desc=මෙම කණ්ඩායම ප්රදානය කරයි පරිපාලක වෙත ප්රවේශය සියලු ගබඩාවන්ට: සාමාජිකයින්ට කියවීමට, තල්ලු කිරීමට සහ ගබඩාවන්ට සහයෝගීකයින් එකතු කිරීමට. + + [admin] dashboard=උපකරණ පුවරුව users=පරිශීලක ගිණුම් diff --git a/options/locale/locale_sk-SK.ini b/options/locale/locale_sk-SK.ini index 39c13c358ef..e323c2befa8 100644 --- a/options/locale/locale_sk-SK.ini +++ b/options/locale/locale_sk-SK.ini @@ -53,6 +53,7 @@ webauthn_reload=Znovu načítať repository=Repozitár organization=Organizácia mirror=Zrkadlo +issue_milestone=Míľnik new_repo=Nový repozitár new_migrate=Nová migrácia new_mirror=Nové zrkadlo @@ -817,7 +818,6 @@ visibility.private=Súkromný owner=Vlastník owner_helper=Niektoré organizácie sa nemusia zobraziť v rozbaľovacej ponuke z dôvodu maximálneho limitu počtu repozitárov. repo_name=Názov repozitára -repo_name_helper=Dobrý názov repozitára sa zvyčajne skladá z krátkych, jedinečných a ľahko zapamätateľných kľúčových slov. repo_size=Veľkosť repozitára template=Šablóna template_select=Vyberte šablónu. @@ -968,6 +968,7 @@ labels=Štítky org_labels_desc=Štítky na úrovni organizácie, ktoré možno použiť so všetkými repozitármi v rámci tejto organizácie org_labels_desc_manage=spravovať +milestone=Míľnik milestones=Míľniky commits=Commitov release=Vydanie @@ -1069,6 +1070,7 @@ issues.dismiss_review=Zamietnuť revíziu issues.dismiss_review_warning=Naozaj chcete zrušiť túto revíziu? issues.cancel=Zrušiť issues.save=Uložiť +issues.unpin=Odopnúť @@ -1235,6 +1237,8 @@ teams.all_repositories_read_permission_desc=Tomuto tímu je pridelený prístup teams.all_repositories_write_permission_desc=Tomuto tímu je pridelený prístup na Zápis do všetkých repozitárov: členovia môžu prezerať a nahrávať do repozitárov. teams.all_repositories_admin_permission_desc=Tomuto tímu je pridelený Admin prístup ku všetkým repozitárom: členovia môžu prezerať, nahrávať do repozitárov a pridávať do nich spolupracovníkov. + + [admin] repositories=Repozitáre hooks=Webhooky diff --git a/options/locale/locale_sv-SE.ini b/options/locale/locale_sv-SE.ini index 2fd8277b76f..d43cf655aea 100644 --- a/options/locale/locale_sv-SE.ini +++ b/options/locale/locale_sv-SE.ini @@ -598,7 +598,6 @@ visibility.private=Privat [repo] owner=Ägare repo_name=Utvecklingskatalogens namn -repo_name_helper=Bra namn på utvecklingskataloger består utav korta, unika nyckelord som är enkla att komma ihåg. repo_size=Utvecklingskatalogens storlek template=Mall template_select=Välj mall. @@ -1592,6 +1591,8 @@ teams.all_repositories_read_permission_desc=Detta team beviljar LäsSkriv-rättigheter till alla utvecklingskataloger: medlemmar kan läsa från och pusha till utvecklingskataloger. teams.all_repositories_admin_permission_desc=Detta team beviljar Admin-rättigheter till alla utvecklingskataloger: medlemmar kan läsa från, pusha till och lägga till kollaboratörer för utvecklingskatalogerna. + + [admin] dashboard=Instrumentpanel users=Användarkonto diff --git a/options/locale/locale_tr-TR.ini b/options/locale/locale_tr-TR.ini index 0c3884fd648..39f5bd59789 100644 --- a/options/locale/locale_tr-TR.ini +++ b/options/locale/locale_tr-TR.ini @@ -54,6 +54,7 @@ webauthn_reload=Yeniden yükle repository=Depo organization=Organizasyon mirror=Yansı +issue_milestone=Dönüm noktası new_repo=Yeni Depo new_migrate=Yeni Göç new_mirror=Yeni Yansı @@ -78,7 +79,7 @@ forks=Çatallar activities=Etkinlikler pull_requests=Değişiklik İstekleri issues=Konular -milestones=Kilometre Taşları +milestones=Dönüm noktaları ok=Tamam cancel=İptal @@ -104,6 +105,7 @@ copy_url=URL'yi kopyala copy_hash=Hash'i kopyala copy_content=İçeriği kopyala copy_branch=Dal adını kopyala +copy_path=Yolu kopyala copy_success=Kopyalandı! copy_error=Kopyalama başarısız oldu copy_type_unsupported=Bu dosya türü kopyalanamaz @@ -144,6 +146,7 @@ confirm_delete_selected=Tüm seçili öğeleri gerçekten silmek istiyor musunuz name=İsim value=Değer +readme=Benioku filter=Filtre filter.clear=Filtreyi Temizle @@ -159,6 +162,7 @@ filter.public=Genel filter.private=Özel no_results_found=Sonuç bulunamadı. +internal_error_skipped=Dahili bir hata oluştu ama atlandı: %s [search] search=Ara... @@ -177,6 +181,8 @@ code_search_by_git_grep=Mevcut kod arama sonuçları "git grep" ile sağlanıyor package_kind=Paketleri ara... project_kind=Projeleri ara... branch_kind=Dalları ara... +tag_kind=Etiketleri ara... +tag_tooltip=Eşleşen etiketler için arama. Herhangi bir numara serisi bulmak için '%' kullanın. commit_kind=İşlemeleri ara... runner_kind=Çalıştırıcıları ara... no_results=Eşleşen sonuç bulunamadı. @@ -206,7 +212,10 @@ buttons.link.tooltip=Bağlantı ekle buttons.list.unordered.tooltip=Maddeli liste ekle buttons.list.ordered.tooltip=Numaralandırılmış liste ekle buttons.list.task.tooltip=Görev listesi ekle +buttons.table.add.tooltip=Tablo ekle buttons.table.add.insert=Ekle +buttons.table.rows=Satırlar +buttons.table.cols=Sütunlar buttons.mention.tooltip=Bir kişiye veya takıma değin buttons.ref.tooltip=Bir konuya veya değişiklik isteğine değin buttons.switch_to_legacy.tooltip=Eski düzenleyiciyi kullan @@ -219,6 +228,7 @@ string.desc=Z - A [error] occurred=Bir hata oluştu +report_message=Bunun bir Gitea hatası olduğunu düşünüyorsanız, lütfen GitHub sayfasında sorunu arayın veya gerekiyorsa yeni bir sorun oluşturun. not_found=Hedef bulunamadı. network_error=Ağ hatası @@ -984,7 +994,6 @@ new_repo_helper=Bir depo, sürüm geçmişi dahil tüm proje dosyalarını içer owner=Sahibi owner_helper=Bazı organizasyonlar, en çok depo sayısı sınırı nedeniyle açılır menüde görünmeyebilir. repo_name=Depo İsmi -repo_name_helper=İyi bir depo ismi kısa, akılda kalıcı ve özgün anahtar kelimelerden oluşur. repo_size=Depo Boyutu template=Şablon template_select=Bir şablon seçin. @@ -1075,9 +1084,6 @@ blame_prior=Bu değişiklikten önceki suçu görüntüle blame.ignore_revs=.git-blame-ignore-revs dosyasındaki sürümler yok sayılıyor. Bunun yerine normal sorumlu görüntüsü için buraya tıklayın. blame.ignore_revs.failed=.git-blame-ignore-revs dosyasındaki sürümler yok sayılamadı. -tree_path_not_found_commit=%[1] yolu, %[2]s işlemesinde mevcut değil -tree_path_not_found_branch=%[1] yolu, %[2]s dalında mevcut değil -tree_path_not_found_tag=%[1] yolu, %[2]s etiketinde mevcut değil transfer.accept=Aktarımı Kabul Et transfer.reject=Aktarımı Reddet @@ -1123,7 +1129,7 @@ migrate_options_lfs_endpoint.description.local=Yerel bir sunucu yolu da destekle migrate_options_lfs_endpoint.placeholder=Boş bırakılırsa, uç nokta klon URL'sinden türetilecektir migrate_items=Göç Öğeleri migrate_items_wiki=Wiki -migrate_items_milestones=Kilometre Taşları +migrate_items_milestones=Dönüm noktaları migrate_items_labels=Etiketler migrate_items_issues=Konular migrate_items_pullrequests=Değişiklik İstekleri @@ -1207,6 +1213,7 @@ labels=Etiketler org_labels_desc=Bu organizasyon altında tüm depolarla kullanılabilen organizasyon düzeyinde etiketler org_labels_desc_manage=yönet +milestone=Dönüm noktası milestones=Kilometre Taşları commits=İşleme commit=İşle @@ -1597,7 +1604,7 @@ issues.attachment.open_tab=`Yeni bir sekmede "%s" görmek için tıkla` issues.attachment.download=`"%s" indirmek için tıkla` issues.subscribe=Abone Ol issues.unsubscribe=Abonelikten Çık -issues.unpin_issue=Konuyu Sabitlemeden Kaldır +issues.unpin=Sabitlemeyi kaldır issues.max_pinned=Daha fazla konuyu sabitleyemezsiniz issues.pin_comment=%s sabitlendi issues.unpin_comment=%s sabitlenmesi kaldırıldı @@ -2075,7 +2082,6 @@ settings.advanced_settings=Gelişmiş Ayarlar settings.wiki_desc=Depo Wiki'sini Etkinkleştir settings.use_internal_wiki=Dahili Wiki Kullan settings.default_wiki_branch_name=Varsayılan Viki Dal Adı -settings.default_wiki_everyone_access=Oturum açmış kullanıcılar için Varsayılan Erişim İzinleri: settings.failed_to_change_default_wiki_branch=Varsayılan viki dalı değiştirilemedi. settings.use_external_wiki=Harici Wiki Kullan settings.external_wiki_url=Harici Wiki bağlantısı @@ -2747,6 +2753,8 @@ teams.invite.title=%s takımına (Organizasyon: %sв усіх репозиторіях цієї організації org_labels_desc_manage=керувати +milestone=Етап milestones=Етап commits=Коміти commit=Коміт @@ -2003,6 +2004,8 @@ teams.all_repositories_read_permission_desc=Ця команда надає до teams.all_repositories_write_permission_desc=Ця команда надає дозвіл Запис для всіх репозиторіїв: учасники можуть переглядати та виконувати push в репозиторіях. teams.all_repositories_admin_permission_desc=Ця команда надає дозвіл Адміністрування для всіх репозиторіїв: учасники можуть переглядати, виконувати push та додавати співробітників. + + [admin] dashboard=Панель управління users=Облікові записи користувачів diff --git a/options/locale/locale_zh-CN.ini b/options/locale/locale_zh-CN.ini index 572ad2d667c..a1c3d75913c 100644 --- a/options/locale/locale_zh-CN.ini +++ b/options/locale/locale_zh-CN.ini @@ -54,6 +54,7 @@ webauthn_reload=重新加载 repository=仓库 organization=组织 mirror=镜像 +issue_milestone=里程碑 new_repo=创建仓库 new_migrate=迁移外部仓库 new_mirror=创建新的镜像 @@ -1015,7 +1016,6 @@ new_repo_helper=代码仓库包含了所有的项目文件,包括版本历史 owner=拥有者 owner_helper=由于最大仓库数量限制,一些组织可能不会显示在下拉列表中。 repo_name=仓库名称 -repo_name_helper=好的仓库名称应当使用简短、有意义和独特的关键字。 repo_size=仓库大小 template=模板 template_select=选择模板 @@ -1112,9 +1112,6 @@ blame.ignore_revs=忽略 .git-blame-ignore-revs 的修订。点 blame.ignore_revs.failed=忽略 .git-blame-ignore-revs 版本失败。 user_search_tooltip=最多显示30名用户 -tree_path_not_found_commit=路径%[1]s 在提交 %[2]s 中不存在 -tree_path_not_found_branch=路径 %[1]s 不存在于分支 %[2]s 中。 -tree_path_not_found_tag=路径 %[1]s 不存在于标签 %[2]s 中 transfer.accept=接受转移 transfer.accept_desc=`转移到 "%s"` @@ -1251,6 +1248,7 @@ labels=标签 org_labels_desc=组织级别的标签,可以被本组织下的 所有仓库 使用 org_labels_desc_manage=管理 +milestone=里程碑 milestones=里程碑 commits=提交 commit=提交 @@ -1647,7 +1645,7 @@ issues.attachment.open_tab=`在新的标签页中查看 '%s'` issues.attachment.download=`点击下载 '%s'` issues.subscribe=订阅 issues.unsubscribe=取消订阅 -issues.unpin_issue=取消置顶 +issues.unpin=取消置顶 issues.max_pinned=您不能置顶更多工单 issues.pin_comment=于 %s 被置顶 issues.unpin_comment=于 %s 取消置顶 @@ -1682,16 +1680,13 @@ issues.timetracker_timer_manually_add=添加时间 issues.time_estimate_set=设置预计时间 issues.time_estimate_display=预计: %s -issues.change_time_estimate_at=将预计时间修改为 %s %s issues.remove_time_estimate_at=删除预计时间 %s issues.time_estimate_invalid=预计时间格式无效 issues.start_tracking_history=`开始工作 %s` issues.tracker_auto_close=当此工单关闭时,自动停止计时器 issues.tracking_already_started=`你已经开始对 另一个工单 进行时间跟踪!` -issues.stop_tracking_history=`停止工作 %s` issues.cancel_tracking_history=`取消时间跟踪 %s` issues.del_time=删除此时间跟踪日志 -issues.add_time_history=`添加计时 %s` issues.del_time_history=`已删除时间 %s` issues.add_time_manually=手动添加时间 issues.add_time_hours=小时 @@ -2155,7 +2150,6 @@ settings.advanced_settings=高级设置 settings.wiki_desc=启用仓库百科 settings.use_internal_wiki=使用内置百科 settings.default_wiki_branch_name=默认百科分支名称 -settings.default_wiki_everyone_access=登录用户的默认访问权限: settings.failed_to_change_default_wiki_branch=更改百科默认分支失败。 settings.use_external_wiki=使用外部百科 settings.external_wiki_url=外部 Wiki 链接 @@ -2861,6 +2855,8 @@ teams.invite.title=您已被邀请加入组织 %s 中的团队 teams.invite.by=邀请人 %s teams.invite.description=请点击下面的按钮加入团队。 + + [admin] maintenance=维护 dashboard=管理面板 @@ -3363,7 +3359,6 @@ monitor.execute_time=执行时长 monitor.last_execution_result=结果 monitor.process.cancel=中止进程 monitor.process.cancel_desc=中止一个进程可能导致数据丢失 -monitor.process.cancel_notices=中止:%s ? monitor.process.children=子进程 monitor.queues=队列 @@ -3559,7 +3554,6 @@ conda.install=要使用 Conda 安装软件包,请运行以下命令: container.details.type=镜像类型 container.details.platform=平台 container.pull=从命令行拉取镜像: -container.digest=摘要: container.multi_arch=OS / Arch container.layers=镜像层 container.labels=标签 diff --git a/options/locale/locale_zh-HK.ini b/options/locale/locale_zh-HK.ini index e1ca6ebfc83..eb11ee3a4ad 100644 --- a/options/locale/locale_zh-HK.ini +++ b/options/locale/locale_zh-HK.ini @@ -684,6 +684,8 @@ teams.add_team_member=新增團隊成員 teams.delete_team_success=該團隊已被刪除。 teams.repositories=團隊儲存庫 + + [admin] dashboard=控制面版 organizations=組織管理 diff --git a/options/locale/locale_zh-TW.ini b/options/locale/locale_zh-TW.ini index a3bf6ca8886..b84aaf1e1f1 100644 --- a/options/locale/locale_zh-TW.ini +++ b/options/locale/locale_zh-TW.ini @@ -54,6 +54,7 @@ webauthn_reload=重新載入 repository=儲存庫 organization=組織 mirror=鏡像 +issue_milestone=里程碑 new_repo=新增儲存庫 new_migrate=遷移外部儲存庫 new_mirror=新鏡像 @@ -1012,7 +1013,6 @@ new_repo_helper=儲存庫包含所有專案檔案,包括修訂歷史。已經 owner=擁有者 owner_helper=組織可能因為儲存庫數量上限而未列入此選單。 repo_name=儲存庫名稱 -repo_name_helper=好的儲存庫名稱通常是簡短的、好記的、且獨特的。 repo_size=儲存庫大小 template=範本 template_select=選擇範本 @@ -1109,9 +1109,6 @@ blame.ignore_revs=忽略 .git-blame-ignore-revs 中的修訂。 blame.ignore_revs.failed=忽略 .git-blame-ignore-revs 中的修訂失敗。 user_search_tooltip=顯示最多 30 個使用者 -tree_path_not_found_commit=路徑 %[1]s 在提交 %[2]s 中不存在 -tree_path_not_found_branch=路徑 %[1]s 在分支 %[2]s 中不存在 -tree_path_not_found_tag=路徑 %[1]s 在標籤 %[2]s 中不存在 transfer.accept=同意轉移 transfer.accept_desc=轉移到「%s」 @@ -1245,6 +1242,7 @@ labels=標籤 org_labels_desc=組織層級標籤可用於此組織下的所有存儲庫。 org_labels_desc_manage=管理 +milestone=里程碑 milestones=里程碑 commits=提交歷史 commit=提交 @@ -1641,7 +1639,7 @@ issues.attachment.open_tab=`在新分頁中查看「%s」` issues.attachment.download=`點擊下載「%s」` issues.subscribe=訂閱 issues.unsubscribe=取消訂閱 -issues.unpin_issue=取消固定問題 +issues.unpin=取消固定 issues.max_pinned=您不能固定更多問題 issues.pin_comment=固定於 %s issues.unpin_comment=取消固定於 %s @@ -1676,16 +1674,13 @@ issues.timetracker_timer_manually_add=手動新增時間 issues.time_estimate_set=設定預估時間 issues.time_estimate_display=預估時間:%s -issues.change_time_estimate_at=將預估時間更改為 %s %s issues.remove_time_estimate_at=移除預估時間 %s issues.time_estimate_invalid=預估時間格式無效 issues.start_tracking_history=`開始工作 %s` issues.tracker_auto_close=當這個問題被關閉時,自動停止計時器 issues.tracking_already_started=`您已在另一個問題上開始時間追蹤!` -issues.stop_tracking_history=`結束工作 %s` issues.cancel_tracking_history=`取消時間追蹤 %s` issues.del_time=刪除此時間記錄 -issues.add_time_history=`加入了花費時間 %s` issues.del_time_history=`刪除了花費時間 %s` issues.add_time_manually=手動新增時間 issues.add_time_hours=小時 @@ -2146,7 +2141,6 @@ settings.advanced_settings=進階設定 settings.wiki_desc=啟用儲存庫 Wiki settings.use_internal_wiki=使用內建 Wiki settings.default_wiki_branch_name=預設 Wiki 分支名稱 -settings.default_wiki_everyone_access=登入使用者的預設存取權限: settings.failed_to_change_default_wiki_branch=更改預設 Wiki 分支失敗。 settings.use_external_wiki=使用外部 Wiki settings.external_wiki_url=外部 Wiki 連結 @@ -2852,6 +2846,8 @@ teams.invite.title=您已被邀請加入組織 %s 中的團隊 teams.invite.by=邀請人 %s teams.invite.description=請點擊下方按鈕加入團隊。 + + [admin] maintenance=維護 dashboard=資訊主頁 @@ -3354,7 +3350,6 @@ monitor.execute_time=已執行時間 monitor.last_execution_result=結果 monitor.process.cancel=結束處理程序 monitor.process.cancel_desc=結束處理程序可能造成資料遺失 -monitor.process.cancel_notices=結束: %s? monitor.process.children=子程序 monitor.queues=佇列 @@ -3550,7 +3545,6 @@ conda.install=執行下列命令以使用 Conda 安裝此套件: container.details.type=映像檔類型 container.details.platform=平台 container.pull=透過下列命令拉取映像檔: -container.digest=摘要: container.multi_arch=作業系統 / 架構 container.layers=映像檔 Layers container.labels=標籤 diff --git a/package-lock.json b/package-lock.json index 1e3c5ab155d..ac25c50dde3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,18 +6,18 @@ "": { "dependencies": { "@citation-js/core": "0.7.14", - "@citation-js/plugin-bibtex": "0.7.16", + "@citation-js/plugin-bibtex": "0.7.17", "@citation-js/plugin-csl": "0.7.14", "@citation-js/plugin-software-formats": "0.6.1", "@github/markdown-toolbar-element": "2.2.3", - "@github/relative-time-element": "4.4.4", - "@github/text-expander-element": "2.8.0", + "@github/relative-time-element": "4.4.5", + "@github/text-expander-element": "2.9.1", "@mcaptcha/vanilla-glue": "0.1.0-alpha-3", "@primer/octicons": "19.14.0", "@silverwind/vue3-calendar-heatmap": "2.0.6", "add-asset-webpack-plugin": "3.0.0", "ansi_up": "6.0.2", - "asciinema-player": "3.8.1", + "asciinema-player": "3.8.2", "chart.js": "4.4.7", "chartjs-adapter-dayjs-4": "1.0.4", "chartjs-plugin-zoom": "2.2.0", @@ -29,11 +29,11 @@ "easymde": "2.18.0", "esbuild-loader": "4.2.2", "escape-goat": "4.0.0", - "fast-glob": "3.3.2", + "fast-glob": "3.3.3", "htmx.org": "2.0.4", - "idiomorph": "0.3.0", + "idiomorph": "0.4.0", "jquery": "3.7.1", - "katex": "0.16.18", + "katex": "0.16.21", "license-checker-webpack-plugin": "0.2.1", "mermaid": "11.4.1", "mini-css-extract-plugin": "2.9.2", @@ -42,7 +42,7 @@ "monaco-editor-webpack-plugin": "7.1.0", "pdfobject": "2.3.0", "perfect-debounce": "1.0.0", - "postcss": "8.4.49", + "postcss": "8.5.1", "postcss-loader": "8.1.1", "postcss-nesting": "13.0.1", "sortablejs": "1.15.6", @@ -53,7 +53,7 @@ "tippy.js": "6.3.7", "toastify-js": "1.12.0", "tributejs": "5.1.3", - "typescript": "5.7.2", + "typescript": "5.7.3", "uint8-to-base64": "0.2.0", "vanilla-colorful": "0.7.2", "vue": "3.5.13", @@ -61,15 +61,14 @@ "vue-chartjs": "5.3.2", "vue-loader": "17.4.2", "webpack": "5.97.1", - "webpack-cli": "5.1.4", + "webpack-cli": "6.0.1", "wrap-ansi": "9.0.0" }, "devDependencies": { "@eslint-community/eslint-plugin-eslint-comments": "4.4.1", "@playwright/test": "1.49.1", - "@silverwind/vue-tsc": "2.1.13", "@stoplight/spectral-cli": "6.14.2", - "@stylistic/eslint-plugin-js": "2.12.1", + "@stylistic/eslint-plugin-js": "2.13.0", "@stylistic/stylelint-plugin": "3.1.1", "@types/dropzone": "5.7.9", "@types/jquery": "3.5.32", @@ -81,8 +80,8 @@ "@types/throttle-debounce": "5.0.2", "@types/tinycolor2": "1.4.6", "@types/toastify-js": "1.12.3", - "@typescript-eslint/eslint-plugin": "8.18.1", - "@typescript-eslint/parser": "8.18.1", + "@typescript-eslint/eslint-plugin": "8.21.0", + "@typescript-eslint/parser": "8.21.0", "@vitejs/plugin-vue": "5.2.1", "eslint": "8.57.0", "eslint-import-resolver-typescript": "3.7.0", @@ -100,19 +99,22 @@ "eslint-plugin-vue": "9.32.0", "eslint-plugin-vue-scoped-css": "2.9.0", "eslint-plugin-wc": "2.2.0", - "happy-dom": "15.11.7", + "happy-dom": "16.7.2", "markdownlint-cli": "0.43.0", "nolyfill": "1.0.43", - "postcss-html": "1.7.0", - "stylelint": "16.12.0", + "postcss-html": "1.8.0", + "stylelint": "16.14.1", + "stylelint-config-recommended": "15.0.0", "stylelint-declaration-block-no-ignored-properties": "2.8.0", - "stylelint-declaration-strict-value": "1.10.6", + "stylelint-declaration-strict-value": "1.10.7", + "stylelint-define-config": "16.14.0", "stylelint-value-no-unknown-custom-properties": "6.0.1", "svgo": "3.3.2", - "type-fest": "4.30.2", + "type-fest": "4.33.0", "updates": "16.4.1", - "vite-string-plugin": "1.3.4", - "vitest": "2.1.8" + "vite-string-plugin": "1.4.3", + "vitest": "3.0.3", + "vue-tsc": "2.2.0" }, "engines": { "node": ">= 18.0.0" @@ -191,9 +193,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.3.tgz", - "integrity": "sha512-nHIxvKPniQXpmQLb0vhY3VaFb3S0YrTAwpOWJZh1wn3oJPjJk9Asva204PsBdmAE8vpzfHudT8DB0scYvy9q0g==", + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.5.tgz", + "integrity": "sha512-XvcZi1KWf88RVbF9wn8MN6tYFloU5qX8KjuF3E1PVBmJ9eypXfs4GRiJwLuTZL0iSnJUKn1BFPa5BPZZJyFzPg==", "dev": true, "license": "MIT", "engines": { @@ -281,14 +283,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.3.tgz", - "integrity": "sha512-6FF/urZvD0sTeO7k6/B15pMLC4CHUv1426lzr3N01aHJTl046uCAh9LXW/fzeXXjPNCJ6iABW5XaWOsIZB93aQ==", + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.5.tgz", + "integrity": "sha512-2caSP6fN9I7HOe6nqhtft7V4g7/V/gfDsC3Ag4W7kEzzvRGKqiv0pu0HogPiZ3KaVSoNDhUws6IJjDjpfmYIXw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.26.3", - "@babel/types": "^7.26.3", + "@babel/parser": "^7.26.5", + "@babel/types": "^7.26.5", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" @@ -311,13 +313,13 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.9.tgz", - "integrity": "sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ==", + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.26.5.tgz", + "integrity": "sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.25.9", + "@babel/compat-data": "^7.26.5", "@babel/helper-validator-option": "^7.25.9", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", @@ -474,9 +476,9 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.9.tgz", - "integrity": "sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw==", + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz", + "integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==", "dev": true, "license": "MIT", "engines": { @@ -502,15 +504,15 @@ } }, "node_modules/@babel/helper-replace-supers": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.25.9.tgz", - "integrity": "sha512-IiDqTOTBQy0sWyeXyGSC5TBJpGFXBkRynjBeXsvbhQFKj2viwJC76Epz35YLU1fpe/Am6Vppb7W7zM4fPQzLsQ==", + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.26.5.tgz", + "integrity": "sha512-bJ6iIVdYX1YooY2X7w1q6VITt+LnUILtNk7zT78ykuwStx8BauCzxvFqFaHjOpW1bVnSUM1PN1f0p5P21wHxvg==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-member-expression-to-functions": "^7.25.9", "@babel/helper-optimise-call-expression": "^7.25.9", - "@babel/traverse": "^7.25.9" + "@babel/traverse": "^7.26.5" }, "engines": { "node": ">=6.9.0" @@ -591,12 +593,12 @@ } }, "node_modules/@babel/parser": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.3.tgz", - "integrity": "sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA==", + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.5.tgz", + "integrity": "sha512-SRJ4jYmXRqV1/Xc+TIVG84WjHBXKlxO9sHQnA2Pf12QQEAp1LOh6kDzNHXcUnbH1QI0FDoPPVOt+vyUDucxpaw==", "license": "MIT", "dependencies": { - "@babel/types": "^7.26.3" + "@babel/types": "^7.26.5" }, "bin": { "parser": "bin/babel-parser.js" @@ -870,13 +872,13 @@ } }, "node_modules/@babel/plugin-transform-block-scoped-functions": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.25.9.tgz", - "integrity": "sha512-toHc9fzab0ZfenFpsyYinOX0J/5dgJVA2fm64xPewu7CoYHWEivIWKxkK2rMi4r3yQqLnVmheMXRdG+k239CgA==", + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.26.5.tgz", + "integrity": "sha512-chuTSY+hq09+/f5lMj8ZSYgCFpppV2CbYrhNFJ1BFoXpiWPnnAb7R0MqrafCpN8E1+YRrtM1MXZHJdIx8B6rMQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.26.5" }, "engines": { "node": ">=6.9.0" @@ -1098,14 +1100,14 @@ } }, "node_modules/@babel/plugin-transform-flow-strip-types": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.25.9.tgz", - "integrity": "sha512-/VVukELzPDdci7UUsWQaSkhgnjIWXnIyRpM02ldxaVoFK96c41So8JcKT3m0gYjyv7j5FNPGS5vfELrWalkbDA==", + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.26.5.tgz", + "integrity": "sha512-eGK26RsbIkYUns3Y8qKl362juDDYK+wEdPGHGrhzUl6CewZFo55VZ7hg+CyMFU4dd5QQakBN86nBMpRsFpRvbQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/plugin-syntax-flow": "^7.25.9" + "@babel/helper-plugin-utils": "^7.26.5", + "@babel/plugin-syntax-flow": "^7.26.0" }, "engines": { "node": ">=6.9.0" @@ -1317,13 +1319,13 @@ } }, "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.25.9.tgz", - "integrity": "sha512-ENfftpLZw5EItALAD4WsY/KUWvhUlZndm5GC7G3evUsVeSJB6p0pBeLQUnRnBCBx7zV0RKQjR9kCuwrsIrjWog==", + "version": "7.26.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.26.6.tgz", + "integrity": "sha512-CKW8Vu+uUZneQCPtXmSBUC6NCAUdya26hWCElAWh5mVSlSRsmiCPUUDKb3Z0szng1hiAJa098Hkhg9o4SE35Qw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.26.5" }, "engines": { "node": ">=6.9.0" @@ -1926,17 +1928,17 @@ } }, "node_modules/@babel/traverse": { - "version": "7.26.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.4.tgz", - "integrity": "sha512-fH+b7Y4p3yqvApJALCPJcwb0/XaOSgtK4pzV6WVjPR5GLFQBRI7pfoX2V2iM48NXvX07NUxxm1Vw98YjqTcU5w==", + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.5.tgz", + "integrity": "sha512-rkOSPOw+AXbgtwUga3U4u8RpoK9FEFWBNAlTpcnkLFjL5CT+oyHNuUUC/xx6XefEJ16r38r8Bc/lfp6rYuHeJQ==", "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.26.3", - "@babel/parser": "^7.26.3", + "@babel/generator": "^7.26.5", + "@babel/parser": "^7.26.5", "@babel/template": "^7.25.9", - "@babel/types": "^7.26.3", + "@babel/types": "^7.26.5", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -1955,9 +1957,9 @@ } }, "node_modules/@babel/types": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.3.tgz", - "integrity": "sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==", + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.5.tgz", + "integrity": "sha512-L6mZmwFDK6Cjh1nRCLXpa6no13ZIioJDz7mdkzHv399pThrTa/k0nUlNaenOeh2kWu/iaOQYElEpKPUswUa9Vg==", "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.25.9", @@ -1968,9 +1970,9 @@ } }, "node_modules/@braintree/sanitize-url": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.0.tgz", - "integrity": "sha512-o+UlMLt49RvtCASlOMW0AkHnabN9wR9rwCCherxO0yG4Npy34GkvrAqdXQvrhNs+jh+gkK8gB8Lf05qL/O7KWg==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.1.tgz", + "integrity": "sha512-i1L7noDNxtFyL5DmZafWy1wRVhGehQmzZaz1HiN5e7iylJMSZR7ekOV7NsIqa5qBldlLrsKv4HbgFUVlQrz8Mw==", "license": "MIT" }, "node_modules/@chevrotain/cst-dts-gen": { @@ -2046,9 +2048,9 @@ } }, "node_modules/@citation-js/plugin-bibtex": { - "version": "0.7.16", - "resolved": "https://registry.npmjs.org/@citation-js/plugin-bibtex/-/plugin-bibtex-0.7.16.tgz", - "integrity": "sha512-Udeli19VAoFjOw0H1bB1KgmekRoW6XP5cdR3OQF5c2Mt1tZatXWcSQVdq+FeLKzodRocZXG5NFRvjyUZjVbV6A==", + "version": "0.7.17", + "resolved": "https://registry.npmjs.org/@citation-js/plugin-bibtex/-/plugin-bibtex-0.7.17.tgz", + "integrity": "sha512-pyMW6UR6iMPCk1mVwagNHabprajOCQO+TibxKI6ymdv5VOX3zoqeQF0utwjFnViquL/BZfM5SGUZCQdu+ZZYag==", "license": "MIT", "dependencies": { "@citation-js/date": "^0.5.0", @@ -2226,12 +2228,12 @@ } }, "node_modules/@discoveryjs/json-ext": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", - "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.6.3.tgz", + "integrity": "sha512-4B4OijXeVNOPZlYA2oEwWOTkzyltLao+xbotHQeqN++Rv27Y6s818+n2Qkp8q+Fxhn0t/5lA5X1Mxktud8eayQ==", "license": "MIT", "engines": { - "node": ">=10.0.0" + "node": ">=14.17.0" } }, "node_modules/@dual-bundle/import-meta-resolve": { @@ -2517,6 +2519,23 @@ "node": ">=12" } }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz", + "integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/netbsd-x64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", @@ -2533,6 +2552,23 @@ "node": ">=12" } }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz", + "integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/openbsd-x64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", @@ -2808,15 +2844,15 @@ "license": "MIT" }, "node_modules/@github/relative-time-element": { - "version": "4.4.4", - "resolved": "https://registry.npmjs.org/@github/relative-time-element/-/relative-time-element-4.4.4.tgz", - "integrity": "sha512-Oi8uOL8O+ZWLD7dHRWCkm2cudcTYtB3VyOYf9BtzCgDGm+OKomyOREtItNMtWl1dxvec62BTKErq36uy+RYxQg==", + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/@github/relative-time-element/-/relative-time-element-4.4.5.tgz", + "integrity": "sha512-9ejPtayBDIJfEU8x1fg/w2o5mahHkkp1SC6uObDtoKs4Gn+2a1vNK8XIiNDD8rMeEfpvDjydgSZZ+uk+7N0VsQ==", "license": "MIT" }, "node_modules/@github/text-expander-element": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@github/text-expander-element/-/text-expander-element-2.8.0.tgz", - "integrity": "sha512-kkS2rZ/CG8HGKblpLDQ8vcK/K7l/Jsvzi/N4ovwPAsFSOImcIbJh2MgCv9tzqE3wAm/qXlscvh3Ms4Hh1vtZvw==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@github/text-expander-element/-/text-expander-element-2.9.1.tgz", + "integrity": "sha512-T/pCDjB/diMaarmcdc01hP026v0b9lidluyZD5z/EPOExXRdNDqb11kOXevoMZY42WiI3Yhoqsj3nbM+HthLgQ==", "license": "MIT", "dependencies": { "@github/combobox-nav": "^2.0.2", @@ -3106,6 +3142,41 @@ "jsep": "^0.4.0||^1.0.0" } }, + "node_modules/@keyv/serialize": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.0.2.tgz", + "integrity": "sha512-+E/LyaAeuABniD/RvUezWVXKpeuvwLEA9//nE9952zBaOdBd2mQ3pPoM8cUe2X6IcMByfuSLzmYqnYshG60+HQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^6.0.3" + } + }, + "node_modules/@keyv/serialize/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, "node_modules/@kurkle/color": { "version": "0.3.4", "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", @@ -3364,9 +3435,9 @@ "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.28.1.tgz", - "integrity": "sha512-2aZp8AES04KI2dy3Ss6/MDjXbwBzj+i0GqKtWXgw2/Ma6E4jJvujryO6gJAghIRVz7Vwr9Gtl/8na3nDUKpraQ==", + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.31.0.tgz", + "integrity": "sha512-9NrR4033uCbUBRgvLcBrJofa2KY9DzxL2UKZ1/4xA/mnTNyhZCWBuD8X3tPm1n4KxcgaraOYgrFKSgwjASfmlA==", "cpu": [ "arm" ], @@ -3378,9 +3449,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.28.1.tgz", - "integrity": "sha512-EbkK285O+1YMrg57xVA+Dp0tDBRB93/BZKph9XhMjezf6F4TpYjaUSuPt5J0fZXlSag0LmZAsTmdGGqPp4pQFA==", + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.31.0.tgz", + "integrity": "sha512-iBbODqT86YBFHajxxF8ebj2hwKm1k8PTBQSojSt3d1FFt1gN+xf4CowE47iN0vOSdnd+5ierMHBbu/rHc7nq5g==", "cpu": [ "arm64" ], @@ -3392,9 +3463,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.28.1.tgz", - "integrity": "sha512-prduvrMKU6NzMq6nxzQw445zXgaDBbMQvmKSJaxpaZ5R1QDM8w+eGxo6Y/jhT/cLoCvnZI42oEqf9KQNYz1fqQ==", + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.31.0.tgz", + "integrity": "sha512-WHIZfXgVBX30SWuTMhlHPXTyN20AXrLH4TEeH/D0Bolvx9PjgZnn4H677PlSGvU6MKNsjCQJYczkpvBbrBnG6g==", "cpu": [ "arm64" ], @@ -3406,9 +3477,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.28.1.tgz", - "integrity": "sha512-WsvbOunsUk0wccO/TV4o7IKgloJ942hVFK1CLatwv6TJspcCZb9umQkPdvB7FihmdxgaKR5JyxDjWpCOp4uZlQ==", + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.31.0.tgz", + "integrity": "sha512-hrWL7uQacTEF8gdrQAqcDy9xllQ0w0zuL1wk1HV8wKGSGbKPVjVUv/DEwT2+Asabf8Dh/As+IvfdU+H8hhzrQQ==", "cpu": [ "x64" ], @@ -3420,9 +3491,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.28.1.tgz", - "integrity": "sha512-HTDPdY1caUcU4qK23FeeGxCdJF64cKkqajU0iBnTVxS8F7H/7BewvYoG+va1KPSL63kQ1PGNyiwKOfReavzvNA==", + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.31.0.tgz", + "integrity": "sha512-S2oCsZ4hJviG1QjPY1h6sVJLBI6ekBeAEssYKad1soRFv3SocsQCzX6cwnk6fID6UQQACTjeIMB+hyYrFacRew==", "cpu": [ "arm64" ], @@ -3434,9 +3505,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.28.1.tgz", - "integrity": "sha512-m/uYasxkUevcFTeRSM9TeLyPe2QDuqtjkeoTpP9SW0XxUWfcYrGDMkO/m2tTw+4NMAF9P2fU3Mw4ahNvo7QmsQ==", + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.31.0.tgz", + "integrity": "sha512-pCANqpynRS4Jirn4IKZH4tnm2+2CqCNLKD7gAdEjzdLGbH1iO0zouHz4mxqg0uEMpO030ejJ0aA6e1PJo2xrPA==", "cpu": [ "x64" ], @@ -3448,9 +3519,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.28.1.tgz", - "integrity": "sha512-QAg11ZIt6mcmzpNE6JZBpKfJaKkqTm1A9+y9O+frdZJEuhQxiugM05gnCWiANHj4RmbgeVJpTdmKRmH/a+0QbA==", + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.31.0.tgz", + "integrity": "sha512-0O8ViX+QcBd3ZmGlcFTnYXZKGbFu09EhgD27tgTdGnkcYXLat4KIsBBQeKLR2xZDCXdIBAlWLkiXE1+rJpCxFw==", "cpu": [ "arm" ], @@ -3462,9 +3533,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.28.1.tgz", - "integrity": "sha512-dRP9PEBfolq1dmMcFqbEPSd9VlRuVWEGSmbxVEfiq2cs2jlZAl0YNxFzAQS2OrQmsLBLAATDMb3Z6MFv5vOcXg==", + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.31.0.tgz", + "integrity": "sha512-w5IzG0wTVv7B0/SwDnMYmbr2uERQp999q8FMkKG1I+j8hpPX2BYFjWe69xbhbP6J9h2gId/7ogesl9hwblFwwg==", "cpu": [ "arm" ], @@ -3476,9 +3547,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.28.1.tgz", - "integrity": "sha512-uGr8khxO+CKT4XU8ZUH1TTEUtlktK6Kgtv0+6bIFSeiSlnGJHG1tSFSjm41uQ9sAO/5ULx9mWOz70jYLyv1QkA==", + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.31.0.tgz", + "integrity": "sha512-JyFFshbN5xwy6fulZ8B/8qOqENRmDdEkcIMF0Zz+RsfamEW+Zabl5jAb0IozP/8UKnJ7g2FtZZPEUIAlUSX8cA==", "cpu": [ "arm64" ], @@ -3490,9 +3561,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.28.1.tgz", - "integrity": "sha512-QF54q8MYGAqMLrX2t7tNpi01nvq5RI59UBNx+3+37zoKX5KViPo/gk2QLhsuqok05sSCRluj0D00LzCwBikb0A==", + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.31.0.tgz", + "integrity": "sha512-kpQXQ0UPFeMPmPYksiBL9WS/BDiQEjRGMfklVIsA0Sng347H8W2iexch+IEwaR7OVSKtr2ZFxggt11zVIlZ25g==", "cpu": [ "arm64" ], @@ -3504,9 +3575,9 @@ ] }, "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.28.1.tgz", - "integrity": "sha512-vPul4uodvWvLhRco2w0GcyZcdyBfpfDRgNKU+p35AWEbJ/HPs1tOUrkSueVbBS0RQHAf/A+nNtDpvw95PeVKOA==", + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.31.0.tgz", + "integrity": "sha512-pMlxLjt60iQTzt9iBb3jZphFIl55a70wexvo8p+vVFK+7ifTRookdoXX3bOsRdmfD+OKnMozKO6XM4zR0sHRrQ==", "cpu": [ "loong64" ], @@ -3518,9 +3589,9 @@ ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.28.1.tgz", - "integrity": "sha512-pTnTdBuC2+pt1Rmm2SV7JWRqzhYpEILML4PKODqLz+C7Ou2apEV52h19CR7es+u04KlqplggmN9sqZlekg3R1A==", + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.31.0.tgz", + "integrity": "sha512-D7TXT7I/uKEuWiRkEFbed1UUYZwcJDU4vZQdPTcepK7ecPhzKOYk4Er2YR4uHKme4qDeIh6N3XrLfpuM7vzRWQ==", "cpu": [ "ppc64" ], @@ -3532,9 +3603,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.28.1.tgz", - "integrity": "sha512-vWXy1Nfg7TPBSuAncfInmAI/WZDd5vOklyLJDdIRKABcZWojNDY0NJwruY2AcnCLnRJKSaBgf/GiJfauu8cQZA==", + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.31.0.tgz", + "integrity": "sha512-wal2Tc8O5lMBtoePLBYRKj2CImUCJ4UNGJlLwspx7QApYny7K1cUYlzQ/4IGQBLmm+y0RS7dwc3TDO/pmcneTw==", "cpu": [ "riscv64" ], @@ -3546,9 +3617,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.28.1.tgz", - "integrity": "sha512-/yqC2Y53oZjb0yz8PVuGOQQNOTwxcizudunl/tFs1aLvObTclTwZ0JhXF2XcPT/zuaymemCDSuuUPXJJyqeDOg==", + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.31.0.tgz", + "integrity": "sha512-O1o5EUI0+RRMkK9wiTVpk2tyzXdXefHtRTIjBbmFREmNMy7pFeYXCFGbhKFwISA3UOExlo5GGUuuj3oMKdK6JQ==", "cpu": [ "s390x" ], @@ -3560,9 +3631,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.28.1.tgz", - "integrity": "sha512-fzgeABz7rrAlKYB0y2kSEiURrI0691CSL0+KXwKwhxvj92VULEDQLpBYLHpF49MSiPG4sq5CK3qHMnb9tlCjBw==", + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.31.0.tgz", + "integrity": "sha512-zSoHl356vKnNxwOWnLd60ixHNPRBglxpv2g7q0Cd3Pmr561gf0HiAcUBRL3S1vPqRC17Zo2CX/9cPkqTIiai1g==", "cpu": [ "x64" ], @@ -3574,9 +3645,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.28.1.tgz", - "integrity": "sha512-xQTDVzSGiMlSshpJCtudbWyRfLaNiVPXt1WgdWTwWz9n0U12cI2ZVtWe/Jgwyv/6wjL7b66uu61Vg0POWVfz4g==", + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.31.0.tgz", + "integrity": "sha512-ypB/HMtcSGhKUQNiFwqgdclWNRrAYDH8iMYH4etw/ZlGwiTVxBz2tDrGRrPlfZu6QjXwtd+C3Zib5pFqID97ZA==", "cpu": [ "x64" ], @@ -3588,9 +3659,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.28.1.tgz", - "integrity": "sha512-wSXmDRVupJstFP7elGMgv+2HqXelQhuNf+IS4V+nUpNVi/GUiBgDmfwD0UGN3pcAnWsgKG3I52wMOBnk1VHr/A==", + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.31.0.tgz", + "integrity": "sha512-JuhN2xdI/m8Hr+aVO3vspO7OQfUFO6bKLIRTAy0U15vmWjnZDLrEgCZ2s6+scAYaQVpYSh9tZtRijApw9IXyMw==", "cpu": [ "arm64" ], @@ -3602,9 +3673,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.28.1.tgz", - "integrity": "sha512-ZkyTJ/9vkgrE/Rk9vhMXhf8l9D+eAhbAVbsGsXKy2ohmJaWg0LPQLnIxRdRp/bKyr8tXuPlXhIoGlEB5XpJnGA==", + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.31.0.tgz", + "integrity": "sha512-U1xZZXYkvdf5MIWmftU8wrM5PPXzyaY1nGCI4KI4BFfoZxHamsIe+BtnPLIvvPykvQWlVbqUXdLa4aJUuilwLQ==", "cpu": [ "ia32" ], @@ -3616,9 +3687,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.28.1.tgz", - "integrity": "sha512-ZvK2jBafvttJjoIdKm/Q/Bh7IJ1Ose9IBOwpOXcOvW3ikGTQGmKDgxTC6oCAzW6PynbkKP8+um1du81XJHZ0JA==", + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.31.0.tgz", + "integrity": "sha512-ul8rnCsUumNln5YWwz0ted2ZHFhzhRRnkpBZ+YRuHoRAlUji9KChpOUOndY7uykrPEPXVbHLlsdo6v5yXo/TXw==", "cpu": [ "x64" ], @@ -3643,24 +3714,6 @@ "hasInstallScript": true, "license": "Apache-2.0" }, - "node_modules/@silverwind/vue-tsc": { - "version": "2.1.13", - "resolved": "https://registry.npmjs.org/@silverwind/vue-tsc/-/vue-tsc-2.1.13.tgz", - "integrity": "sha512-ejFxz1KZiUGAESbC+eURnjqt0N95qkU9eZU7W15wgF9zV+v2FEu3ZLduuXTC7D/Sg6lL1R/QjPfUbxbAbBQOsw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@volar/typescript": "~2.4.11", - "@vue/language-core": "2.1.10", - "semver": "^7.5.4" - }, - "bin": { - "vue-tsc": "bin/vue-tsc.js" - }, - "peerDependencies": { - "typescript": ">=5.0.0" - } - }, "node_modules/@silverwind/vue3-calendar-heatmap": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@silverwind/vue3-calendar-heatmap/-/vue3-calendar-heatmap-2.0.6.tgz", @@ -4193,9 +4246,9 @@ } }, "node_modules/@stylistic/eslint-plugin-js": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-js/-/eslint-plugin-js-2.12.1.tgz", - "integrity": "sha512-5ybogtEgWIGCR6dMnaabztbWyVdAPDsf/5XOk6jBonWug875Q9/a6gm9QxnU3rhdyDEnckWKX7dduwYJMOWrVA==", + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-js/-/eslint-plugin-js-2.13.0.tgz", + "integrity": "sha512-GPPDK4+fcbsQD58a3abbng2Dx+jBoxM5cnYjBM4T24WFZRZdlNSKvR19TxP8CPevzMOodQ9QVzNeqWvMXzfJRA==", "dev": true, "license": "MIT", "dependencies": { @@ -4465,9 +4518,9 @@ "license": "MIT" }, "node_modules/@types/d3-shape": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.6.tgz", - "integrity": "sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA==", + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", "license": "MIT", "dependencies": { "@types/d3-path": "*" @@ -4629,9 +4682,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.10.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz", - "integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==", + "version": "22.10.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.7.tgz", + "integrity": "sha512-V09KvXxFiutGp6B7XkpaDXlNadZxrzajcY50EuoLIpQ6WWYCSvf19lVIazzfIzQvhUN2HjX12spLojTnhuKlGg==", "license": "MIT", "dependencies": { "undici-types": "~6.20.0" @@ -4785,21 +4838,21 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.18.1.tgz", - "integrity": "sha512-Ncvsq5CT3Gvh+uJG0Lwlho6suwDfUXH0HztslDf5I+F2wAFAZMRwYLEorumpKLzmO2suAXZ/td1tBg4NZIi9CQ==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.21.0.tgz", + "integrity": "sha512-eTH+UOR4I7WbdQnG4Z48ebIA6Bgi7WO8HvFEneeYBxG8qCOYgTOFPSg6ek9ITIDvGjDQzWHcoWHCDO2biByNzA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.18.1", - "@typescript-eslint/type-utils": "8.18.1", - "@typescript-eslint/utils": "8.18.1", - "@typescript-eslint/visitor-keys": "8.18.1", + "@typescript-eslint/scope-manager": "8.21.0", + "@typescript-eslint/type-utils": "8.21.0", + "@typescript-eslint/utils": "8.21.0", + "@typescript-eslint/visitor-keys": "8.21.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", - "ts-api-utils": "^1.3.0" + "ts-api-utils": "^2.0.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4815,16 +4868,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.18.1.tgz", - "integrity": "sha512-rBnTWHCdbYM2lh7hjyXqxk70wvon3p2FyaniZuey5TrcGBpfhVp0OxOa6gxr9Q9YhZFKyfbEnxc24ZnVbbUkCA==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.21.0.tgz", + "integrity": "sha512-Wy+/sdEH9kI3w9civgACwabHbKl+qIOu0uFZ9IMKzX3Jpv9og0ZBJrZExGrPpFAY7rWsXuxs5e7CPPP17A4eYA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.18.1", - "@typescript-eslint/types": "8.18.1", - "@typescript-eslint/typescript-estree": "8.18.1", - "@typescript-eslint/visitor-keys": "8.18.1", + "@typescript-eslint/scope-manager": "8.21.0", + "@typescript-eslint/types": "8.21.0", + "@typescript-eslint/typescript-estree": "8.21.0", + "@typescript-eslint/visitor-keys": "8.21.0", "debug": "^4.3.4" }, "engines": { @@ -4840,14 +4893,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.18.1.tgz", - "integrity": "sha512-HxfHo2b090M5s2+/9Z3gkBhI6xBH8OJCFjH9MhQ+nnoZqxU3wNxkLT+VWXWSFWc3UF3Z+CfPAyqdCTdoXtDPCQ==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.21.0.tgz", + "integrity": "sha512-G3IBKz0/0IPfdeGRMbp+4rbjfSSdnGkXsM/pFZA8zM9t9klXDnB/YnKOBQ0GoPmoROa4bCq2NeHgJa5ydsQ4mA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.18.1", - "@typescript-eslint/visitor-keys": "8.18.1" + "@typescript-eslint/types": "8.21.0", + "@typescript-eslint/visitor-keys": "8.21.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4858,16 +4911,16 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.18.1.tgz", - "integrity": "sha512-jAhTdK/Qx2NJPNOTxXpMwlOiSymtR2j283TtPqXkKBdH8OAMmhiUfP0kJjc/qSE51Xrq02Gj9NY7MwK+UxVwHQ==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.21.0.tgz", + "integrity": "sha512-95OsL6J2BtzoBxHicoXHxgk3z+9P3BEcQTpBKriqiYzLKnM2DeSqs+sndMKdamU8FosiadQFT3D+BSL9EKnAJQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.18.1", - "@typescript-eslint/utils": "8.18.1", + "@typescript-eslint/typescript-estree": "8.21.0", + "@typescript-eslint/utils": "8.21.0", "debug": "^4.3.4", - "ts-api-utils": "^1.3.0" + "ts-api-utils": "^2.0.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4882,9 +4935,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.18.1.tgz", - "integrity": "sha512-7uoAUsCj66qdNQNpH2G8MyTFlgerum8ubf21s3TSM3XmKXuIn+H2Sifh/ES2nPOPiYSRJWAk0fDkW0APBWcpfw==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.21.0.tgz", + "integrity": "sha512-PAL6LUuQwotLW2a8VsySDBwYMm129vFm4tMVlylzdoTybTHaAi0oBp7Ac6LhSrHHOdLM3efH+nAR6hAWoMF89A==", "dev": true, "license": "MIT", "engines": { @@ -4896,20 +4949,20 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.18.1.tgz", - "integrity": "sha512-z8U21WI5txzl2XYOW7i9hJhxoKKNG1kcU4RzyNvKrdZDmbjkmLBo8bgeiOJmA06kizLI76/CCBAAGlTlEeUfyg==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.21.0.tgz", + "integrity": "sha512-x+aeKh/AjAArSauz0GiQZsjT8ciadNMHdkUSwBB9Z6PrKc/4knM4g3UfHml6oDJmKC88a6//cdxnO/+P2LkMcg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.18.1", - "@typescript-eslint/visitor-keys": "8.18.1", + "@typescript-eslint/types": "8.21.0", + "@typescript-eslint/visitor-keys": "8.21.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", - "ts-api-utils": "^1.3.0" + "ts-api-utils": "^2.0.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4939,16 +4992,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.18.1.tgz", - "integrity": "sha512-8vikiIj2ebrC4WRdcAdDcmnu9Q/MXXwg+STf40BVfT8exDqBCUPdypvzcUPxEqRGKg9ALagZ0UWcYCtn+4W2iQ==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.21.0.tgz", + "integrity": "sha512-xcXBfcq0Kaxgj7dwejMbFyq7IOHgpNMtVuDveK7w3ZGwG9owKzhALVwKpTF2yrZmEwl9SWdetf3fxNzJQaVuxw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.18.1", - "@typescript-eslint/types": "8.18.1", - "@typescript-eslint/typescript-estree": "8.18.1" + "@typescript-eslint/scope-manager": "8.21.0", + "@typescript-eslint/types": "8.21.0", + "@typescript-eslint/typescript-estree": "8.21.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4963,13 +5016,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.18.1.tgz", - "integrity": "sha512-Vj0WLm5/ZsD013YeUKn+K0y8p1M0jPpxOkKdbD1wB0ns53a5piVY02zjf072TblEweAbcYiFiPoSMF3kp+VhhQ==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.21.0.tgz", + "integrity": "sha512-BkLMNpdV6prozk8LlyK/SOoWLmUFi+ZD+pcqti9ILCbVvHGk1ui1g4jJOc2WDLaeExz2qWwojxlPce5PljcT3w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.18.1", + "@typescript-eslint/types": "8.21.0", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -5002,38 +5055,38 @@ } }, "node_modules/@vitest/expect": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.8.tgz", - "integrity": "sha512-8ytZ/fFHq2g4PJVAtDX57mayemKgDR6X3Oa2Foro+EygiOJHUXhCqBAAKQYYajZpFoIfvBCF1j6R6IYRSIUFuw==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.0.3.tgz", + "integrity": "sha512-SbRCHU4qr91xguu+dH3RUdI5dC86zm8aZWydbp961aIR7G8OYNN6ZiayFuf9WAngRbFOfdrLHCGgXTj3GtoMRQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "2.1.8", - "@vitest/utils": "2.1.8", + "@vitest/spy": "3.0.3", + "@vitest/utils": "3.0.3", "chai": "^5.1.2", - "tinyrainbow": "^1.2.0" + "tinyrainbow": "^2.0.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/mocker": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.8.tgz", - "integrity": "sha512-7guJ/47I6uqfttp33mgo6ga5Gr1VnL58rcqYKyShoRK9ebu8T5Rs6HN3s1NABiBeVTdWNrwUMcHH54uXZBN4zA==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.0.3.tgz", + "integrity": "sha512-XT2XBc4AN9UdaxJAeIlcSZ0ILi/GzmG5G8XSly4gaiqIvPV3HMTSIDZWJVX6QRJ0PX1m+W8Cy0K9ByXNb/bPIA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "2.1.8", + "@vitest/spy": "3.0.3", "estree-walker": "^3.0.3", - "magic-string": "^0.30.12" + "magic-string": "^0.30.17" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { "msw": "^2.4.9", - "vite": "^5.0.0" + "vite": "^5.0.0 || ^6.0.0" }, "peerDependenciesMeta": { "msw": { @@ -5072,42 +5125,42 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.8.tgz", - "integrity": "sha512-9HiSZ9zpqNLKlbIDRWOnAWqgcA7xu+8YxXSekhr0Ykab7PAYFkhkwoqVArPOtJhPmYeE2YHgKZlj3CP36z2AJQ==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.3.tgz", + "integrity": "sha512-gCrM9F7STYdsDoNjGgYXKPq4SkSxwwIU5nkaQvdUxiQ0EcNlez+PdKOVIsUJvh9P9IeIFmjn4IIREWblOBpP2Q==", "dev": true, "license": "MIT", "dependencies": { - "tinyrainbow": "^1.2.0" + "tinyrainbow": "^2.0.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/runner": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.8.tgz", - "integrity": "sha512-17ub8vQstRnRlIU5k50bG+QOMLHRhYPAna5tw8tYbj+jzjcspnwnwtPtiOlkuKC4+ixDPTuLZiqiWWQ2PSXHVg==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.0.3.tgz", + "integrity": "sha512-Rgi2kOAk5ZxWZlwPguRJFOBmWs6uvvyAAR9k3MvjRvYrG7xYvKChZcmnnpJCS98311CBDMqsW9MzzRFsj2gX3g==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "2.1.8", - "pathe": "^1.1.2" + "@vitest/utils": "3.0.3", + "pathe": "^2.0.1" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/snapshot": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.8.tgz", - "integrity": "sha512-20T7xRFbmnkfcmgVEz+z3AU/3b0cEzZOt/zmnvZEctg64/QZbSDJEVm9fLnnlSi74KibmRsO9/Qabi+t0vCRPg==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.0.3.tgz", + "integrity": "sha512-kNRcHlI4txBGztuJfPEJ68VezlPAXLRT1u5UCx219TU3kOG2DplNxhWLwDf2h6emwmTPogzLnGVwP6epDaJN6Q==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "2.1.8", - "magic-string": "^0.30.12", - "pathe": "^1.1.2" + "@vitest/pretty-format": "3.0.3", + "magic-string": "^0.30.17", + "pathe": "^2.0.1" }, "funding": { "url": "https://opencollective.com/vitest" @@ -5124,9 +5177,9 @@ } }, "node_modules/@vitest/spy": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.8.tgz", - "integrity": "sha512-5swjf2q95gXeYPevtW0BLk6H8+bPlMb4Vw/9Em4hFxDcaOxS+e0LOX4yqNxoHzMR2akEB2xfpnWUzkZokmgWDg==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.0.3.tgz", + "integrity": "sha512-7/dgux8ZBbF7lEIKNnEqQlyRaER9nkAL9eTmdKJkDO3hS8p59ATGwKOCUDHcBLKr7h/oi/6hP+7djQk8049T2A==", "dev": true, "license": "MIT", "dependencies": { @@ -5137,15 +5190,15 @@ } }, "node_modules/@vitest/utils": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.8.tgz", - "integrity": "sha512-dwSoui6djdwbfFmIgbIjX2ZhIoG7Ex/+xpxyiEgIGzjliY8xGkcpITKTlp6B4MgtGkF2ilvm97cPM96XZaAgcA==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.0.3.tgz", + "integrity": "sha512-f+s8CvyzPtMFY1eZKkIHGhPsQgYo5qCm6O8KZoim9qm1/jT64qBgGpO5tHscNH6BzRHM+edLNOP+3vO8+8pE/A==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "2.1.8", + "@vitest/pretty-format": "3.0.3", "loupe": "^3.1.2", - "tinyrainbow": "^1.2.0" + "tinyrainbow": "^2.0.0" }, "funding": { "url": "https://opencollective.com/vitest" @@ -5251,17 +5304,17 @@ } }, "node_modules/@vue/language-core": { - "version": "2.1.10", - "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.1.10.tgz", - "integrity": "sha512-DAI289d0K3AB5TUG3xDp9OuQ71CnrujQwJrQnfuZDwo6eGNf0UoRlPuaVNO+Zrn65PC3j0oB2i7mNmVPggeGeQ==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.2.0.tgz", + "integrity": "sha512-O1ZZFaaBGkKbsRfnVH1ifOK1/1BUkyK+3SQsfnh6PmMmD4qJcTU8godCeA96jjDRTL6zgnK7YzCHfaUlH2r0Mw==", "dev": true, "license": "MIT", "dependencies": { - "@volar/language-core": "~2.4.8", + "@volar/language-core": "~2.4.11", "@vue/compiler-dom": "^3.5.0", "@vue/compiler-vue2": "^2.7.16", "@vue/shared": "^3.5.0", - "alien-signals": "^0.2.0", + "alien-signals": "^0.4.9", "minimatch": "^9.0.3", "muggle-string": "^0.4.1", "path-browserify": "^1.0.1" @@ -5488,42 +5541,42 @@ } }, "node_modules/@webpack-cli/configtest": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz", - "integrity": "sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-3.0.1.tgz", + "integrity": "sha512-u8d0pJ5YFgneF/GuvEiDA61Tf1VDomHHYMjv/wc9XzYj7nopltpG96nXN5dJRstxZhcNpV1g+nT6CydO7pHbjA==", "license": "MIT", "engines": { - "node": ">=14.15.0" + "node": ">=18.12.0" }, "peerDependencies": { - "webpack": "5.x.x", - "webpack-cli": "5.x.x" + "webpack": "^5.82.0", + "webpack-cli": "6.x.x" } }, "node_modules/@webpack-cli/info": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.2.tgz", - "integrity": "sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-3.0.1.tgz", + "integrity": "sha512-coEmDzc2u/ffMvuW9aCjoRzNSPDl/XLuhPdlFRpT9tZHmJ/039az33CE7uH+8s0uL1j5ZNtfdv0HkfaKRBGJsQ==", "license": "MIT", "engines": { - "node": ">=14.15.0" + "node": ">=18.12.0" }, "peerDependencies": { - "webpack": "5.x.x", - "webpack-cli": "5.x.x" + "webpack": "^5.82.0", + "webpack-cli": "6.x.x" } }, "node_modules/@webpack-cli/serve": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.5.tgz", - "integrity": "sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-3.0.1.tgz", + "integrity": "sha512-sbgw03xQaCLiT6gcY/6u3qBDn01CWw/nbaXl3gTdTFuJJ75Gffv3E3DBpgvY2fkkrdS1fpjaXNOmJlnbtKauKg==", "license": "MIT", "engines": { - "node": ">=14.15.0" + "node": ">=18.12.0" }, "peerDependencies": { - "webpack": "5.x.x", - "webpack-cli": "5.x.x" + "webpack": "^5.82.0", + "webpack-cli": "6.x.x" }, "peerDependenciesMeta": { "webpack-dev-server": { @@ -5664,9 +5717,9 @@ } }, "node_modules/alien-signals": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-0.2.2.tgz", - "integrity": "sha512-cZIRkbERILsBOXTQmMrxc9hgpxglstn69zm+F1ARf4aPAzdAFYd6sBq87ErO0Fj3DV94tglcyHG5kQz9nDC/8A==", + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-0.4.14.tgz", + "integrity": "sha512-itUAVzhczTmP2U5yX67xVpsbbOiquusbWVyA9N+sy6+r6YVbFkahXvNCeEPWEOMhwDYwbVbGHFkVL03N9I5g+Q==", "dev": true, "license": "MIT" }, @@ -5837,9 +5890,9 @@ } }, "node_modules/asciinema-player": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/asciinema-player/-/asciinema-player-3.8.1.tgz", - "integrity": "sha512-NkpbFg81Y6iJFpDRndakLCQ0G26XSpvuT3vJTFjMRgHb26lqHgRNY9gun54e5MehZ4fEDNYkMZv+z6MfZ8c2aA==", + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/asciinema-player/-/asciinema-player-3.8.2.tgz", + "integrity": "sha512-Lgcnj9u/H6sRpGRX1my7Azcay6llLmB/GVkCGcDbPwdTVTisS1ir8SQ9jRWRvjlLUjpSJkN0euruvy3sLRM8tw==", "license": "Apache-2.0", "dependencies": { "@babel/runtime": "^7.21.0", @@ -6057,9 +6110,9 @@ } }, "node_modules/browserslist": { - "version": "4.24.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.3.tgz", - "integrity": "sha512-1CPmv8iobE2fyRMV97dAcMVegvvWKxmq94hkLiAkUGwKVTyDLw33K+ZxiFrREKmmps4rIw6grcCFCnTMSZ/YiA==", + "version": "4.24.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", + "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", "funding": [ { "type": "opencollective", @@ -6158,6 +6211,27 @@ "node": ">=8" } }, + "node_modules/cacheable": { + "version": "1.8.7", + "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-1.8.7.tgz", + "integrity": "sha512-AbfG7dAuYNjYxFUtL1lAqmlWdxczCJ47w7cFjhGcnGnUdwSo6VgmSojfoW3tUI12HUkgTJ5kqj78yyq6TsFtlg==", + "dev": true, + "license": "MIT", + "dependencies": { + "hookified": "^1.6.0", + "keyv": "^5.2.3" + } + }, + "node_modules/cacheable/node_modules/keyv": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.2.3.tgz", + "integrity": "sha512-AGKecUfzrowabUv0bH1RIR5Vf7w+l4S3xtQAypKaUpTdIR1EbrAcTxHCrpo9Q+IWeUlFE2palRtgIQcgm+PQJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@keyv/serialize": "^1.0.2" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -6177,9 +6251,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001690", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001690.tgz", - "integrity": "sha512-5ExiE3qQN6oF8Clf8ifIDcMRCRE/dMGcETG/XGMD8/XiXm6HXQgQTh1yZYLXXpSOsEUlJm1Xr7kGULZTuGtP/w==", + "version": "1.0.30001695", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001695.tgz", + "integrity": "sha512-vHyLade6wTgI2u1ec3WQBxv+2BrTERV28UXQu9LO6lZ9pYeMk34vjXFLOxo1A4UBA8XTL4njRQZdno/yYaSmWw==", "funding": [ { "type": "opencollective", @@ -6547,13 +6621,13 @@ "license": "MIT" }, "node_modules/core-js-compat": { - "version": "3.39.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.39.0.tgz", - "integrity": "sha512-VgEUx3VwlExr5no0tXlBt+silBvhTryPwCXRI2Id1PN8WTKu7MreethvddqOubrYxkFdv/RnYrqlv1sFNAUelw==", + "version": "3.40.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.40.0.tgz", + "integrity": "sha512-0XEDpr5y5mijvw8Lbc6E5AkjrHfp7eEoPlu36SWeAbcL8fn1G1ANe8DBlo2XoNN89oVpxWwOjYIPVzR4ZvsKCQ==", "dev": true, "license": "MIT", "dependencies": { - "browserslist": "^4.24.2" + "browserslist": "^4.24.3" }, "funding": { "type": "opencollective", @@ -6771,9 +6845,9 @@ "license": "MIT" }, "node_modules/cytoscape": { - "version": "3.30.4", - "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.30.4.tgz", - "integrity": "sha512-OxtlZwQl1WbwMmLiyPSEBuzeTIQnwZhJYYWFzZ2PhEHVFwpeaqNIkUzSiso00D98qk60l8Gwon2RP304d3BJ1A==", + "version": "3.31.0", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.31.0.tgz", + "integrity": "sha512-zDGn1K/tfZwEnoGOcHc0H4XazqAAXAuDpcYw9mUnUjATjqljyCNGJv8uEvbvxGaGHaVshxMecyl6oc6uKzRfbw==", "license": "MIT", "engines": { "node": ">=0.10" @@ -7479,9 +7553,9 @@ } }, "node_modules/domutils": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", - "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -7523,9 +7597,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.74", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.74.tgz", - "integrity": "sha512-ck3//9RC+6oss/1Bh9tiAVFy5vfSKbRHAFh7Z3/eTRkEqJeWgymloShB17Vg3Z4nmDNp35vAd1BZ6CMW4Wt6Iw==", + "version": "1.5.84", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.84.tgz", + "integrity": "sha512-I+DQ8xgafao9Ha6y0qjHHvpZ9OfyA1qKlkHkjywxzniORU2awxyz7f/iVJcULmrF2yrM3nHQf+iDjJtbbexd/g==", "license": "ISC" }, "node_modules/emoji-regex": { @@ -7544,9 +7618,9 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.17.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", - "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", + "version": "5.18.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.0.tgz", + "integrity": "sha512-0/r0MySGYG8YqlayBZ6MuCfECmHFdJ5qyPh8s8wa5Hnm6SaFLSK1VYCbj+NKp090Nm1caZhD+QTnmxO7esYGyQ==", "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", @@ -7613,9 +7687,9 @@ } }, "node_modules/es-module-lexer": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz", - "integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz", + "integrity": "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==", "license": "MIT" }, "node_modules/esbuild": { @@ -7782,13 +7856,13 @@ } }, "node_modules/eslint-config-prettier": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", - "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.0.1.tgz", + "integrity": "sha512-lZBts941cyJyeaooiKxAtzoPHTN+GbQTJFAIdQbRhA4/8whaAraEh47Whw/ZFfrjNSnlAxqfm9i0XVAEkULjCw==", "dev": true, "license": "MIT", "bin": { - "eslint-config-prettier": "bin/cli.js" + "eslint-config-prettier": "build/bin/cli.js" }, "peerDependencies": { "eslint": ">=7.0.0" @@ -8239,9 +8313,9 @@ } }, "node_modules/eslint-plugin-prettier": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.1.tgz", - "integrity": "sha512-gH3iR3g4JfF+yYPaJYkN7jEl9QbweL/YfkoRlNnuIEHEz1vHVlCmWOS+eGGiRuzHQXdJFCOTxRgvju9b8VUmrw==", + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.3.tgz", + "integrity": "sha512-qJ+y0FfCp/mQYQ/vWQ3s7eUlFEL4PyKfAJxsnYTJ4YT73nsJBWqmEpFryxV9OeUiqmsTsYJ5Y+KDNaeP31wrRw==", "dev": true, "license": "MIT", "dependencies": { @@ -8544,6 +8618,19 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/eslint-plugin-vitest/node_modules/ts-api-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", + "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, "node_modules/eslint-plugin-vue": { "version": "9.32.0", "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.32.0.tgz", @@ -8838,16 +8925,16 @@ "license": "Apache-2.0" }, "node_modules/fast-glob": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", - "micromatch": "^4.0.4" + "micromatch": "^4.0.8" }, "engines": { "node": ">=8.6.0" @@ -8886,9 +8973,19 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz", - "integrity": "sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==", + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", + "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], "license": "BSD-3-Clause" }, "node_modules/fastest-levenshtein": { @@ -8901,9 +8998,9 @@ } }, "node_modules/fastq": { - "version": "1.17.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", - "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.18.0.tgz", + "integrity": "sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw==", "license": "ISC", "dependencies": { "reusify": "^1.0.4" @@ -9123,9 +9220,9 @@ } }, "node_modules/get-tsconfig": { - "version": "4.8.1", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.8.1.tgz", - "integrity": "sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==", + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.0.tgz", + "integrity": "sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==", "license": "MIT", "dependencies": { "resolve-pkg-maps": "^1.0.0" @@ -9329,13 +9426,12 @@ } }, "node_modules/happy-dom": { - "version": "15.11.7", - "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-15.11.7.tgz", - "integrity": "sha512-KyrFvnl+J9US63TEzwoiJOQzZBJY7KgBushJA8X61DMbNsH+2ONkDuLDnCnwUiPTF42tLoEmrPyoqbenVA5zrg==", + "version": "16.7.2", + "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-16.7.2.tgz", + "integrity": "sha512-zOzw0xyYlDaF/ylwbAsduYZZVRTd5u7IwlFkGbEathIeJMLp3vrN3cHm3RS7PZpD9gr/IO16bHEswcgNyWTsqw==", "dev": true, "license": "MIT", "dependencies": { - "entities": "^4.5.0", "webidl-conversions": "^7.0.0", "whatwg-mimetype": "^3.0.0" }, @@ -9379,6 +9475,13 @@ "he": "bin/he" } }, + "node_modules/hookified": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.7.0.tgz", + "integrity": "sha512-XQdMjqC1AyeOzfs+17cnIk7Wdfu1hh2JtcyNfBf5u9jHrT3iZUlGHxLTntFBuk5lwkqJ6l3+daeQdHK5yByHVA==", + "dev": true, + "license": "MIT" + }, "node_modules/hosted-git-info": { "version": "2.8.9", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", @@ -9460,10 +9563,10 @@ } }, "node_modules/idiomorph": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/idiomorph/-/idiomorph-0.3.0.tgz", - "integrity": "sha512-UhV1Ey5xCxIwR9B+OgIjQa+1Jx99XQ1vQHUsKBU1RpQzCx1u+b+N6SOXgf5mEJDqemUI/ffccu6+71l2mJUsRA==", - "license": "BSD 2-Clause" + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/idiomorph/-/idiomorph-0.4.0.tgz", + "integrity": "sha512-VdXFpZOTXhLatJmhCWJR5oQKLXT01O6sFCJqT0/EqG71C4tYZdPJ5etvttwWsT2WKRYWz160XkNr1DUqXNMZHg==", + "license": "BSD-2-Clause" }, "node_modules/ieee754": { "version": "1.2.1", @@ -10042,9 +10145,9 @@ "license": "MIT" }, "node_modules/katex": { - "version": "0.16.18", - "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.18.tgz", - "integrity": "sha512-LRuk0rPdXrecAFwQucYjMiIs0JFefk6N1q/04mlw14aVIVgxq1FO0MA9RiIIGVaKOB5GIP5GH4aBBNraZERmaQ==", + "version": "0.16.21", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.21.tgz", + "integrity": "sha512-XvqR7FgOHtWupfMiigNzmh+MgUVmDGU2kXZm899ZkPfcuoPuFxyHmXsgATDpFZDAXCI8tvinaVcDo8PIIJSo4A==", "funding": [ "https://opencollective.com/katex", "https://github.com/sponsors/katex" @@ -10524,9 +10627,9 @@ } }, "node_modules/markdownlint-cli/node_modules/glob": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.0.tgz", - "integrity": "sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g==", + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.1.tgz", + "integrity": "sha512-zrQDm8XPnYEKawJScsnM0QzobJxlT/kHOOlRTio8IH/GrmxRE5fjllkzdaHclIuNjUQTJYH2xHNIGfdpJkDJUw==", "dev": true, "license": "ISC", "dependencies": { @@ -10781,14 +10884,14 @@ } }, "node_modules/mlly": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.3.tgz", - "integrity": "sha512-xUsx5n/mN0uQf4V548PKQ+YShA4/IW0KI1dZhrNrPCLG+xizETbHTkOa1f8/xut9JRPp8kQuMnz0oqwkTiLo/A==", + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.4.tgz", + "integrity": "sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==", "license": "MIT", "dependencies": { "acorn": "^8.14.0", - "pathe": "^1.1.2", - "pkg-types": "^1.2.1", + "pathe": "^2.0.1", + "pkg-types": "^1.3.0", "ufo": "^1.5.4" } }, @@ -11156,9 +11259,9 @@ "license": "BlueOak-1.0.0" }, "node_modules/package-manager-detector": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-0.2.7.tgz", - "integrity": "sha512-g4+387DXDKlZzHkP+9FLt8yKj8+/3tOkPv7DVTJGGRm00RkEWgqbFstX1mXJ4M0VDYhUqsTOiISqNOJnhAu3PQ==", + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-0.2.8.tgz", + "integrity": "sha512-ts9KSdroZisdvKMWVAVCXiKqnqNfXz4+IbrBG8/BWx/TR5le+jfenvoBuIZ6UWM9nz47W7AbD9qYfAwfWMIwzA==", "license": "MIT" }, "node_modules/parent-module": { @@ -11275,9 +11378,9 @@ } }, "node_modules/pathe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.2.tgz", + "integrity": "sha512-15Ztpk+nov8DR524R4BF7uEuzESgzUEAV4Ah7CUMNGXdE5ELuvxElxGXndBl32vMSsWa1jpNf22Z+Er3sKwq+w==", "license": "MIT" }, "node_modules/pathval": { @@ -11403,14 +11506,14 @@ } }, "node_modules/pkg-types": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.2.1.tgz", - "integrity": "sha512-sQoqa8alT3nHjGuTjuKgOnvjo4cljkufdtLMnO2LBP/wRwuDlo1tkaEdMxCRhyGRPacv/ztlZgDPm2b7FAmEvw==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", "license": "MIT", "dependencies": { "confbox": "^0.1.8", - "mlly": "^1.7.2", - "pathe": "^1.1.2" + "mlly": "^1.7.4", + "pathe": "^2.0.1" } }, "node_modules/playwright": { @@ -11482,9 +11585,9 @@ } }, "node_modules/postcss": { - "version": "8.4.49", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", - "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz", + "integrity": "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==", "funding": [ { "type": "opencollective", @@ -11501,7 +11604,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.7", + "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -11510,15 +11613,15 @@ } }, "node_modules/postcss-html": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/postcss-html/-/postcss-html-1.7.0.tgz", - "integrity": "sha512-MfcMpSUIaR/nNgeVS8AyvyDugXlADjN9AcV7e5rDfrF1wduIAGSkL4q2+wgrZgA3sHVAHLDO9FuauHhZYW2nBw==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/postcss-html/-/postcss-html-1.8.0.tgz", + "integrity": "sha512-5mMeb1TgLWoRKxZ0Xh9RZDfwUUIqRrcxO2uXO+Ezl1N5lqpCiSU5Gk6+1kZediBfBHFtPCdopr2UZ2SgUsKcgQ==", "dev": true, "license": "MIT", "dependencies": { "htmlparser2": "^8.0.0", "js-tokens": "^9.0.0", - "postcss": "^8.4.0", + "postcss": "^8.5.0", "postcss-safe-parser": "^6.0.0" }, "engines": { @@ -12340,9 +12443,9 @@ } }, "node_modules/resolve": { - "version": "1.22.9", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.9.tgz", - "integrity": "sha512-QxrmX1DzraFIi9PxdG5VkRfRwIgjwyud+z/iBwfRRrVmHc+P9Q7u2lSSpQ6bjr2gy5lrqIiU9vb6iAeGf2400A==", + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", "license": "MIT", "dependencies": { "is-core-module": "^2.16.0", @@ -12352,6 +12455,9 @@ "bin": { "resolve": "bin/resolve" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -12602,18 +12708,18 @@ } }, "node_modules/seroval": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.1.1.tgz", - "integrity": "sha512-rqEO6FZk8mv7Hyv4UCj3FD3b6Waqft605TLfsCe/BiaylRpyyMC0b+uA5TJKawX3KzMrdi3wsLbCaLplrQmBvQ==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.2.0.tgz", + "integrity": "sha512-GURoU99ko2UiAgUC3qDCk59Jb3Ss4Po8VIMGkG8j5PFo2Q7y0YSMP8QG9NuL/fJCoTz9V1XZUbpNIMXPOfaGpA==", "license": "MIT", "engines": { "node": ">=10" } }, "node_modules/seroval-plugins": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.1.1.tgz", - "integrity": "sha512-qNSy1+nUj7hsCOon7AO4wdAIo9P0jrzAMp18XhiOzA6/uO5TKtP7ScozVJ8T293oRIvi5wyCHSM4TrJo/c/GJA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.2.0.tgz", + "integrity": "sha512-hULTbfzSe81jGWLH8TAJjkEvw6JWMqOo9Uq+4V4vg+HNq53hyHldM9ZOfjdzokcFysiTp9aFdV2vJpZFqKeDjQ==", "license": "MIT", "engines": { "node": ">=10" @@ -12729,9 +12835,9 @@ } }, "node_modules/solid-js": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.9.3.tgz", - "integrity": "sha512-5ba3taPoZGt9GY3YlsCB24kCg0Lv/rie/HTD4kG6h4daZZz7+yK02xn8Vx8dLYBc9i6Ps5JwAbEiqjmKaLB3Ag==", + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.9.4.tgz", + "integrity": "sha512-ipQl8FJ31bFUoBNScDQTG3BjN6+9Rg+Q+f10bUbnO6EOTTf5NGerJeHc7wyu5I4RMHEl/WwZwUmy/PTRgxxZ8g==", "license": "MIT", "dependencies": { "csstype": "^3.1.0", @@ -12847,9 +12953,9 @@ } }, "node_modules/spdx-license-ids": { - "version": "3.0.20", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.20.tgz", - "integrity": "sha512-jg25NiDV/1fLtSgEgyvVyDunvaNHbuwF9lfNV17gSmPFAlYzdfNBlLtLzXTevwkPj7DhGbmN9VnmJIgLnhvaBw==", + "version": "3.0.21", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.21.tgz", + "integrity": "sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==", "license": "CC0-1.0" }, "node_modules/spdx-ranges": { @@ -13039,9 +13145,9 @@ "license": "ISC" }, "node_modules/stylelint": { - "version": "16.12.0", - "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.12.0.tgz", - "integrity": "sha512-F8zZ3L/rBpuoBZRvI4JVT20ZanPLXfQLzMOZg1tzPflRVh9mKpOZ8qcSIhh1my3FjAjZWG4T2POwGnmn6a6hbg==", + "version": "16.14.1", + "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.14.1.tgz", + "integrity": "sha512-oqCL7AC3786oTax35T/nuLL8p2C3k/8rHKAooezrPGRvUX0wX+qqs5kMWh5YYT4PHQgVDobHT4tw55WgpYG6Sw==", "dev": true, "funding": [ { @@ -13064,16 +13170,16 @@ "colord": "^2.9.3", "cosmiconfig": "^9.0.0", "css-functions-list": "^3.2.3", - "css-tree": "^3.0.1", + "css-tree": "^3.1.0", "debug": "^4.3.7", - "fast-glob": "^3.3.2", + "fast-glob": "^3.3.3", "fastest-levenshtein": "^1.0.16", - "file-entry-cache": "^9.1.0", + "file-entry-cache": "^10.0.5", "global-modules": "^2.0.0", "globby": "^11.1.0", "globjoin": "^0.1.4", "html-tags": "^3.3.1", - "ignore": "^6.0.2", + "ignore": "^7.0.3", "imurmurhash": "^0.1.4", "is-plain-object": "^5.0.0", "known-css-properties": "^0.35.0", @@ -13082,7 +13188,7 @@ "micromatch": "^4.0.8", "normalize-path": "^3.0.0", "picocolors": "^1.1.1", - "postcss": "^8.4.49", + "postcss": "^8.5.1", "postcss-resolve-nested-selector": "^0.1.6", "postcss-safe-parser": "^7.0.1", "postcss-selector-parser": "^7.0.0", @@ -13101,6 +13207,29 @@ "node": ">=18.12.0" } }, + "node_modules/stylelint-config-recommended": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/stylelint-config-recommended/-/stylelint-config-recommended-15.0.0.tgz", + "integrity": "sha512-9LejMFsat7L+NXttdHdTq94byn25TD+82bzGRiV1Pgasl99pWnwipXS5DguTpp3nP1XjvLXVnEJIuYBfsRjRkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/stylelint" + }, + { + "type": "github", + "url": "https://github.com/sponsors/stylelint" + } + ], + "license": "MIT", + "engines": { + "node": ">=18.12.0" + }, + "peerDependencies": { + "stylelint": "^16.13.0" + } + }, "node_modules/stylelint-declaration-block-no-ignored-properties": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/stylelint-declaration-block-no-ignored-properties/-/stylelint-declaration-block-no-ignored-properties-2.8.0.tgz", @@ -13115,9 +13244,9 @@ } }, "node_modules/stylelint-declaration-strict-value": { - "version": "1.10.6", - "resolved": "https://registry.npmjs.org/stylelint-declaration-strict-value/-/stylelint-declaration-strict-value-1.10.6.tgz", - "integrity": "sha512-aZGEW4Ee26Tx4UvpQJbcElVXZ42EleujEByiyKDTT7t83EeSe9t0lAG3OOLJnnvLjz/dQnp+L+3IYTMeQI51vQ==", + "version": "1.10.7", + "resolved": "https://registry.npmjs.org/stylelint-declaration-strict-value/-/stylelint-declaration-strict-value-1.10.7.tgz", + "integrity": "sha512-FlMvc3uoQtMcItW3Zh8lHJ7oN2KGns3vZDCaTZoGFRiRIjImQoxO+6gAeRf+Dgi0nXFICIPq9xxFsMi8zuYUsg==", "dev": true, "license": "MIT", "engines": { @@ -13127,6 +13256,24 @@ "stylelint": ">=7 <=16" } }, + "node_modules/stylelint-define-config": { + "version": "16.14.0", + "resolved": "https://registry.npmjs.org/stylelint-define-config/-/stylelint-define-config-16.14.0.tgz", + "integrity": "sha512-5R7/Vv6awCkNaPcedo1GuUp+7YTFvDnexogO4l/C0i349pBDYbefN6XzsDGsGOhU++maQSh2fp3mWNO0F16IjA==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.1.3" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=9.0.0", + "pnpm": ">=8.6.0" + }, + "peerDependencies": { + "stylelint": ">=16.0.0" + } + }, "node_modules/stylelint-value-no-unknown-custom-properties": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/stylelint-value-no-unknown-custom-properties/-/stylelint-value-no-unknown-custom-properties-6.0.1.tgz", @@ -13199,36 +13346,31 @@ "license": "MIT" }, "node_modules/stylelint/node_modules/file-entry-cache": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-9.1.0.tgz", - "integrity": "sha512-/pqPFG+FdxWQj+/WSuzXSDaNzxgTLr/OrR1QuqfEZzDakpdYE70PwUxL7BPUa8hpjbvY1+qvCl8k+8Tq34xJgg==", + "version": "10.0.5", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-10.0.5.tgz", + "integrity": "sha512-umpQsJrBNsdMDgreSryMEXvJh66XeLtZUwA8Gj7rHGearGufUFv6rB/bcXRFsiGWw/VeSUgUofF4Rf2UKEOrTA==", "dev": true, "license": "MIT", "dependencies": { - "flat-cache": "^5.0.0" - }, - "engines": { - "node": ">=18" + "flat-cache": "^6.1.5" } }, "node_modules/stylelint/node_modules/flat-cache": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-5.0.0.tgz", - "integrity": "sha512-JrqFmyUl2PnPi1OvLyTVHnQvwQ0S+e6lGSwu8OkAZlSaNIZciTY2H/cOOROxsBA1m/LZNHDsqAgDZt6akWcjsQ==", + "version": "6.1.5", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.5.tgz", + "integrity": "sha512-QR+2kN38f8nMfiIQ1LHYjuDEmZNZVjxuxY+HufbS3BW0EX01Q5OnH7iduOYRutmgiXb797HAKcXUeXrvRjjgSQ==", "dev": true, "license": "MIT", "dependencies": { - "flatted": "^3.3.1", - "keyv": "^4.5.4" - }, - "engines": { - "node": ">=18" + "cacheable": "^1.8.7", + "flatted": "^3.3.2", + "hookified": "^1.6.0" } }, "node_modules/stylelint/node_modules/ignore": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-6.0.2.tgz", - "integrity": "sha512-InwqeHHN2XpumIkMvpl/DCJVrAHgCsG5+cn1XlnLWGwtZBm8QJfSusItfrwx81CTp5agNZqpKU2J/ccC5nGT4A==", + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.3.tgz", + "integrity": "sha512-bAH5jbK/F3T3Jls4I0SO1hmPR0dKU0a7+SY6n1yzRtG54FLO8d6w/nxLFX2Nb7dBu6cCWXPaAME6cYqFUMmuCA==", "dev": true, "license": "MIT", "engines": { @@ -13287,9 +13429,9 @@ } }, "node_modules/stylis": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.4.tgz", - "integrity": "sha512-osIBl6BGUmSfDkyH2mB7EFvCJntXDrLhKjHTRj/rK6xLH0yuPrHULDRQzKokSOD4VoorhtKpfcfW1GAntu8now==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.5.tgz", + "integrity": "sha512-K7npNOKGRYuhAFFzkzMGfxFDpN6gDwf8hcMiE+uveTVbBgm93HrNP3ZDUpKqzZ4pG7TP6fmb+EMAQPjq9FqqvA==", "license": "MIT" }, "node_modules/stylus": { @@ -13758,9 +13900,9 @@ "license": "MIT" }, "node_modules/tinyexec": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.1.tgz", - "integrity": "sha512-WiCJLEECkO18gwqIp6+hJg0//p23HXp4S+gGtAKu3mI2F2/sXC4FvHvXvB0zJVVaTPhx1/tOwdbRsa1sOBIKqQ==", + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", "license": "MIT" }, "node_modules/tinypool": { @@ -13774,9 +13916,9 @@ } }, "node_modules/tinyrainbow": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", - "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", "dev": true, "license": "MIT", "engines": { @@ -13833,16 +13975,16 @@ "license": "MIT" }, "node_modules/ts-api-utils": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", - "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.0.tgz", + "integrity": "sha512-xCt/TOAc+EOHS1XPnijD3/yzpH6qg2xppZO1YDqGoVsNXfQfzHpOdNuXwrwOU8u4ITXJyDCTyt8w5g1sZv9ynQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=16" + "node": ">=18.12" }, "peerDependencies": { - "typescript": ">=4.2.0" + "typescript": ">=4.8.4" } }, "node_modules/ts-dedent": { @@ -13907,9 +14049,9 @@ } }, "node_modules/type-fest": { - "version": "4.30.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.30.2.tgz", - "integrity": "sha512-UJShLPYi1aWqCdq9HycOL/gwsuqda1OISdBO3t8RlXQC4QvtuIz4b5FCfe2dQIWEpmlRExKmcTBfP1r9bhY7ig==", + "version": "4.33.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.33.0.tgz", + "integrity": "sha512-s6zVrxuyKbbAsSAD5ZPTB77q4YIdRctkTbJ2/Dqlinwz+8ooH2gd+YA7VA6Pa93KML9GockVvoxjZ2vHP+mu8g==", "dev": true, "license": "(MIT OR CC0-1.0)", "engines": { @@ -13920,9 +14062,9 @@ } }, "node_modules/typescript": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", - "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -14018,9 +14160,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", - "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.2.tgz", + "integrity": "sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==", "funding": [ { "type": "opencollective", @@ -14038,7 +14180,7 @@ "license": "MIT", "dependencies": { "escalade": "^3.2.0", - "picocolors": "^1.1.0" + "picocolors": "^1.1.1" }, "bin": { "update-browserslist-db": "cli.js" @@ -14133,21 +14275,21 @@ "license": "MIT" }, "node_modules/vite": { - "version": "5.4.11", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz", - "integrity": "sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==", + "version": "6.0.11", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.0.11.tgz", + "integrity": "sha512-4VL9mQPKoHy4+FE0NnRE/kbY51TOfaknxAjt3fJbGJxhIpBZiqVzlZDEesWWsuREXHwNdAoOFZ9MkPEVXczHwg==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" + "esbuild": "^0.24.2", + "postcss": "^8.4.49", + "rollup": "^4.23.0" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" @@ -14156,19 +14298,25 @@ "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", - "terser": "^5.4.0" + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" }, "peerDependenciesMeta": { "@types/node": { "optional": true }, + "jiti": { + "optional": true + }, "less": { "optional": true }, @@ -14189,39 +14337,436 @@ }, "terser": { "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true } } }, "node_modules/vite-node": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.8.tgz", - "integrity": "sha512-uPAwSr57kYjAUux+8E2j0q0Fxpn8M9VoyfGiRI8Kfktz9NcYMCenwY5RnZxnF1WTu3TGiYipirIzacLL3VVGFg==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.0.3.tgz", + "integrity": "sha512-0sQcwhwAEw/UJGojbhOrnq3HtiZ3tC7BzpAa0lx3QaTX0S3YX70iGcik25UBdB96pmdwjyY2uyKNYruxCDmiEg==", "dev": true, "license": "MIT", "dependencies": { "cac": "^6.7.14", - "debug": "^4.3.7", - "es-module-lexer": "^1.5.4", - "pathe": "^1.1.2", - "vite": "^5.0.0" + "debug": "^4.4.0", + "es-module-lexer": "^1.6.0", + "pathe": "^2.0.1", + "vite": "^5.0.0 || ^6.0.0" }, "bin": { "vite-node": "vite-node.mjs" }, "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/vite-string-plugin": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/vite-string-plugin/-/vite-string-plugin-1.3.4.tgz", - "integrity": "sha512-mHvcooHgZ0nVbHtj9o+c5dzD2/nclr/SOG023EFYF/zRnO8bxB63bV9WUA9X+njlgLpOwCJ3LI2IdihKoi0gZQ==", + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/vite-string-plugin/-/vite-string-plugin-1.4.3.tgz", + "integrity": "sha512-srlSRDwWxjG4MwHOmeXA9H8b69VjR3PfQRv2hCkHsotnGbJOLY3gggClSmNTeKUJpnURckgTFeVVVdCXX/Gwpg==", "dev": true, "license": "BSD-2-Clause" }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz", + "integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.2.tgz", + "integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz", + "integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.2.tgz", + "integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz", + "integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz", + "integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz", + "integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz", + "integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz", + "integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz", + "integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz", + "integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz", + "integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz", + "integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz", + "integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz", + "integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz", + "integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz", + "integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz", + "integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz", + "integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz", + "integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz", + "integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz", + "integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz", + "integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/vite/node_modules/@types/estree": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", @@ -14229,6 +14774,47 @@ "dev": true, "license": "MIT" }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz", + "integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.24.2", + "@esbuild/android-arm": "0.24.2", + "@esbuild/android-arm64": "0.24.2", + "@esbuild/android-x64": "0.24.2", + "@esbuild/darwin-arm64": "0.24.2", + "@esbuild/darwin-x64": "0.24.2", + "@esbuild/freebsd-arm64": "0.24.2", + "@esbuild/freebsd-x64": "0.24.2", + "@esbuild/linux-arm": "0.24.2", + "@esbuild/linux-arm64": "0.24.2", + "@esbuild/linux-ia32": "0.24.2", + "@esbuild/linux-loong64": "0.24.2", + "@esbuild/linux-mips64el": "0.24.2", + "@esbuild/linux-ppc64": "0.24.2", + "@esbuild/linux-riscv64": "0.24.2", + "@esbuild/linux-s390x": "0.24.2", + "@esbuild/linux-x64": "0.24.2", + "@esbuild/netbsd-arm64": "0.24.2", + "@esbuild/netbsd-x64": "0.24.2", + "@esbuild/openbsd-arm64": "0.24.2", + "@esbuild/openbsd-x64": "0.24.2", + "@esbuild/sunos-x64": "0.24.2", + "@esbuild/win32-arm64": "0.24.2", + "@esbuild/win32-ia32": "0.24.2", + "@esbuild/win32-x64": "0.24.2" + } + }, "node_modules/vite/node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -14245,9 +14831,9 @@ } }, "node_modules/vite/node_modules/rollup": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.28.1.tgz", - "integrity": "sha512-61fXYl/qNVinKmGSTHAZ6Yy8I3YIJC/r2m9feHo6SwVAVcLT5MPwOUFe7EuURA/4m0NR8lXG4BBXuo/IZEsjMg==", + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.31.0.tgz", + "integrity": "sha512-9cCE8P4rZLx9+PjoyqHLs31V9a9Vpvfo4qNcs6JCiGWYhw2gijSetFbH6SSy1whnkgcefnUwr8sad7tgqsGvnw==", "dev": true, "license": "MIT", "dependencies": { @@ -14261,70 +14847,70 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.28.1", - "@rollup/rollup-android-arm64": "4.28.1", - "@rollup/rollup-darwin-arm64": "4.28.1", - "@rollup/rollup-darwin-x64": "4.28.1", - "@rollup/rollup-freebsd-arm64": "4.28.1", - "@rollup/rollup-freebsd-x64": "4.28.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.28.1", - "@rollup/rollup-linux-arm-musleabihf": "4.28.1", - "@rollup/rollup-linux-arm64-gnu": "4.28.1", - "@rollup/rollup-linux-arm64-musl": "4.28.1", - "@rollup/rollup-linux-loongarch64-gnu": "4.28.1", - "@rollup/rollup-linux-powerpc64le-gnu": "4.28.1", - "@rollup/rollup-linux-riscv64-gnu": "4.28.1", - "@rollup/rollup-linux-s390x-gnu": "4.28.1", - "@rollup/rollup-linux-x64-gnu": "4.28.1", - "@rollup/rollup-linux-x64-musl": "4.28.1", - "@rollup/rollup-win32-arm64-msvc": "4.28.1", - "@rollup/rollup-win32-ia32-msvc": "4.28.1", - "@rollup/rollup-win32-x64-msvc": "4.28.1", + "@rollup/rollup-android-arm-eabi": "4.31.0", + "@rollup/rollup-android-arm64": "4.31.0", + "@rollup/rollup-darwin-arm64": "4.31.0", + "@rollup/rollup-darwin-x64": "4.31.0", + "@rollup/rollup-freebsd-arm64": "4.31.0", + "@rollup/rollup-freebsd-x64": "4.31.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.31.0", + "@rollup/rollup-linux-arm-musleabihf": "4.31.0", + "@rollup/rollup-linux-arm64-gnu": "4.31.0", + "@rollup/rollup-linux-arm64-musl": "4.31.0", + "@rollup/rollup-linux-loongarch64-gnu": "4.31.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.31.0", + "@rollup/rollup-linux-riscv64-gnu": "4.31.0", + "@rollup/rollup-linux-s390x-gnu": "4.31.0", + "@rollup/rollup-linux-x64-gnu": "4.31.0", + "@rollup/rollup-linux-x64-musl": "4.31.0", + "@rollup/rollup-win32-arm64-msvc": "4.31.0", + "@rollup/rollup-win32-ia32-msvc": "4.31.0", + "@rollup/rollup-win32-x64-msvc": "4.31.0", "fsevents": "~2.3.2" } }, "node_modules/vitest": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.8.tgz", - "integrity": "sha512-1vBKTZskHw/aosXqQUlVWWlGUxSJR8YtiyZDJAFeW2kPAeX6S3Sool0mjspO+kXLuxVWlEDDowBAeqeAQefqLQ==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.0.3.tgz", + "integrity": "sha512-dWdwTFUW9rcnL0LyF2F+IfvNQWB0w9DERySCk8VMG75F8k25C7LsZoh6XfCjPvcR8Nb+Lqi9JKr6vnzH7HSrpQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "2.1.8", - "@vitest/mocker": "2.1.8", - "@vitest/pretty-format": "^2.1.8", - "@vitest/runner": "2.1.8", - "@vitest/snapshot": "2.1.8", - "@vitest/spy": "2.1.8", - "@vitest/utils": "2.1.8", + "@vitest/expect": "3.0.3", + "@vitest/mocker": "3.0.3", + "@vitest/pretty-format": "^3.0.3", + "@vitest/runner": "3.0.3", + "@vitest/snapshot": "3.0.3", + "@vitest/spy": "3.0.3", + "@vitest/utils": "3.0.3", "chai": "^5.1.2", - "debug": "^4.3.7", + "debug": "^4.4.0", "expect-type": "^1.1.0", - "magic-string": "^0.30.12", - "pathe": "^1.1.2", + "magic-string": "^0.30.17", + "pathe": "^2.0.1", "std-env": "^3.8.0", "tinybench": "^2.9.0", - "tinyexec": "^0.3.1", - "tinypool": "^1.0.1", - "tinyrainbow": "^1.2.0", - "vite": "^5.0.0", - "vite-node": "2.1.8", + "tinyexec": "^0.3.2", + "tinypool": "^1.0.2", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0", + "vite-node": "3.0.3", "why-is-node-running": "^2.3.0" }, "bin": { "vitest": "vitest.mjs" }, "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { "@edge-runtime/vm": "*", - "@types/node": "^18.0.0 || >=20.0.0", - "@vitest/browser": "2.1.8", - "@vitest/ui": "2.1.8", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.0.3", + "@vitest/ui": "3.0.3", "happy-dom": "*", "jsdom": "*" }, @@ -14526,6 +15112,23 @@ } } }, + "node_modules/vue-tsc": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.2.0.tgz", + "integrity": "sha512-gtmM1sUuJ8aSb0KoAFmK9yMxb8TxjewmxqTJ1aKphD5Cbu0rULFY6+UQT51zW7SpUcenfPUuflKyVwyx9Qdnxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/typescript": "~2.4.11", + "@vue/language-core": "2.2.0" + }, + "bin": { + "vue-tsc": "bin/vue-tsc.js" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + } + }, "node_modules/watchpack": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", @@ -14596,42 +15199,39 @@ } }, "node_modules/webpack-cli": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", - "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-6.0.1.tgz", + "integrity": "sha512-MfwFQ6SfwinsUVi0rNJm7rHZ31GyTcpVE5pgVA3hwFRb7COD4TzjUUwhGWKfO50+xdc2MQPuEBBJoqIMGt3JDw==", "license": "MIT", "dependencies": { - "@discoveryjs/json-ext": "^0.5.0", - "@webpack-cli/configtest": "^2.1.1", - "@webpack-cli/info": "^2.0.2", - "@webpack-cli/serve": "^2.0.5", + "@discoveryjs/json-ext": "^0.6.1", + "@webpack-cli/configtest": "^3.0.1", + "@webpack-cli/info": "^3.0.1", + "@webpack-cli/serve": "^3.0.1", "colorette": "^2.0.14", - "commander": "^10.0.1", + "commander": "^12.1.0", "cross-spawn": "^7.0.3", - "envinfo": "^7.7.3", + "envinfo": "^7.14.0", "fastest-levenshtein": "^1.0.12", "import-local": "^3.0.2", "interpret": "^3.1.1", "rechoir": "^0.8.0", - "webpack-merge": "^5.7.3" + "webpack-merge": "^6.0.1" }, "bin": { "webpack-cli": "bin/cli.js" }, "engines": { - "node": ">=14.15.0" + "node": ">=18.12.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/webpack" }, "peerDependencies": { - "webpack": "5.x.x" + "webpack": "^5.82.0" }, "peerDependenciesMeta": { - "@webpack-cli/generators": { - "optional": true - }, "webpack-bundle-analyzer": { "optional": true }, @@ -14641,26 +15241,26 @@ } }, "node_modules/webpack-cli/node_modules/commander": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", - "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", "license": "MIT", "engines": { - "node": ">=14" + "node": ">=18" } }, "node_modules/webpack-merge": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", - "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-6.0.1.tgz", + "integrity": "sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg==", "license": "MIT", "dependencies": { "clone-deep": "^4.0.1", "flat": "^5.0.2", - "wildcard": "^2.0.0" + "wildcard": "^2.0.1" }, "engines": { - "node": ">=10.0.0" + "node": ">=18.0.0" } }, "node_modules/webpack-sources": { @@ -14978,9 +15578,9 @@ "license": "ISC" }, "node_modules/yaml": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.1.tgz", - "integrity": "sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz", + "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==", "license": "ISC", "bin": { "yaml": "bin.mjs" diff --git a/package.json b/package.json index 6881ddb306c..7eb9000bb51 100644 --- a/package.json +++ b/package.json @@ -5,18 +5,18 @@ }, "dependencies": { "@citation-js/core": "0.7.14", - "@citation-js/plugin-bibtex": "0.7.16", + "@citation-js/plugin-bibtex": "0.7.17", "@citation-js/plugin-csl": "0.7.14", "@citation-js/plugin-software-formats": "0.6.1", "@github/markdown-toolbar-element": "2.2.3", - "@github/relative-time-element": "4.4.4", - "@github/text-expander-element": "2.8.0", + "@github/relative-time-element": "4.4.5", + "@github/text-expander-element": "2.9.1", "@mcaptcha/vanilla-glue": "0.1.0-alpha-3", "@primer/octicons": "19.14.0", "@silverwind/vue3-calendar-heatmap": "2.0.6", "add-asset-webpack-plugin": "3.0.0", "ansi_up": "6.0.2", - "asciinema-player": "3.8.1", + "asciinema-player": "3.8.2", "chart.js": "4.4.7", "chartjs-adapter-dayjs-4": "1.0.4", "chartjs-plugin-zoom": "2.2.0", @@ -28,11 +28,11 @@ "easymde": "2.18.0", "esbuild-loader": "4.2.2", "escape-goat": "4.0.0", - "fast-glob": "3.3.2", + "fast-glob": "3.3.3", "htmx.org": "2.0.4", - "idiomorph": "0.3.0", + "idiomorph": "0.4.0", "jquery": "3.7.1", - "katex": "0.16.18", + "katex": "0.16.21", "license-checker-webpack-plugin": "0.2.1", "mermaid": "11.4.1", "mini-css-extract-plugin": "2.9.2", @@ -41,7 +41,7 @@ "monaco-editor-webpack-plugin": "7.1.0", "pdfobject": "2.3.0", "perfect-debounce": "1.0.0", - "postcss": "8.4.49", + "postcss": "8.5.1", "postcss-loader": "8.1.1", "postcss-nesting": "13.0.1", "sortablejs": "1.15.6", @@ -52,7 +52,7 @@ "tippy.js": "6.3.7", "toastify-js": "1.12.0", "tributejs": "5.1.3", - "typescript": "5.7.2", + "typescript": "5.7.3", "uint8-to-base64": "0.2.0", "vanilla-colorful": "0.7.2", "vue": "3.5.13", @@ -60,15 +60,14 @@ "vue-chartjs": "5.3.2", "vue-loader": "17.4.2", "webpack": "5.97.1", - "webpack-cli": "5.1.4", + "webpack-cli": "6.0.1", "wrap-ansi": "9.0.0" }, "devDependencies": { "@eslint-community/eslint-plugin-eslint-comments": "4.4.1", "@playwright/test": "1.49.1", - "@silverwind/vue-tsc": "2.1.13", "@stoplight/spectral-cli": "6.14.2", - "@stylistic/eslint-plugin-js": "2.12.1", + "@stylistic/eslint-plugin-js": "2.13.0", "@stylistic/stylelint-plugin": "3.1.1", "@types/dropzone": "5.7.9", "@types/jquery": "3.5.32", @@ -80,8 +79,8 @@ "@types/throttle-debounce": "5.0.2", "@types/tinycolor2": "1.4.6", "@types/toastify-js": "1.12.3", - "@typescript-eslint/eslint-plugin": "8.18.1", - "@typescript-eslint/parser": "8.18.1", + "@typescript-eslint/eslint-plugin": "8.21.0", + "@typescript-eslint/parser": "8.21.0", "@vitejs/plugin-vue": "5.2.1", "eslint": "8.57.0", "eslint-import-resolver-typescript": "3.7.0", @@ -99,19 +98,22 @@ "eslint-plugin-vue": "9.32.0", "eslint-plugin-vue-scoped-css": "2.9.0", "eslint-plugin-wc": "2.2.0", - "happy-dom": "15.11.7", + "happy-dom": "16.7.2", "markdownlint-cli": "0.43.0", "nolyfill": "1.0.43", - "postcss-html": "1.7.0", - "stylelint": "16.12.0", + "postcss-html": "1.8.0", + "stylelint": "16.14.1", + "stylelint-config-recommended": "15.0.0", "stylelint-declaration-block-no-ignored-properties": "2.8.0", - "stylelint-declaration-strict-value": "1.10.6", + "stylelint-declaration-strict-value": "1.10.7", + "stylelint-define-config": "16.14.0", "stylelint-value-no-unknown-custom-properties": "6.0.1", "svgo": "3.3.2", - "type-fest": "4.30.2", + "type-fest": "4.33.0", "updates": "16.4.1", - "vite-string-plugin": "1.3.4", - "vitest": "2.1.8" + "vite-string-plugin": "1.4.3", + "vitest": "3.0.3", + "vue-tsc": "2.2.0" }, "browserslist": [ "defaults" diff --git a/poetry.lock b/poetry.lock index 5b9029eb8c7..ab5bcf05aca 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,14 +1,15 @@ -# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand. [[package]] name = "click" -version = "8.1.7" +version = "8.1.8" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ - {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, - {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, + {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, + {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, ] [package.dependencies] @@ -20,6 +21,7 @@ version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["dev"] files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, @@ -31,6 +33,7 @@ version = "1.15.1" description = "CSS unobfuscator and beautifier." optional = false python-versions = "*" +groups = ["dev"] files = [ {file = "cssbeautifier-1.15.1.tar.gz", hash = "sha256:9f7064362aedd559c55eeecf6b6bed65e05f33488dcbe39044f0403c26e1c006"}, ] @@ -42,33 +45,34 @@ six = ">=1.13.0" [[package]] name = "djlint" -version = "1.36.3" +version = "1.36.4" description = "HTML Template Linter and Formatter" optional = false python-versions = ">=3.9" +groups = ["dev"] files = [ - {file = "djlint-1.36.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2ae7c620b58e16d6bf003bd7de3f71376a7a3daa79dc02e77f3726d5a75243f2"}, - {file = "djlint-1.36.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e155ce0970d4a28d0a2e9f2e106733a2ad05910eee90e056b056d48049e4a97b"}, - {file = "djlint-1.36.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e8bb0406e60cc696806aa6226df137618f3889c72f2dbdfa76c908c99151579"}, - {file = "djlint-1.36.3-cp310-cp310-win_amd64.whl", hash = "sha256:76d32faf988ad58ef2e7a11d04046fc984b98391761bf1b61f9a6044da53d414"}, - {file = "djlint-1.36.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:32f7a5834000fff22e94d1d35f95aaf2e06f2af2cae18af0ed2a4e215d60e730"}, - {file = "djlint-1.36.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3eb1b9c0be499e63e8822a051e7e55f188ff1ab8172a85d338a8ae21c872060e"}, - {file = "djlint-1.36.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c2e0dd1f26eb472b8c84eb70d6482877b6497a1fd031d7534864088f016d5ea"}, - {file = "djlint-1.36.3-cp311-cp311-win_amd64.whl", hash = "sha256:a06b531ab9d049c46ad4d2365d1857004a1a9dd0c23c8eae94aa0d233c6ec00d"}, - {file = "djlint-1.36.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e66361a865e5e5a4bbcb40f56af7f256fd02cbf9d48b763a40172749cc294084"}, - {file = "djlint-1.36.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:36e102b80d83e9ac2e6be9a9ded32fb925945f6dbc7a7156e4415de1b0aa0dba"}, - {file = "djlint-1.36.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ac4b7370d80bd82281e57a470de8923ac494ffb571b89d8787cef57c738c69a"}, - {file = "djlint-1.36.3-cp312-cp312-win_amd64.whl", hash = "sha256:107cc56bbef13d60cc0ae774a4d52881bf98e37c02412e573827a3e549217e3a"}, - {file = "djlint-1.36.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2a9f51971d6e63c41ea9b3831c928e1f21ae6fe57e87a3452cfe672d10232433"}, - {file = "djlint-1.36.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:080c98714b55d8f0fef5c42beaee8247ebb2e3d46b0936473bd6c47808bb6302"}, - {file = "djlint-1.36.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f65a80e0b5cb13d357ea51ca6570b34c2d9d18974c1e57142de760ea27d49ed0"}, - {file = "djlint-1.36.3-cp313-cp313-win_amd64.whl", hash = "sha256:95ef6b67ef7f2b90d9434bba37d572031079001dc8524add85c00ef0386bda1e"}, - {file = "djlint-1.36.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8e2317a32094d525bc41cd11c8dc064bf38d1b442c99cc3f7c4a2616b5e6ce6e"}, - {file = "djlint-1.36.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e82266c28793cd15f97b93535d72bfbc77306eaaf6b210dd90910383a814ee6c"}, - {file = "djlint-1.36.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01b2101c2d1b079e8d545e6d9d03487fcca14d2371e44cbfdedee15b0bf4567c"}, - {file = "djlint-1.36.3-cp39-cp39-win_amd64.whl", hash = "sha256:15cde63ef28beb5194ff4137883025f125676ece1b574b64a3e1c6daed734639"}, - {file = "djlint-1.36.3-py3-none-any.whl", hash = "sha256:0c05cd5b76785de2c41a2420c06ffd112800bfc0f9c0f399cc7cea7c42557f4c"}, - {file = "djlint-1.36.3.tar.gz", hash = "sha256:d85735da34bc7ac93ad8ef9b4822cc2a23d5f0ce33f25438737b8dca1d404f78"}, + {file = "djlint-1.36.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a2dfb60883ceb92465201bfd392291a7597c6752baede6fbb6f1980cac8d6c5c"}, + {file = "djlint-1.36.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4bc6a1320c0030244b530ac200642f883d3daa451a115920ef3d56d08b644292"}, + {file = "djlint-1.36.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3164a048c7bb0baf042387b1e33f9bbbf99d90d1337bb4c3d66eb0f96f5400a1"}, + {file = "djlint-1.36.4-cp310-cp310-win_amd64.whl", hash = "sha256:3196d5277da5934962d67ad6c33a948ba77a7b6eadf064648bef6ee5f216b03c"}, + {file = "djlint-1.36.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d68da0ed10ee9ca1e32e225cbb8e9b98bf7e6f8b48a8e4836117b6605b88cc7"}, + {file = "djlint-1.36.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c0478d5392247f1e6ee29220bbdbf7fb4e1bc0e7e83d291fda6fb926c1787ba7"}, + {file = "djlint-1.36.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:962f7b83aee166e499eff916d631c6dde7f1447d7610785a60ed2a75a5763483"}, + {file = "djlint-1.36.4-cp311-cp311-win_amd64.whl", hash = "sha256:53cbc450aa425c832f09bc453b8a94a039d147b096740df54a3547fada77ed08"}, + {file = "djlint-1.36.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ff9faffd7d43ac20467493fa71d5355b5b330a00ade1c4d1e859022f4195223b"}, + {file = "djlint-1.36.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:79489e262b5ac23a8dfb7ca37f1eea979674cfc2d2644f7061d95bea12c38f7e"}, + {file = "djlint-1.36.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e58c5fa8c6477144a0be0a87273706a059e6dd0d6efae01146ae8c29cdfca675"}, + {file = "djlint-1.36.4-cp312-cp312-win_amd64.whl", hash = "sha256:bb6903777bf3124f5efedcddf1f4716aef097a7ec4223fc0fa54b865829a6e08"}, + {file = "djlint-1.36.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ead475013bcac46095b1bbc8cf97ed2f06e83422335734363f8a76b4ba7e47c2"}, + {file = "djlint-1.36.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6c601dfa68ea253311deb4a29a7362b7a64933bdfcfb5a06618f3e70ad1fa835"}, + {file = "djlint-1.36.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bda5014f295002363381969864addeb2db13955f1b26e772657c3b273ed7809f"}, + {file = "djlint-1.36.4-cp313-cp313-win_amd64.whl", hash = "sha256:16ce37e085afe5a30953b2bd87cbe34c37843d94c701fc68a2dda06c1e428ff4"}, + {file = "djlint-1.36.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:89678661888c03d7bc6cadd75af69db29962b5ecbf93a81518262f5c48329f04"}, + {file = "djlint-1.36.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5b01a98df3e1ab89a552793590875bc6e954cad661a9304057db75363d519fa0"}, + {file = "djlint-1.36.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dabbb4f7b93223d471d09ae34ed515fef98b2233cbca2449ad117416c44b1351"}, + {file = "djlint-1.36.4-cp39-cp39-win_amd64.whl", hash = "sha256:7a483390d17e44df5bc23dcea29bdf6b63f3ed8b4731d844773a4829af4f5e0b"}, + {file = "djlint-1.36.4-py3-none-any.whl", hash = "sha256:e9699b8ac3057a6ed04fb90835b89bee954ed1959c01541ce4f8f729c938afdd"}, + {file = "djlint-1.36.4.tar.gz", hash = "sha256:17254f218b46fe5a714b224c85074c099bcb74e3b2e1f15c2ddc2cf415a408a1"}, ] [package.dependencies] @@ -82,15 +86,18 @@ pyyaml = ">=6" regex = ">=2023" tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""} tqdm = ">=4.62.2" +typing-extensions = {version = ">=3.6.6", markers = "python_version < \"3.11\""} [[package]] name = "editorconfig" -version = "0.12.4" +version = "0.17.0" description = "EditorConfig File Locator and Interpreter for Python" optional = false python-versions = "*" +groups = ["dev"] files = [ - {file = "EditorConfig-0.12.4.tar.gz", hash = "sha256:24857fa1793917dd9ccf0c7810a07e05404ce9b823521c7dce22a4fb5d125f80"}, + {file = "EditorConfig-0.17.0-py3-none-any.whl", hash = "sha256:fe491719c5f65959ec00b167d07740e7ffec9a3f362038c72b289330b9991dfc"}, + {file = "editorconfig-0.17.0.tar.gz", hash = "sha256:8739052279699840065d3a9f5c125d7d5a98daeefe53b0e5274261d77cb49aa2"}, ] [[package]] @@ -99,6 +106,7 @@ version = "1.15.1" description = "JavaScript unobfuscator and beautifier." optional = false python-versions = "*" +groups = ["dev"] files = [ {file = "jsbeautifier-1.15.1.tar.gz", hash = "sha256:ebd733b560704c602d744eafc839db60a1ee9326e30a2a80c4adb8718adc1b24"}, ] @@ -113,6 +121,7 @@ version = "0.10.0" description = "A Python implementation of the JSON5 data format." optional = false python-versions = ">=3.8.0" +groups = ["dev"] files = [ {file = "json5-0.10.0-py3-none-any.whl", hash = "sha256:19b23410220a7271e8377f81ba8aacba2fdd56947fbb137ee5977cbe1f5e8dfa"}, {file = "json5-0.10.0.tar.gz", hash = "sha256:e66941c8f0a02026943c52c2eb34ebeb2a6f819a0be05920a6f5243cd30fd559"}, @@ -127,6 +136,7 @@ version = "0.12.1" description = "Utility library for gitignore style pattern matching of file paths." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, @@ -138,6 +148,7 @@ version = "6.0.2" description = "YAML parser and emitter for Python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, @@ -200,6 +211,7 @@ version = "2024.11.6" description = "Alternative regular expression module, to replace re." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "regex-2024.11.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ff590880083d60acc0433f9c3f713c51f7ac6ebb9adf889c79a261ecf541aa91"}, {file = "regex-2024.11.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:658f90550f38270639e83ce492f27d2c8d2cd63805c65a13a14d36ca126753f0"}, @@ -303,6 +315,7 @@ version = "1.17.0" description = "Python 2 and 3 compatibility utilities" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["dev"] files = [ {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, @@ -314,6 +327,8 @@ version = "2.2.1" description = "A lil' TOML parser" optional = false python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version < \"3.11\"" files = [ {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, @@ -355,6 +370,7 @@ version = "4.67.1" description = "Fast, Extensible Progress Meter" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2"}, {file = "tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2"}, @@ -370,12 +386,26 @@ notebook = ["ipywidgets (>=6)"] slack = ["slack-sdk"] telegram = ["requests"] +[[package]] +name = "typing-extensions" +version = "4.12.2" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version < \"3.11\"" +files = [ + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, +] + [[package]] name = "yamllint" version = "1.35.1" description = "A linter for YAML files." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "yamllint-1.35.1-py3-none-any.whl", hash = "sha256:2e16e504bb129ff515b37823b472750b36b6de07963bd74b307341ef5ad8bdc3"}, {file = "yamllint-1.35.1.tar.gz", hash = "sha256:7a003809f88324fd2c877734f2d575ee7881dd9043360657cc8049c809eba6cd"}, @@ -389,6 +419,6 @@ pyyaml = "*" dev = ["doc8", "flake8", "flake8-import-order", "rstcheck[sphinx]", "sphinx"] [metadata] -lock-version = "2.0" +lock-version = "2.1" python-versions = "^3.10" -content-hash = "01b1e2f910276dd20a70ebb665c83415c37531709d90874f5b7a86a5305e2369" +content-hash = "f2e8260efe6e25f77ef387daff9551e41d25027e4794b42bc7a851ed0dfafd85" diff --git a/poetry.toml b/poetry.toml index 0299355b5dd..6eda9f8effc 100644 --- a/poetry.toml +++ b/poetry.toml @@ -1,4 +1,3 @@ [virtualenvs] in-project = true options.no-pip = true -options.no-setuptools = true diff --git a/public/assets/img/feishu.png b/public/assets/img/feishu.png deleted file mode 100644 index 2c3ab744136..00000000000 Binary files a/public/assets/img/feishu.png and /dev/null differ diff --git a/public/assets/img/svg/gitea-feishu.svg b/public/assets/img/svg/gitea-feishu.svg new file mode 100644 index 00000000000..d7a5ead4997 --- /dev/null +++ b/public/assets/img/svg/gitea-feishu.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 4d36b30ea78..851504e72b1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ package-mode = false python = "^3.10" [tool.poetry.group.dev.dependencies] -djlint = "1.36.3" +djlint = "1.36.4" yamllint = "1.35.1" [tool.djlint] diff --git a/routers/api/actions/runner/main_test.go b/routers/api/actions/runner/main_test.go deleted file mode 100644 index 1e80a4f5caf..00000000000 --- a/routers/api/actions/runner/main_test.go +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright 2024 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package runner - -import ( - "testing" - - "code.gitea.io/gitea/models/unittest" -) - -func TestMain(m *testing.M) { - unittest.MainTest(m) -} diff --git a/routers/api/actions/runner/utils.go b/routers/api/actions/runner/utils.go index 539be8d8890..0fd7ca5c447 100644 --- a/routers/api/actions/runner/utils.go +++ b/routers/api/actions/runner/utils.go @@ -8,14 +8,8 @@ import ( "fmt" actions_model "code.gitea.io/gitea/models/actions" - "code.gitea.io/gitea/models/db" secret_model "code.gitea.io/gitea/models/secret" - actions_module "code.gitea.io/gitea/modules/actions" - "code.gitea.io/gitea/modules/container" - "code.gitea.io/gitea/modules/git" - "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/services/actions" runnerv1 "code.gitea.io/actions-proto-go/runner/v1" @@ -65,82 +59,16 @@ func pickTask(ctx context.Context, runner *actions_model.ActionRunner) (*runnerv } func generateTaskContext(t *actions_model.ActionTask) *structpb.Struct { - event := map[string]any{} - _ = json.Unmarshal([]byte(t.Job.Run.EventPayload), &event) - - // TriggerEvent is added in https://github.com/go-gitea/gitea/pull/25229 - // This fallback is for the old ActionRun that doesn't have the TriggerEvent field - // and should be removed in 1.22 - eventName := t.Job.Run.TriggerEvent - if eventName == "" { - eventName = t.Job.Run.Event.Event() - } - - baseRef := "" - headRef := "" - ref := t.Job.Run.Ref - sha := t.Job.Run.CommitSHA - if pullPayload, err := t.Job.Run.GetPullRequestEventPayload(); err == nil && pullPayload.PullRequest != nil && pullPayload.PullRequest.Base != nil && pullPayload.PullRequest.Head != nil { - baseRef = pullPayload.PullRequest.Base.Ref - headRef = pullPayload.PullRequest.Head.Ref - - // if the TriggerEvent is pull_request_target, ref and sha need to be set according to the base of pull request - // In GitHub's documentation, ref should be the branch or tag that triggered workflow. But when the TriggerEvent is pull_request_target, - // the ref will be the base branch. - if t.Job.Run.TriggerEvent == actions_module.GithubEventPullRequestTarget { - ref = git.BranchPrefix + pullPayload.PullRequest.Base.Name - sha = pullPayload.PullRequest.Base.Sha - } - } - - refName := git.RefName(ref) - giteaRuntimeToken, err := actions.CreateAuthorizationToken(t.ID, t.Job.RunID, t.JobID) if err != nil { log.Error("actions.CreateAuthorizationToken failed: %v", err) } - taskContext, err := structpb.NewStruct(map[string]any{ - // standard contexts, see https://docs.github.com/en/actions/learn-github-actions/contexts#github-context - "action": "", // string, The name of the action currently running, or the id of a step. GitHub removes special characters, and uses the name __run when the current step runs a script without an id. If you use the same action more than once in the same job, the name will include a suffix with the sequence number with underscore before it. For example, the first script you run will have the name __run, and the second script will be named __run_2. Similarly, the second invocation of actions/checkout will be actionscheckout2. - "action_path": "", // string, The path where an action is located. This property is only supported in composite actions. You can use this path to access files located in the same repository as the action. - "action_ref": "", // string, For a step executing an action, this is the ref of the action being executed. For example, v2. - "action_repository": "", // string, For a step executing an action, this is the owner and repository name of the action. For example, actions/checkout. - "action_status": "", // string, For a composite action, the current result of the composite action. - "actor": t.Job.Run.TriggerUser.Name, // string, The username of the user that triggered the initial workflow run. If the workflow run is a re-run, this value may differ from github.triggering_actor. Any workflow re-runs will use the privileges of github.actor, even if the actor initiating the re-run (github.triggering_actor) has different privileges. - "api_url": setting.AppURL + "api/v1", // string, The URL of the GitHub REST API. - "base_ref": baseRef, // string, The base_ref or target branch of the pull request in a workflow run. This property is only available when the event that triggers a workflow run is either pull_request or pull_request_target. - "env": "", // string, Path on the runner to the file that sets environment variables from workflow commands. This file is unique to the current step and is a different file for each step in a job. For more information, see "Workflow commands for GitHub Actions." - "event": event, // object, The full event webhook payload. You can access individual properties of the event using this context. This object is identical to the webhook payload of the event that triggered the workflow run, and is different for each event. The webhooks for each GitHub Actions event is linked in "Events that trigger workflows." For example, for a workflow run triggered by the push event, this object contains the contents of the push webhook payload. - "event_name": eventName, // string, The name of the event that triggered the workflow run. - "event_path": "", // string, The path to the file on the runner that contains the full event webhook payload. - "graphql_url": "", // string, The URL of the GitHub GraphQL API. - "head_ref": headRef, // string, The head_ref or source branch of the pull request in a workflow run. This property is only available when the event that triggers a workflow run is either pull_request or pull_request_target. - "job": fmt.Sprint(t.JobID), // string, The job_id of the current job. - "ref": ref, // string, The fully-formed ref of the branch or tag that triggered the workflow run. For workflows triggered by push, this is the branch or tag ref that was pushed. For workflows triggered by pull_request, this is the pull request merge branch. For workflows triggered by release, this is the release tag created. For other triggers, this is the branch or tag ref that triggered the workflow run. This is only set if a branch or tag is available for the event type. The ref given is fully-formed, meaning that for branches the format is refs/heads/, for pull requests it is refs/pull//merge, and for tags it is refs/tags/. For example, refs/heads/feature-branch-1. - "ref_name": refName.ShortName(), // string, The short ref name of the branch or tag that triggered the workflow run. This value matches the branch or tag name shown on GitHub. For example, feature-branch-1. - "ref_protected": false, // boolean, true if branch protections are configured for the ref that triggered the workflow run. - "ref_type": refName.RefType(), // string, The type of ref that triggered the workflow run. Valid values are branch or tag. - "path": "", // string, Path on the runner to the file that sets system PATH variables from workflow commands. This file is unique to the current step and is a different file for each step in a job. For more information, see "Workflow commands for GitHub Actions." - "repository": t.Job.Run.Repo.OwnerName + "/" + t.Job.Run.Repo.Name, // string, The owner and repository name. For example, Codertocat/Hello-World. - "repository_owner": t.Job.Run.Repo.OwnerName, // string, The repository owner's name. For example, Codertocat. - "repositoryUrl": t.Job.Run.Repo.HTMLURL(), // string, The Git URL to the repository. For example, git://github.com/codertocat/hello-world.git. - "retention_days": "", // string, The number of days that workflow run logs and artifacts are kept. - "run_id": fmt.Sprint(t.Job.RunID), // string, A unique number for each workflow run within a repository. This number does not change if you re-run the workflow run. - "run_number": fmt.Sprint(t.Job.Run.Index), // string, A unique number for each run of a particular workflow in a repository. This number begins at 1 for the workflow's first run, and increments with each new run. This number does not change if you re-run the workflow run. - "run_attempt": fmt.Sprint(t.Job.Attempt), // string, A unique number for each attempt of a particular workflow run in a repository. This number begins at 1 for the workflow run's first attempt, and increments with each re-run. - "secret_source": "Actions", // string, The source of a secret used in a workflow. Possible values are None, Actions, Dependabot, or Codespaces. - "server_url": setting.AppURL, // string, The URL of the GitHub server. For example: https://github.com. - "sha": sha, // string, The commit SHA that triggered the workflow. The value of this commit SHA depends on the event that triggered the workflow. For more information, see "Events that trigger workflows." For example, ffac537e6cbbf934b08745a378932722df287a53. - "token": t.Token, // string, A token to authenticate on behalf of the GitHub App installed on your repository. This is functionally equivalent to the GITHUB_TOKEN secret. For more information, see "Automatic token authentication." - "triggering_actor": "", // string, The username of the user that initiated the workflow run. If the workflow run is a re-run, this value may differ from github.actor. Any workflow re-runs will use the privileges of github.actor, even if the actor initiating the re-run (github.triggering_actor) has different privileges. - "workflow": t.Job.Run.WorkflowID, // string, The name of the workflow. If the workflow file doesn't specify a name, the value of this property is the full path of the workflow file in the repository. - "workspace": "", // string, The default working directory on the runner for steps, and the default location of your repository when using the checkout action. + gitCtx := actions.GenerateGiteaContext(t.Job.Run, t.Job) + gitCtx["token"] = t.Token + gitCtx["gitea_runtime_token"] = giteaRuntimeToken - // additional contexts - "gitea_default_actions_url": setting.Actions.DefaultActionsURL.URL(), - "gitea_runtime_token": giteaRuntimeToken, - }) + taskContext, err := structpb.NewStruct(gitCtx) if err != nil { log.Error("structpb.NewStruct failed: %v", err) } @@ -150,68 +78,18 @@ func generateTaskContext(t *actions_model.ActionTask) *structpb.Struct { func findTaskNeeds(ctx context.Context, task *actions_model.ActionTask) (map[string]*runnerv1.TaskNeed, error) { if err := task.LoadAttributes(ctx); err != nil { - return nil, fmt.Errorf("LoadAttributes: %w", err) + return nil, fmt.Errorf("task LoadAttributes: %w", err) } - if len(task.Job.Needs) == 0 { - return nil, nil - } - needs := container.SetOf(task.Job.Needs...) - - jobs, err := db.Find[actions_model.ActionRunJob](ctx, actions_model.FindRunJobOptions{RunID: task.Job.RunID}) + taskNeeds, err := actions.FindTaskNeeds(ctx, task.Job) if err != nil { - return nil, fmt.Errorf("FindRunJobs: %w", err) + return nil, err } - - jobIDJobs := make(map[string][]*actions_model.ActionRunJob) - for _, job := range jobs { - jobIDJobs[job.JobID] = append(jobIDJobs[job.JobID], job) - } - - ret := make(map[string]*runnerv1.TaskNeed, len(needs)) - for jobID, jobsWithSameID := range jobIDJobs { - if !needs.Contains(jobID) { - continue - } - var jobOutputs map[string]string - for _, job := range jobsWithSameID { - if job.TaskID == 0 || !job.Status.IsDone() { - // it shouldn't happen, or the job has been rerun - continue - } - got, err := actions_model.FindTaskOutputByTaskID(ctx, job.TaskID) - if err != nil { - return nil, fmt.Errorf("FindTaskOutputByTaskID: %w", err) - } - outputs := make(map[string]string, len(got)) - for _, v := range got { - outputs[v.OutputKey] = v.OutputValue - } - if len(jobOutputs) == 0 { - jobOutputs = outputs - } else { - jobOutputs = mergeTwoOutputs(outputs, jobOutputs) - } - } + ret := make(map[string]*runnerv1.TaskNeed, len(taskNeeds)) + for jobID, taskNeed := range taskNeeds { ret[jobID] = &runnerv1.TaskNeed{ - Outputs: jobOutputs, - Result: runnerv1.Result(actions_model.AggregateJobStatus(jobsWithSameID)), + Outputs: taskNeed.Outputs, + Result: runnerv1.Result(taskNeed.Result), } } - return ret, nil } - -// mergeTwoOutputs merges two outputs from two different ActionRunJobs -// Values with the same output name may be overridden. The user should ensure the output names are unique. -// See https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#using-job-outputs-in-a-matrix-job -func mergeTwoOutputs(o1, o2 map[string]string) map[string]string { - ret := make(map[string]string, len(o1)) - for k1, v1 := range o1 { - if len(v1) > 0 { - ret[k1] = v1 - } else { - ret[k1] = o2[k1] - } - } - return ret -} diff --git a/routers/api/packages/alpine/alpine.go b/routers/api/packages/alpine/alpine.go index 4b652c9ecc3..f35cff3df23 100644 --- a/routers/api/packages/alpine/alpine.go +++ b/routers/api/packages/alpine/alpine.go @@ -114,7 +114,7 @@ func UploadPackageFile(ctx *context.Context) { pck, err := alpine_module.ParsePackage(buf) if err != nil { - if errors.Is(err, util.ErrInvalidArgument) || err == io.EOF { + if errors.Is(err, util.ErrInvalidArgument) || errors.Is(err, io.EOF) { apiError(ctx, http.StatusBadRequest, err) } else { apiError(ctx, http.StatusInternalServerError, err) diff --git a/routers/api/packages/api.go b/routers/api/packages/api.go index 8c06836ff80..41c3eb95e90 100644 --- a/routers/api/packages/api.go +++ b/routers/api/packages/api.go @@ -138,7 +138,8 @@ func CommonRoutes() *web.Router { }, reqPackageAccess(perm.AccessModeRead)) r.Group("/arch", func() { r.Methods("HEAD,GET", "/repository.key", arch.GetRepositoryKey) - r.PathGroup("*", func(g *web.RouterPathGroup) { + r.Methods("PUT", "" /* no repository */, reqPackageAccess(perm.AccessModeWrite), arch.UploadPackageFile) + r.PathGroup("/*", func(g *web.RouterPathGroup) { g.MatchPath("PUT", "/", reqPackageAccess(perm.AccessModeWrite), arch.UploadPackageFile) g.MatchPath("HEAD,GET", "///", arch.GetPackageOrRepositoryFile) g.MatchPath("DELETE", "////", reqPackageAccess(perm.AccessModeWrite), arch.DeletePackageVersion) @@ -430,8 +431,6 @@ func CommonRoutes() *web.Router { r.Post("/api/charts", reqPackageAccess(perm.AccessModeWrite), helm.UploadPackage) }, reqPackageAccess(perm.AccessModeRead)) r.Group("/maven", func() { - // FIXME: this path design is not right. - // It should be `/.../{groupId}/{artifactId}/{version}`, but not `/.../{groupId}-{artifactId}/{version}` r.Put("/*", reqPackageAccess(perm.AccessModeWrite), maven.UploadPackageFile) r.Get("/*", maven.DownloadPackageFile) r.Head("/*", maven.ProvidePackageFileHeader) @@ -700,150 +699,28 @@ func ContainerRoutes() *web.Router { }) r.Get("/_catalog", container.ReqContainerAccess, container.GetRepositoryList) r.Group("/{username}", func() { - r.Group("/{image}", func() { - r.Group("/blobs/uploads", func() { - r.Post("", container.InitiateUploadBlob) - r.Group("/{uuid}", func() { - r.Get("", container.GetUploadBlob) - r.Patch("", container.UploadBlob) - r.Put("", container.EndUploadBlob) - r.Delete("", container.CancelUploadBlob) - }) - }, reqPackageAccess(perm.AccessModeWrite)) - r.Group("/blobs/{digest}", func() { - r.Head("", container.HeadBlob) - r.Get("", container.GetBlob) - r.Delete("", reqPackageAccess(perm.AccessModeWrite), container.DeleteBlob) - }) - r.Group("/manifests/{reference}", func() { - r.Put("", reqPackageAccess(perm.AccessModeWrite), container.UploadManifest) - r.Head("", container.HeadManifest) - r.Get("", container.GetManifest) - r.Delete("", reqPackageAccess(perm.AccessModeWrite), container.DeleteManifest) - }) - r.Get("/tags/list", container.GetTagList) - }, container.VerifyImageName) - - var ( - blobsUploadsPattern = regexp.MustCompile(`\A(.+)/blobs/uploads/([a-zA-Z0-9-_.=]+)\z`) - blobsPattern = regexp.MustCompile(`\A(.+)/blobs/([^/]+)\z`) - manifestsPattern = regexp.MustCompile(`\A(.+)/manifests/([^/]+)\z`) - ) - - // Manual mapping of routes because {image} can contain slashes which chi does not support - r.Methods("HEAD,GET,POST,PUT,PATCH,DELETE", "/*", func(ctx *context.Context) { - path := ctx.PathParam("*") - isHead := ctx.Req.Method == "HEAD" - isGet := ctx.Req.Method == "GET" - isPost := ctx.Req.Method == "POST" - isPut := ctx.Req.Method == "PUT" - isPatch := ctx.Req.Method == "PATCH" - isDelete := ctx.Req.Method == "DELETE" - - if isPost && strings.HasSuffix(path, "/blobs/uploads") { - reqPackageAccess(perm.AccessModeWrite)(ctx) - if ctx.Written() { - return - } - - ctx.SetPathParam("image", path[:len(path)-14]) - container.VerifyImageName(ctx) - if ctx.Written() { - return - } - - container.InitiateUploadBlob(ctx) - return - } - if isGet && strings.HasSuffix(path, "/tags/list") { - ctx.SetPathParam("image", path[:len(path)-10]) - container.VerifyImageName(ctx) - if ctx.Written() { - return - } - - container.GetTagList(ctx) - return - } - - m := blobsUploadsPattern.FindStringSubmatch(path) - if len(m) == 3 && (isGet || isPut || isPatch || isDelete) { - reqPackageAccess(perm.AccessModeWrite)(ctx) - if ctx.Written() { - return - } - - ctx.SetPathParam("image", m[1]) - container.VerifyImageName(ctx) - if ctx.Written() { - return - } - - ctx.SetPathParam("uuid", m[2]) - - if isGet { + r.PathGroup("/*", func(g *web.RouterPathGroup) { + g.MatchPath("POST", "//blobs/uploads", reqPackageAccess(perm.AccessModeWrite), container.VerifyImageName, container.InitiateUploadBlob) + g.MatchPath("GET", "//tags/list", container.VerifyImageName, container.GetTagList) + g.MatchPath("GET,PATCH,PUT,DELETE", `//blobs/uploads/`, reqPackageAccess(perm.AccessModeWrite), container.VerifyImageName, func(ctx *context.Context) { + if ctx.Req.Method == http.MethodGet { container.GetUploadBlob(ctx) - } else if isPatch { + } else if ctx.Req.Method == http.MethodPatch { container.UploadBlob(ctx) - } else if isPut { + } else if ctx.Req.Method == http.MethodPut { container.EndUploadBlob(ctx) - } else { + } else /* DELETE */ { container.CancelUploadBlob(ctx) } - return - } - m = blobsPattern.FindStringSubmatch(path) - if len(m) == 3 && (isHead || isGet || isDelete) { - ctx.SetPathParam("image", m[1]) - container.VerifyImageName(ctx) - if ctx.Written() { - return - } + }) + g.MatchPath("HEAD", `//blobs/`, container.VerifyImageName, container.HeadBlob) + g.MatchPath("GET", `//blobs/`, container.VerifyImageName, container.GetBlob) + g.MatchPath("DELETE", `//blobs/`, container.VerifyImageName, reqPackageAccess(perm.AccessModeWrite), container.DeleteBlob) - ctx.SetPathParam("digest", m[2]) - - if isHead { - container.HeadBlob(ctx) - } else if isGet { - container.GetBlob(ctx) - } else { - reqPackageAccess(perm.AccessModeWrite)(ctx) - if ctx.Written() { - return - } - container.DeleteBlob(ctx) - } - return - } - m = manifestsPattern.FindStringSubmatch(path) - if len(m) == 3 && (isHead || isGet || isPut || isDelete) { - ctx.SetPathParam("image", m[1]) - container.VerifyImageName(ctx) - if ctx.Written() { - return - } - - ctx.SetPathParam("reference", m[2]) - - if isHead { - container.HeadManifest(ctx) - } else if isGet { - container.GetManifest(ctx) - } else { - reqPackageAccess(perm.AccessModeWrite)(ctx) - if ctx.Written() { - return - } - if isPut { - container.UploadManifest(ctx) - } else { - container.DeleteManifest(ctx) - } - } - return - } - - ctx.Status(http.StatusNotFound) + g.MatchPath("HEAD", `//manifests/`, container.VerifyImageName, container.HeadManifest) + g.MatchPath("GET", `//manifests/`, container.VerifyImageName, container.GetManifest) + g.MatchPath("PUT", `//manifests/`, container.VerifyImageName, reqPackageAccess(perm.AccessModeWrite), container.UploadManifest) + g.MatchPath("DELETE", `//manifests/`, container.VerifyImageName, reqPackageAccess(perm.AccessModeWrite), container.DeleteManifest) }) }, container.ReqContainerAccess, context.UserAssignmentWeb(), context.PackageAssignment(), reqPackageAccess(perm.AccessModeRead)) diff --git a/routers/api/packages/arch/arch.go b/routers/api/packages/arch/arch.go index 573e93cfb01..f5dc6c1d019 100644 --- a/routers/api/packages/arch/arch.go +++ b/routers/api/packages/arch/arch.go @@ -62,7 +62,7 @@ func UploadPackageFile(ctx *context.Context) { pck, err := arch_module.ParsePackage(buf) if err != nil { - if errors.Is(err, util.ErrInvalidArgument) || err == io.EOF { + if errors.Is(err, util.ErrInvalidArgument) || errors.Is(err, io.EOF) { apiError(ctx, http.StatusBadRequest, err) } else { apiError(ctx, http.StatusInternalServerError, err) diff --git a/routers/api/packages/cargo/cargo.go b/routers/api/packages/cargo/cargo.go index 3d8407e6b60..42ef13476c1 100644 --- a/routers/api/packages/cargo/cargo.go +++ b/routers/api/packages/cargo/cargo.go @@ -181,7 +181,7 @@ func DownloadPackageFile(ctx *context.Context) { }, ) if err != nil { - if err == packages_model.ErrPackageNotExist || err == packages_model.ErrPackageFileNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) || errors.Is(err, packages_model.ErrPackageFileNotExist) { apiError(ctx, http.StatusNotFound, err) return } @@ -276,7 +276,7 @@ func UnyankPackage(ctx *context.Context) { func yankPackage(ctx *context.Context, yank bool) { pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeCargo, ctx.PathParam("package"), ctx.PathParam("version")) if err != nil { - if err == packages_model.ErrPackageNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) { apiError(ctx, http.StatusNotFound, err) return } diff --git a/routers/api/packages/chef/chef.go b/routers/api/packages/chef/chef.go index b3cdf126977..a0c8c5696c6 100644 --- a/routers/api/packages/chef/chef.go +++ b/routers/api/packages/chef/chef.go @@ -216,7 +216,7 @@ func PackageVersionMetadata(ctx *context.Context) { pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeChef, packageName, packageVersion) if err != nil { - if err == packages_model.ErrPackageNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) { apiError(ctx, http.StatusNotFound, err) return } @@ -327,7 +327,7 @@ func UploadPackage(ctx *context.Context) { func DownloadPackage(ctx *context.Context) { pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeChef, ctx.PathParam("name"), ctx.PathParam("version")) if err != nil { - if err == packages_model.ErrPackageNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) { apiError(ctx, http.StatusNotFound, err) return } @@ -368,7 +368,7 @@ func DeletePackageVersion(ctx *context.Context) { }, ) if err != nil { - if err == packages_model.ErrPackageNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) { apiError(ctx, http.StatusNotFound, err) } else { apiError(ctx, http.StatusInternalServerError, err) diff --git a/routers/api/packages/composer/composer.go b/routers/api/packages/composer/composer.go index 40f72f64848..c6c14e5cf4f 100644 --- a/routers/api/packages/composer/composer.go +++ b/routers/api/packages/composer/composer.go @@ -176,7 +176,7 @@ func DownloadPackageFile(ctx *context.Context) { }, ) if err != nil { - if err == packages_model.ErrPackageNotExist || err == packages_model.ErrPackageFileNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) || errors.Is(err, packages_model.ErrPackageFileNotExist) { apiError(ctx, http.StatusNotFound, err) return } diff --git a/routers/api/packages/conan/conan.go b/routers/api/packages/conan/conan.go index 4a9f0a3ffc7..8019eee9f74 100644 --- a/routers/api/packages/conan/conan.go +++ b/routers/api/packages/conan/conan.go @@ -5,6 +5,7 @@ package conan import ( std_ctx "context" + "errors" "fmt" "io" "net/http" @@ -183,7 +184,7 @@ func serveSnapshot(ctx *context.Context, fileKey string) { pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeConan, rref.Name, rref.Version) if err != nil { - if err == packages_model.ErrPackageNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) { apiError(ctx, http.StatusNotFound, err) } else { apiError(ctx, http.StatusInternalServerError, err) @@ -244,7 +245,7 @@ func serveDownloadURLs(ctx *context.Context, fileKey, downloadURL string) { pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeConan, rref.Name, rref.Version) if err != nil { - if err == packages_model.ErrPackageNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) { apiError(ctx, http.StatusNotFound, err) } else { apiError(ctx, http.StatusInternalServerError, err) @@ -493,7 +494,7 @@ func downloadFile(ctx *context.Context, fileFilter container.Set[string], fileKe }, ) if err != nil { - if err == packages_model.ErrPackageNotExist || err == packages_model.ErrPackageFileNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) || errors.Is(err, packages_model.ErrPackageFileNotExist) { apiError(ctx, http.StatusNotFound, err) return } @@ -509,7 +510,7 @@ func DeleteRecipeV1(ctx *context.Context) { rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference) if err := deleteRecipeOrPackage(ctx, rref, true, nil, false); err != nil { - if err == packages_model.ErrPackageNotExist || err == conan_model.ErrPackageReferenceNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) || errors.Is(err, conan_model.ErrPackageReferenceNotExist) { apiError(ctx, http.StatusNotFound, err) } else { apiError(ctx, http.StatusInternalServerError, err) @@ -524,7 +525,7 @@ func DeleteRecipeV2(ctx *context.Context) { rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference) if err := deleteRecipeOrPackage(ctx, rref, rref.Revision == "", nil, false); err != nil { - if err == packages_model.ErrPackageNotExist || err == conan_model.ErrPackageReferenceNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) || errors.Is(err, conan_model.ErrPackageReferenceNotExist) { apiError(ctx, http.StatusNotFound, err) } else { apiError(ctx, http.StatusInternalServerError, err) @@ -571,7 +572,7 @@ func DeletePackageV1(ctx *context.Context) { for _, reference := range references { pref, _ := conan_module.NewPackageReference(currentRref, reference.Value, conan_module.DefaultRevision) if err := deleteRecipeOrPackage(ctx, currentRref, true, pref, true); err != nil { - if err == packages_model.ErrPackageNotExist || err == conan_model.ErrPackageReferenceNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) || errors.Is(err, conan_model.ErrPackageReferenceNotExist) { apiError(ctx, http.StatusNotFound, err) } else { apiError(ctx, http.StatusInternalServerError, err) @@ -590,7 +591,7 @@ func DeletePackageV2(ctx *context.Context) { if pref != nil { // has package reference if err := deleteRecipeOrPackage(ctx, rref, false, pref, pref.Revision == ""); err != nil { - if err == packages_model.ErrPackageNotExist || err == conan_model.ErrPackageReferenceNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) || errors.Is(err, conan_model.ErrPackageReferenceNotExist) { apiError(ctx, http.StatusNotFound, err) } else { apiError(ctx, http.StatusInternalServerError, err) @@ -615,7 +616,7 @@ func DeletePackageV2(ctx *context.Context) { pref, _ := conan_module.NewPackageReference(rref, reference.Value, conan_module.DefaultRevision) if err := deleteRecipeOrPackage(ctx, rref, false, pref, true); err != nil { - if err == packages_model.ErrPackageNotExist || err == conan_model.ErrPackageReferenceNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) || errors.Is(err, conan_model.ErrPackageReferenceNotExist) { apiError(ctx, http.StatusNotFound, err) } else { apiError(ctx, http.StatusInternalServerError, err) @@ -749,7 +750,7 @@ func LatestRecipeRevision(ctx *context.Context) { revision, err := conan_model.GetLastRecipeRevision(ctx, ctx.Package.Owner.ID, rref) if err != nil { - if err == conan_model.ErrRecipeReferenceNotExist || err == conan_model.ErrPackageReferenceNotExist { + if errors.Is(err, conan_model.ErrRecipeReferenceNotExist) || errors.Is(err, conan_model.ErrPackageReferenceNotExist) { apiError(ctx, http.StatusNotFound, err) } else { apiError(ctx, http.StatusInternalServerError, err) @@ -766,7 +767,7 @@ func LatestPackageRevision(ctx *context.Context) { revision, err := conan_model.GetLastPackageRevision(ctx, ctx.Package.Owner.ID, pref) if err != nil { - if err == conan_model.ErrRecipeReferenceNotExist || err == conan_model.ErrPackageReferenceNotExist { + if errors.Is(err, conan_model.ErrRecipeReferenceNotExist) || errors.Is(err, conan_model.ErrPackageReferenceNotExist) { apiError(ctx, http.StatusNotFound, err) } else { apiError(ctx, http.StatusInternalServerError, err) @@ -796,7 +797,7 @@ func listRevisionFiles(ctx *context.Context, fileKey string) { pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeConan, rref.Name, rref.Version) if err != nil { - if err == packages_model.ErrPackageNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) { apiError(ctx, http.StatusNotFound, err) } else { apiError(ctx, http.StatusInternalServerError, err) diff --git a/routers/api/packages/conan/search.go b/routers/api/packages/conan/search.go index 7370c702cd6..0dbbd500d28 100644 --- a/routers/api/packages/conan/search.go +++ b/routers/api/packages/conan/search.go @@ -4,6 +4,7 @@ package conan import ( + "errors" "net/http" "strings" @@ -76,7 +77,7 @@ func searchPackages(ctx *context.Context, searchAllRevisions bool) { if !searchAllRevisions && rref.Revision == "" { lastRevision, err := conan_model.GetLastRecipeRevision(ctx, ctx.Package.Owner.ID, rref) if err != nil { - if err == conan_model.ErrRecipeReferenceNotExist { + if errors.Is(err, conan_model.ErrRecipeReferenceNotExist) { apiError(ctx, http.StatusNotFound, err) } else { apiError(ctx, http.StatusInternalServerError, err) @@ -87,7 +88,7 @@ func searchPackages(ctx *context.Context, searchAllRevisions bool) { } else { has, err := conan_model.RecipeExists(ctx, ctx.Package.Owner.ID, rref) if err != nil { - if err == conan_model.ErrRecipeReferenceNotExist { + if errors.Is(err, conan_model.ErrRecipeReferenceNotExist) { apiError(ctx, http.StatusNotFound, err) } else { apiError(ctx, http.StatusInternalServerError, err) @@ -119,7 +120,7 @@ func searchPackages(ctx *context.Context, searchAllRevisions bool) { } packageReferences, err := conan_model.GetPackageReferences(ctx, ctx.Package.Owner.ID, currentRef) if err != nil { - if err == conan_model.ErrRecipeReferenceNotExist { + if errors.Is(err, conan_model.ErrRecipeReferenceNotExist) { apiError(ctx, http.StatusNotFound, err) } else { apiError(ctx, http.StatusInternalServerError, err) @@ -133,7 +134,7 @@ func searchPackages(ctx *context.Context, searchAllRevisions bool) { pref, _ := conan_module.NewPackageReference(currentRef, packageReference.Value, "") lastPackageRevision, err := conan_model.GetLastPackageRevision(ctx, ctx.Package.Owner.ID, pref) if err != nil { - if err == conan_model.ErrPackageReferenceNotExist { + if errors.Is(err, conan_model.ErrPackageReferenceNotExist) { apiError(ctx, http.StatusNotFound, err) } else { apiError(ctx, http.StatusInternalServerError, err) @@ -143,7 +144,7 @@ func searchPackages(ctx *context.Context, searchAllRevisions bool) { pref = pref.WithRevision(lastPackageRevision.Value) infoRaw, err := conan_model.GetPackageInfo(ctx, ctx.Package.Owner.ID, pref) if err != nil { - if err == conan_model.ErrPackageReferenceNotExist { + if errors.Is(err, conan_model.ErrPackageReferenceNotExist) { apiError(ctx, http.StatusNotFound, err) } else { apiError(ctx, http.StatusInternalServerError, err) diff --git a/routers/api/packages/container/blob.go b/routers/api/packages/container/blob.go index 4595b9a33de..671803788aa 100644 --- a/routers/api/packages/container/blob.go +++ b/routers/api/packages/container/blob.go @@ -111,12 +111,11 @@ func getOrCreateUploadVersion(ctx context.Context, pi *packages_service.PackageI } var err error if p, err = packages_model.TryInsertPackage(ctx, p); err != nil { - if err == packages_model.ErrDuplicatePackage { - created = false - } else { + if !errors.Is(err, packages_model.ErrDuplicatePackage) { log.Error("Error inserting package: %v", err) return err } + created = false } if created { @@ -135,7 +134,7 @@ func getOrCreateUploadVersion(ctx context.Context, pi *packages_service.PackageI MetadataJSON: "null", } if pv, err = packages_model.GetOrInsertVersion(ctx, pv); err != nil { - if err != packages_model.ErrDuplicatePackageVersion { + if !errors.Is(err, packages_model.ErrDuplicatePackageVersion) { log.Error("Error inserting package: %v", err) return err } @@ -161,7 +160,7 @@ func createFileForBlob(ctx context.Context, pv *packages_model.PackageVersion, p } var err error if pf, err = packages_model.TryInsertFile(ctx, pf); err != nil { - if err == packages_model.ErrDuplicatePackageFile { + if errors.Is(err, packages_model.ErrDuplicatePackageFile) { return nil } log.Error("Error inserting package file: %v", err) diff --git a/routers/api/packages/container/container.go b/routers/api/packages/container/container.go index 3a470ad6852..bb14db9db79 100644 --- a/routers/api/packages/container/container.go +++ b/routers/api/packages/container/container.go @@ -324,7 +324,7 @@ func GetUploadBlob(ctx *context.Context) { upload, err := packages_model.GetBlobUploadByID(ctx, uuid) if err != nil { - if err == packages_model.ErrPackageBlobUploadNotExist { + if errors.Is(err, packages_model.ErrPackageBlobUploadNotExist) { apiErrorDefined(ctx, errBlobUploadUnknown) } else { apiError(ctx, http.StatusInternalServerError, err) @@ -345,7 +345,7 @@ func UploadBlob(ctx *context.Context) { uploader, err := container_service.NewBlobUploader(ctx, ctx.PathParam("uuid")) if err != nil { - if err == packages_model.ErrPackageBlobUploadNotExist { + if errors.Is(err, packages_model.ErrPackageBlobUploadNotExist) { apiErrorDefined(ctx, errBlobUploadUnknown) } else { apiError(ctx, http.StatusInternalServerError, err) @@ -396,7 +396,7 @@ func EndUploadBlob(ctx *context.Context) { uploader, err := container_service.NewBlobUploader(ctx, ctx.PathParam("uuid")) if err != nil { - if err == packages_model.ErrPackageBlobUploadNotExist { + if errors.Is(err, packages_model.ErrPackageBlobUploadNotExist) { apiErrorDefined(ctx, errBlobUploadUnknown) } else { apiError(ctx, http.StatusInternalServerError, err) @@ -465,7 +465,7 @@ func CancelUploadBlob(ctx *context.Context) { _, err := packages_model.GetBlobUploadByID(ctx, uuid) if err != nil { - if err == packages_model.ErrPackageBlobUploadNotExist { + if errors.Is(err, packages_model.ErrPackageBlobUploadNotExist) { apiErrorDefined(ctx, errBlobUploadUnknown) } else { apiError(ctx, http.StatusInternalServerError, err) @@ -501,7 +501,7 @@ func getBlobFromContext(ctx *context.Context) (*packages_model.PackageFileDescri func HeadBlob(ctx *context.Context) { blob, err := getBlobFromContext(ctx) if err != nil { - if err == container_model.ErrContainerBlobNotExist { + if errors.Is(err, container_model.ErrContainerBlobNotExist) { apiErrorDefined(ctx, errBlobUnknown) } else { apiError(ctx, http.StatusInternalServerError, err) @@ -520,7 +520,7 @@ func HeadBlob(ctx *context.Context) { func GetBlob(ctx *context.Context) { blob, err := getBlobFromContext(ctx) if err != nil { - if err == container_model.ErrContainerBlobNotExist { + if errors.Is(err, container_model.ErrContainerBlobNotExist) { apiErrorDefined(ctx, errBlobUnknown) } else { apiError(ctx, http.StatusInternalServerError, err) @@ -639,7 +639,7 @@ func getManifestFromContext(ctx *context.Context) (*packages_model.PackageFileDe func HeadManifest(ctx *context.Context) { manifest, err := getManifestFromContext(ctx) if err != nil { - if err == container_model.ErrContainerBlobNotExist { + if errors.Is(err, container_model.ErrContainerBlobNotExist) { apiErrorDefined(ctx, errManifestUnknown) } else { apiError(ctx, http.StatusInternalServerError, err) @@ -659,7 +659,7 @@ func HeadManifest(ctx *context.Context) { func GetManifest(ctx *context.Context) { manifest, err := getManifestFromContext(ctx) if err != nil { - if err == container_model.ErrContainerBlobNotExist { + if errors.Is(err, container_model.ErrContainerBlobNotExist) { apiErrorDefined(ctx, errManifestUnknown) } else { apiError(ctx, http.StatusInternalServerError, err) @@ -739,7 +739,7 @@ func GetTagList(ctx *context.Context) { image := ctx.PathParam("image") if _, err := packages_model.GetPackageByName(ctx, ctx.Package.Owner.ID, packages_model.TypeContainer, image); err != nil { - if err == packages_model.ErrPackageNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) { apiErrorDefined(ctx, errNameUnknown) } else { apiError(ctx, http.StatusInternalServerError, err) diff --git a/routers/api/packages/container/manifest.go b/routers/api/packages/container/manifest.go index 4a79a58f51f..ad035cf473e 100644 --- a/routers/api/packages/container/manifest.go +++ b/routers/api/packages/container/manifest.go @@ -240,7 +240,7 @@ func processImageManifestIndex(ctx context.Context, mci *manifestCreationInfo, b IsManifest: true, }) if err != nil { - if err == container_model.ErrContainerBlobNotExist { + if errors.Is(err, container_model.ErrContainerBlobNotExist) { return errManifestBlobUnknown } return err @@ -321,12 +321,11 @@ func createPackageAndVersion(ctx context.Context, mci *manifestCreationInfo, met } var err error if p, err = packages_model.TryInsertPackage(ctx, p); err != nil { - if err == packages_model.ErrDuplicatePackage { - created = false - } else { + if !errors.Is(err, packages_model.ErrDuplicatePackage) { log.Error("Error inserting package: %v", err) return nil, err } + created = false } if created { @@ -352,21 +351,23 @@ func createPackageAndVersion(ctx context.Context, mci *manifestCreationInfo, met } var pv *packages_model.PackageVersion if pv, err = packages_model.GetOrInsertVersion(ctx, _pv); err != nil { - if err == packages_model.ErrDuplicatePackageVersion { - if err := packages_service.DeletePackageVersionAndReferences(ctx, pv); err != nil { - return nil, err - } + if !errors.Is(err, packages_model.ErrDuplicatePackageVersion) { + log.Error("Error inserting package: %v", err) + return nil, err + } - // keep download count on overwrite - _pv.DownloadCount = pv.DownloadCount + if err = packages_service.DeletePackageVersionAndReferences(ctx, pv); err != nil { + return nil, err + } - if pv, err = packages_model.GetOrInsertVersion(ctx, _pv); err != nil { + // keep download count on overwrite + _pv.DownloadCount = pv.DownloadCount + + if pv, err = packages_model.GetOrInsertVersion(ctx, _pv); err != nil { + if !errors.Is(err, packages_model.ErrDuplicatePackageVersion) { log.Error("Error inserting package: %v", err) return nil, err } - } else { - log.Error("Error inserting package: %v", err) - return nil, err } } @@ -417,7 +418,7 @@ func createFileFromBlobReference(ctx context.Context, pv, uploadVersion *package } var err error if pf, err = packages_model.TryInsertFile(ctx, pf); err != nil { - if err == packages_model.ErrDuplicatePackageFile { + if errors.Is(err, packages_model.ErrDuplicatePackageFile) { // Skip this blob because the manifest contains the same filesystem layer multiple times. return nil } diff --git a/routers/api/packages/debian/debian.go b/routers/api/packages/debian/debian.go index 162122ccbdc..fec34c91a6e 100644 --- a/routers/api/packages/debian/debian.go +++ b/routers/api/packages/debian/debian.go @@ -68,7 +68,7 @@ func GetRepositoryFile(ctx *context.Context) { }, ) if err != nil { - if err == packages_model.ErrPackageNotExist || err == packages_model.ErrPackageFileNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) || errors.Is(err, packages_model.ErrPackageFileNotExist) { apiError(ctx, http.StatusNotFound, err) } else { apiError(ctx, http.StatusInternalServerError, err) diff --git a/routers/api/packages/generic/generic.go b/routers/api/packages/generic/generic.go index 868caf9cf06..0b5daa7334a 100644 --- a/routers/api/packages/generic/generic.go +++ b/routers/api/packages/generic/generic.go @@ -44,7 +44,7 @@ func DownloadPackageFile(ctx *context.Context) { }, ) if err != nil { - if err == packages_model.ErrPackageNotExist || err == packages_model.ErrPackageFileNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) || errors.Is(err, packages_model.ErrPackageFileNotExist) { apiError(ctx, http.StatusNotFound, err) return } @@ -155,7 +155,7 @@ func DeletePackage(ctx *context.Context) { }, ) if err != nil { - if err == packages_model.ErrPackageNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) { apiError(ctx, http.StatusNotFound, err) return } @@ -182,7 +182,7 @@ func DeletePackageFile(ctx *context.Context) { return pv, pf, nil }() if err != nil { - if err == packages_model.ErrPackageNotExist || err == packages_model.ErrPackageFileNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) || errors.Is(err, packages_model.ErrPackageFileNotExist) { apiError(ctx, http.StatusNotFound, err) return } diff --git a/routers/api/packages/helm/helm.go b/routers/api/packages/helm/helm.go index cb30a20074e..fb12daaa46a 100644 --- a/routers/api/packages/helm/helm.go +++ b/routers/api/packages/helm/helm.go @@ -130,7 +130,7 @@ func DownloadPackageFile(ctx *context.Context) { }, ) if err != nil { - if err == packages_model.ErrPackageFileNotExist { + if errors.Is(err, packages_model.ErrPackageFileNotExist) { apiError(ctx, http.StatusNotFound, err) return } diff --git a/routers/api/packages/maven/maven.go b/routers/api/packages/maven/maven.go index 9474b17bc79..4d04d4d1e9f 100644 --- a/routers/api/packages/maven/maven.go +++ b/routers/api/packages/maven/maven.go @@ -13,7 +13,7 @@ import ( "errors" "io" "net/http" - "path/filepath" + "path" "regexp" "sort" "strconv" @@ -25,6 +25,7 @@ import ( "code.gitea.io/gitea/modules/log" packages_module "code.gitea.io/gitea/modules/packages" maven_module "code.gitea.io/gitea/modules/packages/maven" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/routers/api/packages/helper" "code.gitea.io/gitea/services/context" packages_service "code.gitea.io/gitea/services/packages" @@ -44,7 +45,7 @@ const ( var ( errInvalidParameters = errors.New("request parameters are invalid") - illegalCharacters = regexp.MustCompile(`[\\/:"<>|?\*]`) + illegalCharacters = regexp.MustCompile(`[\\/:"<>|?*]`) ) func apiError(ctx *context.Context, status int, obj any) { @@ -85,8 +86,10 @@ func handlePackageFile(ctx *context.Context, serveContent bool) { func serveMavenMetadata(ctx *context.Context, params parameters) { // /com/foo/project/maven-metadata.xml[.md5/.sha1/.sha256/.sha512] - packageName := params.GroupID + "-" + params.ArtifactID - pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeMaven, packageName) + pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeMaven, params.toInternalPackageName()) + if errors.Is(err, util.ErrNotExist) { + pvs, err = packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeMaven, params.toInternalPackageNameLegacy()) + } if err != nil { apiError(ctx, http.StatusInternalServerError, err) return @@ -116,10 +119,10 @@ func serveMavenMetadata(ctx *context.Context, params parameters) { latest := pds[len(pds)-1] // http.TimeFormat required a UTC time, refer to https://pkg.go.dev/net/http#TimeFormat - lastModifed := latest.Version.CreatedUnix.AsTime().UTC().Format(http.TimeFormat) - ctx.Resp.Header().Set("Last-Modified", lastModifed) + lastModified := latest.Version.CreatedUnix.AsTime().UTC().Format(http.TimeFormat) + ctx.Resp.Header().Set("Last-Modified", lastModified) - ext := strings.ToLower(filepath.Ext(params.Filename)) + ext := strings.ToLower(path.Ext(params.Filename)) if isChecksumExtension(ext) { var hash []byte switch ext { @@ -147,11 +150,12 @@ func serveMavenMetadata(ctx *context.Context, params parameters) { } func servePackageFile(ctx *context.Context, params parameters, serveContent bool) { - packageName := params.GroupID + "-" + params.ArtifactID - - pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeMaven, packageName, params.Version) + pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeMaven, params.toInternalPackageName(), params.Version) + if errors.Is(err, util.ErrNotExist) { + pv, err = packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeMaven, params.toInternalPackageNameLegacy(), params.Version) + } if err != nil { - if err == packages_model.ErrPackageNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) { apiError(ctx, http.StatusNotFound, err) } else { apiError(ctx, http.StatusInternalServerError, err) @@ -161,14 +165,14 @@ func servePackageFile(ctx *context.Context, params parameters, serveContent bool filename := params.Filename - ext := strings.ToLower(filepath.Ext(filename)) + ext := strings.ToLower(path.Ext(filename)) if isChecksumExtension(ext) { filename = filename[:len(filename)-len(ext)] } pf, err := packages_model.GetFileForVersionByName(ctx, pv.ID, filename, packages_model.EmptyFileKey) if err != nil { - if err == packages_model.ErrPackageFileNotExist { + if errors.Is(err, packages_model.ErrPackageFileNotExist) { apiError(ctx, http.StatusNotFound, err) } else { apiError(ctx, http.StatusInternalServerError, err) @@ -238,15 +242,17 @@ func UploadPackageFile(ctx *context.Context) { return } - log.Trace("Parameters: %+v", params) - // Ignore the package index //maven-metadata.xml if params.IsMeta && params.Version == "" { ctx.Status(http.StatusOK) return } - packageName := params.GroupID + "-" + params.ArtifactID + packageName := params.toInternalPackageName() + if ctx.FormBool("use_legacy_package_name") { + // for testing purpose only + packageName = params.toInternalPackageNameLegacy() + } // for the same package, only one upload at a time releaser, err := globallock.Lock(ctx, mavenPkgNameKey(packageName)) @@ -274,13 +280,26 @@ func UploadPackageFile(ctx *context.Context) { Creator: ctx.Doer, } - ext := filepath.Ext(params.Filename) + // old maven package uses "groupId-artifactId" as package name, so we need to update to the new format "groupId:artifactId" + legacyPackage, err := packages_model.GetPackageByName(ctx, ctx.Package.Owner.ID, packages_model.TypeMaven, params.toInternalPackageNameLegacy()) + if err != nil && !errors.Is(err, packages_model.ErrPackageNotExist) { + apiError(ctx, http.StatusInternalServerError, err) + return + } else if legacyPackage != nil { + err = packages_model.UpdatePackageNameByID(ctx, ctx.Package.Owner.ID, packages_model.TypeMaven, legacyPackage.ID, packageName) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + } + + ext := path.Ext(params.Filename) // Do not upload checksum files but compare the hashes. if isChecksumExtension(ext) { pv, err := packages_model.GetVersionByNameAndVersion(ctx, pvci.Owner.ID, pvci.PackageType, pvci.Name, pvci.Version) if err != nil { - if err == packages_model.ErrPackageNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) { apiError(ctx, http.StatusNotFound, err) return } @@ -289,7 +308,7 @@ func UploadPackageFile(ctx *context.Context) { } pf, err := packages_model.GetFileForVersionByName(ctx, pv.ID, params.Filename[:len(params.Filename)-len(ext)], packages_model.EmptyFileKey) if err != nil { - if err == packages_model.ErrPackageFileNotExist { + if errors.Is(err, packages_model.ErrPackageFileNotExist) { apiError(ctx, http.StatusNotFound, err) return } @@ -343,7 +362,7 @@ func UploadPackageFile(ctx *context.Context) { if pvci.Metadata != nil { pv, err := packages_model.GetVersionByNameAndVersion(ctx, pvci.Owner.ID, pvci.PackageType, pvci.Name, pvci.Version) - if err != nil && err != packages_model.ErrPackageNotExist { + if err != nil && !errors.Is(err, packages_model.ErrPackageNotExist) { apiError(ctx, http.StatusInternalServerError, err) return } @@ -399,9 +418,26 @@ type parameters struct { IsMeta bool } +func (p *parameters) toInternalPackageName() string { + // there cuold be 2 choices: "/" or ":" + // Maven says: "groupId:artifactId:version" in their document: https://maven.apache.org/pom.html#Maven_Coordinates + // but it would be slightly ugly in URL: "/-/packages/maven/group-id%3Aartifact-id" + return p.GroupID + ":" + p.ArtifactID +} + +func (p *parameters) toInternalPackageNameLegacy() string { + return p.GroupID + "-" + p.ArtifactID +} + func extractPathParameters(ctx *context.Context) (parameters, error) { parts := strings.Split(ctx.PathParam("*"), "/") + // formats: + // * /com/group/id/artifactId/maven-metadata.xml[.md5|.sha1|.sha256|.sha512] + // * /com/group/id/artifactId/version-SNAPSHOT/maven-metadata.xml[.md5|.sha1|.sha256|.sha512] + // * /com/group/id/artifactId/version/any-file + // * /com/group/id/artifactId/version-SNAPSHOT/any-file + p := parameters{ Filename: parts[len(parts)-1], } diff --git a/routers/api/packages/npm/api.go b/routers/api/packages/npm/api.go index b4379f3f497..636680242a4 100644 --- a/routers/api/packages/npm/api.go +++ b/routers/api/packages/npm/api.go @@ -67,6 +67,7 @@ func createPackageMetadataVersion(registryURL string, pd *packages_model.Package BundleDependencies: metadata.BundleDependencies, DevDependencies: metadata.DevelopmentDependencies, PeerDependencies: metadata.PeerDependencies, + PeerDependenciesMeta: metadata.PeerDependenciesMeta, OptionalDependencies: metadata.OptionalDependencies, Readme: metadata.Readme, Bin: metadata.Bin, diff --git a/routers/api/packages/npm/npm.go b/routers/api/packages/npm/npm.go index 284723e0d7d..6ec46bcb365 100644 --- a/routers/api/packages/npm/npm.go +++ b/routers/api/packages/npm/npm.go @@ -98,7 +98,7 @@ func DownloadPackageFile(ctx *context.Context) { }, ) if err != nil { - if err == packages_model.ErrPackageNotExist || err == packages_model.ErrPackageFileNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) || errors.Is(err, packages_model.ErrPackageFileNotExist) { apiError(ctx, http.StatusNotFound, err) return } @@ -140,7 +140,7 @@ func DownloadPackageFileByName(ctx *context.Context) { }, ) if err != nil { - if err == packages_model.ErrPackageFileNotExist { + if errors.Is(err, packages_model.ErrPackageFileNotExist) { apiError(ctx, http.StatusNotFound, err) return } @@ -163,7 +163,7 @@ func UploadPackage(ctx *context.Context) { return } - repo, err := repo_model.GetRepositoryByURL(ctx, npmPackage.Metadata.Repository.URL) + repo, err := repo_model.GetRepositoryByURLRelax(ctx, npmPackage.Metadata.Repository.URL) if err == nil { canWrite := repo.OwnerID == ctx.Doer.ID @@ -267,7 +267,7 @@ func DeletePackageVersion(ctx *context.Context) { }, ) if err != nil { - if err == packages_model.ErrPackageNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) { apiError(ctx, http.StatusNotFound, err) return } @@ -341,7 +341,7 @@ func AddPackageTag(ctx *context.Context) { pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeNpm, packageName, version) if err != nil { - if err == packages_model.ErrPackageNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) { apiError(ctx, http.StatusNotFound, err) return } diff --git a/routers/api/packages/nuget/nuget.go b/routers/api/packages/nuget/nuget.go index 70b95e6a774..07a8de0a684 100644 --- a/routers/api/packages/nuget/nuget.go +++ b/routers/api/packages/nuget/nuget.go @@ -259,7 +259,7 @@ func RegistrationLeafV2(ctx *context.Context) { pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeNuGet, packageName, packageVersion) if err != nil { - if err == packages_model.ErrPackageNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) { apiError(ctx, http.StatusNotFound, err) return } @@ -288,7 +288,7 @@ func RegistrationLeafV3(ctx *context.Context) { pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeNuGet, packageName, packageVersion) if err != nil { - if err == packages_model.ErrPackageNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) { apiError(ctx, http.StatusNotFound, err) return } @@ -418,7 +418,7 @@ func DownloadPackageFile(ctx *context.Context) { }, ) if err != nil { - if err == packages_model.ErrPackageNotExist || err == packages_model.ErrPackageFileNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) || errors.Is(err, packages_model.ErrPackageFileNotExist) { apiError(ctx, http.StatusNotFound, err) return } @@ -671,7 +671,7 @@ func DownloadSymbolFile(ctx *context.Context) { s, u, pf, err := packages_service.GetPackageFileStream(ctx, pfs[0]) if err != nil { - if err == packages_model.ErrPackageNotExist || err == packages_model.ErrPackageFileNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) || errors.Is(err, packages_model.ErrPackageFileNotExist) { apiError(ctx, http.StatusNotFound, err) return } @@ -699,7 +699,7 @@ func DeletePackage(ctx *context.Context) { }, ) if err != nil { - if err == packages_model.ErrPackageNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) { apiError(ctx, http.StatusNotFound, err) return } diff --git a/routers/api/packages/pub/pub.go b/routers/api/packages/pub/pub.go index 2be27323fd1..e7b07aefd07 100644 --- a/routers/api/packages/pub/pub.go +++ b/routers/api/packages/pub/pub.go @@ -124,7 +124,7 @@ func PackageVersionMetadata(ctx *context.Context) { pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypePub, packageName, packageVersion) if err != nil { - if err == packages_model.ErrPackageNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) { apiError(ctx, http.StatusNotFound, err) return } @@ -233,7 +233,7 @@ func FinalizePackage(ctx *context.Context) { _, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypePub, packageName, packageVersion) if err != nil { - if err == packages_model.ErrPackageNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) { apiError(ctx, http.StatusNotFound, err) return } @@ -258,7 +258,7 @@ func DownloadPackageFile(ctx *context.Context) { pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypePub, packageName, packageVersion) if err != nil { - if err == packages_model.ErrPackageNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) { apiError(ctx, http.StatusNotFound, err) return } diff --git a/routers/api/packages/pypi/pypi.go b/routers/api/packages/pypi/pypi.go index 5ea86071a9e..199f4e74787 100644 --- a/routers/api/packages/pypi/pypi.go +++ b/routers/api/packages/pypi/pypi.go @@ -5,11 +5,13 @@ package pypi import ( "encoding/hex" + "errors" "io" "net/http" "regexp" "sort" "strings" + "unicode" packages_model "code.gitea.io/gitea/models/packages" packages_module "code.gitea.io/gitea/modules/packages" @@ -93,7 +95,7 @@ func DownloadPackageFile(ctx *context.Context) { }, ) if err != nil { - if err == packages_model.ErrPackageNotExist || err == packages_model.ErrPackageFileNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) || errors.Is(err, packages_model.ErrPackageFileNotExist) { apiError(ctx, http.StatusNotFound, err) return } @@ -139,9 +141,30 @@ func UploadPackageFile(ctx *context.Context) { return } - projectURL := ctx.Req.FormValue("home_page") - if !validation.IsValidURL(projectURL) { - projectURL = "" + // Ensure ctx.Req.Form exists. + _ = ctx.Req.ParseForm() + + var homepageURL string + projectURLs := ctx.Req.Form["project_urls"] + for _, purl := range projectURLs { + label, url, found := strings.Cut(purl, ",") + if !found { + continue + } + if normalizeLabel(label) != "homepage" { + continue + } + homepageURL = strings.TrimSpace(url) + break + } + + if len(homepageURL) == 0 { + // TODO: Home-page is a deprecated metadata field. Remove this branch once it's no longer apart of the spec. + homepageURL = ctx.Req.FormValue("home_page") + } + + if !validation.IsValidURL(homepageURL) { + homepageURL = "" } _, _, err = packages_service.CreatePackageOrAddFileToExisting( @@ -160,7 +183,7 @@ func UploadPackageFile(ctx *context.Context) { Description: ctx.Req.FormValue("description"), LongDescription: ctx.Req.FormValue("long_description"), Summary: ctx.Req.FormValue("summary"), - ProjectURL: projectURL, + ProjectURL: homepageURL, License: ctx.Req.FormValue("license"), RequiresPython: ctx.Req.FormValue("requires_python"), }, @@ -189,6 +212,23 @@ func UploadPackageFile(ctx *context.Context) { ctx.Status(http.StatusCreated) } +// Normalizes a Project-URL label. +// See https://packaging.python.org/en/latest/specifications/well-known-project-urls/#label-normalization. +func normalizeLabel(label string) string { + var builder strings.Builder + + // "A label is normalized by deleting all ASCII punctuation and whitespace, and then converting the result + // to lowercase." + for _, r := range label { + if unicode.IsPunct(r) || unicode.IsSpace(r) { + continue + } + builder.WriteRune(unicode.ToLower(r)) + } + + return builder.String() +} + func isValidNameAndVersion(packageName, packageVersion string) bool { return nameMatcher.MatchString(packageName) && versionMatcher.MatchString(packageVersion) } diff --git a/routers/api/packages/pypi/pypi_test.go b/routers/api/packages/pypi/pypi_test.go index 3023692177f..786105693f1 100644 --- a/routers/api/packages/pypi/pypi_test.go +++ b/routers/api/packages/pypi/pypi_test.go @@ -36,3 +36,13 @@ func TestIsValidNameAndVersion(t *testing.T) { assert.False(t, isValidNameAndVersion("test-name", "1.0.1aa")) assert.False(t, isValidNameAndVersion("test-name", "1.0.0-alpha.beta")) } + +func TestNormalizeLabel(t *testing.T) { + // Cases fetched from https://packaging.python.org/en/latest/specifications/well-known-project-urls/#label-normalization. + assert.Equal(t, "homepage", normalizeLabel("Homepage")) + assert.Equal(t, "homepage", normalizeLabel("Home-page")) + assert.Equal(t, "homepage", normalizeLabel("Home page")) + assert.Equal(t, "changelog", normalizeLabel("Change_Log")) + assert.Equal(t, "whatsnew", normalizeLabel("What's New?")) + assert.Equal(t, "github", normalizeLabel("github")) +} diff --git a/routers/api/packages/rubygems/rubygems.go b/routers/api/packages/rubygems/rubygems.go index 958063e70ab..de8c7ef3ed0 100644 --- a/routers/api/packages/rubygems/rubygems.go +++ b/routers/api/packages/rubygems/rubygems.go @@ -185,7 +185,7 @@ func DownloadPackageFile(ctx *context.Context) { }, ) if err != nil { - if err == packages_model.ErrPackageFileNotExist { + if errors.Is(err, packages_model.ErrPackageFileNotExist) { apiError(ctx, http.StatusNotFound, err) return } @@ -288,7 +288,7 @@ func DeletePackage(ctx *context.Context) { }, ) if err != nil { - if err == packages_model.ErrPackageNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) { apiError(ctx, http.StatusNotFound, err) return } diff --git a/routers/api/packages/vagrant/vagrant.go b/routers/api/packages/vagrant/vagrant.go index 1daf2a0527a..3afaa5de1f3 100644 --- a/routers/api/packages/vagrant/vagrant.go +++ b/routers/api/packages/vagrant/vagrant.go @@ -4,6 +4,7 @@ package vagrant import ( + "errors" "fmt" "io" "net/http" @@ -230,7 +231,7 @@ func DownloadPackageFile(ctx *context.Context) { }, ) if err != nil { - if err == packages_model.ErrPackageNotExist || err == packages_model.ErrPackageFileNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) || errors.Is(err, packages_model.ErrPackageFileNotExist) { apiError(ctx, http.StatusNotFound, err) return } diff --git a/routers/api/v1/admin/hooks.go b/routers/api/v1/admin/hooks.go index 6b4689047b5..c812ca182d5 100644 --- a/routers/api/v1/admin/hooks.go +++ b/routers/api/v1/admin/hooks.go @@ -34,11 +34,30 @@ func ListHooks(ctx *context.APIContext) { // in: query // description: page size of results // type: integer + // - type: string + // enum: + // - system + // - default + // - all + // description: system, default or both kinds of webhooks + // name: type + // default: system + // in: query + // // responses: // "200": // "$ref": "#/responses/HookList" - sysHooks, err := webhook.GetSystemWebhooks(ctx, optional.None[bool]()) + // for compatibility the default value is true + isSystemWebhook := optional.Some(true) + typeValue := ctx.FormString("type") + if typeValue == "default" { + isSystemWebhook = optional.Some(false) + } else if typeValue == "all" { + isSystemWebhook = optional.None[bool]() + } + + sysHooks, err := webhook.GetSystemOrDefaultWebhooks(ctx, isSystemWebhook) if err != nil { ctx.Error(http.StatusInternalServerError, "GetSystemWebhooks", err) return diff --git a/routers/api/v1/admin/user.go b/routers/api/v1/admin/user.go index 21cb2f9ccdf..53eee72631f 100644 --- a/routers/api/v1/admin/user.go +++ b/routers/api/v1/admin/user.go @@ -477,26 +477,16 @@ func RenameUser(ctx *context.APIContext) { return } - oldName := ctx.ContextUser.Name newName := web.GetForm(ctx).(*api.RenameUserOption).NewName - // Check if user name has been changed + // Check if username has been changed if err := user_service.RenameUser(ctx, ctx.ContextUser, newName); err != nil { - switch { - case user_model.IsErrUserAlreadyExist(err): - ctx.Error(http.StatusUnprocessableEntity, "", ctx.Tr("form.username_been_taken")) - case db.IsErrNameReserved(err): - ctx.Error(http.StatusUnprocessableEntity, "", ctx.Tr("user.form.name_reserved", newName)) - case db.IsErrNamePatternNotAllowed(err): - ctx.Error(http.StatusUnprocessableEntity, "", ctx.Tr("user.form.name_pattern_not_allowed", newName)) - case db.IsErrNameCharsNotAllowed(err): - ctx.Error(http.StatusUnprocessableEntity, "", ctx.Tr("user.form.name_chars_not_allowed", newName)) - default: + if user_model.IsErrUserAlreadyExist(err) || db.IsErrNameReserved(err) || db.IsErrNamePatternNotAllowed(err) || db.IsErrNameCharsNotAllowed(err) { + ctx.Error(http.StatusUnprocessableEntity, "", err) + } else { ctx.ServerError("ChangeUserName", err) } return } - - log.Trace("User name changed: %s -> %s", oldName, newName) ctx.Status(http.StatusNoContent) } diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 2f943d306cc..2ffd6b129b9 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -268,12 +268,12 @@ func checkTokenPublicOnly() func(ctx *context.APIContext) { return } case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryUser): - if ctx.ContextUser != nil && ctx.ContextUser.IsUser() && ctx.ContextUser.Visibility != api.VisibleTypePublic { + if ctx.ContextUser != nil && ctx.ContextUser.IsTokenAccessAllowed() && ctx.ContextUser.Visibility != api.VisibleTypePublic { ctx.Error(http.StatusForbidden, "reqToken", "token scope is limited to public users") return } case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryActivityPub): - if ctx.ContextUser != nil && ctx.ContextUser.IsUser() && ctx.ContextUser.Visibility != api.VisibleTypePublic { + if ctx.ContextUser != nil && ctx.ContextUser.IsTokenAccessAllowed() && ctx.ContextUser.Visibility != api.VisibleTypePublic { ctx.Error(http.StatusForbidden, "reqToken", "token scope is limited to public activitypub") return } @@ -580,6 +580,16 @@ func reqWebhooksEnabled() func(ctx *context.APIContext) { } } +// reqStarsEnabled requires Starring to be enabled in the config. +func reqStarsEnabled() func(ctx *context.APIContext) { + return func(ctx *context.APIContext) { + if setting.Repository.DisableStars { + ctx.Error(http.StatusForbidden, "", "stars disabled by administrator") + return + } + } +} + func orgAssignment(args ...bool) func(ctx *context.APIContext) { var ( assignOrg bool @@ -995,7 +1005,7 @@ func Routes() *web.Router { m.Get("/{target}", user.CheckFollowing) }) - m.Get("/starred", user.GetStarredRepos) + m.Get("/starred", reqStarsEnabled(), user.GetStarredRepos) m.Get("/subscriptions", user.GetWatchedRepos) }, context.UserAssignmentAPI(), checkTokenPublicOnly()) @@ -1086,7 +1096,7 @@ func Routes() *web.Router { m.Put("", user.Star) m.Delete("", user.Unstar) }, repoAssignment(), checkTokenPublicOnly()) - }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository)) + }, reqStarsEnabled(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository)) m.Get("/times", repo.ListMyTrackedTimes) m.Get("/stopwatches", repo.GetStopwatches) m.Get("/subscriptions", user.GetMyWatchedRepos) @@ -1190,6 +1200,7 @@ func Routes() *web.Router { m.Get("/archive/*", reqRepoReader(unit.TypeCode), repo.GetArchive) m.Combo("/forks").Get(repo.ListForks). Post(reqToken(), reqRepoReader(unit.TypeCode), bind(api.CreateForkOption{}), repo.CreateFork) + m.Post("/merge-upstream", reqToken(), mustNotBeArchived, reqRepoWriter(unit.TypeCode), bind(api.MergeUpstreamRequest{}), repo.MergeUpstream) m.Group("/branches", func() { m.Get("", repo.ListBranches) m.Get("/*", repo.GetBranch) @@ -1247,7 +1258,7 @@ func Routes() *web.Router { m.Post("/markup", reqToken(), bind(api.MarkupOption{}), misc.Markup) m.Post("/markdown", reqToken(), bind(api.MarkdownOption{}), misc.Markdown) m.Post("/markdown/raw", reqToken(), misc.MarkdownRaw) - m.Get("/stargazers", repo.ListStargazers) + m.Get("/stargazers", reqStarsEnabled(), repo.ListStargazers) m.Get("/subscribers", repo.ListSubscribers) m.Group("/subscription", func() { m.Get("", user.IsWatching) @@ -1529,6 +1540,7 @@ func Routes() *web.Router { m.Combo("").Get(org.Get). Patch(reqToken(), reqOrgOwnership(), bind(api.EditOrgOption{}), org.Edit). Delete(reqToken(), reqOrgOwnership(), org.Delete) + m.Post("/rename", reqToken(), reqOrgOwnership(), bind(api.RenameOrgOption{}), org.Rename) m.Combo("/repos").Get(user.ListOrgRepos). Post(reqToken(), bind(api.CreateRepoOption{}), repo.CreateOrgRepo) m.Group("/members", func() { diff --git a/routers/api/v1/org/org.go b/routers/api/v1/org/org.go index d65f9224344..2fcba0bf1ae 100644 --- a/routers/api/v1/org/org.go +++ b/routers/api/v1/org/org.go @@ -315,6 +315,44 @@ func Get(ctx *context.APIContext) { ctx.JSON(http.StatusOK, org) } +func Rename(ctx *context.APIContext) { + // swagger:operation POST /orgs/{org}/rename organization renameOrg + // --- + // summary: Rename an organization + // produces: + // - application/json + // parameters: + // - name: org + // in: path + // description: existing org name + // type: string + // required: true + // - name: body + // in: body + // required: true + // schema: + // "$ref": "#/definitions/RenameOrgOption" + // responses: + // "204": + // "$ref": "#/responses/empty" + // "403": + // "$ref": "#/responses/forbidden" + // "422": + // "$ref": "#/responses/validationError" + + form := web.GetForm(ctx).(*api.RenameOrgOption) + orgUser := ctx.Org.Organization.AsUser() + if err := user_service.RenameUser(ctx, orgUser, form.NewName); err != nil { + if user_model.IsErrUserAlreadyExist(err) || db.IsErrNameReserved(err) || db.IsErrNamePatternNotAllowed(err) || db.IsErrNameCharsNotAllowed(err) { + ctx.Error(http.StatusUnprocessableEntity, "RenameOrg", err) + } else { + ctx.ServerError("RenameOrg", err) + } + return + } + ctx.Status(http.StatusNoContent) +} + // Edit change an organization's information func Edit(ctx *context.APIContext) { // swagger:operation PATCH /orgs/{org} organization orgEdit diff --git a/routers/api/v1/repo/branch.go b/routers/api/v1/repo/branch.go index 67dfda39a81..a5ea752cf10 100644 --- a/routers/api/v1/repo/branch.go +++ b/routers/api/v1/repo/branch.go @@ -12,12 +12,14 @@ import ( "code.gitea.io/gitea/models/db" git_model "code.gitea.io/gitea/models/git" "code.gitea.io/gitea/models/organization" + repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/optional" repo_module "code.gitea.io/gitea/modules/repository" api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/routers/api/v1/utils" "code.gitea.io/gitea/services/context" @@ -150,7 +152,7 @@ func DeleteBranch(ctx *context.APIContext) { } } - if err := repo_service.DeleteBranch(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.GitRepo, branchName); err != nil { + if err := repo_service.DeleteBranch(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.GitRepo, branchName, nil); err != nil { switch { case git.IsErrBranchNotExist(err): ctx.NotFound(err) @@ -442,7 +444,14 @@ func UpdateBranch(ctx *context.APIContext) { msg, err := repo_service.RenameBranch(ctx, repo, ctx.Doer, ctx.Repo.GitRepo, oldName, opt.Name) if err != nil { - ctx.Error(http.StatusInternalServerError, "RenameBranch", err) + switch { + case repo_model.IsErrUserDoesNotHaveAccessToRepo(err): + ctx.Error(http.StatusForbidden, "", "User must be a repo or site admin to rename default or protected branches.") + case errors.Is(err, git_model.ErrBranchIsProtected): + ctx.Error(http.StatusForbidden, "", "Branch is protected by glob-based protection rules.") + default: + ctx.Error(http.StatusInternalServerError, "RenameBranch", err) + } return } if msg == "target_exist" { @@ -729,7 +738,7 @@ func CreateBranchProtection(ctx *context.APIContext) { } else { if !isPlainRule { if ctx.Repo.GitRepo == nil { - ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx, ctx.Repo.Repository) + ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx.Repo.Repository) if err != nil { ctx.Error(http.StatusInternalServerError, "OpenRepository", err) return @@ -1057,7 +1066,7 @@ func EditBranchProtection(ctx *context.APIContext) { } else { if !isPlainRule { if ctx.Repo.GitRepo == nil { - ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx, ctx.Repo.Repository) + ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx.Repo.Repository) if err != nil { ctx.Error(http.StatusInternalServerError, "OpenRepository", err) return @@ -1186,3 +1195,47 @@ func UpdateBranchProtectionPriories(ctx *context.APIContext) { ctx.Status(http.StatusNoContent) } + +func MergeUpstream(ctx *context.APIContext) { + // swagger:operation POST /repos/{owner}/{repo}/merge-upstream repository repoMergeUpstream + // --- + // summary: Merge a branch from upstream + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // - name: body + // in: body + // schema: + // "$ref": "#/definitions/MergeUpstreamRequest" + // responses: + // "200": + // "$ref": "#/responses/MergeUpstreamResponse" + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + form := web.GetForm(ctx).(*api.MergeUpstreamRequest) + mergeStyle, err := repo_service.MergeUpstream(ctx, ctx.Doer, ctx.Repo.Repository, form.Branch) + if err != nil { + if errors.Is(err, util.ErrInvalidArgument) { + ctx.Error(http.StatusBadRequest, "MergeUpstream", err) + return + } else if errors.Is(err, util.ErrNotExist) { + ctx.Error(http.StatusNotFound, "MergeUpstream", err) + return + } + ctx.Error(http.StatusInternalServerError, "MergeUpstream", err) + return + } + ctx.JSON(http.StatusOK, &api.MergeUpstreamResponse{MergeStyle: mergeStyle}) +} diff --git a/routers/api/v1/repo/compare.go b/routers/api/v1/repo/compare.go index 87b890cb62b..a1813a8a765 100644 --- a/routers/api/v1/repo/compare.go +++ b/routers/api/v1/repo/compare.go @@ -45,7 +45,7 @@ func CompareDiff(ctx *context.APIContext) { if ctx.Repo.GitRepo == nil { var err error - ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx, ctx.Repo.Repository) + ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx.Repo.Repository) if err != nil { ctx.Error(http.StatusInternalServerError, "OpenRepository", err) return diff --git a/routers/api/v1/repo/download.go b/routers/api/v1/repo/download.go index eb967772edf..e6296c9fe73 100644 --- a/routers/api/v1/repo/download.go +++ b/routers/api/v1/repo/download.go @@ -17,11 +17,11 @@ func DownloadArchive(ctx *context.APIContext) { var tp git.ArchiveType switch ballType := ctx.PathParam("ball_type"); ballType { case "tarball": - tp = git.TARGZ + tp = git.ArchiveTarGz case "zipball": - tp = git.ZIP + tp = git.ArchiveZip case "bundle": - tp = git.BUNDLE + tp = git.ArchiveBundle default: ctx.Error(http.StatusBadRequest, "", fmt.Sprintf("Unknown archive type: %s", ballType)) return @@ -29,14 +29,14 @@ func DownloadArchive(ctx *context.APIContext) { if ctx.Repo.GitRepo == nil { var err error - ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx, ctx.Repo.Repository) + ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx.Repo.Repository) if err != nil { ctx.Error(http.StatusInternalServerError, "OpenRepository", err) return } } - r, err := archiver_service.NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, ctx.PathParam("*"), tp) + r, err := archiver_service.NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, ctx.PathParam("*")+"."+tp.String()) if err != nil { ctx.ServerError("NewRequest", err) return diff --git a/routers/api/v1/repo/file.go b/routers/api/v1/repo/file.go index 8a4f78a3d77..045db7a291f 100644 --- a/routers/api/v1/repo/file.go +++ b/routers/api/v1/repo/file.go @@ -188,7 +188,7 @@ func GetRawFileOrLFS(ctx *context.APIContext) { meta, err := git_model.GetLFSMetaObjectByOid(ctx, ctx.Repo.Repository.ID, pointer.Oid) // If there isn't one, just serve the data directly - if err == git_model.ErrLFSObjectNotExist { + if errors.Is(err, git_model.ErrLFSObjectNotExist) { // Handle caching for the blob SHA (not the LFS object OID) if httpcache.HandleGenericETagTimeCache(ctx.Req, ctx.Resp, `"`+blob.ID.String()+`"`, lastModified) { return @@ -282,7 +282,7 @@ func GetArchive(ctx *context.APIContext) { if ctx.Repo.GitRepo == nil { var err error - ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx, ctx.Repo.Repository) + ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx.Repo.Repository) if err != nil { ctx.Error(http.StatusInternalServerError, "OpenRepository", err) return @@ -293,14 +293,7 @@ func GetArchive(ctx *context.APIContext) { } func archiveDownload(ctx *context.APIContext) { - uri := ctx.PathParam("*") - ext, tp, err := archiver_service.ParseFileName(uri) - if err != nil { - ctx.Error(http.StatusBadRequest, "ParseFileName", err) - return - } - - aReq, err := archiver_service.NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, strings.TrimSuffix(uri, ext), tp) + aReq, err := archiver_service.NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, ctx.PathParam("*")) if err != nil { if errors.Is(err, archiver_service.ErrUnknownArchiveFormat{}) { ctx.Error(http.StatusBadRequest, "unknown archive format", err) @@ -496,12 +489,12 @@ func ChangeFiles(ctx *context.APIContext) { OldBranch: apiOpts.BranchName, NewBranch: apiOpts.NewBranchName, Committer: &files_service.IdentityOptions{ - Name: apiOpts.Committer.Name, - Email: apiOpts.Committer.Email, + GitUserName: apiOpts.Committer.Name, + GitUserEmail: apiOpts.Committer.Email, }, Author: &files_service.IdentityOptions{ - Name: apiOpts.Author.Name, - Email: apiOpts.Author.Email, + GitUserName: apiOpts.Author.Name, + GitUserEmail: apiOpts.Author.Email, }, Dates: &files_service.CommitDateOptions{ Author: apiOpts.Dates.Author, @@ -593,12 +586,12 @@ func CreateFile(ctx *context.APIContext) { OldBranch: apiOpts.BranchName, NewBranch: apiOpts.NewBranchName, Committer: &files_service.IdentityOptions{ - Name: apiOpts.Committer.Name, - Email: apiOpts.Committer.Email, + GitUserName: apiOpts.Committer.Name, + GitUserEmail: apiOpts.Committer.Email, }, Author: &files_service.IdentityOptions{ - Name: apiOpts.Author.Name, - Email: apiOpts.Author.Email, + GitUserName: apiOpts.Author.Name, + GitUserEmail: apiOpts.Author.Email, }, Dates: &files_service.CommitDateOptions{ Author: apiOpts.Dates.Author, @@ -696,12 +689,12 @@ func UpdateFile(ctx *context.APIContext) { OldBranch: apiOpts.BranchName, NewBranch: apiOpts.NewBranchName, Committer: &files_service.IdentityOptions{ - Name: apiOpts.Committer.Name, - Email: apiOpts.Committer.Email, + GitUserName: apiOpts.Committer.Name, + GitUserEmail: apiOpts.Committer.Email, }, Author: &files_service.IdentityOptions{ - Name: apiOpts.Author.Name, - Email: apiOpts.Author.Email, + GitUserName: apiOpts.Author.Name, + GitUserEmail: apiOpts.Author.Email, }, Dates: &files_service.CommitDateOptions{ Author: apiOpts.Dates.Author, @@ -855,12 +848,12 @@ func DeleteFile(ctx *context.APIContext) { OldBranch: apiOpts.BranchName, NewBranch: apiOpts.NewBranchName, Committer: &files_service.IdentityOptions{ - Name: apiOpts.Committer.Name, - Email: apiOpts.Committer.Email, + GitUserName: apiOpts.Committer.Name, + GitUserEmail: apiOpts.Committer.Email, }, Author: &files_service.IdentityOptions{ - Name: apiOpts.Author.Name, - Email: apiOpts.Author.Email, + GitUserName: apiOpts.Author.Name, + GitUserEmail: apiOpts.Author.Email, }, Dates: &files_service.CommitDateOptions{ Author: apiOpts.Dates.Author, diff --git a/routers/api/v1/repo/fork.go b/routers/api/v1/repo/fork.go index 14a1a8d1c4a..f96c432b929 100644 --- a/routers/api/v1/repo/fork.go +++ b/routers/api/v1/repo/fork.go @@ -132,13 +132,15 @@ func CreateFork(ctx *context.APIContext) { } return } - isMember, err := org.IsOrgMember(ctx, ctx.Doer.ID) - if err != nil { - ctx.Error(http.StatusInternalServerError, "IsOrgMember", err) - return - } else if !isMember { - ctx.Error(http.StatusForbidden, "isMemberNot", fmt.Sprintf("User is no Member of Organisation '%s'", org.Name)) - return + if !ctx.Doer.IsAdmin { + isMember, err := org.IsOrgMember(ctx, ctx.Doer.ID) + if err != nil { + ctx.Error(http.StatusInternalServerError, "IsOrgMember", err) + return + } else if !isMember { + ctx.Error(http.StatusForbidden, "isMemberNot", fmt.Sprintf("User is no Member of Organisation '%s'", org.Name)) + return + } } forker = org.AsUser() } diff --git a/routers/api/v1/repo/git_hook.go b/routers/api/v1/repo/git_hook.go index 9b66b69068a..868acf3d853 100644 --- a/routers/api/v1/repo/git_hook.go +++ b/routers/api/v1/repo/git_hook.go @@ -4,6 +4,7 @@ package repo import ( + "errors" "net/http" "code.gitea.io/gitea/modules/git" @@ -82,7 +83,7 @@ func GetGitHook(ctx *context.APIContext) { hookID := ctx.PathParam("id") hook, err := ctx.Repo.GitRepo.GetHook(hookID) if err != nil { - if err == git.ErrNotValidHook { + if errors.Is(err, git.ErrNotValidHook) { ctx.NotFound() } else { ctx.Error(http.StatusInternalServerError, "GetHook", err) @@ -129,7 +130,7 @@ func EditGitHook(ctx *context.APIContext) { hookID := ctx.PathParam("id") hook, err := ctx.Repo.GitRepo.GetHook(hookID) if err != nil { - if err == git.ErrNotValidHook { + if errors.Is(err, git.ErrNotValidHook) { ctx.NotFound() } else { ctx.Error(http.StatusInternalServerError, "GetHook", err) @@ -178,7 +179,7 @@ func DeleteGitHook(ctx *context.APIContext) { hookID := ctx.PathParam("id") hook, err := ctx.Repo.GitRepo.GetHook(hookID) if err != nil { - if err == git.ErrNotValidHook { + if errors.Is(err, git.ErrNotValidHook) { ctx.NotFound() } else { ctx.Error(http.StatusInternalServerError, "GetHook", err) diff --git a/routers/api/v1/repo/hook_test.go b/routers/api/v1/repo/hook_test.go index c659a16f544..2d15c6e0781 100644 --- a/routers/api/v1/repo/hook_test.go +++ b/routers/api/v1/repo/hook_test.go @@ -23,7 +23,7 @@ func TestTestHook(t *testing.T) { contexttest.LoadRepoCommit(t, ctx) contexttest.LoadUser(t, ctx, 2) TestHook(ctx) - assert.EqualValues(t, http.StatusNoContent, ctx.Resp.Status()) + assert.EqualValues(t, http.StatusNoContent, ctx.Resp.WrittenStatus()) unittest.AssertExistsAndLoadBean(t, &webhook.HookTask{ HookID: 1, diff --git a/routers/api/v1/repo/patch.go b/routers/api/v1/repo/patch.go index 5e24dcf8912..95d7631da7b 100644 --- a/routers/api/v1/repo/patch.go +++ b/routers/api/v1/repo/patch.go @@ -58,12 +58,12 @@ func ApplyDiffPatch(ctx *context.APIContext) { OldBranch: apiOpts.BranchName, NewBranch: apiOpts.NewBranchName, Committer: &files.IdentityOptions{ - Name: apiOpts.Committer.Name, - Email: apiOpts.Committer.Email, + GitUserName: apiOpts.Committer.Name, + GitUserEmail: apiOpts.Committer.Email, }, Author: &files.IdentityOptions{ - Name: apiOpts.Author.Name, - Email: apiOpts.Author.Email, + GitUserName: apiOpts.Author.Name, + GitUserEmail: apiOpts.Author.Email, }, Dates: &files.CommitDateOptions{ Author: apiOpts.Dates.Author, diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go index d0c3459b634..f7fdc93f81d 100644 --- a/routers/api/v1/repo/pull.go +++ b/routers/api/v1/repo/pull.go @@ -971,7 +971,7 @@ func MergePullRequest(ctx *context.APIContext) { } if form.MergeWhenChecksSucceed { - scheduled, err := automerge.ScheduleAutoMerge(ctx, ctx.Doer, pr, repo_model.MergeStyle(form.Do), message) + scheduled, err := automerge.ScheduleAutoMerge(ctx, ctx.Doer, pr, repo_model.MergeStyle(form.Do), message, form.DeleteBranchAfterMerge) if err != nil { if pull_model.IsErrAlreadyScheduledToAutoMerge(err) { ctx.Error(http.StatusConflict, "ScheduleAutoMerge", err) @@ -1043,11 +1043,8 @@ func MergePullRequest(ctx *context.APIContext) { } defer headRepo.Close() } - if err := pull_service.RetargetChildrenOnMerge(ctx, ctx.Doer, pr); err != nil { - ctx.Error(http.StatusInternalServerError, "RetargetChildrenOnMerge", err) - return - } - if err := repo_service.DeleteBranch(ctx, ctx.Doer, pr.HeadRepo, headRepo, pr.HeadBranch); err != nil { + + if err := repo_service.DeleteBranch(ctx, ctx.Doer, pr.HeadRepo, headRepo, pr.HeadBranch, pr); err != nil { switch { case git.IsErrBranchNotExist(err): ctx.NotFound(err) @@ -1060,10 +1057,6 @@ func MergePullRequest(ctx *context.APIContext) { } return } - if err := issues_model.AddDeletePRBranchComment(ctx, ctx.Doer, pr.BaseRepo, pr.Issue.ID, pr.HeadBranch); err != nil { - // Do not fail here as branch has already been deleted - log.Error("DeleteBranch: %v", err) - } } } diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go index a192e241b7b..ce09e7fc0f9 100644 --- a/routers/api/v1/repo/repo.go +++ b/routers/api/v1/repo/repo.go @@ -726,7 +726,7 @@ func updateBasicProperties(ctx *context.APIContext, opts api.EditRepoOption) err if ctx.Repo.GitRepo == nil && !repo.IsEmpty { var err error - ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx, repo) + ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, repo) if err != nil { ctx.Error(http.StatusInternalServerError, "Unable to OpenRepository", err) return err diff --git a/routers/api/v1/repo/repo_test.go b/routers/api/v1/repo/repo_test.go index 8d6ca9e3b54..0a63b16a99b 100644 --- a/routers/api/v1/repo/repo_test.go +++ b/routers/api/v1/repo/repo_test.go @@ -58,7 +58,7 @@ func TestRepoEdit(t *testing.T) { web.SetForm(ctx, &opts) Edit(ctx) - assert.EqualValues(t, http.StatusOK, ctx.Resp.Status()) + assert.EqualValues(t, http.StatusOK, ctx.Resp.WrittenStatus()) unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ ID: 1, }, unittest.Cond("name = ? AND is_archived = 1", *opts.Name)) @@ -78,7 +78,7 @@ func TestRepoEditNameChange(t *testing.T) { web.SetForm(ctx, &opts) Edit(ctx) - assert.EqualValues(t, http.StatusOK, ctx.Resp.Status()) + assert.EqualValues(t, http.StatusOK, ctx.Resp.WrittenStatus()) unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ ID: 1, diff --git a/routers/api/v1/repo/star.go b/routers/api/v1/repo/star.go index 99676de119c..46ed17ad91e 100644 --- a/routers/api/v1/repo/star.go +++ b/routers/api/v1/repo/star.go @@ -44,6 +44,8 @@ func ListStargazers(ctx *context.APIContext) { // "$ref": "#/responses/UserList" // "404": // "$ref": "#/responses/notFound" + // "403": + // "$ref": "#/responses/forbidden" stargazers, err := repo_model.GetStargazers(ctx, ctx.Repo.Repository, utils.GetListOptions(ctx)) if err != nil { diff --git a/routers/api/v1/repo/transfer.go b/routers/api/v1/repo/transfer.go index b2090cac41c..bb666f6487e 100644 --- a/routers/api/v1/repo/transfer.go +++ b/routers/api/v1/repo/transfer.go @@ -15,6 +15,7 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/log" api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/convert" @@ -161,12 +162,16 @@ func AcceptTransfer(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - err := acceptOrRejectRepoTransfer(ctx, true) - if ctx.Written() { - return - } + err := repo_service.AcceptTransferOwnership(ctx, ctx.Repo.Repository, ctx.Doer) if err != nil { - ctx.Error(http.StatusInternalServerError, "acceptOrRejectRepoTransfer", err) + switch { + case repo_model.IsErrNoPendingTransfer(err): + ctx.Error(http.StatusNotFound, "AcceptTransferOwnership", err) + case errors.Is(err, util.ErrPermissionDenied): + ctx.Error(http.StatusForbidden, "AcceptTransferOwnership", err) + default: + ctx.Error(http.StatusInternalServerError, "AcceptTransferOwnership", err) + } return } @@ -199,40 +204,18 @@ func RejectTransfer(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - err := acceptOrRejectRepoTransfer(ctx, false) - if ctx.Written() { - return - } + err := repo_service.RejectRepositoryTransfer(ctx, ctx.Repo.Repository, ctx.Doer) if err != nil { - ctx.Error(http.StatusInternalServerError, "acceptOrRejectRepoTransfer", err) + switch { + case repo_model.IsErrNoPendingTransfer(err): + ctx.Error(http.StatusNotFound, "RejectRepositoryTransfer", err) + case errors.Is(err, util.ErrPermissionDenied): + ctx.Error(http.StatusForbidden, "RejectRepositoryTransfer", err) + default: + ctx.Error(http.StatusInternalServerError, "RejectRepositoryTransfer", err) + } return } ctx.JSON(http.StatusOK, convert.ToRepo(ctx, ctx.Repo.Repository, ctx.Repo.Permission)) } - -func acceptOrRejectRepoTransfer(ctx *context.APIContext, accept bool) error { - repoTransfer, err := repo_model.GetPendingRepositoryTransfer(ctx, ctx.Repo.Repository) - if err != nil { - if repo_model.IsErrNoPendingTransfer(err) { - ctx.NotFound() - return nil - } - return err - } - - if err := repoTransfer.LoadAttributes(ctx); err != nil { - return err - } - - if !repoTransfer.CanUserAcceptTransfer(ctx, ctx.Doer) { - ctx.Error(http.StatusForbidden, "CanUserAcceptTransfer", nil) - return fmt.Errorf("user does not have permissions to do this") - } - - if accept { - return repo_service.TransferOwnership(ctx, repoTransfer.Doer, repoTransfer.Recipient, ctx.Repo.Repository, repoTransfer.Teams) - } - - return repo_service.CancelRepositoryTransfer(ctx, ctx.Repo.Repository) -} diff --git a/routers/api/v1/swagger/options.go b/routers/api/v1/swagger/options.go index 125605d98f5..353d6de89b7 100644 --- a/routers/api/v1/swagger/options.go +++ b/routers/api/v1/swagger/options.go @@ -208,6 +208,9 @@ type swaggerParameterBodies struct { // in:body CreateVariableOption api.CreateVariableOption + // in:body + RenameOrgOption api.RenameOrgOption + // in:body UpdateVariableOption api.UpdateVariableOption } diff --git a/routers/api/v1/swagger/repo.go b/routers/api/v1/swagger/repo.go index b9d2a0217cd..f754c80a5b3 100644 --- a/routers/api/v1/swagger/repo.go +++ b/routers/api/v1/swagger/repo.go @@ -448,3 +448,15 @@ type swaggerCompare struct { // in:body Body api.Compare `json:"body"` } + +// swagger:response MergeUpstreamRequest +type swaggerMergeUpstreamRequest struct { + // in:body + Body api.MergeUpstreamRequest `json:"body"` +} + +// swagger:response MergeUpstreamResponse +type swaggerMergeUpstreamResponse struct { + // in:body + Body api.MergeUpstreamResponse `json:"body"` +} diff --git a/routers/api/v1/user/star.go b/routers/api/v1/user/star.go index ad9ed9548d0..70e54bc1ae4 100644 --- a/routers/api/v1/user/star.go +++ b/routers/api/v1/user/star.go @@ -66,6 +66,8 @@ func GetStarredRepos(ctx *context.APIContext) { // "$ref": "#/responses/RepositoryList" // "404": // "$ref": "#/responses/notFound" + // "403": + // "$ref": "#/responses/forbidden" private := ctx.ContextUser.ID == ctx.Doer.ID repos, err := getStarredRepos(ctx, ctx.ContextUser, private) @@ -97,6 +99,8 @@ func GetMyStarredRepos(ctx *context.APIContext) { // responses: // "200": // "$ref": "#/responses/RepositoryList" + // "403": + // "$ref": "#/responses/forbidden" repos, err := getStarredRepos(ctx, ctx.Doer, true) if err != nil { @@ -128,6 +132,8 @@ func IsStarring(ctx *context.APIContext) { // "$ref": "#/responses/empty" // "404": // "$ref": "#/responses/notFound" + // "403": + // "$ref": "#/responses/forbidden" if repo_model.IsStaring(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID) { ctx.Status(http.StatusNoContent) @@ -193,6 +199,8 @@ func Unstar(ctx *context.APIContext) { // "$ref": "#/responses/empty" // "404": // "$ref": "#/responses/notFound" + // "403": + // "$ref": "#/responses/forbidden" err := repo_model.StarRepo(ctx, ctx.Doer, ctx.Repo.Repository, false) if err != nil { diff --git a/routers/api/v1/utils/hook.go b/routers/api/v1/utils/hook.go index 4328878e196..2ad9eeb0ec3 100644 --- a/routers/api/v1/utils/hook.go +++ b/routers/api/v1/utils/hook.go @@ -185,26 +185,28 @@ func addHook(ctx *context.APIContext, form *api.CreateHookOption, ownerID, repoI HookEvent: &webhook_module.HookEvent{ ChooseEvents: true, HookEvents: webhook_module.HookEvents{ - Create: util.SliceContainsString(form.Events, string(webhook_module.HookEventCreate), true), - Delete: util.SliceContainsString(form.Events, string(webhook_module.HookEventDelete), true), - Fork: util.SliceContainsString(form.Events, string(webhook_module.HookEventFork), true), - Issues: issuesHook(form.Events, "issues_only"), - IssueAssign: issuesHook(form.Events, string(webhook_module.HookEventIssueAssign)), - IssueLabel: issuesHook(form.Events, string(webhook_module.HookEventIssueLabel)), - IssueMilestone: issuesHook(form.Events, string(webhook_module.HookEventIssueMilestone)), - IssueComment: issuesHook(form.Events, string(webhook_module.HookEventIssueComment)), - Push: util.SliceContainsString(form.Events, string(webhook_module.HookEventPush), true), - PullRequest: pullHook(form.Events, "pull_request_only"), - PullRequestAssign: pullHook(form.Events, string(webhook_module.HookEventPullRequestAssign)), - PullRequestLabel: pullHook(form.Events, string(webhook_module.HookEventPullRequestLabel)), - PullRequestMilestone: pullHook(form.Events, string(webhook_module.HookEventPullRequestMilestone)), - PullRequestComment: pullHook(form.Events, string(webhook_module.HookEventPullRequestComment)), - PullRequestReview: pullHook(form.Events, "pull_request_review"), - PullRequestReviewRequest: pullHook(form.Events, string(webhook_module.HookEventPullRequestReviewRequest)), - PullRequestSync: pullHook(form.Events, string(webhook_module.HookEventPullRequestSync)), - Wiki: util.SliceContainsString(form.Events, string(webhook_module.HookEventWiki), true), - Repository: util.SliceContainsString(form.Events, string(webhook_module.HookEventRepository), true), - Release: util.SliceContainsString(form.Events, string(webhook_module.HookEventRelease), true), + webhook_module.HookEventCreate: util.SliceContainsString(form.Events, string(webhook_module.HookEventCreate), true), + webhook_module.HookEventDelete: util.SliceContainsString(form.Events, string(webhook_module.HookEventDelete), true), + webhook_module.HookEventFork: util.SliceContainsString(form.Events, string(webhook_module.HookEventFork), true), + webhook_module.HookEventIssues: issuesHook(form.Events, "issues_only"), + webhook_module.HookEventIssueAssign: issuesHook(form.Events, string(webhook_module.HookEventIssueAssign)), + webhook_module.HookEventIssueLabel: issuesHook(form.Events, string(webhook_module.HookEventIssueLabel)), + webhook_module.HookEventIssueMilestone: issuesHook(form.Events, string(webhook_module.HookEventIssueMilestone)), + webhook_module.HookEventIssueComment: issuesHook(form.Events, string(webhook_module.HookEventIssueComment)), + webhook_module.HookEventPush: util.SliceContainsString(form.Events, string(webhook_module.HookEventPush), true), + webhook_module.HookEventPullRequest: pullHook(form.Events, "pull_request_only"), + webhook_module.HookEventPullRequestAssign: pullHook(form.Events, string(webhook_module.HookEventPullRequestAssign)), + webhook_module.HookEventPullRequestLabel: pullHook(form.Events, string(webhook_module.HookEventPullRequestLabel)), + webhook_module.HookEventPullRequestMilestone: pullHook(form.Events, string(webhook_module.HookEventPullRequestMilestone)), + webhook_module.HookEventPullRequestComment: pullHook(form.Events, string(webhook_module.HookEventPullRequestComment)), + webhook_module.HookEventPullRequestReview: pullHook(form.Events, "pull_request_review"), + webhook_module.HookEventPullRequestReviewRequest: pullHook(form.Events, string(webhook_module.HookEventPullRequestReviewRequest)), + webhook_module.HookEventPullRequestSync: pullHook(form.Events, string(webhook_module.HookEventPullRequestSync)), + webhook_module.HookEventWiki: util.SliceContainsString(form.Events, string(webhook_module.HookEventWiki), true), + webhook_module.HookEventRepository: util.SliceContainsString(form.Events, string(webhook_module.HookEventRepository), true), + webhook_module.HookEventRelease: util.SliceContainsString(form.Events, string(webhook_module.HookEventRelease), true), + webhook_module.HookEventPackage: util.SliceContainsString(form.Events, string(webhook_module.HookEventPackage), true), + webhook_module.HookEventStatus: util.SliceContainsString(form.Events, string(webhook_module.HookEventStatus), true), }, BranchFilter: form.BranchFilter, }, @@ -356,14 +358,13 @@ func editHook(ctx *context.APIContext, form *api.EditHookOption, w *webhook.Webh w.PushOnly = false w.SendEverything = false w.ChooseEvents = true - w.Create = util.SliceContainsString(form.Events, string(webhook_module.HookEventCreate), true) - w.Push = util.SliceContainsString(form.Events, string(webhook_module.HookEventPush), true) - w.Create = util.SliceContainsString(form.Events, string(webhook_module.HookEventCreate), true) - w.Delete = util.SliceContainsString(form.Events, string(webhook_module.HookEventDelete), true) - w.Fork = util.SliceContainsString(form.Events, string(webhook_module.HookEventFork), true) - w.Repository = util.SliceContainsString(form.Events, string(webhook_module.HookEventRepository), true) - w.Wiki = util.SliceContainsString(form.Events, string(webhook_module.HookEventWiki), true) - w.Release = util.SliceContainsString(form.Events, string(webhook_module.HookEventRelease), true) + w.HookEvents[webhook_module.HookEventCreate] = util.SliceContainsString(form.Events, string(webhook_module.HookEventCreate), true) + w.HookEvents[webhook_module.HookEventPush] = util.SliceContainsString(form.Events, string(webhook_module.HookEventPush), true) + w.HookEvents[webhook_module.HookEventDelete] = util.SliceContainsString(form.Events, string(webhook_module.HookEventDelete), true) + w.HookEvents[webhook_module.HookEventFork] = util.SliceContainsString(form.Events, string(webhook_module.HookEventFork), true) + w.HookEvents[webhook_module.HookEventRepository] = util.SliceContainsString(form.Events, string(webhook_module.HookEventRepository), true) + w.HookEvents[webhook_module.HookEventWiki] = util.SliceContainsString(form.Events, string(webhook_module.HookEventWiki), true) + w.HookEvents[webhook_module.HookEventRelease] = util.SliceContainsString(form.Events, string(webhook_module.HookEventRelease), true) w.BranchFilter = form.BranchFilter err := w.SetHeaderAuthorization(form.AuthorizationHeader) @@ -373,21 +374,21 @@ func editHook(ctx *context.APIContext, form *api.EditHookOption, w *webhook.Webh } // Issues - w.Issues = issuesHook(form.Events, "issues_only") - w.IssueAssign = issuesHook(form.Events, string(webhook_module.HookEventIssueAssign)) - w.IssueLabel = issuesHook(form.Events, string(webhook_module.HookEventIssueLabel)) - w.IssueMilestone = issuesHook(form.Events, string(webhook_module.HookEventIssueMilestone)) - w.IssueComment = issuesHook(form.Events, string(webhook_module.HookEventIssueComment)) + w.HookEvents[webhook_module.HookEventIssues] = issuesHook(form.Events, "issues_only") + w.HookEvents[webhook_module.HookEventIssueAssign] = issuesHook(form.Events, string(webhook_module.HookEventIssueAssign)) + w.HookEvents[webhook_module.HookEventIssueLabel] = issuesHook(form.Events, string(webhook_module.HookEventIssueLabel)) + w.HookEvents[webhook_module.HookEventIssueMilestone] = issuesHook(form.Events, string(webhook_module.HookEventIssueMilestone)) + w.HookEvents[webhook_module.HookEventIssueComment] = issuesHook(form.Events, string(webhook_module.HookEventIssueComment)) // Pull requests - w.PullRequest = pullHook(form.Events, "pull_request_only") - w.PullRequestAssign = pullHook(form.Events, string(webhook_module.HookEventPullRequestAssign)) - w.PullRequestLabel = pullHook(form.Events, string(webhook_module.HookEventPullRequestLabel)) - w.PullRequestMilestone = pullHook(form.Events, string(webhook_module.HookEventPullRequestMilestone)) - w.PullRequestComment = pullHook(form.Events, string(webhook_module.HookEventPullRequestComment)) - w.PullRequestReview = pullHook(form.Events, "pull_request_review") - w.PullRequestReviewRequest = pullHook(form.Events, string(webhook_module.HookEventPullRequestReviewRequest)) - w.PullRequestSync = pullHook(form.Events, string(webhook_module.HookEventPullRequestSync)) + w.HookEvents[webhook_module.HookEventPullRequest] = pullHook(form.Events, "pull_request_only") + w.HookEvents[webhook_module.HookEventPullRequestAssign] = pullHook(form.Events, string(webhook_module.HookEventPullRequestAssign)) + w.HookEvents[webhook_module.HookEventPullRequestLabel] = pullHook(form.Events, string(webhook_module.HookEventPullRequestLabel)) + w.HookEvents[webhook_module.HookEventPullRequestMilestone] = pullHook(form.Events, string(webhook_module.HookEventPullRequestMilestone)) + w.HookEvents[webhook_module.HookEventPullRequestComment] = pullHook(form.Events, string(webhook_module.HookEventPullRequestComment)) + w.HookEvents[webhook_module.HookEventPullRequestReview] = pullHook(form.Events, "pull_request_review") + w.HookEvents[webhook_module.HookEventPullRequestReviewRequest] = pullHook(form.Events, string(webhook_module.HookEventPullRequestReviewRequest)) + w.HookEvents[webhook_module.HookEventPullRequestSync] = pullHook(form.Events, string(webhook_module.HookEventPullRequestSync)) if err := w.UpdateEvent(); err != nil { ctx.Error(http.StatusInternalServerError, "UpdateEvent", err) diff --git a/routers/common/codesearch.go b/routers/common/codesearch.go new file mode 100644 index 00000000000..a14af126e55 --- /dev/null +++ b/routers/common/codesearch.go @@ -0,0 +1,39 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package common + +import ( + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/services/context" +) + +func PrepareCodeSearch(ctx *context.Context) (ret struct { + Keyword string + Language string + IsFuzzy bool +}, +) { + ret.Language = ctx.FormTrim("l") + ret.Keyword = ctx.FormTrim("q") + + fuzzyDefault := setting.Indexer.RepoIndexerEnabled + fuzzyAllow := true + if setting.Indexer.RepoType == "bleve" && setting.Indexer.TypeBleveMaxFuzzniess == 0 { + fuzzyDefault = false + fuzzyAllow = false + } + isFuzzy := ctx.FormOptionalBool("fuzzy").ValueOrDefault(fuzzyDefault) + if isFuzzy && !fuzzyAllow { + ctx.Flash.Info("Fuzzy search is disabled by default due to performance reasons") + isFuzzy = false + } + + ctx.Data["IsBleveFuzzyDisabled"] = true + ctx.Data["Keyword"] = ret.Keyword + ctx.Data["Language"] = ret.Language + ctx.Data["IsFuzzy"] = isFuzzy + + ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled + return ret +} diff --git a/routers/common/middleware.go b/routers/common/middleware.go index 12b0c67b018..2ba02de8edc 100644 --- a/routers/common/middleware.go +++ b/routers/common/middleware.go @@ -9,6 +9,7 @@ import ( "strings" "code.gitea.io/gitea/modules/cache" + "code.gitea.io/gitea/modules/gtprof" "code.gitea.io/gitea/modules/httplib" "code.gitea.io/gitea/modules/reqctx" "code.gitea.io/gitea/modules/setting" @@ -43,14 +44,26 @@ func ProtocolMiddlewares() (handlers []any) { func RequestContextHandler() func(h http.Handler) http.Handler { return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { - profDesc := fmt.Sprintf("%s: %s", req.Method, req.RequestURI) + return http.HandlerFunc(func(respOrig http.ResponseWriter, req *http.Request) { + // this response writer might not be the same as the one in context.Base.Resp + // because there might be a "gzip writer" in the middle, so the "written size" here is the compressed size + respWriter := context.WrapResponseWriter(respOrig) + + profDesc := fmt.Sprintf("HTTP: %s %s", req.Method, req.RequestURI) ctx, finished := reqctx.NewRequestContext(req.Context(), profDesc) defer finished() + ctx, span := gtprof.GetTracer().Start(ctx, gtprof.TraceSpanHTTP) + req = req.WithContext(ctx) + defer func() { + chiCtx := chi.RouteContext(req.Context()) + span.SetAttributeString(gtprof.TraceAttrHTTPRoute, chiCtx.RoutePattern()) + span.End() + }() + defer func() { if err := recover(); err != nil { - RenderPanicErrorPage(resp, req, err) // it should never panic + RenderPanicErrorPage(respWriter, req, err) // it should never panic } }() @@ -62,7 +75,7 @@ func RequestContextHandler() func(h http.Handler) http.Handler { _ = req.MultipartForm.RemoveAll() // remove the temp files buffered to tmp directory } }) - next.ServeHTTP(context.WrapResponseWriter(resp), req) + next.ServeHTTP(respWriter, req) }) } } @@ -71,11 +84,11 @@ func ChiRoutePathHandler() func(h http.Handler) http.Handler { // make sure chi uses EscapedPath(RawPath) as RoutePath, then "%2f" could be handled correctly return func(next http.Handler) http.Handler { return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { - ctx := chi.RouteContext(req.Context()) + chiCtx := chi.RouteContext(req.Context()) if req.URL.RawPath == "" { - ctx.RoutePath = req.URL.EscapedPath() + chiCtx.RoutePath = req.URL.EscapedPath() } else { - ctx.RoutePath = req.URL.RawPath + chiCtx.RoutePath = req.URL.RawPath } next.ServeHTTP(resp, req) }) diff --git a/routers/init.go b/routers/init.go index e7aa765bf03..744feee2f0d 100644 --- a/routers/init.go +++ b/routers/init.go @@ -213,7 +213,7 @@ func NormalRoutes() *web.Router { } r.NotFound(func(w http.ResponseWriter, req *http.Request) { - routing.UpdateFuncInfo(req.Context(), routing.GetFuncInfo(http.NotFound, "GlobalNotFound")) + defer routing.RecordFuncInfo(req.Context(), routing.GetFuncInfo(http.NotFound, "GlobalNotFound"))() http.NotFound(w, req) }) return r diff --git a/routers/private/hook_post_receive.go b/routers/private/hook_post_receive.go index 63ea2728195..8530715b568 100644 --- a/routers/private/hook_post_receive.go +++ b/routers/private/hook_post_receive.go @@ -8,11 +8,9 @@ import ( "fmt" "net/http" - "code.gitea.io/gitea/models/db" git_model "code.gitea.io/gitea/models/git" issues_model "code.gitea.io/gitea/models/issues" access_model "code.gitea.io/gitea/models/perm/access" - pull_model "code.gitea.io/gitea/models/pull" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/cache" @@ -22,7 +20,7 @@ import ( "code.gitea.io/gitea/modules/private" repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" - timeutil "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" gitea_context "code.gitea.io/gitea/services/context" @@ -363,21 +361,9 @@ func handlePullRequestMerging(ctx *gitea_context.PrivateContext, opts *private.H return } - pr.MergedCommitID = updates[len(updates)-1].NewCommitID - pr.MergedUnix = timeutil.TimeStampNow() - pr.Merger = pusher - pr.MergerID = pusher.ID - err = db.WithTx(ctx, func(ctx context.Context) error { - // Removing an auto merge pull and ignore if not exist - if err := pull_model.DeleteScheduledAutoMerge(ctx, pr.ID); err != nil && !db.IsErrNotExist(err) { - return fmt.Errorf("DeleteScheduledAutoMerge[%d]: %v", opts.PullRequestID, err) - } - if _, err := pull_service.SetMerged(ctx, pr); err != nil { - return fmt.Errorf("SetMerged failed: %s/%s Error: %v", ownerName, repoName, err) - } - return nil - }) - if err != nil { + // FIXME: Maybe we need a `PullRequestStatusMerged` status for PRs that are merged, currently we use the previous status + // here to keep it as before, that maybe PullRequestStatusMergeable + if _, err := pull_service.SetMerged(ctx, pr, updates[len(updates)-1].NewCommitID, timeutil.TimeStampNow(), pusher, pr.Status); err != nil { log.Error("Failed to update PR to merged: %v", err) ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{Err: "Failed to update PR to merged"}) } diff --git a/routers/private/hook_post_receive_test.go b/routers/private/hook_post_receive_test.go index a089739d156..34722f910d9 100644 --- a/routers/private/hook_post_receive_test.go +++ b/routers/private/hook_post_receive_test.go @@ -27,7 +27,7 @@ func TestHandlePullRequestMerging(t *testing.T) { user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) - err = pull_model.ScheduleAutoMerge(db.DefaultContext, user1, pr.ID, repo_model.MergeStyleSquash, "squash merge a pr") + err = pull_model.ScheduleAutoMerge(db.DefaultContext, user1, pr.ID, repo_model.MergeStyleSquash, "squash merge a pr", false) assert.NoError(t, err) autoMerge := unittest.AssertExistsAndLoadBean(t, &pull_model.AutoMerge{PullID: pr.ID}) diff --git a/routers/private/hook_proc_receive.go b/routers/private/hook_proc_receive.go index efb3f5831e4..4076a57dbaa 100644 --- a/routers/private/hook_proc_receive.go +++ b/routers/private/hook_proc_receive.go @@ -4,9 +4,11 @@ package private import ( + "errors" "net/http" - repo_model "code.gitea.io/gitea/models/repo" + issues_model "code.gitea.io/gitea/models/issues" + user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/private" @@ -25,10 +27,16 @@ func HookProcReceive(ctx *gitea_context.PrivateContext) { results, err := agit.ProcReceive(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, opts) if err != nil { - if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) { - ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err.Error()) + if errors.Is(err, issues_model.ErrMustCollaborator) { + ctx.JSON(http.StatusUnauthorized, private.Response{ + Err: err.Error(), UserMsg: "You must be a collaborator to create pull request.", + }) + } else if errors.Is(err, user_model.ErrBlockedUser) { + ctx.JSON(http.StatusUnauthorized, private.Response{ + Err: err.Error(), UserMsg: "Cannot create pull request because you are blocked by the repository owner.", + }) } else { - log.Error(err.Error()) + log.Error("agit.ProcReceive failed: %v", err) ctx.JSON(http.StatusInternalServerError, private.Response{ Err: err.Error(), }) diff --git a/routers/private/internal_repo.go b/routers/private/internal_repo.go index 8a53e1ed237..e111d6689e0 100644 --- a/routers/private/internal_repo.go +++ b/routers/private/internal_repo.go @@ -27,7 +27,7 @@ func RepoAssignment(ctx *gitea_context.PrivateContext) { return } - gitRepo, err := gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx, repo) + gitRepo, err := gitrepo.RepositoryFromRequestContextOrOpen(ctx, repo) if err != nil { log.Error("Failed to open repository: %s/%s Error: %v", ownerName, repoName, err) ctx.JSON(http.StatusInternalServerError, private.Response{ diff --git a/routers/utils/utils_test.go b/routers/utils/utils_test.go index 6e7f3c33cda..cc7c888a758 100644 --- a/routers/utils/utils_test.go +++ b/routers/utils/utils_test.go @@ -5,6 +5,8 @@ package utils import ( "testing" + + "github.com/stretchr/testify/assert" ) func TestSanitizeFlashErrorString(t *testing.T) { @@ -32,9 +34,8 @@ func TestSanitizeFlashErrorString(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := SanitizeFlashErrorString(tt.arg); got != tt.want { - t.Errorf("SanitizeFlashErrorString() = '%v', want '%v'", got, tt.want) - } + got := SanitizeFlashErrorString(tt.arg) + assert.Equal(t, tt.want, got) }) } } diff --git a/routers/web/admin/admin.go b/routers/web/admin/admin.go index 3902a1efb14..0cd13acf601 100644 --- a/routers/web/admin/admin.go +++ b/routers/web/admin/admin.go @@ -37,6 +37,7 @@ const ( tplSelfCheck templates.TplName = "admin/self_check" tplCron templates.TplName = "admin/cron" tplQueue templates.TplName = "admin/queue" + tplPerfTrace templates.TplName = "admin/perftrace" tplStacktrace templates.TplName = "admin/stacktrace" tplQueueManage templates.TplName = "admin/queue_manage" tplStats templates.TplName = "admin/stats" diff --git a/routers/web/admin/diagnosis.go b/routers/web/admin/diagnosis.go index 020554a35a4..d040dbe0ba3 100644 --- a/routers/web/admin/diagnosis.go +++ b/routers/web/admin/diagnosis.go @@ -10,13 +10,15 @@ import ( "time" "code.gitea.io/gitea/modules/httplib" + "code.gitea.io/gitea/modules/tailmsg" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/services/context" ) func MonitorDiagnosis(ctx *context.Context) { seconds := ctx.FormInt64("seconds") - if seconds <= 5 { - seconds = 5 + if seconds <= 1 { + seconds = 1 } if seconds > 300 { seconds = 300 @@ -65,4 +67,16 @@ func MonitorDiagnosis(ctx *context.Context) { return } _ = pprof.Lookup("heap").WriteTo(f, 0) + + f, err = zipWriter.CreateHeader(&zip.FileHeader{Name: "perftrace.txt", Method: zip.Deflate, Modified: time.Now()}) + if err != nil { + ctx.ServerError("Failed to create zip file", err) + return + } + for _, record := range tailmsg.GetManager().GetTraceRecorder().GetRecords() { + _, _ = f.Write(util.UnsafeStringToBytes(record.Time.Format(time.RFC3339))) + _, _ = f.Write([]byte(" ")) + _, _ = f.Write(util.UnsafeStringToBytes((record.Content))) + _, _ = f.Write([]byte("\n\n")) + } } diff --git a/routers/web/admin/perftrace.go b/routers/web/admin/perftrace.go new file mode 100644 index 00000000000..51ee57da10f --- /dev/null +++ b/routers/web/admin/perftrace.go @@ -0,0 +1,18 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package admin + +import ( + "net/http" + + "code.gitea.io/gitea/modules/tailmsg" + "code.gitea.io/gitea/services/context" +) + +func PerfTrace(ctx *context.Context) { + monitorTraceCommon(ctx) + ctx.Data["PageIsAdminMonitorPerfTrace"] = true + ctx.Data["PerfTraceRecords"] = tailmsg.GetManager().GetTraceRecorder().GetRecords() + ctx.HTML(http.StatusOK, tplPerfTrace) +} diff --git a/routers/web/admin/stacktrace.go b/routers/web/admin/stacktrace.go index ff751be6217..2b8c2fb4afb 100644 --- a/routers/web/admin/stacktrace.go +++ b/routers/web/admin/stacktrace.go @@ -12,10 +12,17 @@ import ( "code.gitea.io/gitea/services/context" ) +func monitorTraceCommon(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("admin.monitor") + ctx.Data["PageIsAdminMonitorTrace"] = true + // Hide the performance trace tab in production, because it shows a lot of SQLs and is not that useful for end users. + // To avoid confusing end users, do not let them know this tab. End users should "download diagnosis report" instead. + ctx.Data["ShowAdminPerformanceTraceTab"] = !setting.IsProd +} + // Stacktrace show admin monitor goroutines page func Stacktrace(ctx *context.Context) { - ctx.Data["Title"] = ctx.Tr("admin.monitor") - ctx.Data["PageIsAdminMonitorStacktrace"] = true + monitorTraceCommon(ctx) ctx.Data["GoroutineCount"] = runtime.NumGoroutine() diff --git a/routers/web/admin/users.go b/routers/web/admin/users.go index be2ba4424cd..f6a3af1c866 100644 --- a/routers/web/admin/users.go +++ b/routers/web/admin/users.go @@ -313,6 +313,8 @@ func editUserCommon(ctx *context.Context) { ctx.Data["PageIsAdminUsers"] = true ctx.Data["DisableRegularOrgCreation"] = setting.Admin.DisableRegularOrgCreation ctx.Data["DisableMigrations"] = setting.Repository.DisableMigrations + ctx.Data["DisableGitHooks"] = setting.DisableGitHooks + ctx.Data["DisableImportLocal"] = !setting.ImportLocalPaths ctx.Data["AllowedUserVisibilityModes"] = setting.Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice() ctx.Data["DisableGravatar"] = setting.Config().Picture.DisableGravatar.Value(ctx) } diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go index 3fe1d5970e8..363da8f3929 100644 --- a/routers/web/auth/auth.go +++ b/routers/web/auth/auth.go @@ -169,6 +169,7 @@ func prepareSignInPageData(ctx *context.Context) { ctx.Data["PageIsLogin"] = true ctx.Data["EnableSSPI"] = auth.IsSSPIEnabled(ctx) ctx.Data["EnablePasswordSignInForm"] = setting.Service.EnablePasswordSignInForm + ctx.Data["EnablePasskeyAuth"] = setting.Service.EnablePasskeyAuth if setting.Service.EnableCaptcha && setting.Service.RequireCaptchaForLogin { context.SetCaptchaData(ctx) diff --git a/routers/web/auth/linkaccount.go b/routers/web/auth/linkaccount.go index 147d8d38024..386241225e0 100644 --- a/routers/web/auth/linkaccount.go +++ b/routers/web/auth/linkaccount.go @@ -29,6 +29,7 @@ var tplLinkAccount templates.TplName = "user/auth/link_account" // LinkAccount shows the page where the user can decide to login or create a new account func LinkAccount(ctx *context.Context) { + // FIXME: these common template variables should be prepared in one common function, but not just copy-paste again and again. ctx.Data["DisablePassword"] = !setting.Service.RequireExternalRegistrationPassword || setting.Service.AllowOnlyExternalRegistration ctx.Data["Title"] = ctx.Tr("link_account") ctx.Data["LinkAccountMode"] = true @@ -43,13 +44,20 @@ func LinkAccount(ctx *context.Context) { ctx.Data["CfTurnstileSitekey"] = setting.Service.CfTurnstileSitekey ctx.Data["DisableRegistration"] = setting.Service.DisableRegistration ctx.Data["AllowOnlyInternalRegistration"] = setting.Service.AllowOnlyInternalRegistration + ctx.Data["EnablePasswordSignInForm"] = setting.Service.EnablePasswordSignInForm ctx.Data["ShowRegistrationButton"] = false + ctx.Data["EnablePasskeyAuth"] = setting.Service.EnablePasskeyAuth // use this to set the right link into the signIn and signUp templates in the link_account template ctx.Data["SignInLink"] = setting.AppSubURL + "/user/link_account_signin" ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/link_account_signup" gothUser, ok := ctx.Session.Get("linkAccountGothUser").(goth.User) + + // If you'd like to quickly debug the "link account" page layout, just uncomment the blow line + // Don't worry, when the below line exists, the lint won't pass: ineffectual assignment to gothUser (ineffassign) + // gothUser, ok = goth.User{Email: "invalid-email", Name: "."}, true // intentionally use invalid data to avoid pass the registration check + if !ok { // no account in session, so just redirect to the login page, then the user could restart the process ctx.Redirect(setting.AppSubURL + "/user/login") @@ -135,7 +143,10 @@ func LinkAccountPostSignIn(ctx *context.Context) { ctx.Data["McaptchaURL"] = setting.Service.McaptchaURL ctx.Data["CfTurnstileSitekey"] = setting.Service.CfTurnstileSitekey ctx.Data["DisableRegistration"] = setting.Service.DisableRegistration + ctx.Data["AllowOnlyInternalRegistration"] = setting.Service.AllowOnlyInternalRegistration + ctx.Data["EnablePasswordSignInForm"] = setting.Service.EnablePasswordSignInForm ctx.Data["ShowRegistrationButton"] = false + ctx.Data["EnablePasskeyAuth"] = setting.Service.EnablePasskeyAuth // use this to set the right link into the signIn and signUp templates in the link_account template ctx.Data["SignInLink"] = setting.AppSubURL + "/user/link_account_signin" @@ -223,7 +234,10 @@ func LinkAccountPostRegister(ctx *context.Context) { ctx.Data["McaptchaURL"] = setting.Service.McaptchaURL ctx.Data["CfTurnstileSitekey"] = setting.Service.CfTurnstileSitekey ctx.Data["DisableRegistration"] = setting.Service.DisableRegistration + ctx.Data["AllowOnlyInternalRegistration"] = setting.Service.AllowOnlyInternalRegistration + ctx.Data["EnablePasswordSignInForm"] = setting.Service.EnablePasswordSignInForm ctx.Data["ShowRegistrationButton"] = false + ctx.Data["EnablePasskeyAuth"] = setting.Service.EnablePasskeyAuth // use this to set the right link into the signIn and signUp templates in the link_account template ctx.Data["SignInLink"] = setting.AppSubURL + "/user/link_account_signin" diff --git a/routers/web/auth/webauthn.go b/routers/web/auth/webauthn.go index 69031adeaa6..8dbe34b2b13 100644 --- a/routers/web/auth/webauthn.go +++ b/routers/web/auth/webauthn.go @@ -50,6 +50,11 @@ func WebAuthn(ctx *context.Context) { // WebAuthnPasskeyAssertion submits a WebAuthn challenge for the passkey login to the browser func WebAuthnPasskeyAssertion(ctx *context.Context) { + if !setting.Service.EnablePasskeyAuth { + ctx.Error(http.StatusForbidden) + return + } + assertion, sessionData, err := wa.WebAuthn.BeginDiscoverableLogin() if err != nil { ctx.ServerError("webauthn.BeginDiscoverableLogin", err) @@ -66,6 +71,11 @@ func WebAuthnPasskeyAssertion(ctx *context.Context) { // WebAuthnPasskeyLogin handles the WebAuthn login process using a Passkey func WebAuthnPasskeyLogin(ctx *context.Context) { + if !setting.Service.EnablePasskeyAuth { + ctx.Error(http.StatusForbidden) + return + } + sessionData, okData := ctx.Session.Get("webauthnPasskeyAssertion").(*webauthn.SessionData) if !okData || sessionData == nil { ctx.ServerError("ctx.Session.Get", errors.New("not in WebAuthn session")) diff --git a/routers/web/base.go b/routers/web/base.go index aa0b43c16a1..abe11593f75 100644 --- a/routers/web/base.go +++ b/routers/web/base.go @@ -34,7 +34,7 @@ func storageHandler(storageSetting *setting.Storage, prefix string, objStore sto http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) return } - routing.UpdateFuncInfo(req.Context(), funcInfo) + defer routing.RecordFuncInfo(req.Context(), funcInfo)() rPath := strings.TrimPrefix(req.URL.Path, "/"+prefix+"/") rPath = util.PathJoinRelX(rPath) @@ -65,7 +65,7 @@ func storageHandler(storageSetting *setting.Storage, prefix string, objStore sto http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) return } - routing.UpdateFuncInfo(req.Context(), funcInfo) + defer routing.RecordFuncInfo(req.Context(), funcInfo)() rPath := strings.TrimPrefix(req.URL.Path, "/"+prefix+"/") rPath = util.PathJoinRelX(rPath) diff --git a/routers/web/explore/code.go b/routers/web/explore/code.go index 36581446109..ae5ff3db761 100644 --- a/routers/web/explore/code.go +++ b/routers/web/explore/code.go @@ -11,6 +11,7 @@ import ( code_indexer "code.gitea.io/gitea/modules/indexer/code" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/templates" + "code.gitea.io/gitea/routers/common" "code.gitea.io/gitea/services/context" ) @@ -32,18 +33,10 @@ func Code(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("explore") ctx.Data["PageIsExplore"] = true ctx.Data["PageIsExploreCode"] = true - - language := ctx.FormTrim("l") - keyword := ctx.FormTrim("q") - - isFuzzy := ctx.FormOptionalBool("fuzzy").ValueOrDefault(true) - - ctx.Data["Keyword"] = keyword - ctx.Data["Language"] = language - ctx.Data["IsFuzzy"] = isFuzzy ctx.Data["PageIsViewCode"] = true - if keyword == "" { + prepareSearch := common.PrepareCodeSearch(ctx) + if prepareSearch.Keyword == "" { ctx.HTML(http.StatusOK, tplExploreCode) return } @@ -80,9 +73,9 @@ func Code(ctx *context.Context) { if (len(repoIDs) > 0) || isAdmin { total, searchResults, searchResultLanguages, err = code_indexer.PerformSearch(ctx, &code_indexer.SearchOptions{ RepoIDs: repoIDs, - Keyword: keyword, - IsKeywordFuzzy: isFuzzy, - Language: language, + Keyword: prepareSearch.Keyword, + IsKeywordFuzzy: prepareSearch.IsFuzzy, + Language: prepareSearch.Language, Paginator: &db.ListOptions{ Page: page, PageSize: setting.UI.RepoSearchPagingNum, diff --git a/routers/web/feed/branch.go b/routers/web/feed/branch.go index 80ce2ad198b..d3dae9503e8 100644 --- a/routers/web/feed/branch.go +++ b/routers/web/feed/branch.go @@ -23,7 +23,7 @@ func ShowBranchFeed(ctx *context.Context, repo *repo.Repository, formatType stri } title := fmt.Sprintf("Latest commits for branch %s", ctx.Repo.BranchName) - link := &feeds.Link{Href: repo.HTMLURL() + "/" + ctx.Repo.BranchNameSubURL()} + link := &feeds.Link{Href: repo.HTMLURL() + "/" + ctx.Repo.RefTypeNameSubURL()} feed := &feeds.Feed{ Title: title, @@ -43,6 +43,7 @@ func ShowBranchFeed(ctx *context.Context, repo *repo.Repository, formatType stri }, Description: commit.Message(), Content: commit.Message(), + Created: commit.Committer.When, }) } diff --git a/routers/web/feed/convert.go b/routers/web/feed/convert.go index b7f849dc659..4ede5796b18 100644 --- a/routers/web/feed/convert.go +++ b/routers/web/feed/convert.go @@ -258,18 +258,18 @@ func feedActionsToFeedItems(ctx *context.Context, actions activities_model.Actio } // GetFeedType return if it is a feed request and altered name and feed type. -func GetFeedType(name string, req *http.Request) (bool, string, string) { +func GetFeedType(name string, req *http.Request) (showFeed bool, feedType string) { if strings.HasSuffix(name, ".rss") || strings.Contains(req.Header.Get("Accept"), "application/rss+xml") { - return true, strings.TrimSuffix(name, ".rss"), "rss" + return true, "rss" } if strings.HasSuffix(name, ".atom") || strings.Contains(req.Header.Get("Accept"), "application/atom+xml") { - return true, strings.TrimSuffix(name, ".atom"), "atom" + return true, "atom" } - return false, name, "" + return false, "" } // feedActionsToFeedItems convert gitea's Repo's Releases to feeds Item diff --git a/routers/web/feed/file.go b/routers/web/feed/file.go index 1ab768ff27f..407e4fa2d5d 100644 --- a/routers/web/feed/file.go +++ b/routers/web/feed/file.go @@ -24,7 +24,7 @@ func ShowFileFeed(ctx *context.Context, repo *repo.Repository, formatType string } commits, err := ctx.Repo.GitRepo.CommitsByFileAndRange( git.CommitsByFileAndRangeOptions{ - Revision: ctx.Repo.RefName, + Revision: ctx.Repo.RefFullName.ShortName(), // FIXME: legacy code used ShortName File: fileName, Page: 1, }) @@ -35,7 +35,7 @@ func ShowFileFeed(ctx *context.Context, repo *repo.Repository, formatType string title := fmt.Sprintf("Latest commits for file %s", ctx.Repo.TreePath) - link := &feeds.Link{Href: repo.HTMLURL() + "/" + ctx.Repo.BranchNameSubURL() + "/" + util.PathEscapeSegments(ctx.Repo.TreePath)} + link := &feeds.Link{Href: repo.HTMLURL() + "/" + ctx.Repo.RefTypeNameSubURL() + "/" + util.PathEscapeSegments(ctx.Repo.TreePath)} feed := &feeds.Feed{ Title: title, @@ -55,6 +55,7 @@ func ShowFileFeed(ctx *context.Context, repo *repo.Repository, formatType string }, Description: commit.Message(), Content: commit.Message(), + Created: commit.Committer.When, }) } diff --git a/routers/web/feed/render.go b/routers/web/feed/render.go index 462ebb97b5a..d1a229e5b27 100644 --- a/routers/web/feed/render.go +++ b/routers/web/feed/render.go @@ -9,7 +9,7 @@ import ( // RenderBranchFeed render format for branch or file func RenderBranchFeed(ctx *context.Context) { - _, _, showFeedType := GetFeedType(ctx.PathParam("reponame"), ctx.Req) + _, showFeedType := GetFeedType(ctx.PathParam("reponame"), ctx.Req) if ctx.Repo.TreePath == "" { ShowBranchFeed(ctx, ctx.Repo.Repository, showFeedType) } else { diff --git a/routers/web/goget.go b/routers/web/goget.go index 3714dd8eb0f..79d5c2b2076 100644 --- a/routers/web/goget.go +++ b/routers/web/goget.go @@ -69,9 +69,9 @@ func goGet(ctx *context.Context) { var cloneURL string if setting.Repository.GoGetCloneURLProtocol == "ssh" { - cloneURL = repo_model.ComposeSSHCloneURL(ownerName, repoName) + cloneURL = repo_model.ComposeSSHCloneURL(ctx.Doer, ownerName, repoName) } else { - cloneURL = repo_model.ComposeHTTPSCloneURL(ownerName, repoName) + cloneURL = repo_model.ComposeHTTPSCloneURL(ctx, ownerName, repoName) } goImportContent := fmt.Sprintf("%s git %s", goGetImport, cloneURL /*CloneLink*/) goSourceContent := fmt.Sprintf("%s _ %s %s", goGetImport, prefix+"{/dir}" /*GoDocDirectory*/, prefix+"{/dir}/{file}#L{line}" /*GoDocFile*/) diff --git a/routers/web/org/home.go b/routers/web/org/home.go index deeb18ae7c2..27d1e14d858 100644 --- a/routers/web/org/home.go +++ b/routers/web/org/home.go @@ -12,6 +12,7 @@ import ( "code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/renderhelper" repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/markup/markdown" "code.gitea.io/gitea/modules/setting" @@ -21,9 +22,7 @@ import ( "code.gitea.io/gitea/services/context" ) -const ( - tplOrgHome templates.TplName = "org/home" -) +const tplOrgHome templates.TplName = "org/home" // Home show organization home page func Home(ctx *context.Context) { @@ -35,7 +34,7 @@ func Home(ctx *context.Context) { } ctx.SetPathParam("org", uname) - context.HandleOrgAssignment(ctx) + context.OrgAssignment(context.OrgAssignmentOptions{})(ctx) if ctx.Written() { return } @@ -110,15 +109,19 @@ func home(ctx *context.Context, viewRepositories bool) { ctx.Data["DisableNewPullMirrors"] = setting.Mirror.DisableNewPull ctx.Data["ShowMemberAndTeamTab"] = ctx.Org.IsMember || len(members) > 0 - if !prepareOrgProfileReadme(ctx, viewRepositories) { - ctx.Data["PageIsViewRepositories"] = true + prepareResult, err := shared_user.PrepareOrgHeader(ctx) + if err != nil { + ctx.ServerError("PrepareOrgHeader", err) + return } - var ( - repos []*repo_model.Repository - count int64 - ) - repos, count, err = repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{ + // if no profile readme, it still means "view repositories" + isViewOverview := !viewRepositories && prepareOrgProfileReadme(ctx, prepareResult) + ctx.Data["PageIsViewRepositories"] = !isViewOverview + ctx.Data["PageIsViewOverview"] = isViewOverview + ctx.Data["ShowOrgProfileReadmeSelector"] = isViewOverview && prepareResult.ProfilePublicReadmeBlob != nil && prepareResult.ProfilePrivateReadmeBlob != nil + + repos, count, err := repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{ ListOptions: db.ListOptions{ PageSize: setting.UI.User.RepoPagingNum, Page: page, @@ -151,28 +154,45 @@ func home(ctx *context.Context, viewRepositories bool) { ctx.HTML(http.StatusOK, tplOrgHome) } -func prepareOrgProfileReadme(ctx *context.Context, viewRepositories bool) bool { - profileDbRepo, profileGitRepo, profileReadme, profileClose := shared_user.FindUserProfileReadme(ctx, ctx.Doer) - defer profileClose() - ctx.Data["HasProfileReadme"] = profileReadme != nil +func prepareOrgProfileReadme(ctx *context.Context, prepareResult *shared_user.PrepareOrgHeaderResult) bool { + viewAs := ctx.FormString("view_as", util.Iif(ctx.Org.IsMember, "member", "public")) + viewAsMember := viewAs == "member" - if profileGitRepo == nil || profileReadme == nil || viewRepositories { + var profileRepo *repo_model.Repository + var readmeBlob *git.Blob + if viewAsMember { + if prepareResult.ProfilePrivateReadmeBlob != nil { + profileRepo, readmeBlob = prepareResult.ProfilePrivateRepo, prepareResult.ProfilePrivateReadmeBlob + } else { + profileRepo, readmeBlob = prepareResult.ProfilePublicRepo, prepareResult.ProfilePublicReadmeBlob + viewAsMember = false + } + } else { + if prepareResult.ProfilePublicReadmeBlob != nil { + profileRepo, readmeBlob = prepareResult.ProfilePublicRepo, prepareResult.ProfilePublicReadmeBlob + } else { + profileRepo, readmeBlob = prepareResult.ProfilePrivateRepo, prepareResult.ProfilePrivateReadmeBlob + viewAsMember = true + } + } + if readmeBlob == nil { return false } - if bytes, err := profileReadme.GetBlobContent(setting.UI.MaxDisplayFileSize); err != nil { - log.Error("failed to GetBlobContent: %v", err) - } else { - rctx := renderhelper.NewRenderContextRepoFile(ctx, profileDbRepo, renderhelper.RepoFileOptions{ - CurrentRefPath: path.Join("branch", util.PathEscapeSegments(profileDbRepo.DefaultBranch)), - }) - if profileContent, err := markdown.RenderString(rctx, bytes); err != nil { - log.Error("failed to RenderString: %v", err) - } else { - ctx.Data["ProfileReadme"] = profileContent - } + readmeBytes, err := readmeBlob.GetBlobContent(setting.UI.MaxDisplayFileSize) + if err != nil { + log.Error("failed to GetBlobContent for profile %q (view as %q) readme: %v", profileRepo.FullName(), viewAs, err) + return false } - ctx.Data["PageIsViewOverview"] = true + rctx := renderhelper.NewRenderContextRepoFile(ctx, profileRepo, renderhelper.RepoFileOptions{ + CurrentRefPath: path.Join("branch", util.PathEscapeSegments(profileRepo.DefaultBranch)), + }) + ctx.Data["ProfileReadmeContent"], err = markdown.RenderString(rctx, readmeBytes) + if err != nil { + log.Error("failed to GetBlobContent for profile %q (view as %q) readme: %v", profileRepo.FullName(), viewAs, err) + return false + } + ctx.Data["IsViewingOrgAsMember"] = viewAsMember return true } diff --git a/routers/web/org/members.go b/routers/web/org/members.go index 5a134caecb0..1665a123025 100644 --- a/routers/web/org/members.go +++ b/routers/web/org/members.go @@ -54,9 +54,9 @@ func Members(ctx *context.Context) { return } - err = shared_user.RenderOrgHeader(ctx) + _, err = shared_user.PrepareOrgHeader(ctx) if err != nil { - ctx.ServerError("RenderOrgHeader", err) + ctx.ServerError("PrepareOrgHeader", err) return } diff --git a/routers/web/org/projects.go b/routers/web/org/projects.go index c037d4a7fd0..32da1b41d16 100644 --- a/routers/web/org/projects.go +++ b/routers/web/org/projects.go @@ -211,7 +211,7 @@ func ChangeProjectStatus(ctx *context.Context) { ctx.NotFoundOrServerError("ChangeProjectStatusByRepoIDAndID", project_model.IsErrProjectNotExist, err) return } - ctx.JSONRedirect(fmt.Sprintf("%s/-/projects/%d", ctx.ContextUser.HomeLink(), id)) + ctx.JSONRedirect(project_model.ProjectLinkForOrg(ctx.ContextUser, id)) } // DeleteProject delete a project @@ -261,7 +261,7 @@ func RenderEditProject(ctx *context.Context) { ctx.Data["redirect"] = ctx.FormString("redirect") ctx.Data["HomeLink"] = ctx.ContextUser.HomeLink() ctx.Data["card_type"] = p.CardType - ctx.Data["CancelLink"] = fmt.Sprintf("%s/-/projects/%d", ctx.ContextUser.HomeLink(), p.ID) + ctx.Data["CancelLink"] = project_model.ProjectLinkForOrg(ctx.ContextUser, p.ID) ctx.HTML(http.StatusOK, tplProjectsNew) } @@ -275,7 +275,7 @@ func EditProjectPost(ctx *context.Context) { ctx.Data["PageIsViewProjects"] = true ctx.Data["CanWriteProjects"] = canWriteProjects(ctx) ctx.Data["CardTypes"] = project_model.GetCardConfig() - ctx.Data["CancelLink"] = fmt.Sprintf("%s/-/projects/%d", ctx.ContextUser.HomeLink(), projectID) + ctx.Data["CancelLink"] = project_model.ProjectLinkForOrg(ctx.ContextUser, projectID) shared_user.RenderUserHeader(ctx) diff --git a/routers/web/org/teams.go b/routers/web/org/teams.go index 0137f2cc961..26031029d66 100644 --- a/routers/web/org/teams.go +++ b/routers/web/org/teams.go @@ -58,9 +58,9 @@ func Teams(ctx *context.Context) { } ctx.Data["Teams"] = ctx.Org.Teams - err := shared_user.RenderOrgHeader(ctx) + _, err := shared_user.PrepareOrgHeader(ctx) if err != nil { - ctx.ServerError("RenderOrgHeader", err) + ctx.ServerError("PrepareOrgHeader", err) return } diff --git a/routers/web/org/worktime.go b/routers/web/org/worktime.go new file mode 100644 index 00000000000..23369848254 --- /dev/null +++ b/routers/web/org/worktime.go @@ -0,0 +1,74 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package org + +import ( + "net/http" + "time" + + "code.gitea.io/gitea/models/organization" + "code.gitea.io/gitea/modules/templates" + "code.gitea.io/gitea/services/context" +) + +const tplByRepos templates.TplName = "org/worktime" + +// parseOrgTimes contains functionality that is required in all these functions, +// like parsing the date from the request, setting default dates, etc. +func parseOrgTimes(ctx *context.Context) (unixFrom, unixTo int64) { + rangeFrom := ctx.FormString("from") + rangeTo := ctx.FormString("to") + if rangeFrom == "" { + rangeFrom = time.Now().Format("2006-01") + "-01" // defaults to start of current month + } + if rangeTo == "" { + rangeTo = time.Now().Format("2006-01-02") // defaults to today + } + + ctx.Data["RangeFrom"] = rangeFrom + ctx.Data["RangeTo"] = rangeTo + + timeFrom, err := time.Parse("2006-01-02", rangeFrom) + if err != nil { + ctx.ServerError("time.Parse", err) + } + timeTo, err := time.Parse("2006-01-02", rangeTo) + if err != nil { + ctx.ServerError("time.Parse", err) + } + unixFrom = timeFrom.Unix() + unixTo = timeTo.Add(1440*time.Minute - 1*time.Second).Unix() // humans expect that we include the ending day too + return unixFrom, unixTo +} + +func Worktime(ctx *context.Context) { + ctx.Data["PageIsOrgTimes"] = true + + unixFrom, unixTo := parseOrgTimes(ctx) + if ctx.Written() { + return + } + + worktimeBy := ctx.FormString("by") + ctx.Data["WorktimeBy"] = worktimeBy + + var worktimeSumResult any + var err error + if worktimeBy == "milestones" { + worktimeSumResult, err = organization.GetWorktimeByMilestones(ctx.Org.Organization, unixFrom, unixTo) + ctx.Data["WorktimeByMilestones"] = true + } else if worktimeBy == "members" { + worktimeSumResult, err = organization.GetWorktimeByMembers(ctx.Org.Organization, unixFrom, unixTo) + ctx.Data["WorktimeByMembers"] = true + } else /* by repos */ { + worktimeSumResult, err = organization.GetWorktimeByRepos(ctx.Org.Organization, unixFrom, unixTo) + ctx.Data["WorktimeByRepos"] = true + } + if err != nil { + ctx.ServerError("GetWorktime", err) + return + } + ctx.Data["WorktimeSumResult"] = worktimeSumResult + ctx.HTML(http.StatusOK, tplByRepos) +} diff --git a/routers/web/repo/actions/actions.go b/routers/web/repo/actions/actions.go index f0d8d81fee5..539c4b6ed00 100644 --- a/routers/web/repo/actions/actions.go +++ b/routers/web/repo/actions/actions.go @@ -32,8 +32,9 @@ import ( ) const ( - tplListActions templates.TplName = "repo/actions/list" - tplViewActions templates.TplName = "repo/actions/view" + tplListActions templates.TplName = "repo/actions/list" + tplDispatchInputsActions templates.TplName = "repo/actions/workflow_dispatch_inputs" + tplViewActions templates.TplName = "repo/actions/view" ) type Workflow struct { @@ -64,107 +65,143 @@ func MustEnableActions(ctx *context.Context) { func List(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("actions.actions") ctx.Data["PageIsActions"] = true - workflowID := ctx.FormString("workflow") - actorID := ctx.FormInt64("actor") - status := ctx.FormInt("status") - ctx.Data["CurWorkflow"] = workflowID - var workflows []Workflow - var curWorkflow *model.Workflow - if empty, err := ctx.Repo.GitRepo.IsEmpty(); err != nil { - ctx.ServerError("IsEmpty", err) + commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch) + if err != nil { + ctx.ServerError("GetBranchCommit", err) return - } else if !empty { - commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch) - if err != nil { - ctx.ServerError("GetBranchCommit", err) - return - } - entries, err := actions.ListWorkflows(commit) - if err != nil { - ctx.ServerError("ListWorkflows", err) - return - } + } - // Get all runner labels - runners, err := db.Find[actions_model.ActionRunner](ctx, actions_model.FindRunnerOptions{ - RepoID: ctx.Repo.Repository.ID, - IsOnline: optional.Some(true), - WithAvailable: true, - }) - if err != nil { - ctx.ServerError("FindRunners", err) - return - } - allRunnerLabels := make(container.Set[string]) - for _, r := range runners { - allRunnerLabels.AddMultiple(r.AgentLabels...) - } + workflows := prepareWorkflowDispatchTemplate(ctx, commit) + if ctx.Written() { + return + } - workflows = make([]Workflow, 0, len(entries)) - for _, entry := range entries { - workflow := Workflow{Entry: *entry} - content, err := actions.GetContentFromEntry(entry) - if err != nil { - ctx.ServerError("GetContentFromEntry", err) - return - } - wf, err := model.ReadWorkflow(bytes.NewReader(content)) - if err != nil { - workflow.ErrMsg = ctx.Locale.TrString("actions.runs.invalid_workflow_helper", err.Error()) - workflows = append(workflows, workflow) + prepareWorkflowList(ctx, workflows) + if ctx.Written() { + return + } + + ctx.HTML(http.StatusOK, tplListActions) +} + +func WorkflowDispatchInputs(ctx *context.Context) { + ref := ctx.FormString("ref") + if ref == "" { + ctx.NotFound("WorkflowDispatchInputs: no ref", nil) + return + } + // get target commit of run from specified ref + refName := git.RefName(ref) + var commit *git.Commit + var err error + if refName.IsTag() { + commit, err = ctx.Repo.GitRepo.GetTagCommit(refName.TagName()) + } else if refName.IsBranch() { + commit, err = ctx.Repo.GitRepo.GetBranchCommit(refName.BranchName()) + } else { + ctx.ServerError("UnsupportedRefType", nil) + return + } + if err != nil { + ctx.ServerError("GetTagCommit/GetBranchCommit", err) + return + } + prepareWorkflowDispatchTemplate(ctx, commit) + if ctx.Written() { + return + } + ctx.HTML(http.StatusOK, tplDispatchInputsActions) +} + +func prepareWorkflowDispatchTemplate(ctx *context.Context, commit *git.Commit) (workflows []Workflow) { + workflowID := ctx.FormString("workflow") + ctx.Data["CurWorkflow"] = workflowID + ctx.Data["CurWorkflowExists"] = false + + var curWorkflow *model.Workflow + + entries, err := actions.ListWorkflows(commit) + if err != nil { + ctx.ServerError("ListWorkflows", err) + return nil + } + + // Get all runner labels + runners, err := db.Find[actions_model.ActionRunner](ctx, actions_model.FindRunnerOptions{ + RepoID: ctx.Repo.Repository.ID, + IsOnline: optional.Some(true), + WithAvailable: true, + }) + if err != nil { + ctx.ServerError("FindRunners", err) + return nil + } + allRunnerLabels := make(container.Set[string]) + for _, r := range runners { + allRunnerLabels.AddMultiple(r.AgentLabels...) + } + + workflows = make([]Workflow, 0, len(entries)) + for _, entry := range entries { + workflow := Workflow{Entry: *entry} + content, err := actions.GetContentFromEntry(entry) + if err != nil { + ctx.ServerError("GetContentFromEntry", err) + return nil + } + wf, err := model.ReadWorkflow(bytes.NewReader(content)) + if err != nil { + workflow.ErrMsg = ctx.Locale.TrString("actions.runs.invalid_workflow_helper", err.Error()) + workflows = append(workflows, workflow) + continue + } + // The workflow must contain at least one job without "needs". Otherwise, a deadlock will occur and no jobs will be able to run. + hasJobWithoutNeeds := false + // Check whether you have matching runner and a job without "needs" + emptyJobsNumber := 0 + for _, j := range wf.Jobs { + if j == nil { + emptyJobsNumber++ continue } - // The workflow must contain at least one job without "needs". Otherwise, a deadlock will occur and no jobs will be able to run. - hasJobWithoutNeeds := false - // Check whether have matching runner and a job without "needs" - emptyJobsNumber := 0 - for _, j := range wf.Jobs { - if j == nil { - emptyJobsNumber++ + if !hasJobWithoutNeeds && len(j.Needs()) == 0 { + hasJobWithoutNeeds = true + } + runsOnList := j.RunsOn() + for _, ro := range runsOnList { + if strings.Contains(ro, "${{") { + // Skip if it contains expressions. + // The expressions could be very complex and could not be evaluated here, + // so just skip it, it's OK since it's just a tooltip message. continue } - if !hasJobWithoutNeeds && len(j.Needs()) == 0 { - hasJobWithoutNeeds = true - } - runsOnList := j.RunsOn() - for _, ro := range runsOnList { - if strings.Contains(ro, "${{") { - // Skip if it contains expressions. - // The expressions could be very complex and could not be evaluated here, - // so just skip it, it's OK since it's just a tooltip message. - continue - } - if !allRunnerLabels.Contains(ro) { - workflow.ErrMsg = ctx.Locale.TrString("actions.runs.no_matching_online_runner_helper", ro) - break - } - } - if workflow.ErrMsg != "" { + if !allRunnerLabels.Contains(ro) { + workflow.ErrMsg = ctx.Locale.TrString("actions.runs.no_matching_online_runner_helper", ro) break } } - if !hasJobWithoutNeeds { - workflow.ErrMsg = ctx.Locale.TrString("actions.runs.no_job_without_needs") - } - if emptyJobsNumber == len(wf.Jobs) { - workflow.ErrMsg = ctx.Locale.TrString("actions.runs.no_job") - } - workflows = append(workflows, workflow) - - if workflow.Entry.Name() == workflowID { - curWorkflow = wf + if workflow.ErrMsg != "" { + break } } + if !hasJobWithoutNeeds { + workflow.ErrMsg = ctx.Locale.TrString("actions.runs.no_job_without_needs") + } + if emptyJobsNumber == len(wf.Jobs) { + workflow.ErrMsg = ctx.Locale.TrString("actions.runs.no_job") + } + workflows = append(workflows, workflow) + + if workflow.Entry.Name() == workflowID { + curWorkflow = wf + ctx.Data["CurWorkflowExists"] = true + } } + ctx.Data["workflows"] = workflows ctx.Data["RepoLink"] = ctx.Repo.Repository.Link() - page := ctx.FormInt("page") - if page <= 0 { - page = 1 - } - actionsConfig := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions).ActionsConfig() ctx.Data["ActionsConfig"] = actionsConfig @@ -188,7 +225,7 @@ func List(ctx *context.Context) { branches, err := git_model.FindBranchNames(ctx, branchOpts) if err != nil { ctx.ServerError("FindBranchNames", err) - return + return nil } // always put default branch on the top if it exists if slices.Contains(branches, ctx.Repo.Repository.DefaultBranch) { @@ -200,12 +237,23 @@ func List(ctx *context.Context) { tags, err := repo_model.GetTagNamesByRepoID(ctx, ctx.Repo.Repository.ID) if err != nil { ctx.ServerError("GetTagNamesByRepoID", err) - return + return nil } ctx.Data["Tags"] = tags } } } + return workflows +} + +func prepareWorkflowList(ctx *context.Context, workflows []Workflow) { + actorID := ctx.FormInt64("actor") + status := ctx.FormInt("status") + workflowID := ctx.FormString("workflow") + page := ctx.FormInt("page") + if page <= 0 { + page = 1 + } // if status or actor query param is not given to frontend href, (href="//actions") // they will be 0 by default, which indicates get all status or actors @@ -264,8 +312,6 @@ func List(ctx *context.Context) { pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.Data["HasWorkflowsOrRuns"] = len(workflows) > 0 || len(runs) > 0 - - ctx.HTML(http.StatusOK, tplListActions) } // loadIsRefDeleted loads the IsRefDeleted field for each run in the list. diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go index ba17fa427d1..fc346b83b47 100644 --- a/routers/web/repo/actions/view.go +++ b/routers/web/repo/actions/view.go @@ -281,86 +281,100 @@ func ViewPost(ctx *context_module.Context) { resp.State.CurrentJob.Steps = make([]*ViewJobStep, 0) // marshal to '[]' instead fo 'null' in json resp.Logs.StepsLog = make([]*ViewStepLog, 0) // marshal to '[]' instead fo 'null' in json if task != nil { - steps := actions.FullSteps(task) - - for _, v := range steps { - resp.State.CurrentJob.Steps = append(resp.State.CurrentJob.Steps, &ViewJobStep{ - Summary: v.Name, - Duration: v.Duration().String(), - Status: v.Status.String(), - }) - } - - for _, cursor := range req.LogCursors { - if !cursor.Expanded { - continue - } - - step := steps[cursor.Step] - - // if task log is expired, return a consistent log line - if task.LogExpired { - if cursor.Cursor == 0 { - resp.Logs.StepsLog = append(resp.Logs.StepsLog, &ViewStepLog{ - Step: cursor.Step, - Cursor: 1, - Lines: []*ViewStepLogLine{ - { - Index: 1, - Message: ctx.Locale.TrString("actions.runs.expire_log_message"), - // Timestamp doesn't mean anything when the log is expired. - // Set it to the task's updated time since it's probably the time when the log has expired. - Timestamp: float64(task.Updated.AsTime().UnixNano()) / float64(time.Second), - }, - }, - Started: int64(step.Started), - }) - } - continue - } - - logLines := make([]*ViewStepLogLine, 0) // marshal to '[]' instead fo 'null' in json - - index := step.LogIndex + cursor.Cursor - validCursor := cursor.Cursor >= 0 && - // !(cursor.Cursor < step.LogLength) when the frontend tries to fetch next line before it's ready. - // So return the same cursor and empty lines to let the frontend retry. - cursor.Cursor < step.LogLength && - // !(index < task.LogIndexes[index]) when task data is older than step data. - // It can be fixed by making sure write/read tasks and steps in the same transaction, - // but it's easier to just treat it as fetching the next line before it's ready. - index < int64(len(task.LogIndexes)) - - if validCursor { - length := step.LogLength - cursor.Cursor - offset := task.LogIndexes[index] - logRows, err := actions.ReadLogs(ctx, task.LogInStorage, task.LogFilename, offset, length) - if err != nil { - ctx.ServerError("actions.ReadLogs", err) - return - } - - for i, row := range logRows { - logLines = append(logLines, &ViewStepLogLine{ - Index: cursor.Cursor + int64(i) + 1, // start at 1 - Message: row.Content, - Timestamp: float64(row.Time.AsTime().UnixNano()) / float64(time.Second), - }) - } - } - - resp.Logs.StepsLog = append(resp.Logs.StepsLog, &ViewStepLog{ - Step: cursor.Step, - Cursor: cursor.Cursor + int64(len(logLines)), - Lines: logLines, - Started: int64(step.Started), - }) + steps, logs, err := convertToViewModel(ctx, req.LogCursors, task) + if err != nil { + ctx.Error(http.StatusInternalServerError, err.Error()) + return } + resp.State.CurrentJob.Steps = append(resp.State.CurrentJob.Steps, steps...) + resp.Logs.StepsLog = append(resp.Logs.StepsLog, logs...) } ctx.JSON(http.StatusOK, resp) } +func convertToViewModel(ctx *context_module.Context, cursors []LogCursor, task *actions_model.ActionTask) ([]*ViewJobStep, []*ViewStepLog, error) { + var viewJobs []*ViewJobStep + var logs []*ViewStepLog + + steps := actions.FullSteps(task) + + for _, v := range steps { + viewJobs = append(viewJobs, &ViewJobStep{ + Summary: v.Name, + Duration: v.Duration().String(), + Status: v.Status.String(), + }) + } + + for _, cursor := range cursors { + if !cursor.Expanded { + continue + } + + step := steps[cursor.Step] + + // if task log is expired, return a consistent log line + if task.LogExpired { + if cursor.Cursor == 0 { + logs = append(logs, &ViewStepLog{ + Step: cursor.Step, + Cursor: 1, + Lines: []*ViewStepLogLine{ + { + Index: 1, + Message: ctx.Locale.TrString("actions.runs.expire_log_message"), + // Timestamp doesn't mean anything when the log is expired. + // Set it to the task's updated time since it's probably the time when the log has expired. + Timestamp: float64(task.Updated.AsTime().UnixNano()) / float64(time.Second), + }, + }, + Started: int64(step.Started), + }) + } + continue + } + + logLines := make([]*ViewStepLogLine, 0) // marshal to '[]' instead fo 'null' in json + + index := step.LogIndex + cursor.Cursor + validCursor := cursor.Cursor >= 0 && + // !(cursor.Cursor < step.LogLength) when the frontend tries to fetch next line before it's ready. + // So return the same cursor and empty lines to let the frontend retry. + cursor.Cursor < step.LogLength && + // !(index < task.LogIndexes[index]) when task data is older than step data. + // It can be fixed by making sure write/read tasks and steps in the same transaction, + // but it's easier to just treat it as fetching the next line before it's ready. + index < int64(len(task.LogIndexes)) + + if validCursor { + length := step.LogLength - cursor.Cursor + offset := task.LogIndexes[index] + logRows, err := actions.ReadLogs(ctx, task.LogInStorage, task.LogFilename, offset, length) + if err != nil { + return nil, nil, fmt.Errorf("actions.ReadLogs: %w", err) + } + + for i, row := range logRows { + logLines = append(logLines, &ViewStepLogLine{ + Index: cursor.Cursor + int64(i) + 1, // start at 1 + Message: row.Content, + Timestamp: float64(row.Time.AsTime().UnixNano()) / float64(time.Second), + }) + } + } + + logs = append(logs, &ViewStepLog{ + Step: cursor.Step, + Cursor: cursor.Cursor + int64(len(logLines)), + Lines: logLines, + Started: int64(step.Started), + }) + } + + return viewJobs, logs, nil +} + // Rerun will rerun jobs in the given run // If jobIndexStr is a blank string, it means rerun all jobs func Rerun(ctx *context_module.Context) { @@ -614,11 +628,6 @@ func getRunJobs(ctx *context_module.Context, runIndex, jobIndex int64) (*actions } func ArtifactsDeleteView(ctx *context_module.Context) { - if !ctx.Repo.CanWrite(unit.TypeActions) { - ctx.Error(http.StatusForbidden, "no permission") - return - } - runIndex := getRunIndex(ctx) artifactName := ctx.PathParam("artifact_name") @@ -812,13 +821,8 @@ func Run(ctx *context_module.Context) { return } - // get workflow entry from default branch commit - defaultBranchCommit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch) - if err != nil { - ctx.Error(http.StatusInternalServerError, err.Error()) - return - } - entries, err := actions.ListWorkflows(defaultBranchCommit) + // get workflow entry from runTargetCommit + entries, err := actions.ListWorkflows(runTargetCommit) if err != nil { ctx.Error(http.StatusInternalServerError, err.Error()) return @@ -855,7 +859,7 @@ func Run(ctx *context_module.Context) { inputs := make(map[string]any) if workflowDispatch := workflow.WorkflowDispatchConfig(); workflowDispatch != nil { for name, config := range workflowDispatch.Inputs { - value := ctx.Req.PostForm.Get(name) + value := ctx.Req.PostFormValue(name) if config.Type == "boolean" { // https://www.w3.org/TR/html401/interact/forms.html // https://stackoverflow.com/questions/11424037/do-checkbox-inputs-only-post-data-if-theyre-checked diff --git a/routers/web/repo/blame.go b/routers/web/repo/blame.go index ad790875136..1022c5e6d6c 100644 --- a/routers/web/repo/blame.go +++ b/routers/web/repo/blame.go @@ -46,9 +46,9 @@ func RefBlame(ctx *context.Context) { return } - branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL() + branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL() treeLink := branchLink - rawLink := ctx.Repo.RepoLink + "/raw/" + ctx.Repo.BranchNameSubURL() + rawLink := ctx.Repo.RepoLink + "/raw/" + ctx.Repo.RefTypeNameSubURL() if len(ctx.Repo.TreePath) > 0 { treeLink += "/" + util.PathEscapeSegments(ctx.Repo.TreePath) diff --git a/routers/web/repo/branch.go b/routers/web/repo/branch.go index 2bcd7821b47..6f8d4d99595 100644 --- a/routers/web/repo/branch.go +++ b/routers/web/repo/branch.go @@ -37,7 +37,6 @@ const ( // Branches render repository branch page func Branches(ctx *context.Context) { ctx.Data["Title"] = "Branches" - ctx.Data["IsRepoToolbarBranches"] = true ctx.Data["AllowsPulls"] = ctx.Repo.Repository.AllowsPulls(ctx) ctx.Data["IsWriter"] = ctx.Repo.CanWrite(unit.TypeCode) ctx.Data["IsMirror"] = ctx.Repo.Repository.IsMirror @@ -94,10 +93,10 @@ func Branches(ctx *context.Context) { // DeleteBranchPost responses for delete merged branch func DeleteBranchPost(ctx *context.Context) { - defer redirect(ctx) + defer jsonRedirectBranches(ctx) branchName := ctx.FormString("name") - if err := repo_service.DeleteBranch(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.GitRepo, branchName); err != nil { + if err := repo_service.DeleteBranch(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.GitRepo, branchName, nil); err != nil { switch { case git.IsErrBranchNotExist(err): log.Debug("DeleteBranch: Can't delete non existing branch '%s'", branchName) @@ -121,7 +120,7 @@ func DeleteBranchPost(ctx *context.Context) { // RestoreBranchPost responses for delete merged branch func RestoreBranchPost(ctx *context.Context) { - defer redirect(ctx) + defer jsonRedirectBranches(ctx) branchID := ctx.FormInt64("branch_id") branchName := ctx.FormString("name") @@ -171,7 +170,7 @@ func RestoreBranchPost(ctx *context.Context) { ctx.Flash.Success(ctx.Tr("repo.branch.restore_success", deletedBranch.Name)) } -func redirect(ctx *context.Context) { +func jsonRedirectBranches(ctx *context.Context) { ctx.JSONRedirect(ctx.Repo.RepoLink + "/branches?page=" + url.QueryEscape(ctx.FormString("page"))) } @@ -185,7 +184,7 @@ func CreateBranch(ctx *context.Context) { if ctx.HasError() { ctx.Flash.Error(ctx.GetErrMsg()) - ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()) + ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()) return } @@ -193,11 +192,11 @@ func CreateBranch(ctx *context.Context) { if form.CreateTag { target := ctx.Repo.CommitID - if ctx.Repo.IsViewBranch { + if ctx.Repo.RefFullName.IsBranch() { target = ctx.Repo.BranchName } err = release_service.CreateNewTag(ctx, ctx.Doer, ctx.Repo.Repository, target, form.NewBranchName, "") - } else if ctx.Repo.IsViewBranch { + } else if ctx.Repo.RefFullName.IsBranch() { err = repo_service.CreateNewBranch(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.GitRepo, ctx.Repo.BranchName, form.NewBranchName) } else { err = repo_service.CreateNewBranchFromCommit(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.GitRepo, ctx.Repo.CommitID, form.NewBranchName) @@ -205,25 +204,25 @@ func CreateBranch(ctx *context.Context) { if err != nil { if release_service.IsErrProtectedTagName(err) { ctx.Flash.Error(ctx.Tr("repo.release.tag_name_protected")) - ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()) + ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()) return } if release_service.IsErrTagAlreadyExists(err) { e := err.(release_service.ErrTagAlreadyExists) ctx.Flash.Error(ctx.Tr("repo.branch.tag_collision", e.TagName)) - ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()) + ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()) return } if git_model.IsErrBranchAlreadyExists(err) || git.IsErrPushOutOfDate(err) { ctx.Flash.Error(ctx.Tr("repo.branch.branch_already_exists", form.NewBranchName)) - ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()) + ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()) return } if git_model.IsErrBranchNameConflict(err) { e := err.(git_model.ErrBranchNameConflict) ctx.Flash.Error(ctx.Tr("repo.branch.branch_name_conflict", form.NewBranchName, e.BranchName)) - ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()) + ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()) return } if git.IsErrPushRejected(err) { @@ -242,7 +241,7 @@ func CreateBranch(ctx *context.Context) { } ctx.Flash.Error(flashError) } - ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()) + ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()) return } diff --git a/routers/web/repo/cherry_pick.go b/routers/web/repo/cherry_pick.go index 35f158df525..616bb8c6cbb 100644 --- a/routers/web/repo/cherry_pick.go +++ b/routers/web/repo/cherry_pick.go @@ -57,7 +57,7 @@ func CherryPick(ctx *context.Context) { ctx.Data["new_branch_name"] = GetUniquePatchBranchName(ctx) ctx.Data["last_commit"] = ctx.Repo.CommitID ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",") - ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL() + ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL() ctx.HTML(200, tplCherryPick) } @@ -85,7 +85,7 @@ func CherryPickPost(ctx *context.Context) { ctx.Data["new_branch_name"] = form.NewBranchName ctx.Data["last_commit"] = ctx.Repo.CommitID ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",") - ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL() + ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL() if ctx.HasError() { ctx.HTML(200, tplCherryPick) @@ -114,11 +114,19 @@ func CherryPickPost(ctx *context.Context) { message += "\n\n" + form.CommitMessage } + gitCommitter, valid := WebGitOperationGetCommitChosenEmailIdentity(ctx, form.CommitEmail) + if !valid { + ctx.Data["Err_CommitEmail"] = true + ctx.RenderWithErr(ctx.Tr("repo.editor.invalid_commit_email"), tplCherryPick, &form) + return + } opts := &files.ApplyDiffPatchOptions{ LastCommitID: form.LastCommit, OldBranch: ctx.Repo.BranchName, NewBranch: branchName, Message: message, + Author: gitCommitter, + Committer: gitCommitter, } // First lets try the simple plain read-tree -m approach diff --git a/routers/web/repo/code_frequency.go b/routers/web/repo/code_frequency.go index 6572adce746..e212d3b60c0 100644 --- a/routers/web/repo/code_frequency.go +++ b/routers/web/repo/code_frequency.go @@ -29,7 +29,7 @@ func CodeFrequency(ctx *context.Context) { // CodeFrequencyData returns JSON of code frequency data func CodeFrequencyData(ctx *context.Context) { - if contributorStats, err := contributors_service.GetContributorStats(ctx, ctx.Cache, ctx.Repo.Repository, ctx.Repo.CommitID); err != nil { + if contributorStats, err := contributors_service.GetContributorStats(ctx, ctx.Cache, ctx.Repo.Repository, ctx.Repo.Repository.DefaultBranch); err != nil { if errors.Is(err, contributors_service.ErrAwaitGeneration) { ctx.Status(http.StatusAccepted) return diff --git a/routers/web/repo/commit.go b/routers/web/repo/commit.go index 3655233312c..c8291d98c62 100644 --- a/routers/web/repo/commit.go +++ b/routers/web/repo/commit.go @@ -22,7 +22,6 @@ import ( "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/charset" "code.gitea.io/gitea/modules/git" - "code.gitea.io/gitea/modules/gitgraph" "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/markup" @@ -32,6 +31,7 @@ import ( "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/gitdiff" repo_service "code.gitea.io/gitea/services/repository" + "code.gitea.io/gitea/services/repository/gitgraph" ) const ( @@ -62,11 +62,7 @@ func Commits(ctx *context.Context) { } ctx.Data["PageIsViewCode"] = true - commitsCount, err := ctx.Repo.GetCommitsCount() - if err != nil { - ctx.ServerError("GetCommitsCount", err) - return - } + commitsCount := ctx.Repo.CommitsCount page := ctx.FormInt("page") if page <= 1 { @@ -129,12 +125,6 @@ func Graph(ctx *context.Context) { ctx.Data["SelectedBranches"] = realBranches files := ctx.FormStrings("file") - commitsCount, err := ctx.Repo.GetCommitsCount() - if err != nil { - ctx.ServerError("GetCommitsCount", err) - return - } - graphCommitsCount, err := ctx.Repo.GetCommitGraphsCount(ctx, hidePRRefs, realBranches, files) if err != nil { log.Warn("GetCommitGraphsCount error for generate graph exclude prs: %t branches: %s in %-v, Will Ignore branches and try again. Underlying Error: %v", hidePRRefs, branches, ctx.Repo.Repository, err) @@ -171,7 +161,6 @@ func Graph(ctx *context.Context) { ctx.Data["Username"] = ctx.Repo.Owner.Name ctx.Data["Reponame"] = ctx.Repo.Repository.Name - ctx.Data["CommitCount"] = commitsCount paginator := context.NewPagination(int(graphCommitsCount), setting.UI.GraphMaxCommitNum, page, 5) paginator.AddParamFromRequest(ctx.Req) @@ -191,7 +180,7 @@ func SearchCommits(ctx *context.Context) { query := ctx.FormTrim("q") if len(query) == 0 { - ctx.Redirect(ctx.Repo.RepoLink + "/commits/" + ctx.Repo.BranchNameSubURL()) + ctx.Redirect(ctx.Repo.RepoLink + "/commits/" + ctx.Repo.RefTypeNameSubURL()) return } @@ -222,7 +211,7 @@ func FileHistory(ctx *context.Context) { return } - commitsCount, err := ctx.Repo.GitRepo.FileCommitsCount(ctx.Repo.RefName, fileName) + commitsCount, err := ctx.Repo.GitRepo.FileCommitsCount(ctx.Repo.RefFullName.ShortName(), fileName) // FIXME: legacy code used ShortName if err != nil { ctx.ServerError("FileCommitsCount", err) return @@ -238,7 +227,7 @@ func FileHistory(ctx *context.Context) { commits, err := ctx.Repo.GitRepo.CommitsByFileAndRange( git.CommitsByFileAndRangeOptions{ - Revision: ctx.Repo.RefName, + Revision: ctx.Repo.RefFullName.ShortName(), // FIXME: legacy code used ShortName File: fileName, Page: page, }) @@ -390,12 +379,6 @@ func Diff(ctx *context.Context) { } } - ctx.Data["BranchName"], err = commit.GetBranchName() - if err != nil { - ctx.ServerError("commit.GetBranchName", err) - return - } - ctx.HTML(http.StatusOK, tplCommitPage) } diff --git a/routers/web/repo/contributors.go b/routers/web/repo/contributors.go index e9c09199556..93dec1f3503 100644 --- a/routers/web/repo/contributors.go +++ b/routers/web/repo/contributors.go @@ -26,7 +26,7 @@ func Contributors(ctx *context.Context) { // ContributorsData renders JSON of contributors along with their weekly commit statistics func ContributorsData(ctx *context.Context) { - if contributorStats, err := contributors_service.GetContributorStats(ctx, ctx.Cache, ctx.Repo.Repository, ctx.Repo.CommitID); err != nil { + if contributorStats, err := contributors_service.GetContributorStats(ctx, ctx.Cache, ctx.Repo.Repository, ctx.Repo.Repository.DefaultBranch); err != nil { if errors.Is(err, contributors_service.ErrAwaitGeneration) { ctx.Status(http.StatusAccepted) return diff --git a/routers/web/repo/editor.go b/routers/web/repo/editor.go index 5fbdeee27e3..cc4ffc698dd 100644 --- a/routers/web/repo/editor.go +++ b/routers/web/repo/editor.go @@ -102,10 +102,18 @@ func getParentTreeFields(treePath string) (treeNames, treePaths []string) { return treeNames, treePaths } -func editFile(ctx *context.Context, isNewFile bool) { - ctx.Data["PageIsViewCode"] = true +func editFileCommon(ctx *context.Context, isNewFile bool) { ctx.Data["PageIsEdit"] = true ctx.Data["IsNewFile"] = isNewFile + ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL() + ctx.Data["PreviewableExtensions"] = strings.Join(markup.PreviewableExtensions(), ",") + ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",") + ctx.Data["IsEditingFileOnly"] = ctx.FormString("return_uri") != "" + ctx.Data["ReturnURI"] = ctx.FormString("return_uri") +} + +func editFile(ctx *context.Context, isNewFile bool) { + editFileCommon(ctx, isNewFile) canCommit := renderCommitRights(ctx) treePath := cleanUploadFileName(ctx.Repo.TreePath) @@ -174,28 +182,19 @@ func editFile(ctx *context.Context, isNewFile bool) { ctx.Data["FileContent"] = content } } else { - // Append filename from query, or empty string to allow user name the new file. + // Append filename from query, or empty string to allow username the new file. treeNames = append(treeNames, fileName) } ctx.Data["TreeNames"] = treeNames ctx.Data["TreePaths"] = treePaths - ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL() ctx.Data["commit_summary"] = "" ctx.Data["commit_message"] = "" - if canCommit { - ctx.Data["commit_choice"] = frmCommitChoiceDirect - } else { - ctx.Data["commit_choice"] = frmCommitChoiceNewBranch - } + ctx.Data["commit_choice"] = util.Iif(canCommit, frmCommitChoiceDirect, frmCommitChoiceNewBranch) ctx.Data["new_branch_name"] = GetUniquePatchBranchName(ctx) ctx.Data["last_commit"] = ctx.Repo.CommitID - ctx.Data["PreviewableExtensions"] = strings.Join(markup.PreviewableExtensions(), ",") - ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",") - ctx.Data["EditorconfigJson"] = GetEditorConfig(ctx, treePath) - ctx.Data["IsEditingFileOnly"] = ctx.FormString("return_uri") != "" - ctx.Data["ReturnURI"] = ctx.FormString("return_uri") + ctx.Data["EditorconfigJson"] = GetEditorConfig(ctx, treePath) ctx.HTML(http.StatusOK, tplEditFile) } @@ -224,6 +223,9 @@ func NewFile(ctx *context.Context) { } func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile bool) { + editFileCommon(ctx, isNewFile) + ctx.Data["PageHasPosted"] = true + canCommit := renderCommitRights(ctx) treeNames, treePaths := getParentTreeFields(form.TreePath) branchName := ctx.Repo.BranchName @@ -231,21 +233,15 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b branchName = form.NewBranchName } - ctx.Data["PageIsEdit"] = true - ctx.Data["PageHasPosted"] = true - ctx.Data["IsNewFile"] = isNewFile ctx.Data["TreePath"] = form.TreePath ctx.Data["TreeNames"] = treeNames ctx.Data["TreePaths"] = treePaths - ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/branch/" + util.PathEscapeSegments(ctx.Repo.BranchName) ctx.Data["FileContent"] = form.Content ctx.Data["commit_summary"] = form.CommitSummary ctx.Data["commit_message"] = form.CommitMessage ctx.Data["commit_choice"] = form.CommitChoice ctx.Data["new_branch_name"] = form.NewBranchName ctx.Data["last_commit"] = ctx.Repo.CommitID - ctx.Data["PreviewableExtensions"] = strings.Join(markup.PreviewableExtensions(), ",") - ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",") ctx.Data["EditorconfigJson"] = GetEditorConfig(ctx, form.TreePath) if ctx.HasError() { @@ -253,7 +249,7 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b return } - // Cannot commit to a an existing branch if user doesn't have rights + // Cannot commit to an existing branch if user doesn't have rights if branchName == ctx.Repo.BranchName && !canCommit { ctx.Data["Err_NewBranchName"] = true ctx.Data["commit_choice"] = frmCommitChoiceNewBranch @@ -276,6 +272,13 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b message += "\n\n" + form.CommitMessage } + gitCommitter, valid := WebGitOperationGetCommitChosenEmailIdentity(ctx, form.CommitEmail) + if !valid { + ctx.Data["Err_CommitEmail"] = true + ctx.RenderWithErr(ctx.Tr("repo.editor.invalid_commit_email"), tplEditFile, &form) + return + } + operation := "update" if isNewFile { operation = "create" @@ -294,7 +297,9 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b ContentReader: strings.NewReader(strings.ReplaceAll(form.Content, "\r", "")), }, }, - Signoff: form.Signoff, + Signoff: form.Signoff, + Author: gitCommitter, + Committer: gitCommitter, }); err != nil { // This is where we handle all the errors thrown by files_service.ChangeRepoFiles if git.IsErrNotExist(err) { @@ -428,7 +433,7 @@ func DiffPreviewPost(ctx *context.Context) { // DeleteFile render delete file page func DeleteFile(ctx *context.Context) { ctx.Data["PageIsDelete"] = true - ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL() + ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL() treePath := cleanUploadFileName(ctx.Repo.TreePath) if treePath != ctx.Repo.TreePath { @@ -462,7 +467,7 @@ func DeleteFilePost(ctx *context.Context) { } ctx.Data["PageIsDelete"] = true - ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL() + ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL() ctx.Data["TreePath"] = ctx.Repo.TreePath ctx.Data["commit_summary"] = form.CommitSummary ctx.Data["commit_message"] = form.CommitMessage @@ -491,6 +496,13 @@ func DeleteFilePost(ctx *context.Context) { message += "\n\n" + form.CommitMessage } + gitCommitter, valid := WebGitOperationGetCommitChosenEmailIdentity(ctx, form.CommitEmail) + if !valid { + ctx.Data["Err_CommitEmail"] = true + ctx.RenderWithErr(ctx.Tr("repo.editor.invalid_commit_email"), tplDeleteFile, &form) + return + } + if _, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{ LastCommitID: form.LastCommit, OldBranch: ctx.Repo.BranchName, @@ -501,8 +513,10 @@ func DeleteFilePost(ctx *context.Context) { TreePath: ctx.Repo.TreePath, }, }, - Message: message, - Signoff: form.Signoff, + Message: message, + Signoff: form.Signoff, + Author: gitCommitter, + Committer: gitCommitter, }); err != nil { // This is where we handle all the errors thrown by repofiles.DeleteRepoFile if git.IsErrNotExist(err) || files_service.IsErrRepoFileDoesNotExist(err) { @@ -604,7 +618,7 @@ func UploadFile(ctx *context.Context) { ctx.Data["TreeNames"] = treeNames ctx.Data["TreePaths"] = treePaths - ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL() + ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL() ctx.Data["commit_summary"] = "" ctx.Data["commit_message"] = "" if canCommit { @@ -702,6 +716,13 @@ func UploadFilePost(ctx *context.Context) { message += "\n\n" + form.CommitMessage } + gitCommitter, valid := WebGitOperationGetCommitChosenEmailIdentity(ctx, form.CommitEmail) + if !valid { + ctx.Data["Err_CommitEmail"] = true + ctx.RenderWithErr(ctx.Tr("repo.editor.invalid_commit_email"), tplUploadFile, &form) + return + } + if err := files_service.UploadRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.UploadRepoFileOptions{ LastCommitID: ctx.Repo.CommitID, OldBranch: oldBranchName, @@ -710,6 +731,8 @@ func UploadFilePost(ctx *context.Context) { Message: message, Files: form.Files, Signoff: form.Signoff, + Author: gitCommitter, + Committer: gitCommitter, }); err != nil { if git_model.IsErrLFSFileLocked(err) { ctx.Data["Err_TreePath"] = true diff --git a/routers/web/repo/helper.go b/routers/web/repo/helper.go index ed6216fa5ce..3bf064c1e38 100644 --- a/routers/web/repo/helper.go +++ b/routers/web/repo/helper.go @@ -4,25 +4,14 @@ package repo import ( - "net/url" - "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/services/context" ) func HandleGitError(ctx *context.Context, msg string, err error) { if git.IsErrNotExist(err) { - refType := "" - switch { - case ctx.Repo.IsViewBranch: - refType = "branch" - case ctx.Repo.IsViewTag: - refType = "tag" - case ctx.Repo.IsViewCommit: - refType = "commit" - } - ctx.Data["NotFoundPrompt"] = ctx.Locale.Tr("repo.tree_path_not_found_"+refType, ctx.Repo.TreePath, url.PathEscape(ctx.Repo.RefName)) - ctx.Data["NotFoundGoBackURL"] = ctx.Repo.RepoLink + "/src/" + refType + "/" + url.PathEscape(ctx.Repo.RefName) + ctx.Data["NotFoundPrompt"] = ctx.Locale.Tr("repo.tree_path_not_found", ctx.Repo.TreePath, ctx.Repo.RefTypeNameSubURL()) + ctx.Data["NotFoundGoBackURL"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL() ctx.NotFound(msg, err) } else { ctx.ServerError(msg, err) diff --git a/routers/web/repo/issue_dependency.go b/routers/web/repo/issue_dependency.go index f1d133edb00..0f6787386d9 100644 --- a/routers/web/repo/issue_dependency.go +++ b/routers/web/repo/issue_dependency.go @@ -109,7 +109,7 @@ func RemoveDependency(ctx *context.Context) { } // Dependency Type - depTypeStr := ctx.Req.PostForm.Get("dependencyType") + depTypeStr := ctx.Req.PostFormValue("dependencyType") var depType issues_model.DependencyType diff --git a/routers/web/repo/issue_label_test.go b/routers/web/repo/issue_label_test.go index 8a613e2c7e5..486c2e35a29 100644 --- a/routers/web/repo/issue_label_test.go +++ b/routers/web/repo/issue_label_test.go @@ -38,7 +38,7 @@ func TestInitializeLabels(t *testing.T) { contexttest.LoadRepo(t, ctx, 2) web.SetForm(ctx, &forms.InitializeLabelsForm{TemplateName: "Default"}) InitializeLabels(ctx) - assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status()) + assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus()) unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ RepoID: 2, Name: "enhancement", @@ -84,7 +84,7 @@ func TestNewLabel(t *testing.T) { Color: "#abcdef", }) NewLabel(ctx) - assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status()) + assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus()) unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ Name: "newlabel", Color: "#abcdef", @@ -104,7 +104,7 @@ func TestUpdateLabel(t *testing.T) { IsArchived: true, }) UpdateLabel(ctx) - assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status()) + assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus()) unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ ID: 2, Name: "newnameforlabel", @@ -120,7 +120,7 @@ func TestDeleteLabel(t *testing.T) { contexttest.LoadRepo(t, ctx, 1) ctx.Req.Form.Set("id", "2") DeleteLabel(ctx) - assert.EqualValues(t, http.StatusOK, ctx.Resp.Status()) + assert.EqualValues(t, http.StatusOK, ctx.Resp.WrittenStatus()) unittest.AssertNotExistsBean(t, &issues_model.Label{ID: 2}) unittest.AssertNotExistsBean(t, &issues_model.IssueLabel{LabelID: 2}) assert.EqualValues(t, ctx.Tr("repo.issues.label_deletion_success"), ctx.Flash.SuccessMsg) @@ -134,7 +134,7 @@ func TestUpdateIssueLabel_Clear(t *testing.T) { ctx.Req.Form.Set("issue_ids", "1,3") ctx.Req.Form.Set("action", "clear") UpdateIssueLabel(ctx) - assert.EqualValues(t, http.StatusOK, ctx.Resp.Status()) + assert.EqualValues(t, http.StatusOK, ctx.Resp.WrittenStatus()) unittest.AssertNotExistsBean(t, &issues_model.IssueLabel{IssueID: 1}) unittest.AssertNotExistsBean(t, &issues_model.IssueLabel{IssueID: 3}) unittest.CheckConsistencyFor(t, &issues_model.Label{}) @@ -160,7 +160,7 @@ func TestUpdateIssueLabel_Toggle(t *testing.T) { ctx.Req.Form.Set("action", testCase.Action) ctx.Req.Form.Set("id", strconv.Itoa(int(testCase.LabelID))) UpdateIssueLabel(ctx) - assert.EqualValues(t, http.StatusOK, ctx.Resp.Status()) + assert.EqualValues(t, http.StatusOK, ctx.Resp.WrittenStatus()) for _, issueID := range testCase.IssueIDs { if testCase.ExpectedAdd { unittest.AssertExistsAndLoadBean(t, &issues_model.IssueLabel{IssueID: issueID, LabelID: testCase.LabelID}) diff --git a/routers/web/repo/issue_new.go b/routers/web/repo/issue_new.go index 9a941ce8571..32daa3e48fa 100644 --- a/routers/web/repo/issue_new.go +++ b/routers/web/repo/issue_new.go @@ -396,8 +396,15 @@ func NewIssuePost(ctx *context.Context) { log.Trace("Issue created: %d/%d", repo.ID, issue.ID) if ctx.FormString("redirect_after_creation") == "project" && projectID > 0 { - ctx.JSONRedirect(ctx.Repo.RepoLink + "/projects/" + strconv.FormatInt(projectID, 10)) - } else { - ctx.JSONRedirect(issue.Link()) + project, err := project_model.GetProjectByID(ctx, projectID) + if err == nil { + if project.Type == project_model.TypeOrganization { + ctx.JSONRedirect(project_model.ProjectLinkForOrg(ctx.Repo.Owner, project.ID)) + } else { + ctx.JSONRedirect(project_model.ProjectLinkForRepo(repo, project.ID)) + } + return + } } + ctx.JSONRedirect(issue.Link()) } diff --git a/routers/web/repo/issue_page_meta.go b/routers/web/repo/issue_page_meta.go index b536b04d7c6..272343f460f 100644 --- a/routers/web/repo/issue_page_meta.go +++ b/routers/web/repo/issue_page_meta.go @@ -193,6 +193,7 @@ func (d *IssuePageMetaData) retrieveReviewersData(ctx *context.Context) { var posterID int64 var isClosed bool var reviews issues_model.ReviewList + var err error if d.Issue == nil { if ctx.Doer != nil { @@ -206,14 +207,7 @@ func (d *IssuePageMetaData) retrieveReviewersData(ctx *context.Context) { isClosed = d.Issue.IsClosed || d.Issue.PullRequest.HasMerged - originalAuthorReviews, err := issues_model.GetReviewersFromOriginalAuthorsByIssueID(ctx, d.Issue.ID) - if err != nil { - ctx.ServerError("GetReviewersFromOriginalAuthorsByIssueID", err) - return - } - data.OriginalReviews = originalAuthorReviews - - reviews, err = issues_model.GetReviewsByIssueID(ctx, d.Issue.ID) + reviews, data.OriginalReviews, err = issues_model.GetReviewsByIssueID(ctx, d.Issue.ID) if err != nil { ctx.ServerError("GetReviewersByIssueID", err) return diff --git a/routers/web/repo/issue_poster.go b/routers/web/repo/issue_poster.go index 91ef947cb46..07059b9b7bf 100644 --- a/routers/web/repo/issue_poster.go +++ b/routers/web/repo/issue_poster.go @@ -26,13 +26,9 @@ type userSearchResponse struct { Results []*userSearchInfo `json:"results"` } -// IssuePosters get posters for current repo's issues/pull requests -func IssuePosters(ctx *context.Context) { - issuePosters(ctx, false) -} - -func PullPosters(ctx *context.Context) { - issuePosters(ctx, true) +func IssuePullPosters(ctx *context.Context) { + isPullList := ctx.PathParam("type") == "pulls" + issuePosters(ctx, isPullList) } func issuePosters(ctx *context.Context, isPullList bool) { diff --git a/routers/web/repo/issue_timetrack.go b/routers/web/repo/issue_timetrack.go index 36e931a48fb..134ded82d10 100644 --- a/routers/web/repo/issue_timetrack.go +++ b/routers/web/repo/issue_timetrack.go @@ -81,7 +81,7 @@ func DeleteTime(c *context.Context) { return } - c.Flash.Success(c.Tr("repo.issues.del_time_history", util.SecToTime(t.Time))) + c.Flash.Success(c.Tr("repo.issues.del_time_history", util.SecToHours(t.Time))) c.JSONRedirect("") } diff --git a/routers/web/repo/issue_view.go b/routers/web/repo/issue_view.go index 29c5e017e63..6370e8e694d 100644 --- a/routers/web/repo/issue_view.go +++ b/routers/web/repo/issue_view.go @@ -4,7 +4,6 @@ package repo import ( - stdCtx "context" "fmt" "math/big" "net/http" @@ -40,72 +39,80 @@ import ( ) // roleDescriptor returns the role descriptor for a comment in/with the given repo, poster and issue -func roleDescriptor(ctx stdCtx.Context, repo *repo_model.Repository, poster *user_model.User, issue *issues_model.Issue, hasOriginalAuthor bool) (issues_model.RoleDescriptor, error) { - roleDescriptor := issues_model.RoleDescriptor{} - +func roleDescriptor(ctx *context.Context, repo *repo_model.Repository, poster *user_model.User, permsCache map[int64]access_model.Permission, issue *issues_model.Issue, hasOriginalAuthor bool) (roleDesc issues_model.RoleDescriptor, err error) { if hasOriginalAuthor { - return roleDescriptor, nil + // the poster is a migrated user, so no need to detect the role + return roleDesc, nil } - perm, err := access_model.GetUserRepoPermission(ctx, repo, poster) - if err != nil { - return roleDescriptor, err + if poster.IsGhost() || !poster.IsIndividual() { + return roleDesc, nil } - // If the poster is the actual poster of the issue, enable Poster role. - roleDescriptor.IsPoster = issue.IsPoster(poster.ID) + roleDesc.IsPoster = issue.IsPoster(poster.ID) // check whether the comment's poster is the issue's poster + + // Guess the role of the poster in the repo by permission + perm, hasPermCache := permsCache[poster.ID] + if !hasPermCache { + perm, err = access_model.GetUserRepoPermission(ctx, repo, poster) + if err != nil { + return roleDesc, err + } + } + if permsCache != nil { + permsCache[poster.ID] = perm + } // Check if the poster is owner of the repo. if perm.IsOwner() { - // If the poster isn't an admin, enable the owner role. + // If the poster isn't a site admin, then is must be the repo's owner if !poster.IsAdmin { - roleDescriptor.RoleInRepo = issues_model.RoleRepoOwner - return roleDescriptor, nil + roleDesc.RoleInRepo = issues_model.RoleRepoOwner + return roleDesc, nil } - - // Otherwise check if poster is the real repo admin. - ok, err := access_model.IsUserRealRepoAdmin(ctx, repo, poster) + // Otherwise (poster is site admin), check if poster is the real repo admin. + isRealRepoAdmin, err := access_model.IsUserRealRepoAdmin(ctx, repo, poster) if err != nil { - return roleDescriptor, err + return roleDesc, err } - if ok { - roleDescriptor.RoleInRepo = issues_model.RoleRepoOwner - return roleDescriptor, nil + if isRealRepoAdmin { + roleDesc.RoleInRepo = issues_model.RoleRepoOwner + return roleDesc, nil } } // If repo is organization, check Member role - if err := repo.LoadOwner(ctx); err != nil { - return roleDescriptor, err + if err = repo.LoadOwner(ctx); err != nil { + return roleDesc, err } if repo.Owner.IsOrganization() { if isMember, err := organization.IsOrganizationMember(ctx, repo.Owner.ID, poster.ID); err != nil { - return roleDescriptor, err + return roleDesc, err } else if isMember { - roleDescriptor.RoleInRepo = issues_model.RoleRepoMember - return roleDescriptor, nil + roleDesc.RoleInRepo = issues_model.RoleRepoMember + return roleDesc, nil } } // If the poster is the collaborator of the repo if isCollaborator, err := repo_model.IsCollaborator(ctx, repo.ID, poster.ID); err != nil { - return roleDescriptor, err + return roleDesc, err } else if isCollaborator { - roleDescriptor.RoleInRepo = issues_model.RoleRepoCollaborator - return roleDescriptor, nil + roleDesc.RoleInRepo = issues_model.RoleRepoCollaborator + return roleDesc, nil } hasMergedPR, err := issues_model.HasMergedPullRequestInRepo(ctx, repo.ID, poster.ID) if err != nil { - return roleDescriptor, err + return roleDesc, err } else if hasMergedPR { - roleDescriptor.RoleInRepo = issues_model.RoleRepoContributor + roleDesc.RoleInRepo = issues_model.RoleRepoContributor } else if issue.IsPull { // only display first time contributor in the first opening pull request - roleDescriptor.RoleInRepo = issues_model.RoleRepoFirstTimeContributor + roleDesc.RoleInRepo = issues_model.RoleRepoFirstTimeContributor } - return roleDescriptor, nil + return roleDesc, nil } func getBranchData(ctx *context.Context, issue *issues_model.Issue) { @@ -577,6 +584,8 @@ func prepareIssueViewCommentsAndSidebarParticipants(ctx *context.Context, issue return } + permCache := make(map[int64]access_model.Permission) + for _, comment = range issue.Comments { comment.Issue = issue @@ -594,7 +603,7 @@ func prepareIssueViewCommentsAndSidebarParticipants(ctx *context.Context, issue continue } - comment.ShowRole, err = roleDescriptor(ctx, issue.Repo, comment.Poster, issue, comment.HasOriginalAuthor()) + comment.ShowRole, err = roleDescriptor(ctx, issue.Repo, comment.Poster, permCache, issue, comment.HasOriginalAuthor()) if err != nil { ctx.ServerError("roleDescriptor", err) return @@ -692,7 +701,7 @@ func prepareIssueViewCommentsAndSidebarParticipants(ctx *context.Context, issue continue } - c.ShowRole, err = roleDescriptor(ctx, issue.Repo, c.Poster, issue, c.HasOriginalAuthor()) + c.ShowRole, err = roleDescriptor(ctx, issue.Repo, c.Poster, permCache, issue, c.HasOriginalAuthor()) if err != nil { ctx.ServerError("roleDescriptor", err) return @@ -941,7 +950,7 @@ func prepareIssueViewContent(ctx *context.Context, issue *issues_model.Issue) { ctx.ServerError("RenderString", err) return } - if issue.ShowRole, err = roleDescriptor(ctx, issue.Repo, issue.Poster, issue, issue.HasOriginalAuthor()); err != nil { + if issue.ShowRole, err = roleDescriptor(ctx, issue.Repo, issue.Poster, nil, issue, issue.HasOriginalAuthor()); err != nil { ctx.ServerError("roleDescriptor", err) return } diff --git a/routers/web/repo/issue_watch.go b/routers/web/repo/issue_watch.go index a2a4be1758a..e7d55e5555e 100644 --- a/routers/web/repo/issue_watch.go +++ b/routers/web/repo/issue_watch.go @@ -46,7 +46,7 @@ func IssueWatch(ctx *context.Context) { return } - watch, err := strconv.ParseBool(ctx.Req.PostForm.Get("watch")) + watch, err := strconv.ParseBool(ctx.Req.PostFormValue("watch")) if err != nil { ctx.ServerError("watch is not bool", err) return diff --git a/routers/web/repo/middlewares.go b/routers/web/repo/middlewares.go index 420931c5fb7..7518e6feae9 100644 --- a/routers/web/repo/middlewares.go +++ b/routers/web/repo/middlewares.go @@ -4,12 +4,9 @@ package repo import ( - "fmt" "strconv" - system_model "code.gitea.io/gitea/models/system" user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/services/context" user_service "code.gitea.io/gitea/services/user" @@ -22,12 +19,9 @@ func SetEditorconfigIfExists(ctx *context.Context) { } ec, _, err := ctx.Repo.GetEditorconfig() - - if err != nil && !git.IsErrNotExist(err) { - description := fmt.Sprintf("Error while getting .editorconfig file: %v", err) - if err := system_model.CreateRepositoryNotice(description); err != nil { - ctx.ServerError("ErrCreatingReporitoryNotice", err) - } + if err != nil { + // it used to check `!git.IsErrNotExist(err)` and create a system notice, but it is quite annoying and useless + // because network errors also happen frequently, so we just ignore it return } diff --git a/routers/web/repo/packages.go b/routers/web/repo/packages.go index 5dcfd0454c8..65a340a7997 100644 --- a/routers/web/repo/packages.go +++ b/routers/web/repo/packages.go @@ -62,9 +62,7 @@ func Packages(ctx *context.Context) { ctx.Data["PackageType"] = packageType ctx.Data["AvailableTypes"] = packages.TypeList ctx.Data["HasPackages"] = hasPackages - if ctx.Repo != nil { - ctx.Data["CanWritePackages"] = ctx.IsUserRepoWriter([]unit.Type{unit.TypePackages}) || ctx.IsUserSiteAdmin() - } + ctx.Data["CanWritePackages"] = ctx.Repo.CanWrite(unit.TypePackages) || ctx.IsUserSiteAdmin() ctx.Data["PackageDescriptors"] = pds ctx.Data["Total"] = total ctx.Data["RepositoryAccessMap"] = map[int64]bool{ctx.Repo.Repository.ID: true} // There is only the current repository diff --git a/routers/web/repo/patch.go b/routers/web/repo/patch.go index 1807cf31a12..120b3469f68 100644 --- a/routers/web/repo/patch.go +++ b/routers/web/repo/patch.go @@ -37,7 +37,7 @@ func NewDiffPatch(ctx *context.Context) { ctx.Data["new_branch_name"] = GetUniquePatchBranchName(ctx) ctx.Data["last_commit"] = ctx.Repo.CommitID ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",") - ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL() + ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL() ctx.HTML(200, tplPatchFile) } @@ -52,7 +52,7 @@ func NewDiffPatchPost(ctx *context.Context) { branchName = form.NewBranchName } ctx.Data["PageIsPatch"] = true - ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL() + ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL() ctx.Data["FileContent"] = form.Content ctx.Data["commit_summary"] = form.CommitSummary ctx.Data["commit_message"] = form.CommitMessage @@ -66,7 +66,7 @@ func NewDiffPatchPost(ctx *context.Context) { return } - // Cannot commit to a an existing branch if user doesn't have rights + // Cannot commit to an existing branch if user doesn't have rights if branchName == ctx.Repo.BranchName && !canCommit { ctx.Data["Err_NewBranchName"] = true ctx.Data["commit_choice"] = frmCommitChoiceNewBranch @@ -86,12 +86,21 @@ func NewDiffPatchPost(ctx *context.Context) { message += "\n\n" + form.CommitMessage } + gitCommitter, valid := WebGitOperationGetCommitChosenEmailIdentity(ctx, form.CommitEmail) + if !valid { + ctx.Data["Err_CommitEmail"] = true + ctx.RenderWithErr(ctx.Tr("repo.editor.invalid_commit_email"), tplPatchFile, &form) + return + } + fileResponse, err := files.ApplyDiffPatch(ctx, ctx.Repo.Repository, ctx.Doer, &files.ApplyDiffPatchOptions{ LastCommitID: form.LastCommit, OldBranch: ctx.Repo.BranchName, NewBranch: branchName, Message: message, Content: strings.ReplaceAll(form.Content, "\r", ""), + Author: gitCommitter, + Committer: gitCommitter, }) if err != nil { if git_model.IsErrBranchAlreadyExists(err) { diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go index 1800f60eaea..346132102f6 100644 --- a/routers/web/repo/projects.go +++ b/routers/web/repo/projects.go @@ -181,7 +181,7 @@ func ChangeProjectStatus(ctx *context.Context) { ctx.NotFoundOrServerError("ChangeProjectStatusByRepoIDAndID", project_model.IsErrProjectNotExist, err) return } - ctx.JSONRedirect(fmt.Sprintf("%s/projects/%d", ctx.Repo.RepoLink, id)) + ctx.JSONRedirect(project_model.ProjectLinkForRepo(ctx.Repo.Repository, id)) } // DeleteProject delete a project @@ -235,7 +235,7 @@ func RenderEditProject(ctx *context.Context) { ctx.Data["content"] = p.Description ctx.Data["card_type"] = p.CardType ctx.Data["redirect"] = ctx.FormString("redirect") - ctx.Data["CancelLink"] = fmt.Sprintf("%s/projects/%d", ctx.Repo.Repository.Link(), p.ID) + ctx.Data["CancelLink"] = project_model.ProjectLinkForRepo(ctx.Repo.Repository, p.ID) ctx.HTML(http.StatusOK, tplProjectsNew) } @@ -249,7 +249,7 @@ func EditProjectPost(ctx *context.Context) { ctx.Data["PageIsEditProjects"] = true ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects) ctx.Data["CardTypes"] = project_model.GetCardConfig() - ctx.Data["CancelLink"] = fmt.Sprintf("%s/projects/%d", ctx.Repo.Repository.Link(), projectID) + ctx.Data["CancelLink"] = project_model.ProjectLinkForRepo(ctx.Repo.Repository, projectID) if ctx.HasError() { ctx.HTML(http.StatusOK, tplProjectsNew) diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go index 9f3d1c1b7c0..e6fb492d6e3 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -1097,7 +1097,7 @@ func MergePullRequest(ctx *context.Context) { // delete all scheduled auto merges _ = pull_model.DeleteScheduledAutoMerge(ctx, pr.ID) // schedule auto merge - scheduled, err := automerge.ScheduleAutoMerge(ctx, ctx.Doer, pr, repo_model.MergeStyle(form.Do), message) + scheduled, err := automerge.ScheduleAutoMerge(ctx, ctx.Doer, pr, repo_model.MergeStyle(form.Do), message, form.DeleteBranchAfterMerge) if err != nil { ctx.ServerError("ScheduleAutoMerge", err) return @@ -1504,12 +1504,7 @@ func CleanUpPullRequest(ctx *context.Context) { func deleteBranch(ctx *context.Context, pr *issues_model.PullRequest, gitRepo *git.Repository) { fullBranchName := pr.HeadRepo.FullName() + ":" + pr.HeadBranch - if err := pull_service.RetargetChildrenOnMerge(ctx, ctx.Doer, pr); err != nil { - ctx.Flash.Error(ctx.Tr("repo.branch.deletion_failed", fullBranchName)) - return - } - - if err := repo_service.DeleteBranch(ctx, ctx.Doer, pr.HeadRepo, gitRepo, pr.HeadBranch); err != nil { + if err := repo_service.DeleteBranch(ctx, ctx.Doer, pr.HeadRepo, gitRepo, pr.HeadBranch, pr); err != nil { switch { case git.IsErrBranchNotExist(err): ctx.Flash.Error(ctx.Tr("repo.branch.deletion_failed", fullBranchName)) @@ -1524,11 +1519,6 @@ func deleteBranch(ctx *context.Context, pr *issues_model.PullRequest, gitRepo *g return } - if err := issues_model.AddDeletePRBranchComment(ctx, ctx.Doer, pr.BaseRepo, pr.IssueID, pr.HeadBranch); err != nil { - // Do not fail here as branch has already been deleted - log.Error("DeleteBranch: %v", err) - } - ctx.Flash.Success(ctx.Tr("repo.branch.deletion_success", fullBranchName)) } diff --git a/routers/web/repo/pull_review_test.go b/routers/web/repo/pull_review_test.go index 8344ff40911..3d0997ab4d8 100644 --- a/routers/web/repo/pull_review_test.go +++ b/routers/web/repo/pull_review_test.go @@ -17,6 +17,7 @@ import ( "code.gitea.io/gitea/services/pull" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestRenderConversation(t *testing.T) { @@ -41,19 +42,16 @@ func TestRenderConversation(t *testing.T) { var preparedComment *issues_model.Comment run("prepare", func(t *testing.T, ctx *context.Context, resp *httptest.ResponseRecorder) { comment, err := pull.CreateCodeComment(ctx, pr.Issue.Poster, ctx.Repo.GitRepo, pr.Issue, 1, "content", "", false, 0, pr.HeadCommitID, nil) - if !assert.NoError(t, err) { - return - } + require.NoError(t, err) + comment.Invalidated = true err = issues_model.UpdateCommentInvalidate(ctx, comment) - if !assert.NoError(t, err) { - return - } + require.NoError(t, err) + preparedComment = comment }) - if !assert.NotNil(t, preparedComment) { - return - } + require.NotNil(t, preparedComment) + run("diff with outdated", func(t *testing.T, ctx *context.Context, resp *httptest.ResponseRecorder) { ctx.Data["ShowOutdatedComments"] = true renderConversation(ctx, preparedComment, "diff") diff --git a/routers/web/repo/recent_commits.go b/routers/web/repo/recent_commits.go index dc720819009..228eb0dbac2 100644 --- a/routers/web/repo/recent_commits.go +++ b/routers/web/repo/recent_commits.go @@ -29,7 +29,7 @@ func RecentCommits(ctx *context.Context) { // RecentCommitsData returns JSON of recent commits data func RecentCommitsData(ctx *context.Context) { - if contributorStats, err := contributors_service.GetContributorStats(ctx, ctx.Cache, ctx.Repo.Repository, ctx.Repo.CommitID); err != nil { + if contributorStats, err := contributors_service.GetContributorStats(ctx, ctx.Cache, ctx.Repo.Repository, ctx.Repo.Repository.DefaultBranch); err != nil { if errors.Is(err, contributors_service.ErrAwaitGeneration) { ctx.Status(http.StatusAccepted) return diff --git a/routers/web/repo/release.go b/routers/web/repo/release.go index 5b099fe8d4f..284fd27abf7 100644 --- a/routers/web/repo/release.go +++ b/routers/web/repo/release.go @@ -17,7 +17,6 @@ import ( "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" - "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/markup/markdown" "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" @@ -149,8 +148,6 @@ func getReleaseInfos(ctx *context.Context, opts *repo_model.FindReleasesOptions) func Releases(ctx *context.Context) { ctx.Data["PageIsReleaseList"] = true ctx.Data["Title"] = ctx.Tr("repo.release.releases") - ctx.Data["IsViewBranch"] = false - ctx.Data["IsViewTag"] = true listOptions := db.ListOptions{ Page: ctx.FormInt("page"), @@ -195,8 +192,6 @@ func Releases(ctx *context.Context) { func TagsList(ctx *context.Context) { ctx.Data["PageIsTagList"] = true ctx.Data["Title"] = ctx.Tr("repo.release.tags") - ctx.Data["IsViewBranch"] = false - ctx.Data["IsViewTag"] = true ctx.Data["CanCreateRelease"] = ctx.Repo.CanWrite(unit.TypeReleases) && !ctx.Repo.Repository.IsArchived namePattern := ctx.FormTrim("q") @@ -300,6 +295,7 @@ func SingleRelease(ctx *context.Context) { } ctx.Data["PageIsSingleTag"] = release.IsTag + ctx.Data["SingleReleaseTagName"] = release.TagName if release.IsTag { ctx.Data["Title"] = release.TagName } else { @@ -330,34 +326,17 @@ func LatestRelease(ctx *context.Context) { ctx.Redirect(release.Link()) } -// NewRelease render creating or edit release page -func NewRelease(ctx *context.Context) { +func newReleaseCommon(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("repo.release.new_release") ctx.Data["PageIsReleaseList"] = true - ctx.Data["tag_target"] = ctx.Repo.Repository.DefaultBranch - if tagName := ctx.FormString("tag"); len(tagName) > 0 { - rel, err := repo_model.GetRelease(ctx, ctx.Repo.Repository.ID, tagName) - if err != nil && !repo_model.IsErrReleaseNotExist(err) { - ctx.ServerError("GetRelease", err) - return - } - if rel != nil { - rel.Repo = ctx.Repo.Repository - if err := rel.LoadAttributes(ctx); err != nil { - ctx.ServerError("LoadAttributes", err) - return - } - - ctx.Data["tag_name"] = rel.TagName - if rel.Target != "" { - ctx.Data["tag_target"] = rel.Target - } - ctx.Data["title"] = rel.Title - ctx.Data["content"] = rel.Note - ctx.Data["attachments"] = rel.Attachments - } + tags, err := repo_model.GetTagNamesByRepoID(ctx, ctx.Repo.Repository.ID) + if err != nil { + ctx.ServerError("GetTagNamesByRepoID", err) + return } + ctx.Data["Tags"] = tags + ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled assigneeUsers, err := repo_model.GetRepoAssignees(ctx, ctx.Repo.Repository) if err != nil { @@ -368,35 +347,74 @@ func NewRelease(ctx *context.Context) { upload.AddUploadContext(ctx, "release") - // For New Release page - PrepareBranchList(ctx) + PrepareBranchList(ctx) // for New Release page +} + +// NewRelease render creating or edit release page +func NewRelease(ctx *context.Context) { + newReleaseCommon(ctx) if ctx.Written() { return } - tags, err := repo_model.GetTagNamesByRepoID(ctx, ctx.Repo.Repository.ID) - if err != nil { - ctx.ServerError("GetTagNamesByRepoID", err) - return + ctx.Data["ShowCreateTagOnlyButton"] = true + + // pre-fill the form with the tag name, target branch and the existing release (if exists) + ctx.Data["tag_target"] = ctx.Repo.Repository.DefaultBranch + if tagName := ctx.FormString("tag"); tagName != "" { + rel, err := repo_model.GetRelease(ctx, ctx.Repo.Repository.ID, tagName) + if err != nil && !repo_model.IsErrReleaseNotExist(err) { + ctx.ServerError("GetRelease", err) + return + } + + if rel != nil { + rel.Repo = ctx.Repo.Repository + if err = rel.LoadAttributes(ctx); err != nil { + ctx.ServerError("LoadAttributes", err) + return + } + + ctx.Data["ShowCreateTagOnlyButton"] = false + ctx.Data["tag_name"] = rel.TagName + ctx.Data["tag_target"] = rel.Target + ctx.Data["title"] = rel.Title + ctx.Data["content"] = rel.Note + ctx.Data["attachments"] = rel.Attachments + } } - ctx.Data["Tags"] = tags ctx.HTML(http.StatusOK, tplReleaseNew) } // NewReleasePost response for creating a release func NewReleasePost(ctx *context.Context) { - form := web.GetForm(ctx).(*forms.NewReleaseForm) - ctx.Data["Title"] = ctx.Tr("repo.release.new_release") - ctx.Data["PageIsReleaseList"] = true - - tags, err := repo_model.GetTagNamesByRepoID(ctx, ctx.Repo.Repository.ID) - if err != nil { - ctx.ServerError("GetTagNamesByRepoID", err) + newReleaseCommon(ctx) + if ctx.Written() { return } - ctx.Data["Tags"] = tags + form := web.GetForm(ctx).(*forms.NewReleaseForm) + + // first, check whether the release exists, and prepare "ShowCreateTagOnlyButton" + // the logic should be done before the form error check to make the tmpl has correct variables + rel, err := repo_model.GetRelease(ctx, ctx.Repo.Repository.ID, form.TagName) + if err != nil && !repo_model.IsErrReleaseNotExist(err) { + ctx.ServerError("GetRelease", err) + return + } + + // We should still show the "tag only" button if the user clicks it, no matter the release exists or not. + // Because if error occurs, end users need to have the chance to edit the name and submit the form with "tag-only" again. + // It is still not completely right, because there could still be cases like this: + // * user visit "new release" page, see the "tag only" button + // * input something, click other buttons but not "tag only" + // * error occurs, the "new release" page is rendered again, but the "tag only" button is gone + // Such cases are not able to be handled by current code, it needs frontend code to toggle the "tag-only" button if the input changes. + // Or another choice is "always show the tag-only button" if error occurs. + ctx.Data["ShowCreateTagOnlyButton"] = form.TagOnly || rel == nil + + // do some form checks if ctx.HasError() { ctx.HTML(http.StatusOK, tplReleaseNew) return @@ -407,59 +425,49 @@ func NewReleasePost(ctx *context.Context) { return } - // Title of release cannot be empty - if len(form.TagOnly) == 0 && len(form.Title) == 0 { + if !form.TagOnly && form.Title == "" { + // if not "tag only", then the title of the release cannot be empty ctx.RenderWithErr(ctx.Tr("repo.release.title_empty"), tplReleaseNew, &form) return } - var attachmentUUIDs []string - if setting.Attachment.Enabled { - attachmentUUIDs = form.Files + handleTagReleaseError := func(err error) { + ctx.Data["Err_TagName"] = true + switch { + case release_service.IsErrTagAlreadyExists(err): + ctx.RenderWithErr(ctx.Tr("repo.branch.tag_collision", form.TagName), tplReleaseNew, &form) + case repo_model.IsErrReleaseAlreadyExist(err): + ctx.RenderWithErr(ctx.Tr("repo.release.tag_name_already_exist"), tplReleaseNew, &form) + case release_service.IsErrInvalidTagName(err): + ctx.RenderWithErr(ctx.Tr("repo.release.tag_name_invalid"), tplReleaseNew, &form) + case release_service.IsErrProtectedTagName(err): + ctx.RenderWithErr(ctx.Tr("repo.release.tag_name_protected"), tplReleaseNew, &form) + default: + ctx.ServerError("handleTagReleaseError", err) + } } - rel, err := repo_model.GetRelease(ctx, ctx.Repo.Repository.ID, form.TagName) - if err != nil { - if !repo_model.IsErrReleaseNotExist(err) { - ctx.ServerError("GetRelease", err) + // prepare the git message for creating a new tag + newTagMsg := "" + if form.Title != "" && form.AddTagMsg { + newTagMsg = form.Title + "\n\n" + form.Content + } + + // no release, and tag only + if rel == nil && form.TagOnly { + if err = release_service.CreateNewTag(ctx, ctx.Doer, ctx.Repo.Repository, form.Target, form.TagName, newTagMsg); err != nil { + handleTagReleaseError(err) return } + ctx.Flash.Success(ctx.Tr("repo.tag.create_success", form.TagName)) + ctx.Redirect(ctx.Repo.RepoLink + "/src/tag/" + util.PathEscapeSegments(form.TagName)) + return + } - msg := "" - if len(form.Title) > 0 && form.AddTagMsg { - msg = form.Title + "\n\n" + form.Content - } - - if len(form.TagOnly) > 0 { - if err = release_service.CreateNewTag(ctx, ctx.Doer, ctx.Repo.Repository, form.Target, form.TagName, msg); err != nil { - if release_service.IsErrTagAlreadyExists(err) { - e := err.(release_service.ErrTagAlreadyExists) - ctx.Flash.Error(ctx.Tr("repo.branch.tag_collision", e.TagName)) - ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()) - return - } - - if release_service.IsErrInvalidTagName(err) { - ctx.Flash.Error(ctx.Tr("repo.release.tag_name_invalid")) - ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()) - return - } - - if release_service.IsErrProtectedTagName(err) { - ctx.Flash.Error(ctx.Tr("repo.release.tag_name_protected")) - ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()) - return - } - - ctx.ServerError("release_service.CreateNewTag", err) - return - } - - ctx.Flash.Success(ctx.Tr("repo.tag.create_success", form.TagName)) - ctx.Redirect(ctx.Repo.RepoLink + "/src/tag/" + util.PathEscapeSegments(form.TagName)) - return - } + attachmentUUIDs := util.Iif(setting.Attachment.Enabled, form.Files, nil) + // no existing release, create a new release + if rel == nil { rel = &repo_model.Release{ RepoID: ctx.Repo.Repository.ID, Repo: ctx.Repo.Repository, @@ -469,48 +477,39 @@ func NewReleasePost(ctx *context.Context) { TagName: form.TagName, Target: form.Target, Note: form.Content, - IsDraft: len(form.Draft) > 0, + IsDraft: form.Draft, IsPrerelease: form.Prerelease, IsTag: false, } - - if err = release_service.CreateRelease(ctx.Repo.GitRepo, rel, attachmentUUIDs, msg); err != nil { - ctx.Data["Err_TagName"] = true - switch { - case repo_model.IsErrReleaseAlreadyExist(err): - ctx.RenderWithErr(ctx.Tr("repo.release.tag_name_already_exist"), tplReleaseNew, &form) - case release_service.IsErrInvalidTagName(err): - ctx.RenderWithErr(ctx.Tr("repo.release.tag_name_invalid"), tplReleaseNew, &form) - case release_service.IsErrProtectedTagName(err): - ctx.RenderWithErr(ctx.Tr("repo.release.tag_name_protected"), tplReleaseNew, &form) - default: - ctx.ServerError("CreateRelease", err) - } - return - } - } else { - if !rel.IsTag { - ctx.Data["Err_TagName"] = true - ctx.RenderWithErr(ctx.Tr("repo.release.tag_name_already_exist"), tplReleaseNew, &form) - return - } - - rel.Title = form.Title - rel.Note = form.Content - rel.Target = form.Target - rel.IsDraft = len(form.Draft) > 0 - rel.IsPrerelease = form.Prerelease - rel.PublisherID = ctx.Doer.ID - rel.IsTag = false - - if err = release_service.UpdateRelease(ctx, ctx.Doer, ctx.Repo.GitRepo, rel, attachmentUUIDs, nil, nil); err != nil { - ctx.Data["Err_TagName"] = true - ctx.ServerError("UpdateRelease", err) + if err = release_service.CreateRelease(ctx.Repo.GitRepo, rel, attachmentUUIDs, newTagMsg); err != nil { + handleTagReleaseError(err) return } + ctx.Redirect(ctx.Repo.RepoLink + "/releases") + return } - log.Trace("Release created: %s/%s:%s", ctx.Doer.LowerName, ctx.Repo.Repository.Name, form.TagName) + // tag exists, try to convert it to a real release + // old logic: if the release is not a tag (it is a real release), do not update it on the "new release" page + // add new logic: if tag-only, do not convert the tag to a release + if form.TagOnly || !rel.IsTag { + ctx.Data["Err_TagName"] = true + ctx.RenderWithErr(ctx.Tr("repo.release.tag_name_already_exist"), tplReleaseNew, &form) + return + } + + // convert a tag to a real release (set is_tag=false) + rel.Title = form.Title + rel.Note = form.Content + rel.Target = form.Target + rel.IsDraft = form.Draft + rel.IsPrerelease = form.Prerelease + rel.PublisherID = ctx.Doer.ID + rel.IsTag = false + if err = release_service.UpdateRelease(ctx, ctx.Doer, ctx.Repo.GitRepo, rel, attachmentUUIDs, nil, nil); err != nil { + handleTagReleaseError(err) + return + } ctx.Redirect(ctx.Repo.RepoLink + "/releases") } diff --git a/routers/web/repo/release_test.go b/routers/web/repo/release_test.go index 7ebea4c3fbe..9f49fc75007 100644 --- a/routers/web/repo/release_test.go +++ b/routers/web/repo/release_test.go @@ -11,60 +11,135 @@ import ( "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/contexttest" "code.gitea.io/gitea/services/forms" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestNewReleasePost(t *testing.T) { - for _, testCase := range []struct { - RepoID int64 - UserID int64 - TagName string - Form forms.NewReleaseForm - }{ - { - RepoID: 1, - UserID: 2, - TagName: "v1.1", // pre-existing tag - Form: forms.NewReleaseForm{ - TagName: "newtag", - Target: "master", - Title: "title", - Content: "content", - }, - }, - { - RepoID: 1, - UserID: 2, - TagName: "newtag", - Form: forms.NewReleaseForm{ - TagName: "newtag", - Target: "master", - Title: "title", - Content: "content", - }, - }, - } { - unittest.PrepareTestEnv(t) + unittest.PrepareTestEnv(t) + get := func(t *testing.T, tagName string) *context.Context { + ctx, _ := contexttest.MockContext(t, "user2/repo1/releases/new?tag="+tagName) + contexttest.LoadUser(t, ctx, 2) + contexttest.LoadRepo(t, ctx, 1) + contexttest.LoadGitRepo(t, ctx) + defer ctx.Repo.GitRepo.Close() + NewRelease(ctx) + return ctx + } + + t.Run("NewReleasePage", func(t *testing.T) { + ctx := get(t, "v1.1") + assert.Empty(t, ctx.Data["ShowCreateTagOnlyButton"]) + ctx = get(t, "new-tag-name") + assert.NotEmpty(t, ctx.Data["ShowCreateTagOnlyButton"]) + }) + + post := func(t *testing.T, form forms.NewReleaseForm) *context.Context { ctx, _ := contexttest.MockContext(t, "user2/repo1/releases/new") contexttest.LoadUser(t, ctx, 2) contexttest.LoadRepo(t, ctx, 1) contexttest.LoadGitRepo(t, ctx) - web.SetForm(ctx, &testCase.Form) + defer ctx.Repo.GitRepo.Close() + web.SetForm(ctx, &form) NewReleasePost(ctx) - unittest.AssertExistsAndLoadBean(t, &repo_model.Release{ - RepoID: 1, - PublisherID: 2, - TagName: testCase.Form.TagName, - Target: testCase.Form.Target, - Title: testCase.Form.Title, - Note: testCase.Form.Content, - }, unittest.Cond("is_draft=?", len(testCase.Form.Draft) > 0)) - ctx.Repo.GitRepo.Close() + return ctx } + + loadRelease := func(t *testing.T, tagName string) *repo_model.Release { + return unittest.GetBean(t, &repo_model.Release{}, unittest.Cond("repo_id=1 AND tag_name=?", tagName)) + } + + t.Run("NewTagRelease", func(t *testing.T) { + post(t, forms.NewReleaseForm{ + TagName: "newtag", + Target: "master", + Title: "title", + Content: "content", + }) + rel := loadRelease(t, "newtag") + require.NotNil(t, rel) + assert.False(t, rel.IsTag) + assert.Equal(t, "master", rel.Target) + assert.Equal(t, "title", rel.Title) + assert.Equal(t, "content", rel.Note) + }) + + t.Run("ReleaseExistsDoUpdate(non-tag)", func(t *testing.T) { + ctx := post(t, forms.NewReleaseForm{ + TagName: "v1.1", + Target: "master", + Title: "updated-title", + Content: "updated-content", + }) + rel := loadRelease(t, "v1.1") + require.NotNil(t, rel) + assert.False(t, rel.IsTag) + assert.Equal(t, "testing-release", rel.Title) + assert.NotEmpty(t, ctx.Flash.ErrorMsg) + }) + + t.Run("ReleaseExistsDoUpdate(tag-only)", func(t *testing.T) { + ctx := post(t, forms.NewReleaseForm{ + TagName: "delete-tag", // a strange name, but it is the only "is_tag=true" fixture + Target: "master", + Title: "updated-title", + Content: "updated-content", + TagOnly: true, + }) + rel := loadRelease(t, "delete-tag") + require.NotNil(t, rel) + assert.True(t, rel.IsTag) // the record should not be updated because the request is "tag-only". TODO: need to improve the logic? + assert.Equal(t, "delete-tag", rel.Title) + assert.NotEmpty(t, ctx.Flash.ErrorMsg) + assert.NotEmpty(t, ctx.Data["ShowCreateTagOnlyButton"]) // still show the "tag-only" button + }) + + t.Run("ReleaseExistsDoUpdate(tag-release)", func(t *testing.T) { + ctx := post(t, forms.NewReleaseForm{ + TagName: "delete-tag", // a strange name, but it is the only "is_tag=true" fixture + Target: "master", + Title: "updated-title", + Content: "updated-content", + }) + rel := loadRelease(t, "delete-tag") + require.NotNil(t, rel) + assert.False(t, rel.IsTag) // the tag has been "updated" to be a real "release" + assert.Equal(t, "updated-title", rel.Title) + assert.Empty(t, ctx.Flash.ErrorMsg) + }) + + t.Run("TagOnly", func(t *testing.T) { + ctx := post(t, forms.NewReleaseForm{ + TagName: "new-tag-only", + Target: "master", + Title: "title", + Content: "content", + TagOnly: true, + }) + rel := loadRelease(t, "new-tag-only") + require.NotNil(t, rel) + assert.True(t, rel.IsTag) + assert.Empty(t, ctx.Flash.ErrorMsg) + }) + + t.Run("TagOnlyConflict", func(t *testing.T) { + ctx := post(t, forms.NewReleaseForm{ + TagName: "v1.1", + Target: "master", + Title: "title", + Content: "content", + TagOnly: true, + }) + rel := loadRelease(t, "v1.1") + require.NotNil(t, rel) + assert.False(t, rel.IsTag) + assert.NotEmpty(t, ctx.Flash.ErrorMsg) + }) } func TestCalReleaseNumCommitsBehind(t *testing.T) { diff --git a/routers/web/repo/render.go b/routers/web/repo/render.go index 856425ae355..efd271c01d6 100644 --- a/routers/web/repo/render.go +++ b/routers/web/repo/render.go @@ -21,7 +21,13 @@ import ( // RenderFile renders a file by repos path func RenderFile(ctx *context.Context) { - blob, err := ctx.Repo.Commit.GetBlobByPath(ctx.Repo.TreePath) + var blob *git.Blob + var err error + if ctx.Repo.TreePath != "" { + blob, err = ctx.Repo.Commit.GetBlobByPath(ctx.Repo.TreePath) + } else { + blob, err = ctx.Repo.GitRepo.GetBlob(ctx.PathParam("sha")) + } if err != nil { if git.IsErrNotExist(err) { ctx.NotFound("GetBlobByPath", err) @@ -58,7 +64,7 @@ func RenderFile(ctx *context.Context) { } rctx := renderhelper.NewRenderContextRepoFile(ctx, ctx.Repo.Repository, renderhelper.RepoFileOptions{ - CurrentRefPath: ctx.Repo.BranchNameSubURL(), + CurrentRefPath: ctx.Repo.RefTypeNameSubURL(), CurrentTreePath: path.Dir(ctx.Repo.TreePath), }).WithRelativePath(ctx.Repo.TreePath).WithInStandalonePage(true) diff --git a/routers/web/repo/repo.go b/routers/web/repo/repo.go index 85a745acbd3..0d4513ec67b 100644 --- a/routers/web/repo/repo.go +++ b/routers/web/repo/repo.go @@ -51,7 +51,7 @@ func MustBeNotEmpty(ctx *context.Context) { // MustBeEditable check that repo can be edited func MustBeEditable(ctx *context.Context) { - if !ctx.Repo.Repository.CanEnableEditor() || ctx.Repo.IsViewCommit { + if !ctx.Repo.Repository.CanEnableEditor() { ctx.NotFound("", nil) return } @@ -147,27 +147,33 @@ func getRepoPrivate(ctx *context.Context) bool { } } -// Create render creating repository page -func Create(ctx *context.Context) { +func createCommon(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("new_repo") - - // Give default value for template to render. ctx.Data["Gitignores"] = repo_module.Gitignores ctx.Data["LabelTemplateFiles"] = repo_module.LabelTemplateFiles ctx.Data["Licenses"] = repo_module.Licenses ctx.Data["Readmes"] = repo_module.Readmes - ctx.Data["readme"] = "Default" - ctx.Data["private"] = getRepoPrivate(ctx) ctx.Data["IsForcedPrivate"] = setting.Repository.ForcePrivate - ctx.Data["default_branch"] = setting.Repository.DefaultBranch + ctx.Data["CanCreateRepo"] = ctx.Doer.CanCreateRepo() + ctx.Data["MaxCreationLimit"] = ctx.Doer.MaxCreationLimit() + ctx.Data["SupportedObjectFormats"] = git.DefaultFeatures().SupportedObjectFormats + ctx.Data["DefaultObjectFormat"] = git.Sha1ObjectFormat +} +// Create render creating repository page +func Create(ctx *context.Context) { + createCommon(ctx) ctxUser := checkContextUser(ctx, ctx.FormInt64("org")) if ctx.Written() { return } ctx.Data["ContextUser"] = ctxUser + ctx.Data["readme"] = "Default" + ctx.Data["private"] = getRepoPrivate(ctx) + ctx.Data["default_branch"] = setting.Repository.DefaultBranch ctx.Data["repo_template_name"] = ctx.Tr("repo.template_select") + templateID := ctx.FormInt64("template_id") if templateID > 0 { templateRepo, err := repo_model.GetRepositoryByID(ctx, templateID) @@ -177,11 +183,6 @@ func Create(ctx *context.Context) { } } - ctx.Data["CanCreateRepo"] = ctx.Doer.CanCreateRepo() - ctx.Data["MaxCreationLimit"] = ctx.Doer.MaxCreationLimit() - ctx.Data["SupportedObjectFormats"] = git.DefaultFeatures().SupportedObjectFormats - ctx.Data["DefaultObjectFormat"] = git.Sha1ObjectFormat - ctx.HTML(http.StatusOK, tplCreate) } @@ -219,16 +220,8 @@ func handleCreateError(ctx *context.Context, owner *user_model.User, err error, // CreatePost response for creating repository func CreatePost(ctx *context.Context) { + createCommon(ctx) form := web.GetForm(ctx).(*forms.CreateRepoForm) - ctx.Data["Title"] = ctx.Tr("new_repo") - - ctx.Data["Gitignores"] = repo_module.Gitignores - ctx.Data["LabelTemplateFiles"] = repo_module.LabelTemplateFiles - ctx.Data["Licenses"] = repo_module.Licenses - ctx.Data["Readmes"] = repo_module.Readmes - - ctx.Data["CanCreateRepo"] = ctx.Doer.CanCreateRepo() - ctx.Data["MaxCreationLimit"] = ctx.Doer.MaxCreationLimit() ctxUser := checkContextUser(ctx, form.UID) if ctx.Written() { @@ -236,6 +229,14 @@ func CreatePost(ctx *context.Context) { } ctx.Data["ContextUser"] = ctxUser + if form.RepoTemplate > 0 { + templateRepo, err := repo_model.GetRepositoryByID(ctx, form.RepoTemplate) + if err == nil && access_model.CheckRepoUnitUser(ctx, templateRepo, ctxUser, unit.TypeCode) { + ctx.Data["repo_template"] = form.RepoTemplate + ctx.Data["repo_template_name"] = templateRepo.Name + } + } + if ctx.HasError() { ctx.HTML(http.StatusOK, tplCreate) return @@ -303,112 +304,14 @@ func CreatePost(ctx *context.Context) { handleCreateError(ctx, ctxUser, err, "CreatePost", tplCreate, &form) } -const ( - tplWatchUnwatch templates.TplName = "repo/watch_unwatch" - tplStarUnstar templates.TplName = "repo/star_unstar" -) - -// Action response for actions to a repository -func Action(ctx *context.Context) { - var err error - switch ctx.PathParam("action") { - case "watch": - err = repo_model.WatchRepo(ctx, ctx.Doer, ctx.Repo.Repository, true) - case "unwatch": - err = repo_model.WatchRepo(ctx, ctx.Doer, ctx.Repo.Repository, false) - case "star": - err = repo_model.StarRepo(ctx, ctx.Doer, ctx.Repo.Repository, true) - case "unstar": - err = repo_model.StarRepo(ctx, ctx.Doer, ctx.Repo.Repository, false) - case "accept_transfer": - err = acceptOrRejectRepoTransfer(ctx, true) - case "reject_transfer": - err = acceptOrRejectRepoTransfer(ctx, false) - case "desc": // FIXME: this is not used - if !ctx.Repo.IsOwner() { - ctx.Error(http.StatusNotFound) - return - } - - ctx.Repo.Repository.Description = ctx.FormString("desc") - ctx.Repo.Repository.Website = ctx.FormString("site") - err = repo_service.UpdateRepository(ctx, ctx.Repo.Repository, false) - } - - if err != nil { - if errors.Is(err, user_model.ErrBlockedUser) { - ctx.Flash.Error(ctx.Tr("repo.action.blocked_user")) - } else { - ctx.ServerError(fmt.Sprintf("Action (%s)", ctx.PathParam("action")), err) - return - } - } - - switch ctx.PathParam("action") { - case "watch", "unwatch": - ctx.Data["IsWatchingRepo"] = repo_model.IsWatching(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID) - case "star", "unstar": - ctx.Data["IsStaringRepo"] = repo_model.IsStaring(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID) - } - - // see the `hx-trigger="refreshUserCards ..."` comments in tmpl - ctx.RespHeader().Add("hx-trigger", "refreshUserCards") - - switch ctx.PathParam("action") { - case "watch", "unwatch", "star", "unstar": - // we have to reload the repository because NumStars or NumWatching (used in the templates) has just changed - ctx.Data["Repository"], err = repo_model.GetRepositoryByName(ctx, ctx.Repo.Repository.OwnerID, ctx.Repo.Repository.Name) - if err != nil { - ctx.ServerError(fmt.Sprintf("Action (%s)", ctx.PathParam("action")), err) - return - } - } - - switch ctx.PathParam("action") { - case "watch", "unwatch": - ctx.HTML(http.StatusOK, tplWatchUnwatch) - return - case "star", "unstar": - ctx.HTML(http.StatusOK, tplStarUnstar) - return - } - - ctx.RedirectToCurrentSite(ctx.FormString("redirect_to"), ctx.Repo.RepoLink) -} - -func acceptOrRejectRepoTransfer(ctx *context.Context, accept bool) error { - repoTransfer, err := repo_model.GetPendingRepositoryTransfer(ctx, ctx.Repo.Repository) - if err != nil { - return err - } - - if err := repoTransfer.LoadAttributes(ctx); err != nil { - return err - } - - if !repoTransfer.CanUserAcceptTransfer(ctx, ctx.Doer) { - return errors.New("user does not have enough permissions") - } - - if accept { - if ctx.Repo.GitRepo != nil { - ctx.Repo.GitRepo.Close() - ctx.Repo.GitRepo = nil - } - - if err := repo_service.TransferOwnership(ctx, repoTransfer.Doer, repoTransfer.Recipient, ctx.Repo.Repository, repoTransfer.Teams); err != nil { - return err - } - ctx.Flash.Success(ctx.Tr("repo.settings.transfer.success")) +func handleActionError(ctx *context.Context, err error) { + if errors.Is(err, user_model.ErrBlockedUser) { + ctx.Flash.Error(ctx.Tr("repo.action.blocked_user")) + } else if errors.Is(err, util.ErrPermissionDenied) { + ctx.Error(http.StatusNotFound) } else { - if err := repo_service.CancelRepositoryTransfer(ctx, ctx.Repo.Repository); err != nil { - return err - } - ctx.Flash.Success(ctx.Tr("repo.settings.transfer.rejected")) + ctx.ServerError(fmt.Sprintf("Action (%s)", ctx.PathParam("action")), err) } - - ctx.Redirect(ctx.Repo.Repository.Link()) - return nil } // RedirectDownload return a file based on the following infos: @@ -462,13 +365,7 @@ func RedirectDownload(ctx *context.Context) { // Download an archive of a repository func Download(ctx *context.Context) { - uri := ctx.PathParam("*") - ext, tp, err := archiver_service.ParseFileName(uri) - if err != nil { - ctx.ServerError("ParseFileName", err) - return - } - aReq, err := archiver_service.NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, strings.TrimSuffix(uri, ext), tp) + aReq, err := archiver_service.NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, ctx.PathParam("*")) if err != nil { if errors.Is(err, archiver_service.ErrUnknownArchiveFormat{}) { ctx.Error(http.StatusBadRequest, err.Error()) @@ -526,15 +423,9 @@ func download(ctx *context.Context, archiveName string, archiver *repo_model.Rep // a request that's already in-progress, but the archiver service will just // kind of drop it on the floor if this is the case. func InitiateDownload(ctx *context.Context) { - uri := ctx.PathParam("*") - ext, tp, err := archiver_service.ParseFileName(uri) + aReq, err := archiver_service.NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, ctx.PathParam("*")) if err != nil { - ctx.ServerError("ParseFileName", err) - return - } - aReq, err := archiver_service.NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, strings.TrimSuffix(uri, ext), tp) - if err != nil { - ctx.ServerError("archiver_service.NewRequest", err) + ctx.Error(http.StatusBadRequest, "invalid archive request") return } if aReq == nil { diff --git a/routers/web/repo/search.go b/routers/web/repo/search.go index cbc7e2e0fe8..bbbe5c1081c 100644 --- a/routers/web/repo/search.go +++ b/routers/web/repo/search.go @@ -12,6 +12,7 @@ import ( code_indexer "code.gitea.io/gitea/modules/indexer/code" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/templates" + "code.gitea.io/gitea/routers/common" "code.gitea.io/gitea/services/context" ) @@ -29,18 +30,9 @@ func indexSettingToGitGrepPathspecList() (list []string) { // Search render repository search page func Search(ctx *context.Context) { - language := ctx.FormTrim("l") - keyword := ctx.FormTrim("q") - - isFuzzy := ctx.FormOptionalBool("fuzzy").ValueOrDefault(true) - - ctx.Data["Keyword"] = keyword - ctx.Data["Language"] = language - ctx.Data["IsFuzzy"] = isFuzzy ctx.Data["PageIsViewCode"] = true - ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled - - if keyword == "" { + prepareSearch := common.PrepareCodeSearch(ctx) + if prepareSearch.Keyword == "" { ctx.HTML(http.StatusOK, tplSearch) return } @@ -57,9 +49,9 @@ func Search(ctx *context.Context) { var err error total, searchResults, searchResultLanguages, err = code_indexer.PerformSearch(ctx, &code_indexer.SearchOptions{ RepoIDs: []int64{ctx.Repo.Repository.ID}, - Keyword: keyword, - IsKeywordFuzzy: isFuzzy, - Language: language, + Keyword: prepareSearch.Keyword, + IsKeywordFuzzy: prepareSearch.IsFuzzy, + Language: prepareSearch.Language, Paginator: &db.ListOptions{ Page: page, PageSize: setting.UI.RepoSearchPagingNum, @@ -75,10 +67,11 @@ func Search(ctx *context.Context) { ctx.Data["CodeIndexerUnavailable"] = !code_indexer.IsAvailable(ctx) } } else { - res, err := git.GrepSearch(ctx, ctx.Repo.GitRepo, keyword, git.GrepOptions{ + searchRefName := git.RefNameFromBranch(ctx.Repo.Repository.DefaultBranch) // BranchName should be default branch or the first existing branch + res, err := git.GrepSearch(ctx, ctx.Repo.GitRepo, prepareSearch.Keyword, git.GrepOptions{ ContextLineNumber: 1, - IsFuzzy: isFuzzy, - RefName: git.RefNameFromBranch(ctx.Repo.BranchName).String(), // BranchName should be default branch or the first existing branch + IsFuzzy: prepareSearch.IsFuzzy, + RefName: searchRefName.String(), PathspecList: indexSettingToGitGrepPathspecList(), }) if err != nil { @@ -86,6 +79,11 @@ func Search(ctx *context.Context) { ctx.ServerError("GrepSearch", err) return } + commitID, err := ctx.Repo.GitRepo.GetRefCommitID(searchRefName.String()) + if err != nil { + ctx.ServerError("GetRefCommitID", err) + return + } total = len(res) pageStart := min((page-1)*setting.UI.RepoSearchPagingNum, len(res)) pageEnd := min(page*setting.UI.RepoSearchPagingNum, len(res)) @@ -94,7 +92,7 @@ func Search(ctx *context.Context) { searchResults = append(searchResults, &code_indexer.Result{ RepoID: ctx.Repo.Repository.ID, Filename: r.Filename, - CommitID: ctx.Repo.CommitID, + CommitID: commitID, // UpdatedUnix: not supported yet // Language: not supported yet // Color: not supported yet diff --git a/routers/web/repo/setting/protected_branch.go b/routers/web/repo/setting/protected_branch.go index 022a24a9ad4..06a9e695071 100644 --- a/routers/web/repo/setting/protected_branch.go +++ b/routers/web/repo/setting/protected_branch.go @@ -4,6 +4,7 @@ package setting import ( + "errors" "fmt" "net/http" "net/url" @@ -14,6 +15,7 @@ import ( "code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/perm" access_model "code.gitea.io/gitea/models/perm/access" + repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/web" @@ -351,9 +353,15 @@ func RenameBranchPost(ctx *context.Context) { msg, err := repository.RenameBranch(ctx, ctx.Repo.Repository, ctx.Doer, ctx.Repo.GitRepo, form.From, form.To) if err != nil { switch { + case repo_model.IsErrUserDoesNotHaveAccessToRepo(err): + ctx.Flash.Error(ctx.Tr("repo.branch.rename_default_or_protected_branch_error")) + ctx.Redirect(fmt.Sprintf("%s/branches", ctx.Repo.RepoLink)) case git_model.IsErrBranchAlreadyExists(err): ctx.Flash.Error(ctx.Tr("repo.branch.branch_already_exists", form.To)) ctx.Redirect(fmt.Sprintf("%s/branches", ctx.Repo.RepoLink)) + case errors.Is(err, git_model.ErrBranchIsProtected): + ctx.Flash.Error(ctx.Tr("repo.branch.rename_protected_branch_failed")) + ctx.Redirect(fmt.Sprintf("%s/branches", ctx.Repo.RepoLink)) default: ctx.ServerError("RenameBranch", err) } diff --git a/routers/web/repo/setting/setting.go b/routers/web/repo/setting/setting.go index 7399c681e2e..be37d0a9c45 100644 --- a/routers/web/repo/setting/setting.go +++ b/routers/web/repo/setting/setting.go @@ -49,6 +49,15 @@ const ( tplDeployKeys templates.TplName = "repo/settings/deploy_keys" ) +func parseEveryoneAccessMode(permission string, allowed ...perm.AccessMode) perm.AccessMode { + // if site admin forces repositories to be private, then do not allow any other access mode, + // otherwise the "force private" setting would be bypassed + if setting.Repository.ForcePrivate { + return perm.AccessModeNone + } + return perm.ParseAccessMode(permission, allowed...) +} + // SettingsCtxData is a middleware that sets all the general context data for the // settings template. func SettingsCtxData(ctx *context.Context) { @@ -447,8 +456,9 @@ func SettingsPost(ctx *context.Context) { if form.EnableCode && !unit_model.TypeCode.UnitGlobalDisabled() { units = append(units, repo_model.RepoUnit{ - RepoID: repo.ID, - Type: unit_model.TypeCode, + RepoID: repo.ID, + Type: unit_model.TypeCode, + EveryoneAccessMode: parseEveryoneAccessMode(form.DefaultCodeEveryoneAccess, perm.AccessModeNone, perm.AccessModeRead), }) } else if !unit_model.TypeCode.UnitGlobalDisabled() { deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeCode) @@ -474,7 +484,7 @@ func SettingsPost(ctx *context.Context) { RepoID: repo.ID, Type: unit_model.TypeWiki, Config: new(repo_model.UnitConfig), - EveryoneAccessMode: perm.ParseAccessMode(form.DefaultWikiEveryoneAccess, perm.AccessModeNone, perm.AccessModeRead, perm.AccessModeWrite), + EveryoneAccessMode: parseEveryoneAccessMode(form.DefaultWikiEveryoneAccess, perm.AccessModeNone, perm.AccessModeRead, perm.AccessModeWrite), }) deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalWiki) } else { @@ -524,6 +534,7 @@ func SettingsPost(ctx *context.Context) { AllowOnlyContributorsToTrackTime: form.AllowOnlyContributorsToTrackTime, EnableDependencies: form.EnableIssueDependencies, }, + EveryoneAccessMode: parseEveryoneAccessMode(form.DefaultIssuesEveryoneAccess, perm.AccessModeNone, perm.AccessModeRead), }) deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalTracker) } else { @@ -821,16 +832,10 @@ func SettingsPost(ctx *context.Context) { } else { ctx.ServerError("GetPendingRepositoryTransfer", err) } - return } - if err := repoTransfer.LoadAttributes(ctx); err != nil { - ctx.ServerError("LoadRecipient", err) - return - } - - if err := repo_service.CancelRepositoryTransfer(ctx, ctx.Repo.Repository); err != nil { + if err := repo_service.CancelRepositoryTransfer(ctx, repoTransfer, ctx.Doer); err != nil { ctx.ServerError("CancelRepositoryTransfer", err) return } diff --git a/routers/web/repo/setting/settings_test.go b/routers/web/repo/setting/settings_test.go index 09586cc68d6..ad33dac5145 100644 --- a/routers/web/repo/setting/settings_test.go +++ b/routers/web/repo/setting/settings_test.go @@ -54,7 +54,7 @@ func TestAddReadOnlyDeployKey(t *testing.T) { } web.SetForm(ctx, &addKeyForm) DeployKeysPost(ctx) - assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status()) + assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus()) unittest.AssertExistsAndLoadBean(t, &asymkey_model.DeployKey{ Name: addKeyForm.Title, @@ -84,7 +84,7 @@ func TestAddReadWriteOnlyDeployKey(t *testing.T) { } web.SetForm(ctx, &addKeyForm) DeployKeysPost(ctx) - assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status()) + assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus()) unittest.AssertExistsAndLoadBean(t, &asymkey_model.DeployKey{ Name: addKeyForm.Title, @@ -121,7 +121,7 @@ func TestCollaborationPost(t *testing.T) { CollaborationPost(ctx) - assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status()) + assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus()) exists, err := repo_model.IsCollaborator(ctx, re.ID, 4) assert.NoError(t, err) @@ -147,7 +147,7 @@ func TestCollaborationPost_InactiveUser(t *testing.T) { CollaborationPost(ctx) - assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status()) + assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus()) assert.NotEmpty(t, ctx.Flash.ErrorMsg) } @@ -179,7 +179,7 @@ func TestCollaborationPost_AddCollaboratorTwice(t *testing.T) { CollaborationPost(ctx) - assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status()) + assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus()) exists, err := repo_model.IsCollaborator(ctx, re.ID, 4) assert.NoError(t, err) @@ -188,7 +188,7 @@ func TestCollaborationPost_AddCollaboratorTwice(t *testing.T) { // Try adding the same collaborator again CollaborationPost(ctx) - assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status()) + assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus()) assert.NotEmpty(t, ctx.Flash.ErrorMsg) } @@ -210,7 +210,7 @@ func TestCollaborationPost_NonExistentUser(t *testing.T) { CollaborationPost(ctx) - assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status()) + assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus()) assert.NotEmpty(t, ctx.Flash.ErrorMsg) } @@ -250,7 +250,7 @@ func TestAddTeamPost(t *testing.T) { AddTeamPost(ctx) assert.True(t, repo_service.HasRepository(db.DefaultContext, team, re.ID)) - assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status()) + assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus()) assert.Empty(t, ctx.Flash.ErrorMsg) } @@ -290,7 +290,7 @@ func TestAddTeamPost_NotAllowed(t *testing.T) { AddTeamPost(ctx) assert.False(t, repo_service.HasRepository(db.DefaultContext, team, re.ID)) - assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status()) + assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus()) assert.NotEmpty(t, ctx.Flash.ErrorMsg) } @@ -331,7 +331,7 @@ func TestAddTeamPost_AddTeamTwice(t *testing.T) { AddTeamPost(ctx) assert.True(t, repo_service.HasRepository(db.DefaultContext, team, re.ID)) - assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status()) + assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus()) assert.NotEmpty(t, ctx.Flash.ErrorMsg) } @@ -364,7 +364,7 @@ func TestAddTeamPost_NonExistentTeam(t *testing.T) { ctx.Repo = repo AddTeamPost(ctx) - assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status()) + assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus()) assert.NotEmpty(t, ctx.Flash.ErrorMsg) } diff --git a/routers/web/repo/setting/webhook.go b/routers/web/repo/setting/webhook.go index 1b0ba83af4d..4ff24670412 100644 --- a/routers/web/repo/setting/webhook.go +++ b/routers/web/repo/setting/webhook.go @@ -163,27 +163,28 @@ func ParseHookEvent(form forms.WebhookForm) *webhook_module.HookEvent { SendEverything: form.SendEverything(), ChooseEvents: form.ChooseEvents(), HookEvents: webhook_module.HookEvents{ - Create: form.Create, - Delete: form.Delete, - Fork: form.Fork, - Issues: form.Issues, - IssueAssign: form.IssueAssign, - IssueLabel: form.IssueLabel, - IssueMilestone: form.IssueMilestone, - IssueComment: form.IssueComment, - Release: form.Release, - Push: form.Push, - PullRequest: form.PullRequest, - PullRequestAssign: form.PullRequestAssign, - PullRequestLabel: form.PullRequestLabel, - PullRequestMilestone: form.PullRequestMilestone, - PullRequestComment: form.PullRequestComment, - PullRequestReview: form.PullRequestReview, - PullRequestSync: form.PullRequestSync, - PullRequestReviewRequest: form.PullRequestReviewRequest, - Wiki: form.Wiki, - Repository: form.Repository, - Package: form.Package, + webhook_module.HookEventCreate: form.Create, + webhook_module.HookEventDelete: form.Delete, + webhook_module.HookEventFork: form.Fork, + webhook_module.HookEventIssues: form.Issues, + webhook_module.HookEventIssueAssign: form.IssueAssign, + webhook_module.HookEventIssueLabel: form.IssueLabel, + webhook_module.HookEventIssueMilestone: form.IssueMilestone, + webhook_module.HookEventIssueComment: form.IssueComment, + webhook_module.HookEventRelease: form.Release, + webhook_module.HookEventPush: form.Push, + webhook_module.HookEventPullRequest: form.PullRequest, + webhook_module.HookEventPullRequestAssign: form.PullRequestAssign, + webhook_module.HookEventPullRequestLabel: form.PullRequestLabel, + webhook_module.HookEventPullRequestMilestone: form.PullRequestMilestone, + webhook_module.HookEventPullRequestComment: form.PullRequestComment, + webhook_module.HookEventPullRequestReview: form.PullRequestReview, + webhook_module.HookEventPullRequestSync: form.PullRequestSync, + webhook_module.HookEventPullRequestReviewRequest: form.PullRequestReviewRequest, + webhook_module.HookEventWiki: form.Wiki, + webhook_module.HookEventRepository: form.Repository, + webhook_module.HookEventPackage: form.Package, + webhook_module.HookEventStatus: form.Status, }, BranchFilter: form.BranchFilter, } @@ -654,6 +655,8 @@ func TestWebhook(ctx *context.Context) { } // Grab latest commit or fake one if it's empty repository. + // Note: in old code, the "ctx.Repo.Commit" is the last commit of the default branch. + // New code doesn't set that commit, so it always uses the fake commit to test webhook. commit := ctx.Repo.Commit if commit == nil { ghost := user_model.NewGhostUser() diff --git a/routers/web/repo/star.go b/routers/web/repo/star.go new file mode 100644 index 00000000000..00c06b7d02d --- /dev/null +++ b/routers/web/repo/star.go @@ -0,0 +1,31 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repo + +import ( + "net/http" + + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/modules/templates" + "code.gitea.io/gitea/services/context" +) + +const tplStarUnstar templates.TplName = "repo/star_unstar" + +func ActionStar(ctx *context.Context) { + err := repo_model.StarRepo(ctx, ctx.Doer, ctx.Repo.Repository, ctx.PathParam("action") == "star") + if err != nil { + handleActionError(ctx, err) + return + } + + ctx.Data["IsStaringRepo"] = repo_model.IsStaring(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID) + ctx.Data["Repository"], err = repo_model.GetRepositoryByName(ctx, ctx.Repo.Repository.OwnerID, ctx.Repo.Repository.Name) + if err != nil { + ctx.ServerError("GetRepositoryByName", err) + return + } + ctx.RespHeader().Add("hx-trigger", "refreshUserCards") // see the `hx-trigger="refreshUserCards ..."` comments in tmpl + ctx.HTML(http.StatusOK, tplStarUnstar) +} diff --git a/routers/web/repo/transfer.go b/routers/web/repo/transfer.go new file mode 100644 index 00000000000..5553eee6741 --- /dev/null +++ b/routers/web/repo/transfer.go @@ -0,0 +1,38 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repo + +import ( + "code.gitea.io/gitea/services/context" + repo_service "code.gitea.io/gitea/services/repository" +) + +func acceptTransfer(ctx *context.Context) { + err := repo_service.AcceptTransferOwnership(ctx, ctx.Repo.Repository, ctx.Doer) + if err == nil { + ctx.Flash.Success(ctx.Tr("repo.settings.transfer.success")) + ctx.Redirect(ctx.Repo.Repository.Link()) + return + } + handleActionError(ctx, err) +} + +func rejectTransfer(ctx *context.Context) { + err := repo_service.RejectRepositoryTransfer(ctx, ctx.Repo.Repository, ctx.Doer) + if err == nil { + ctx.Flash.Success(ctx.Tr("repo.settings.transfer.rejected")) + ctx.Redirect(ctx.Repo.Repository.Link()) + return + } + handleActionError(ctx, err) +} + +func ActionTransfer(ctx *context.Context) { + switch ctx.PathParam("action") { + case "accept_transfer": + acceptTransfer(ctx) + case "reject_transfer": + rejectTransfer(ctx) + } +} diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go index 14fc9038f3e..05ecf2ab794 100644 --- a/routers/web/repo/view.go +++ b/routers/web/repo/view.go @@ -243,7 +243,7 @@ func LastCommit(ctx *context.Context) { ctx.Data["ParentPath"] = "/" + paths[len(paths)-2] } } - branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL() + branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL() ctx.Data["BranchLink"] = branchLink ctx.HTML(http.StatusOK, tplRepoViewList) @@ -301,7 +301,7 @@ func renderDirectoryFiles(ctx *context.Context, timeout time.Duration) git.Entri return nil } - branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL() + branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL() treeLink := branchLink if len(ctx.Repo.TreePath) > 0 { @@ -309,7 +309,6 @@ func renderDirectoryFiles(ctx *context.Context, timeout time.Duration) git.Entri } ctx.Data["TreeLink"] = treeLink - ctx.Data["SSHDomain"] = setting.SSH.Domain return allEntries } diff --git a/routers/web/repo/view_file.go b/routers/web/repo/view_file.go index 17c28218243..57842979184 100644 --- a/routers/web/repo/view_file.go +++ b/routers/web/repo/view_file.go @@ -42,10 +42,10 @@ func prepareToRenderFile(ctx *context.Context, entry *git.TreeEntry) { } defer dataRc.Close() - ctx.Data["Title"] = ctx.Tr("repo.file.title", ctx.Repo.Repository.Name+"/"+path.Base(ctx.Repo.TreePath), ctx.Repo.RefName) + ctx.Data["Title"] = ctx.Tr("repo.file.title", ctx.Repo.Repository.Name+"/"+path.Base(ctx.Repo.TreePath), ctx.Repo.RefFullName.ShortName()) ctx.Data["FileIsSymlink"] = entry.IsLink() ctx.Data["FileName"] = blob.Name() - ctx.Data["RawFileLink"] = ctx.Repo.RepoLink + "/raw/" + ctx.Repo.BranchNameSubURL() + "/" + util.PathEscapeSegments(ctx.Repo.TreePath) + ctx.Data["RawFileLink"] = ctx.Repo.RepoLink + "/raw/" + ctx.Repo.RefTypeNameSubURL() + "/" + util.PathEscapeSegments(ctx.Repo.TreePath) commit, err := ctx.Repo.Commit.GetCommitByPath(ctx.Repo.TreePath) if err != nil { @@ -92,7 +92,7 @@ func prepareToRenderFile(ctx *context.Context, entry *git.TreeEntry) { isDisplayingRendered := !isDisplayingSource if fInfo.isLFSFile { - ctx.Data["RawFileLink"] = ctx.Repo.RepoLink + "/media/" + ctx.Repo.BranchNameSubURL() + "/" + util.PathEscapeSegments(ctx.Repo.TreePath) + ctx.Data["RawFileLink"] = ctx.Repo.RepoLink + "/media/" + ctx.Repo.RefTypeNameSubURL() + "/" + util.PathEscapeSegments(ctx.Repo.TreePath) } isRepresentableAsText := fInfo.st.IsRepresentableAsText() @@ -170,9 +170,9 @@ func prepareToRenderFile(ctx *context.Context, entry *git.TreeEntry) { ctx.Data["IsMarkup"] = true ctx.Data["MarkupType"] = markupType metas := ctx.Repo.Repository.ComposeDocumentMetas(ctx) - metas["BranchNameSubURL"] = ctx.Repo.BranchNameSubURL() + metas["RefTypeNameSubURL"] = ctx.Repo.RefTypeNameSubURL() rctx := renderhelper.NewRenderContextRepoFile(ctx, ctx.Repo.Repository, renderhelper.RepoFileOptions{ - CurrentRefPath: ctx.Repo.BranchNameSubURL(), + CurrentRefPath: ctx.Repo.RefTypeNameSubURL(), CurrentTreePath: path.Dir(ctx.Repo.TreePath), }). WithMarkupType(markupType). @@ -232,7 +232,7 @@ func prepareToRenderFile(ctx *context.Context, entry *git.TreeEntry) { ctx.Data["CanEditFile"] = true ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.edit_this_file") } - } else if !ctx.Repo.IsViewBranch { + } else if !ctx.Repo.RefFullName.IsBranch() { ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.must_be_on_a_branch") } else if !ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, ctx.Repo.BranchName) { ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.fork_before_edit") @@ -262,7 +262,7 @@ func prepareToRenderFile(ctx *context.Context, entry *git.TreeEntry) { ctx.Data["MarkupType"] = markupType rctx := renderhelper.NewRenderContextRepoFile(ctx, ctx.Repo.Repository, renderhelper.RepoFileOptions{ - CurrentRefPath: ctx.Repo.BranchNameSubURL(), + CurrentRefPath: ctx.Repo.RefTypeNameSubURL(), CurrentTreePath: path.Dir(ctx.Repo.TreePath), }). WithMarkupType(markupType). @@ -305,7 +305,7 @@ func prepareToRenderFile(ctx *context.Context, entry *git.TreeEntry) { ctx.Data["CanDeleteFile"] = true ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.delete_this_file") } - } else if !ctx.Repo.IsViewBranch { + } else if !ctx.Repo.RefFullName.IsBranch() { ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.must_be_on_a_branch") } else if !ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, ctx.Repo.BranchName) { ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.must_have_write_access") diff --git a/routers/web/repo/view_home.go b/routers/web/repo/view_home.go index 70ba07f9a89..e7255cde0a3 100644 --- a/routers/web/repo/view_home.go +++ b/routers/web/repo/view_home.go @@ -135,7 +135,7 @@ func prepareToRenderDirectory(ctx *context.Context) { if ctx.Repo.TreePath != "" { ctx.Data["HideRepoInfo"] = true - ctx.Data["Title"] = ctx.Tr("repo.file.title", ctx.Repo.Repository.Name+"/"+path.Base(ctx.Repo.TreePath), ctx.Repo.RefName) + ctx.Data["Title"] = ctx.Tr("repo.file.title", ctx.Repo.Repository.Name+"/"+path.Base(ctx.Repo.TreePath), ctx.Repo.RefFullName.ShortName()) } subfolder, readmeFile, err := findReadmeFileInEntries(ctx, entries, true) @@ -178,7 +178,7 @@ func prepareHomeSidebarLatestRelease(ctx *context.Context) { } func prepareUpstreamDivergingInfo(ctx *context.Context) { - if !ctx.Repo.Repository.IsFork || !ctx.Repo.IsViewBranch || ctx.Repo.TreePath != "" { + if !ctx.Repo.Repository.IsFork || !ctx.Repo.RefFullName.IsBranch() || ctx.Repo.TreePath != "" { return } upstreamDivergingInfo, err := repo_service.GetUpstreamDivergingInfo(ctx, ctx.Repo.Repository, ctx.Repo.BranchName) @@ -215,24 +215,67 @@ func prepareRecentlyPushedNewBranches(ctx *context.Context) { if !opts.Repo.IsMirror && !opts.BaseRepo.IsMirror && opts.BaseRepo.UnitEnabled(ctx, unit_model.TypePullRequests) && baseRepoPerm.CanRead(unit_model.TypePullRequests) { - ctx.Data["RecentlyPushedNewBranches"], err = git_model.FindRecentlyPushedNewBranches(ctx, ctx.Doer, opts) + var finalBranches []*git_model.RecentlyPushedNewBranch + branches, err := git_model.FindRecentlyPushedNewBranches(ctx, ctx.Doer, opts) if err != nil { log.Error("FindRecentlyPushedNewBranches failed: %v", err) } + + for _, branch := range branches { + divergingInfo, err := repo_service.GetBranchDivergingInfo(ctx, + branch.BranchRepo, branch.BranchName, // "base" repo for diverging info + opts.BaseRepo, opts.BaseRepo.DefaultBranch, // "head" repo for diverging info + ) + if err != nil { + log.Error("GetBranchDivergingInfo failed: %v", err) + continue + } + branchRepoHasNewCommits := divergingInfo.BaseHasNewCommits + baseRepoCommitsBehind := divergingInfo.HeadCommitsBehind + if branchRepoHasNewCommits || baseRepoCommitsBehind > 0 { + finalBranches = append(finalBranches, branch) + } + } + ctx.Data["RecentlyPushedNewBranches"] = finalBranches } } } +func updateContextRepoEmptyAndStatus(ctx *context.Context, empty bool, status repo_model.RepositoryStatus) { + if ctx.Repo.Repository.IsEmpty == empty && ctx.Repo.Repository.Status == status { + return + } + ctx.Repo.Repository.IsEmpty = empty + if ctx.Repo.Repository.Status == repo_model.RepositoryReady || ctx.Repo.Repository.Status == repo_model.RepositoryBroken { + ctx.Repo.Repository.Status = status // only handle ready and broken status, leave other status as-is + } + if err := repo_model.UpdateRepositoryColsNoAutoTime(ctx, ctx.Repo.Repository, "is_empty", "status"); err != nil { + ctx.ServerError("updateContextRepoEmptyAndStatus: UpdateRepositoryCols", err) + return + } +} + func handleRepoEmptyOrBroken(ctx *context.Context) { showEmpty := true - var err error if ctx.Repo.GitRepo != nil { - showEmpty, err = ctx.Repo.GitRepo.IsEmpty() + reallyEmpty, err := ctx.Repo.GitRepo.IsEmpty() if err != nil { + showEmpty = true // the repo is broken + updateContextRepoEmptyAndStatus(ctx, true, repo_model.RepositoryBroken) log.Error("GitRepo.IsEmpty: %v", err) - ctx.Repo.Repository.Status = repo_model.RepositoryBroken - showEmpty = true ctx.Flash.Error(ctx.Tr("error.occurred"), true) + } else if reallyEmpty { + showEmpty = true // the repo is really empty + updateContextRepoEmptyAndStatus(ctx, true, repo_model.RepositoryReady) + } else if branches, _, _ := ctx.Repo.GitRepo.GetBranches(0, 1); len(branches) == 0 { + showEmpty = true // it is not really empty, but there is no branch + // at the moment, other repo units like "actions" are not able to handle such case, + // so we just mark the repo as empty to prevent from displaying these units. + ctx.Data["RepoHasContentsWithoutBranch"] = true + updateContextRepoEmptyAndStatus(ctx, true, repo_model.RepositoryReady) + } else { + // the repo is actually not empty and has branches, need to update the database later + showEmpty = false } } if showEmpty { @@ -240,18 +283,11 @@ func handleRepoEmptyOrBroken(ctx *context.Context) { return } - // the repo is not really empty, so we should update the modal in database - // such problem may be caused by: - // 1) an error occurs during pushing/receiving. 2) the user replaces an empty git repo manually - // and even more: the IsEmpty flag is deeply broken and should be removed with the UI changed to manage to cope with empty repos. - // it's possible for a repository to be non-empty by that flag but still 500 - // because there are no branches - only tags -or the default branch is non-extant as it has been 0-pushed. - ctx.Repo.Repository.IsEmpty = false - if err = repo_model.UpdateRepositoryCols(ctx, ctx.Repo.Repository, "is_empty"); err != nil { - ctx.ServerError("UpdateRepositoryCols", err) - return - } - if err = repo_module.UpdateRepoSize(ctx, ctx.Repo.Repository); err != nil { + // The repo is not really empty, so we should update the model in database, such problem may be caused by: + // 1) an error occurs during pushing/receiving. + // 2) the user replaces an empty git repo manually. + updateContextRepoEmptyAndStatus(ctx, false, repo_model.RepositoryReady) + if err := repo_module.UpdateRepoSize(ctx, ctx.Repo.Repository); err != nil { ctx.ServerError("UpdateRepoSize", err) return } @@ -275,21 +311,21 @@ func prepareToRenderDirOrFile(entry *git.TreeEntry) func(ctx *context.Context) { } func handleRepoHomeFeed(ctx *context.Context) bool { - if setting.Other.EnableFeed { - isFeed, _, showFeedType := feed.GetFeedType(ctx.PathParam("reponame"), ctx.Req) - if isFeed { - switch { - case ctx.Link == fmt.Sprintf("%s.%s", ctx.Repo.RepoLink, showFeedType): - feed.ShowRepoFeed(ctx, ctx.Repo.Repository, showFeedType) - case ctx.Repo.TreePath == "": - feed.ShowBranchFeed(ctx, ctx.Repo.Repository, showFeedType) - case ctx.Repo.TreePath != "": - feed.ShowFileFeed(ctx, ctx.Repo.Repository, showFeedType) - } - return true - } + if !setting.Other.EnableFeed { + return false } - return false + isFeed, showFeedType := feed.GetFeedType(ctx.PathParam("reponame"), ctx.Req) + if !isFeed { + return false + } + if ctx.Link == fmt.Sprintf("%s.%s", ctx.Repo.RepoLink, showFeedType) { + feed.ShowRepoFeed(ctx, ctx.Repo.Repository, showFeedType) + } else if ctx.Repo.TreePath == "" { + feed.ShowBranchFeed(ctx, ctx.Repo.Repository, showFeedType) + } else { + feed.ShowFileFeed(ctx, ctx.Repo.Repository, showFeedType) + } + return true } // Home render repository home page @@ -328,7 +364,7 @@ func Home(ctx *context.Context) { // prepare the tree path var treeNames, paths []string - branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL() + branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL() treeLink := branchLink if ctx.Repo.TreePath != "" { treeLink += "/" + util.PathEscapeSegments(ctx.Repo.TreePath) @@ -376,3 +412,20 @@ func Home(ctx *context.Context) { ctx.HTML(http.StatusOK, tplRepoHome) } + +func RedirectRepoTreeToSrc(ctx *context.Context) { + // Redirect "/owner/repo/tree/*" requests to "/owner/repo/src/*", + // then use the deprecated "/src/*" handler to guess the ref type and render a file list page. + // This is done intentionally so that Gitea's repo URL structure matches other forges (GitHub/GitLab) provide, + // allowing us to construct submodule URLs across forges easily. + // For example, when viewing a submodule, we can simply construct the link as: + // * "https://gitea/owner/repo/tree/{CommitID}" + // * "https://github/owner/repo/tree/{CommitID}" + // * "https://gitlab/owner/repo/tree/{CommitID}" + // Then no matter which forge the submodule is using, the link works. + redirect := ctx.Repo.RepoLink + "/src/" + ctx.PathParamRaw("*") + if ctx.Req.URL.RawQuery != "" { + redirect += "?" + ctx.Req.URL.RawQuery + } + ctx.Redirect(redirect) +} diff --git a/routers/web/repo/view_readme.go b/routers/web/repo/view_readme.go index 5bd39de9631..48befe47f8e 100644 --- a/routers/web/repo/view_readme.go +++ b/routers/web/repo/view_readme.go @@ -189,7 +189,7 @@ func prepareToRenderReadmeFile(ctx *context.Context, subfolder string, readmeFil ctx.Data["MarkupType"] = markupType rctx := renderhelper.NewRenderContextRepoFile(ctx, ctx.Repo.Repository, renderhelper.RepoFileOptions{ - CurrentRefPath: ctx.Repo.BranchNameSubURL(), + CurrentRefPath: ctx.Repo.RefTypeNameSubURL(), CurrentTreePath: path.Join(ctx.Repo.TreePath, subfolder), }). WithMarkupType(markupType). diff --git a/routers/web/repo/watch.go b/routers/web/repo/watch.go new file mode 100644 index 00000000000..70c548b8cea --- /dev/null +++ b/routers/web/repo/watch.go @@ -0,0 +1,31 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repo + +import ( + "net/http" + + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/modules/templates" + "code.gitea.io/gitea/services/context" +) + +const tplWatchUnwatch templates.TplName = "repo/watch_unwatch" + +func ActionWatch(ctx *context.Context) { + err := repo_model.WatchRepo(ctx, ctx.Doer, ctx.Repo.Repository, ctx.PathParam("action") == "watch") + if err != nil { + handleActionError(ctx, err) + return + } + + ctx.Data["IsWatchingRepo"] = repo_model.IsWatching(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID) + ctx.Data["Repository"], err = repo_model.GetRepositoryByName(ctx, ctx.Repo.Repository.OwnerID, ctx.Repo.Repository.Name) + if err != nil { + ctx.ServerError("GetRepositoryByName", err) + return + } + ctx.RespHeader().Add("hx-trigger", "refreshUserCards") // see the `hx-trigger="refreshUserCards ..."` comments in tmpl + ctx.HTML(http.StatusOK, tplWatchUnwatch) +} diff --git a/routers/web/repo/webgit.go b/routers/web/repo/webgit.go new file mode 100644 index 00000000000..5f390197e72 --- /dev/null +++ b/routers/web/repo/webgit.go @@ -0,0 +1,40 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repo + +import ( + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/services/context" + files_service "code.gitea.io/gitea/services/repository/files" +) + +func WebGitOperationCommonData(ctx *context.Context) { + // TODO: more places like "wiki page" and "merging a pull request or creating an auto merge merging task" + emails, err := user_model.GetActivatedEmailAddresses(ctx, ctx.Doer.ID) + if err != nil { + log.Error("WebGitOperationCommonData: GetActivatedEmailAddresses: %v", err) + } + if ctx.Doer.KeepEmailPrivate { + emails = append([]string{ctx.Doer.GetPlaceholderEmail()}, emails...) + } + ctx.Data["CommitCandidateEmails"] = emails + ctx.Data["CommitDefaultEmail"] = ctx.Doer.GetEmail() +} + +func WebGitOperationGetCommitChosenEmailIdentity(ctx *context.Context, email string) (_ *files_service.IdentityOptions, valid bool) { + if ctx.Data["CommitCandidateEmails"] == nil { + setting.PanicInDevOrTesting("no CommitCandidateEmails in context data") + } + emails, _ := ctx.Data["CommitCandidateEmails"].([]string) + if email == "" { + return nil, true + } + if util.SliceContainsString(emails, email, true) { + return &files_service.IdentityOptions{GitUserEmail: email}, true + } + return nil, false +} diff --git a/routers/web/repo/wiki_test.go b/routers/web/repo/wiki_test.go index 958ff802d40..99114c93e0f 100644 --- a/routers/web/repo/wiki_test.go +++ b/routers/web/repo/wiki_test.go @@ -20,6 +20,7 @@ import ( wiki_service "code.gitea.io/gitea/services/wiki" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) const ( @@ -66,12 +67,9 @@ func assertWikiNotExists(t *testing.T, repo *repo_model.Repository, wikiName wik func assertPagesMetas(t *testing.T, expectedNames []string, metas any) { pageMetas, ok := metas.([]PageMeta) - if !assert.True(t, ok) { - return - } - if !assert.Len(t, pageMetas, len(expectedNames)) { - return - } + require.True(t, ok) + require.Len(t, pageMetas, len(expectedNames)) + for i, pageMeta := range pageMetas { assert.EqualValues(t, expectedNames[i], pageMeta.Name) } @@ -84,7 +82,7 @@ func TestWiki(t *testing.T) { ctx.SetPathParam("*", "Home") contexttest.LoadRepo(t, ctx, 1) Wiki(ctx) - assert.EqualValues(t, http.StatusOK, ctx.Resp.Status()) + assert.EqualValues(t, http.StatusOK, ctx.Resp.WrittenStatus()) assert.EqualValues(t, "Home", ctx.Data["Title"]) assertPagesMetas(t, []string{"Home", "Page With Image", "Page With Spaced Name", "Unescaped File"}, ctx.Data["Pages"]) @@ -92,7 +90,7 @@ func TestWiki(t *testing.T) { ctx.SetPathParam("*", "jpeg.jpg") contexttest.LoadRepo(t, ctx, 1) Wiki(ctx) - assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status()) + assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus()) assert.Equal(t, "/user2/repo1/wiki/raw/jpeg.jpg", ctx.Resp.Header().Get("Location")) } @@ -102,7 +100,7 @@ func TestWikiPages(t *testing.T) { ctx, _ := contexttest.MockContext(t, "user2/repo1/wiki/?action=_pages") contexttest.LoadRepo(t, ctx, 1) WikiPages(ctx) - assert.EqualValues(t, http.StatusOK, ctx.Resp.Status()) + assert.EqualValues(t, http.StatusOK, ctx.Resp.WrittenStatus()) assertPagesMetas(t, []string{"Home", "Page With Image", "Page With Spaced Name", "Unescaped File"}, ctx.Data["Pages"]) } @@ -113,7 +111,7 @@ func TestNewWiki(t *testing.T) { contexttest.LoadUser(t, ctx, 2) contexttest.LoadRepo(t, ctx, 1) NewWiki(ctx) - assert.EqualValues(t, http.StatusOK, ctx.Resp.Status()) + assert.EqualValues(t, http.StatusOK, ctx.Resp.WrittenStatus()) assert.EqualValues(t, ctx.Tr("repo.wiki.new_page"), ctx.Data["Title"]) } @@ -133,7 +131,7 @@ func TestNewWikiPost(t *testing.T) { Message: message, }) NewWikiPost(ctx) - assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status()) + assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus()) assertWikiExists(t, ctx.Repo.Repository, wiki_service.UserTitleToWebPath("", title)) assert.Equal(t, content, wikiContent(t, ctx.Repo.Repository, wiki_service.UserTitleToWebPath("", title))) } @@ -151,7 +149,7 @@ func TestNewWikiPost_ReservedName(t *testing.T) { Message: message, }) NewWikiPost(ctx) - assert.EqualValues(t, http.StatusOK, ctx.Resp.Status()) + assert.EqualValues(t, http.StatusOK, ctx.Resp.WrittenStatus()) assert.EqualValues(t, ctx.Tr("repo.wiki.reserved_page", "_edit"), ctx.Flash.ErrorMsg) assertWikiNotExists(t, ctx.Repo.Repository, "_edit") } @@ -164,7 +162,7 @@ func TestEditWiki(t *testing.T) { contexttest.LoadUser(t, ctx, 2) contexttest.LoadRepo(t, ctx, 1) EditWiki(ctx) - assert.EqualValues(t, http.StatusOK, ctx.Resp.Status()) + assert.EqualValues(t, http.StatusOK, ctx.Resp.WrittenStatus()) assert.EqualValues(t, "Home", ctx.Data["Title"]) assert.Equal(t, wikiContent(t, ctx.Repo.Repository, "Home"), ctx.Data["content"]) @@ -173,7 +171,7 @@ func TestEditWiki(t *testing.T) { contexttest.LoadUser(t, ctx, 2) contexttest.LoadRepo(t, ctx, 1) EditWiki(ctx) - assert.EqualValues(t, http.StatusForbidden, ctx.Resp.Status()) + assert.EqualValues(t, http.StatusForbidden, ctx.Resp.WrittenStatus()) } func TestEditWikiPost(t *testing.T) { @@ -192,7 +190,7 @@ func TestEditWikiPost(t *testing.T) { Message: message, }) EditWikiPost(ctx) - assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status()) + assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus()) assertWikiExists(t, ctx.Repo.Repository, wiki_service.UserTitleToWebPath("", title)) assert.Equal(t, content, wikiContent(t, ctx.Repo.Repository, wiki_service.UserTitleToWebPath("", title))) if title != "Home" { @@ -208,7 +206,7 @@ func TestDeleteWikiPagePost(t *testing.T) { contexttest.LoadUser(t, ctx, 2) contexttest.LoadRepo(t, ctx, 1) DeleteWikiPagePost(ctx) - assert.EqualValues(t, http.StatusOK, ctx.Resp.Status()) + assert.EqualValues(t, http.StatusOK, ctx.Resp.WrittenStatus()) assertWikiNotExists(t, ctx.Repo.Repository, "Home") } @@ -230,9 +228,9 @@ func TestWikiRaw(t *testing.T) { contexttest.LoadRepo(t, ctx, 1) WikiRaw(ctx) if filetype == "" { - assert.EqualValues(t, http.StatusNotFound, ctx.Resp.Status(), "filepath: %s", filepath) + assert.EqualValues(t, http.StatusNotFound, ctx.Resp.WrittenStatus(), "filepath: %s", filepath) } else { - assert.EqualValues(t, http.StatusOK, ctx.Resp.Status(), "filepath: %s", filepath) + assert.EqualValues(t, http.StatusOK, ctx.Resp.WrittenStatus(), "filepath: %s", filepath) assert.EqualValues(t, filetype, ctx.Resp.Header().Get("Content-Type"), "filepath: %s", filepath) } } diff --git a/routers/web/shared/user/header.go b/routers/web/shared/user/header.go index d388d2b5d9a..62b146c7f32 100644 --- a/routers/web/shared/user/header.go +++ b/routers/web/shared/user/header.go @@ -20,6 +20,7 @@ import ( "code.gitea.io/gitea/modules/markup/markdown" "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/services/context" ) @@ -102,37 +103,46 @@ func PrepareContextForProfileBigAvatar(ctx *context.Context) { } } -func FindUserProfileReadme(ctx *context.Context, doer *user_model.User) (profileDbRepo *repo_model.Repository, profileGitRepo *git.Repository, profileReadmeBlob *git.Blob, profileClose func()) { - profileDbRepo, err := repo_model.GetRepositoryByName(ctx, ctx.ContextUser.ID, ".profile") - if err == nil { - perm, err := access_model.GetUserRepoPermission(ctx, profileDbRepo, doer) - if err == nil && !profileDbRepo.IsEmpty && perm.CanRead(unit.TypeCode) { - if profileGitRepo, err = gitrepo.OpenRepository(ctx, profileDbRepo); err != nil { - log.Error("FindUserProfileReadme failed to OpenRepository: %v", err) - } else { - if commit, err := profileGitRepo.GetBranchCommit(profileDbRepo.DefaultBranch); err != nil { - log.Error("FindUserProfileReadme failed to GetBranchCommit: %v", err) - } else { - profileReadmeBlob, _ = commit.GetBlobByPath("README.md") - } - } +func FindOwnerProfileReadme(ctx *context.Context, doer *user_model.User, optProfileRepoName ...string) (profileDbRepo *repo_model.Repository, profileReadmeBlob *git.Blob) { + profileRepoName := util.OptionalArg(optProfileRepoName, RepoNameProfile) + profileDbRepo, err := repo_model.GetRepositoryByName(ctx, ctx.ContextUser.ID, profileRepoName) + if err != nil { + if !repo_model.IsErrRepoNotExist(err) { + log.Error("FindOwnerProfileReadme failed to GetRepositoryByName: %v", err) } - } else if !repo_model.IsErrRepoNotExist(err) { - log.Error("FindUserProfileReadme failed to GetRepositoryByName: %v", err) + return nil, nil } - return profileDbRepo, profileGitRepo, profileReadmeBlob, func() { - if profileGitRepo != nil { - _ = profileGitRepo.Close() - } + + perm, err := access_model.GetUserRepoPermission(ctx, profileDbRepo, doer) + if err != nil { + log.Error("FindOwnerProfileReadme failed to GetRepositoryByName: %v", err) + return nil, nil } + if profileDbRepo.IsEmpty || !perm.CanRead(unit.TypeCode) { + return nil, nil + } + + profileGitRepo, err := gitrepo.RepositoryFromRequestContextOrOpen(ctx, profileDbRepo) + if err != nil { + log.Error("FindOwnerProfileReadme failed to OpenRepository: %v", err) + return nil, nil + } + + commit, err := profileGitRepo.GetBranchCommit(profileDbRepo.DefaultBranch) + if err != nil { + log.Error("FindOwnerProfileReadme failed to GetBranchCommit: %v", err) + return nil, nil + } + + profileReadmeBlob, _ = commit.GetBlobByPath("README.md") // no need to handle this error + return profileDbRepo, profileReadmeBlob } func RenderUserHeader(ctx *context.Context) { prepareContextForCommonProfile(ctx) - _, _, profileReadmeBlob, profileClose := FindUserProfileReadme(ctx, ctx.Doer) - defer profileClose() - ctx.Data["HasProfileReadme"] = profileReadmeBlob != nil + _, profileReadmeBlob := FindOwnerProfileReadme(ctx, ctx.Doer) + ctx.Data["HasUserProfileReadme"] = profileReadmeBlob != nil } func LoadHeaderCount(ctx *context.Context) error { @@ -169,14 +179,28 @@ func LoadHeaderCount(ctx *context.Context) error { return nil } -func RenderOrgHeader(ctx *context.Context) error { - if err := LoadHeaderCount(ctx); err != nil { - return err +const ( + RepoNameProfilePrivate = ".profile-private" + RepoNameProfile = ".profile" +) + +type PrepareOrgHeaderResult struct { + ProfilePublicRepo *repo_model.Repository + ProfilePublicReadmeBlob *git.Blob + ProfilePrivateRepo *repo_model.Repository + ProfilePrivateReadmeBlob *git.Blob + HasOrgProfileReadme bool +} + +func PrepareOrgHeader(ctx *context.Context) (result *PrepareOrgHeaderResult, err error) { + if err = LoadHeaderCount(ctx); err != nil { + return nil, err } - _, _, profileReadmeBlob, profileClose := FindUserProfileReadme(ctx, ctx.Doer) - defer profileClose() - ctx.Data["HasProfileReadme"] = profileReadmeBlob != nil - - return nil + result = &PrepareOrgHeaderResult{} + result.ProfilePublicRepo, result.ProfilePublicReadmeBlob = FindOwnerProfileReadme(ctx, ctx.Doer) + result.ProfilePrivateRepo, result.ProfilePrivateReadmeBlob = FindOwnerProfileReadme(ctx, ctx.Doer, RepoNameProfilePrivate) + result.HasOrgProfileReadme = result.ProfilePublicReadmeBlob != nil || result.ProfilePrivateReadmeBlob != nil + ctx.Data["HasOrgProfileReadme"] = result.HasOrgProfileReadme // many pages need it to show the "overview" tab + return result, nil } diff --git a/routers/web/user/avatar.go b/routers/web/user/avatar.go index f77bd602b3d..81c00b3bd42 100644 --- a/routers/web/user/avatar.go +++ b/routers/web/user/avatar.go @@ -4,7 +4,6 @@ package user import ( - "strings" "time" "code.gitea.io/gitea/models/avatars" @@ -21,27 +20,18 @@ func cacheableRedirect(ctx *context.Context, location string) { ctx.Redirect(location) } -// AvatarByUserName redirect browser to user avatar of requested size -func AvatarByUserName(ctx *context.Context) { - userName := ctx.PathParam("username") - size := int(ctx.PathParamInt64("size")) - - var user *user_model.User - if strings.ToLower(userName) != user_model.GhostUserLowerName { +// AvatarByUsernameSize redirect browser to user avatar of requested size +func AvatarByUsernameSize(ctx *context.Context) { + username := ctx.PathParam("username") + user := user_model.GetSystemUserByName(username) + if user == nil { var err error - if user, err = user_model.GetUserByName(ctx, userName); err != nil { - if user_model.IsErrUserNotExist(err) { - ctx.NotFound("GetUserByName", err) - return - } - ctx.ServerError("Invalid user: "+userName, err) + if user, err = user_model.GetUserByName(ctx, username); err != nil { + ctx.NotFoundOrServerError("GetUserByName", user_model.IsErrUserNotExist, err) return } - } else { - user = user_model.NewGhostUser() } - - cacheableRedirect(ctx, user.AvatarLinkWithSize(ctx, size)) + cacheableRedirect(ctx, user.AvatarLinkWithSize(ctx, int(ctx.PathParamInt64("size")))) } // AvatarByEmailHash redirects the browser to the email avatar link diff --git a/routers/web/user/code.go b/routers/web/user/code.go index 9d515596f11..665ce1a6a6f 100644 --- a/routers/web/user/code.go +++ b/routers/web/user/code.go @@ -11,6 +11,7 @@ import ( code_indexer "code.gitea.io/gitea/modules/indexer/code" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/templates" + "code.gitea.io/gitea/routers/common" shared_user "code.gitea.io/gitea/routers/web/shared/user" "code.gitea.io/gitea/services/context" ) @@ -34,20 +35,11 @@ func CodeSearch(ctx *context.Context) { } ctx.Data["IsPackageEnabled"] = setting.Packages.Enabled - ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled ctx.Data["Title"] = ctx.Tr("explore.code") - - language := ctx.FormTrim("l") - keyword := ctx.FormTrim("q") - - isFuzzy := ctx.FormOptionalBool("fuzzy").ValueOrDefault(true) - - ctx.Data["Keyword"] = keyword - ctx.Data["Language"] = language - ctx.Data["IsFuzzy"] = isFuzzy ctx.Data["IsCodePage"] = true - if keyword == "" { + prepareSearch := common.PrepareCodeSearch(ctx) + if prepareSearch.Keyword == "" { ctx.HTML(http.StatusOK, tplUserCode) return } @@ -77,9 +69,9 @@ func CodeSearch(ctx *context.Context) { if len(repoIDs) > 0 { total, searchResults, searchResultLanguages, err = code_indexer.PerformSearch(ctx, &code_indexer.SearchOptions{ RepoIDs: repoIDs, - Keyword: keyword, - IsKeywordFuzzy: isFuzzy, - Language: language, + Keyword: prepareSearch.Keyword, + IsKeywordFuzzy: prepareSearch.IsFuzzy, + Language: prepareSearch.Language, Paginator: &db.ListOptions{ Page: page, PageSize: setting.UI.RepoSearchPagingNum, diff --git a/routers/web/user/home.go b/routers/web/user/home.go index c79648a4559..ff9334da6e2 100644 --- a/routers/web/user/home.go +++ b/routers/web/user/home.go @@ -41,8 +41,8 @@ import ( issue_service "code.gitea.io/gitea/services/issue" pull_service "code.gitea.io/gitea/services/pull" - "github.com/keybase/go-crypto/openpgp" - "github.com/keybase/go-crypto/openpgp/armor" + "github.com/ProtonMail/go-crypto/openpgp" + "github.com/ProtonMail/go-crypto/openpgp/armor" "xorm.io/builder" ) @@ -576,17 +576,9 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) { // ------------------------------- // Fill stats to post to ctx.Data. // ------------------------------- - issueStats, err := getUserIssueStats(ctx, filterMode, issue_indexer.ToSearchOptions(keyword, opts).Copy( + issueStats, err := getUserIssueStats(ctx, ctxUser, filterMode, issue_indexer.ToSearchOptions(keyword, opts).Copy( func(o *issue_indexer.SearchOptions) { o.IsFuzzyKeyword = isFuzzy - // If the doer is the same as the context user, which means the doer is viewing his own dashboard, - // it's not enough to show the repos that the doer owns or has been explicitly granted access to, - // because the doer may create issues or be mentioned in any public repo. - // So we need search issues in all public repos. - o.AllPublic = ctx.Doer.ID == ctxUser.ID - o.MentionID = nil - o.ReviewRequestedID = nil - o.ReviewedID = nil }, )) if err != nil { @@ -740,7 +732,7 @@ func UsernameSubRoute(ctx *context.Context) { switch { case strings.HasSuffix(username, ".png"): if reloadParam(".png") { - AvatarByUserName(ctx) + AvatarByUsernameSize(ctx) } case strings.HasSuffix(username, ".keys"): if reloadParam(".keys") { @@ -775,10 +767,19 @@ func UsernameSubRoute(ctx *context.Context) { } } -func getUserIssueStats(ctx *context.Context, filterMode int, opts *issue_indexer.SearchOptions) (ret *issues_model.IssueStats, err error) { +func getUserIssueStats(ctx *context.Context, ctxUser *user_model.User, filterMode int, opts *issue_indexer.SearchOptions) (ret *issues_model.IssueStats, err error) { ret = &issues_model.IssueStats{} doerID := ctx.Doer.ID + opts = opts.Copy(func(o *issue_indexer.SearchOptions) { + // If the doer is the same as the context user, which means the doer is viewing his own dashboard, + // it's not enough to show the repos that the doer owns or has been explicitly granted access to, + // because the doer may create issues or be mentioned in any public repo. + // So we need search issues in all public repos. + o.AllPublic = doerID == ctxUser.ID + }) + + // Open/Closed are for the tabs of the issue list { openClosedOpts := opts.Copy() switch filterMode { @@ -809,6 +810,15 @@ func getUserIssueStats(ctx *context.Context, filterMode int, opts *issue_indexer } } + // Below stats are for the left sidebar + opts = opts.Copy(func(o *issue_indexer.SearchOptions) { + o.AssigneeID = nil + o.PosterID = nil + o.MentionID = nil + o.ReviewRequestedID = nil + o.ReviewedID = nil + }) + ret.YourRepositoriesCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.AllPublic = false })) if err != nil { return nil, err diff --git a/routers/web/user/home_test.go b/routers/web/user/home_test.go index 51246551ea2..b2c8ad98ba2 100644 --- a/routers/web/user/home_test.go +++ b/routers/web/user/home_test.go @@ -45,7 +45,7 @@ func TestArchivedIssues(t *testing.T) { Issues(ctx) // Assert: One Issue (ID 30) from one Repo (ID 50) is retrieved, while nothing from archived Repo 51 is retrieved - assert.EqualValues(t, http.StatusOK, ctx.Resp.Status()) + assert.EqualValues(t, http.StatusOK, ctx.Resp.WrittenStatus()) assert.Len(t, ctx.Data["Issues"], 1) } @@ -58,7 +58,7 @@ func TestIssues(t *testing.T) { contexttest.LoadUser(t, ctx, 2) ctx.Req.Form.Set("state", "closed") Issues(ctx) - assert.EqualValues(t, http.StatusOK, ctx.Resp.Status()) + assert.EqualValues(t, http.StatusOK, ctx.Resp.WrittenStatus()) assert.EqualValues(t, true, ctx.Data["IsShowClosed"]) assert.Len(t, ctx.Data["Issues"], 1) @@ -72,7 +72,7 @@ func TestPulls(t *testing.T) { contexttest.LoadUser(t, ctx, 2) ctx.Req.Form.Set("state", "open") Pulls(ctx) - assert.EqualValues(t, http.StatusOK, ctx.Resp.Status()) + assert.EqualValues(t, http.StatusOK, ctx.Resp.WrittenStatus()) assert.Len(t, ctx.Data["Issues"], 5) } @@ -87,7 +87,7 @@ func TestMilestones(t *testing.T) { ctx.Req.Form.Set("state", "closed") ctx.Req.Form.Set("sort", "furthestduedate") Milestones(ctx) - assert.EqualValues(t, http.StatusOK, ctx.Resp.Status()) + assert.EqualValues(t, http.StatusOK, ctx.Resp.WrittenStatus()) assert.EqualValues(t, map[int64]int64{1: 1}, ctx.Data["Counts"]) assert.EqualValues(t, true, ctx.Data["IsShowClosed"]) assert.EqualValues(t, "furthestduedate", ctx.Data["SortType"]) @@ -107,7 +107,7 @@ func TestMilestonesForSpecificRepo(t *testing.T) { ctx.Req.Form.Set("state", "closed") ctx.Req.Form.Set("sort", "furthestduedate") Milestones(ctx) - assert.EqualValues(t, http.StatusOK, ctx.Resp.Status()) + assert.EqualValues(t, http.StatusOK, ctx.Resp.WrittenStatus()) assert.EqualValues(t, map[int64]int64{1: 1}, ctx.Data["Counts"]) assert.EqualValues(t, true, ctx.Data["IsShowClosed"]) assert.EqualValues(t, "furthestduedate", ctx.Data["SortType"]) diff --git a/routers/web/user/profile.go b/routers/web/user/profile.go index 9c014bffdb7..7cda3c038cf 100644 --- a/routers/web/user/profile.go +++ b/routers/web/user/profile.go @@ -74,8 +74,7 @@ func userProfile(ctx *context.Context) { ctx.Data["HeatmapTotalContributions"] = activities_model.GetTotalContributionsInHeatmap(data) } - profileDbRepo, _ /*profileGitRepo*/, profileReadmeBlob, profileClose := shared_user.FindUserProfileReadme(ctx, ctx.Doer) - defer profileClose() + profileDbRepo, profileReadmeBlob := shared_user.FindOwnerProfileReadme(ctx, ctx.Doer) showPrivate := ctx.IsSigned && (ctx.Doer.IsAdmin || ctx.Doer.ID == ctx.ContextUser.ID) prepareUserProfileTabData(ctx, showPrivate, profileDbRepo, profileReadmeBlob) @@ -96,7 +95,7 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb } } ctx.Data["TabName"] = tab - ctx.Data["HasProfileReadme"] = profileReadme != nil + ctx.Data["HasUserProfileReadme"] = profileReadme != nil page := ctx.FormInt("page") if page <= 0 { @@ -254,7 +253,7 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb if profileContent, err := markdown.RenderString(rctx, bytes); err != nil { log.Error("failed to RenderString: %v", err) } else { - ctx.Data["ProfileReadme"] = profileContent + ctx.Data["ProfileReadmeContent"] = profileContent } } case "organizations": @@ -314,8 +313,8 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb ctx.Data["Page"] = pager } -// Action response for follow/unfollow user request -func Action(ctx *context.Context) { +// ActionUserFollow is for follow/unfollow user request +func ActionUserFollow(ctx *context.Context) { var err error switch ctx.FormString("action") { case "follow": @@ -340,6 +339,6 @@ func Action(ctx *context.Context) { ctx.HTML(http.StatusOK, tplFollowUnfollow) return } - log.Error("Failed to apply action %q: unsupport context user type: %s", ctx.FormString("action"), ctx.ContextUser.Type) + log.Error("Failed to apply action %q: unsupported context user type: %s", ctx.FormString("action"), ctx.ContextUser.Type) ctx.Error(http.StatusBadRequest, fmt.Sprintf("Action %q failed", ctx.FormString("action"))) } diff --git a/routers/web/user/setting/account_test.go b/routers/web/user/setting/account_test.go index 9fdc5e4d532..13caa337713 100644 --- a/routers/web/user/setting/account_test.go +++ b/routers/web/user/setting/account_test.go @@ -95,7 +95,7 @@ func TestChangePassword(t *testing.T) { AccountPost(ctx) assert.Contains(t, ctx.Flash.ErrorMsg, req.Message) - assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status()) + assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus()) }) } } diff --git a/routers/web/web.go b/routers/web/web.go index 9c3819ad95d..13b9cf91784 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -11,6 +11,7 @@ import ( "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/perm" "code.gitea.io/gitea/models/unit" + "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/metrics" "code.gitea.io/gitea/modules/public" @@ -101,7 +102,7 @@ func buildAuthGroup() *auth_service.Group { group.Add(&auth_service.Basic{}) // FIXME: this should be removed and only applied in download and git/lfs routers if setting.Service.EnableReverseProxyAuth { - group.Add(&auth_service.ReverseProxy{}) // reverseproxy should before Session, otherwise the header will be ignored if user has login + group.Add(&auth_service.ReverseProxy{}) // reverse-proxy should before Session, otherwise the header will be ignored if user has login } group.Add(&auth_service.Session{}) @@ -117,7 +118,7 @@ func webAuth(authMethod auth_service.Method) func(*context.Context) { ar, err := common.AuthShared(ctx.Base, ctx.Session, authMethod) if err != nil { log.Error("Failed to verify user: %v", err) - ctx.Error(http.StatusUnauthorized, "Verify") + ctx.Error(http.StatusUnauthorized, "Failed to authenticate user") return } ctx.Doer = ar.Doer @@ -346,6 +347,13 @@ func registerRoutes(m *web.Router) { } } + starsEnabled := func(ctx *context.Context) { + if setting.Repository.DisableStars { + ctx.Error(http.StatusForbidden) + return + } + } + lfsServerEnabled := func(ctx *context.Context) { if !setting.LFS.StartServer { ctx.Error(http.StatusNotFound) @@ -680,7 +688,7 @@ func registerRoutes(m *web.Router) { m.Get("/activate", auth.Activate) m.Post("/activate", auth.ActivatePost) m.Any("/activate_email", auth.ActivateEmail) - m.Get("/avatar/{username}/{size}", user.AvatarByUserName) + m.Get("/avatar/{username}/{size}", user.AvatarByUsernameSize) m.Get("/recover_account", auth.ResetPasswd) m.Post("/recover_account", auth.ResetPasswdPost) m.Get("/forgot_password", auth.ForgotPasswd) @@ -719,6 +727,7 @@ func registerRoutes(m *web.Router) { m.Group("/monitor", func() { m.Get("/stats", admin.MonitorStats) m.Get("/cron", admin.CronTasks) + m.Get("/perftrace", admin.PerfTrace) m.Get("/stacktrace", admin.Stacktrace) m.Post("/stacktrace/cancel/{pid}", admin.StacktraceCancel) m.Get("/queue", admin.Queues) @@ -813,24 +822,26 @@ func registerRoutes(m *web.Router) { m.Methods("GET, OPTIONS", "/attachments/{uuid}", optionsCorsHandler(), repo.GetAttachment) }, optSignIn) - m.Post("/{username}", reqSignIn, context.UserAssignmentWeb(), user.Action) + m.Post("/{username}", reqSignIn, context.UserAssignmentWeb(), user.ActionUserFollow) reqRepoAdmin := context.RequireRepoAdmin() - reqRepoCodeWriter := context.RequireRepoWriter(unit.TypeCode) - canEnableEditor := context.CanEnableEditor() - reqRepoCodeReader := context.RequireRepoReader(unit.TypeCode) - reqRepoReleaseWriter := context.RequireRepoWriter(unit.TypeReleases) - reqRepoReleaseReader := context.RequireRepoReader(unit.TypeReleases) - reqRepoWikiReader := context.RequireRepoReader(unit.TypeWiki) - reqRepoWikiWriter := context.RequireRepoWriter(unit.TypeWiki) - reqRepoIssueReader := context.RequireRepoReader(unit.TypeIssues) - reqRepoPullsReader := context.RequireRepoReader(unit.TypePullRequests) - reqRepoIssuesOrPullsWriter := context.RequireRepoWriterOr(unit.TypeIssues, unit.TypePullRequests) - reqRepoIssuesOrPullsReader := context.RequireRepoReaderOr(unit.TypeIssues, unit.TypePullRequests) - reqRepoProjectsReader := context.RequireRepoReader(unit.TypeProjects) - reqRepoProjectsWriter := context.RequireRepoWriter(unit.TypeProjects) - reqRepoActionsReader := context.RequireRepoReader(unit.TypeActions) - reqRepoActionsWriter := context.RequireRepoWriter(unit.TypeActions) + reqRepoCodeWriter := context.RequireUnitWriter(unit.TypeCode) + reqRepoReleaseWriter := context.RequireUnitWriter(unit.TypeReleases) + reqRepoReleaseReader := context.RequireUnitReader(unit.TypeReleases) + reqRepoIssuesOrPullsWriter := context.RequireUnitWriter(unit.TypeIssues, unit.TypePullRequests) + reqRepoIssuesOrPullsReader := context.RequireUnitReader(unit.TypeIssues, unit.TypePullRequests) + reqRepoProjectsReader := context.RequireUnitReader(unit.TypeProjects) + reqRepoProjectsWriter := context.RequireUnitWriter(unit.TypeProjects) + reqRepoActionsReader := context.RequireUnitReader(unit.TypeActions) + reqRepoActionsWriter := context.RequireUnitWriter(unit.TypeActions) + + // the legacy names "reqRepoXxx" should be renamed to the correct name "reqUnitXxx", these permissions are for units, not repos + reqUnitsWithMarkdown := context.RequireUnitReader(unit.TypeCode, unit.TypeIssues, unit.TypePullRequests, unit.TypeReleases, unit.TypeWiki) + reqUnitCodeReader := context.RequireUnitReader(unit.TypeCode) + reqUnitIssuesReader := context.RequireUnitReader(unit.TypeIssues) + reqUnitPullsReader := context.RequireUnitReader(unit.TypePullRequests) + reqUnitWikiReader := context.RequireUnitReader(unit.TypeWiki) + reqUnitWikiWriter := context.RequireUnitWriter(unit.TypeWiki) reqPackageAccess := func(accessMode perm.AccessMode) func(ctx *context.Context) { return func(ctx *context.Context) { @@ -861,7 +872,7 @@ func registerRoutes(m *web.Router) { m.Group("/org", func() { m.Group("/{org}", func() { m.Get("/members", org.Members) - }, context.OrgAssignment()) + }, context.OrgAssignment(context.OrgAssignmentOptions{})) }, optSignIn) // end "/org": members @@ -887,19 +898,20 @@ func registerRoutes(m *web.Router) { m.Get("/milestones/{team}", reqMilestonesDashboardPageEnabled, user.Milestones) m.Post("/members/action/{action}", org.MembersAction) m.Get("/teams", org.Teams) - }, context.OrgAssignment(true, false, true)) + }, context.OrgAssignment(context.OrgAssignmentOptions{RequireMember: true, RequireTeamMember: true})) m.Group("/{org}", func() { m.Get("/teams/{team}", org.TeamMembers) m.Get("/teams/{team}/repositories", org.TeamRepositories) m.Post("/teams/{team}/action/{action}", org.TeamsAction) m.Post("/teams/{team}/action/repo/{action}", org.TeamsRepoAction) - }, context.OrgAssignment(true, false, true)) + }, context.OrgAssignment(context.OrgAssignmentOptions{RequireMember: true, RequireTeamMember: true})) - // require admin permission + // require member/team-admin permission (old logic is: requireMember=true, requireTeamAdmin=true) + // but it doesn't seem right: requireTeamAdmin does nothing m.Group("/{org}", func() { m.Get("/teams/-/search", org.SearchTeam) - }, context.OrgAssignment(true, false, false, true)) + }, context.OrgAssignment(context.OrgAssignmentOptions{RequireMember: true, RequireTeamAdmin: true})) // require owner permission m.Group("/{org}", func() { @@ -909,6 +921,8 @@ func registerRoutes(m *web.Router) { m.Post("/teams/{team}/edit", web.Bind(forms.CreateTeamForm{}), org.EditTeamPost) m.Post("/teams/{team}/delete", org.DeleteTeam) + m.Get("/worktime", context.OrgAssignment(context.OrgAssignmentOptions{RequireOwner: true}), org.Worktime) + m.Group("/settings", func() { m.Combo("").Get(org.Settings). Post(web.Bind(forms.UpdateOrgSettingForm{}), org.SettingsPost) @@ -976,7 +990,7 @@ func registerRoutes(m *web.Router) { m.Post("", web.Bind(forms.BlockUserForm{}), org.BlockedUsersPost) }) }, ctxDataSet("EnableOAuth2", setting.OAuth2.Enabled, "EnablePackages", setting.Packages.Enabled, "PageIsOrgSettings", true)) - }, context.OrgAssignment(true, true)) + }, context.OrgAssignment(context.OrgAssignmentOptions{RequireOwner: true})) }, reqSignIn) // end "/org": most org routes @@ -1046,14 +1060,14 @@ func registerRoutes(m *web.Router) { m.Group("", func() { m.Get("/code", user.CodeSearch) }, reqUnitAccess(unit.TypeCode, perm.AccessModeRead, false), individualPermsChecker) - }, optSignIn, context.UserAssignmentWeb(), context.OrgAssignment()) + }, optSignIn, context.UserAssignmentWeb(), context.OrgAssignment(context.OrgAssignmentOptions{})) // end "/{username}/-": packages, projects, code m.Group("/{username}/{reponame}/-", func() { m.Group("/migrate", func() { m.Get("/status", repo.MigrateStatus) }) - }, optSignIn, context.RepoAssignment, reqRepoCodeReader) + }, optSignIn, context.RepoAssignment, reqUnitCodeReader) // end "/{username}/{reponame}/-": migrate m.Group("/{username}/{reponame}/settings", func() { @@ -1143,62 +1157,60 @@ func registerRoutes(m *web.Router) { m.Post("/cancel", repo.MigrateCancelPost) }) }, - reqSignIn, context.RepoAssignment, reqRepoAdmin, context.RepoRef(), + reqSignIn, context.RepoAssignment, reqRepoAdmin, ctxDataSet("PageIsRepoSettings", true, "LFSStartServer", setting.LFS.StartServer), ) // end "/{username}/{reponame}/settings" - // user/org home, including rss feeds - m.Get("/{username}/{reponame}", optSignIn, context.RepoAssignment, context.RepoRef(), repo.SetEditorconfigIfExists, repo.Home) + // user/org home, including rss feeds like "/{username}/{reponame}.rss" + m.Get("/{username}/{reponame}", optSignIn, context.RepoAssignment, context.RepoRefByType(git.RefTypeBranch), repo.SetEditorconfigIfExists, repo.Home) - // TODO: maybe it should relax the permission to allow "any access" - m.Post("/{username}/{reponame}/markup", optSignIn, context.RepoAssignment, context.RequireRepoReaderOr(unit.TypeCode, unit.TypeIssues, unit.TypePullRequests, unit.TypeReleases, unit.TypeWiki), web.Bind(structs.MarkupOption{}), misc.Markup) + m.Post("/{username}/{reponame}/markup", optSignIn, context.RepoAssignment, reqUnitsWithMarkdown, web.Bind(structs.MarkupOption{}), misc.Markup) m.Group("/{username}/{reponame}", func() { m.Get("/find/*", repo.FindFiles) m.Group("/tree-list", func() { - m.Get("/branch/*", context.RepoRefByType(context.RepoRefBranch), repo.TreeList) - m.Get("/tag/*", context.RepoRefByType(context.RepoRefTag), repo.TreeList) - m.Get("/commit/*", context.RepoRefByType(context.RepoRefCommit), repo.TreeList) + m.Get("/branch/*", context.RepoRefByType(git.RefTypeBranch), repo.TreeList) + m.Get("/tag/*", context.RepoRefByType(git.RefTypeTag), repo.TreeList) + m.Get("/commit/*", context.RepoRefByType(git.RefTypeCommit), repo.TreeList) }) m.Get("/compare", repo.MustBeNotEmpty, repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.CompareDiff) m.Combo("/compare/*", repo.MustBeNotEmpty, repo.SetEditorconfigIfExists). Get(repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.CompareDiff). - Post(reqSignIn, context.RepoMustNotBeArchived(), reqRepoPullsReader, repo.MustAllowPulls, web.Bind(forms.CreateIssueForm{}), repo.SetWhitespaceBehavior, repo.CompareAndPullRequestPost) - }, optSignIn, context.RepoAssignment, reqRepoCodeReader) + Post(reqSignIn, context.RepoMustNotBeArchived(), reqUnitPullsReader, repo.MustAllowPulls, web.Bind(forms.CreateIssueForm{}), repo.SetWhitespaceBehavior, repo.CompareAndPullRequestPost) + }, optSignIn, context.RepoAssignment, reqUnitCodeReader) // end "/{username}/{reponame}": repo code: find, compare, list + addIssuesPullsViewRoutes := func() { + // for /{username}/{reponame}/issues" or "/{username}/{reponame}/pulls" + m.Get("/posters", repo.IssuePullPosters) + m.Group("/{index}", func() { + m.Get("/info", repo.GetIssueInfo) + m.Get("/attachments", repo.GetIssueAttachments) + m.Get("/attachments/{uuid}", repo.GetAttachment) + m.Group("/content-history", func() { + m.Get("/overview", repo.GetContentHistoryOverview) + m.Get("/list", repo.GetContentHistoryList) + m.Get("/detail", repo.GetContentHistoryDetail) + }) + }) + } m.Group("/{username}/{reponame}", func() { - m.Get("/issues/posters", repo.IssuePosters) // it can't use {type:issues|pulls} because it would conflict with other routes like "/pulls/{index}" - m.Get("/pulls/posters", repo.PullPosters) m.Get("/comments/{id}/attachments", repo.GetCommentAttachments) m.Get("/labels", repo.RetrieveLabelsForList, repo.Labels) m.Get("/milestones", repo.Milestones) m.Get("/milestone/{id}", context.RepoRef(), repo.MilestoneIssuesAndPulls) - m.Group("/{type:issues|pulls}", func() { - m.Group("/{index}", func() { - m.Get("/info", repo.GetIssueInfo) - m.Get("/attachments", repo.GetIssueAttachments) - m.Get("/attachments/{uuid}", repo.GetAttachment) - m.Group("/content-history", func() { - m.Get("/overview", repo.GetContentHistoryOverview) - m.Get("/list", repo.GetContentHistoryList) - m.Get("/detail", repo.GetContentHistoryDetail) - }) - }) - }, context.RepoRef()) m.Get("/issues/suggestions", repo.IssueSuggestions) - }, optSignIn, context.RepoAssignment, reqRepoIssuesOrPullsReader) + }, optSignIn, context.RepoAssignment, reqRepoIssuesOrPullsReader) // issue/pull attachments, labels, milestones + + m.Group("/{username}/{reponame}/{type:issues}", addIssuesPullsViewRoutes, optSignIn, context.RepoAssignment, reqUnitIssuesReader) + m.Group("/{username}/{reponame}/{type:pulls}", addIssuesPullsViewRoutes, optSignIn, context.RepoAssignment, reqUnitPullsReader) // end "/{username}/{reponame}": view milestone, label, issue, pull, etc - m.Group("/{username}/{reponame}", func() { - m.Group("/{type:issues|pulls}", func() { - m.Get("", repo.Issues) - m.Group("/{index}", func() { - m.Get("", repo.ViewIssue) - }) - }) - }, optSignIn, context.RepoAssignment, context.RequireRepoReaderOr(unit.TypeIssues, unit.TypePullRequests, unit.TypeExternalTracker)) + m.Group("/{username}/{reponame}/{type:issues}", func() { + m.Get("", repo.Issues) + m.Get("/{index}", repo.ViewIssue) + }, optSignIn, context.RepoAssignment, context.RequireUnitReader(unit.TypeIssues, unit.TypeExternalTracker)) // end "/{username}/{reponame}": issue/pull list, issue/pull view, external tracker m.Group("/{username}/{reponame}", func() { // edit issues, pulls, labels, milestones, etc @@ -1209,11 +1221,10 @@ func registerRoutes(m *web.Router) { m.Get("/choose", context.RepoRef(), repo.NewIssueChooseTemplate) }) m.Get("/search", repo.SearchRepoIssuesJSON) - }, context.RepoMustNotBeArchived(), reqRepoIssueReader) + }, reqUnitIssuesReader) - // FIXME: should use different URLs but mostly same logic for comments of issue and pull request. - // So they can apply their own enable/disable logic on routers. - m.Group("/{type:issues|pulls}", func() { + addIssuesPullsRoutes := func() { + // for "/{username}/{reponame}/issues" or "/{username}/{reponame}/pulls" m.Group("/{index}", func() { m.Post("/title", repo.UpdateIssueTitle) m.Post("/content", repo.UpdateIssueContent) @@ -1240,40 +1251,38 @@ func registerRoutes(m *web.Router) { m.Post("/lock", reqRepoIssuesOrPullsWriter, web.Bind(forms.IssueLockForm{}), repo.LockIssue) m.Post("/unlock", reqRepoIssuesOrPullsWriter, repo.UnlockIssue) m.Post("/delete", reqRepoAdmin, repo.DeleteIssue) + m.Post("/content-history/soft-delete", repo.SoftDeleteContentHistory) m.Post("/create_branch", web.Bind(forms.NewBranchForm{}), repo.CreateBranchFromIssue) }, context.RepoMustNotBeArchived()) - m.Group("/{index}", func() { - m.Post("/content-history/soft-delete", repo.SoftDeleteContentHistory) - }) + m.Post("/attachments", repo.UploadIssueAttachment) + m.Post("/attachments/remove", repo.DeleteAttachment) m.Post("/labels", reqRepoIssuesOrPullsWriter, repo.UpdateIssueLabel) m.Post("/milestone", reqRepoIssuesOrPullsWriter, repo.UpdateIssueMilestone) m.Post("/projects", reqRepoIssuesOrPullsWriter, reqRepoProjectsReader, repo.UpdateIssueProject) m.Post("/assignee", reqRepoIssuesOrPullsWriter, repo.UpdateIssueAssignee) - m.Post("/request_review", repo.UpdatePullReviewRequest) - m.Post("/dismiss_review", reqRepoAdmin, web.Bind(forms.DismissReviewForm{}), repo.DismissReview) m.Post("/status", reqRepoIssuesOrPullsWriter, repo.UpdateIssueStatus) m.Post("/delete", reqRepoAdmin, repo.BatchDeleteIssues) - m.Post("/resolve_conversation", repo.SetShowOutdatedComments, repo.UpdateResolveConversation) - m.Post("/attachments", repo.UploadIssueAttachment) - m.Post("/attachments/remove", repo.DeleteAttachment) m.Delete("/unpin/{index}", reqRepoAdmin, repo.IssueUnpin) m.Post("/move_pin", reqRepoAdmin, repo.IssuePinMove) - }, context.RepoMustNotBeArchived()) + } + m.Group("/{type:issues}", addIssuesPullsRoutes, reqUnitIssuesReader, context.RepoMustNotBeArchived()) + m.Group("/{type:pulls}", addIssuesPullsRoutes, reqUnitPullsReader, context.RepoMustNotBeArchived()) m.Group("/comments/{id}", func() { m.Post("", repo.UpdateCommentContent) m.Post("/delete", repo.DeleteComment) m.Post("/reactions/{action}", web.Bind(forms.ReactionForm{}), repo.ChangeCommentReaction) - }, context.RepoMustNotBeArchived()) + }, reqRepoIssuesOrPullsReader) // edit issue/pull comment m.Group("/labels", func() { m.Post("/new", web.Bind(forms.CreateLabelForm{}), repo.NewLabel) m.Post("/edit", web.Bind(forms.CreateLabelForm{}), repo.UpdateLabel) m.Post("/delete", repo.DeleteLabel) m.Post("/initialize", web.Bind(forms.InitializeLabelsForm{}), repo.InitializeLabels) - }, context.RepoMustNotBeArchived(), reqRepoIssuesOrPullsWriter, context.RepoRef()) + }, reqRepoIssuesOrPullsWriter, context.RepoRef()) + m.Group("/milestones", func() { m.Combo("/new").Get(repo.NewMilestone). Post(web.Bind(forms.CreateMilestoneForm{}), repo.NewMilestonePost) @@ -1281,42 +1290,46 @@ func registerRoutes(m *web.Router) { m.Post("/{id}/edit", web.Bind(forms.CreateMilestoneForm{}), repo.EditMilestonePost) m.Post("/{id}/{action}", repo.ChangeMilestoneStatus) m.Post("/delete", repo.DeleteMilestone) - }, context.RepoMustNotBeArchived(), reqRepoIssuesOrPullsWriter, context.RepoRef()) - m.Group("/pull", func() { - m.Post("/{index}/target_branch", repo.UpdatePullRequestTarget) - }, context.RepoMustNotBeArchived()) - }, reqSignIn, context.RepoAssignment, reqRepoIssuesOrPullsReader) + }, reqRepoIssuesOrPullsWriter, context.RepoRef()) + + // FIXME: need to move these routes to the proper place + m.Group("/issues", func() { + m.Post("/request_review", repo.UpdatePullReviewRequest) + m.Post("/dismiss_review", reqRepoAdmin, web.Bind(forms.DismissReviewForm{}), repo.DismissReview) + m.Post("/resolve_conversation", repo.SetShowOutdatedComments, repo.UpdateResolveConversation) + }, reqUnitPullsReader) + m.Post("/pull/{index}/target_branch", reqUnitPullsReader, repo.UpdatePullRequestTarget) + }, reqSignIn, context.RepoAssignment, context.RepoMustNotBeArchived()) // end "/{username}/{reponame}": create or edit issues, pulls, labels, milestones m.Group("/{username}/{reponame}", func() { // repo code m.Group("", func() { m.Group("", func() { + m.Post("/_preview/*", web.Bind(forms.EditPreviewDiffForm{}), repo.DiffPreviewPost) m.Combo("/_edit/*").Get(repo.EditFile). Post(web.Bind(forms.EditRepoFileForm{}), repo.EditFilePost) m.Combo("/_new/*").Get(repo.NewFile). Post(web.Bind(forms.EditRepoFileForm{}), repo.NewFilePost) - m.Post("/_preview/*", web.Bind(forms.EditPreviewDiffForm{}), repo.DiffPreviewPost) m.Combo("/_delete/*").Get(repo.DeleteFile). Post(web.Bind(forms.DeleteRepoFileForm{}), repo.DeleteFilePost) - m.Combo("/_upload/*", repo.MustBeAbleToUpload). - Get(repo.UploadFile). + m.Combo("/_upload/*", repo.MustBeAbleToUpload).Get(repo.UploadFile). Post(web.Bind(forms.UploadRepoFileForm{}), repo.UploadFilePost) m.Combo("/_diffpatch/*").Get(repo.NewDiffPatch). Post(web.Bind(forms.EditRepoFileForm{}), repo.NewDiffPatchPost) m.Combo("/_cherrypick/{sha:([a-f0-9]{7,64})}/*").Get(repo.CherryPick). Post(web.Bind(forms.CherryPickForm{}), repo.CherryPickPost) - }, repo.MustBeEditable) + }, context.RepoRefByType(git.RefTypeBranch), context.CanWriteToBranch(), repo.WebGitOperationCommonData) m.Group("", func() { m.Post("/upload-file", repo.UploadFileToServer) m.Post("/upload-remove", web.Bind(forms.RemoveUploadFileForm{}), repo.RemoveUploadFileFromServer) - }, repo.MustBeEditable, repo.MustBeAbleToUpload) - }, context.RepoRef(), canEnableEditor, context.RepoMustNotBeArchived()) + }, repo.MustBeAbleToUpload, reqRepoCodeWriter) + }, repo.MustBeEditable, context.RepoMustNotBeArchived()) m.Group("/branches", func() { m.Group("/_new", func() { - m.Post("/branch/*", context.RepoRefByType(context.RepoRefBranch), repo.CreateBranch) - m.Post("/tag/*", context.RepoRefByType(context.RepoRefTag), repo.CreateBranch) - m.Post("/commit/*", context.RepoRefByType(context.RepoRefCommit), repo.CreateBranch) + m.Post("/branch/*", context.RepoRefByType(git.RefTypeBranch), repo.CreateBranch) + m.Post("/tag/*", context.RepoRefByType(git.RefTypeTag), repo.CreateBranch) + m.Post("/commit/*", context.RepoRefByType(git.RefTypeCommit), repo.CreateBranch) }, web.Bind(forms.NewBranchForm{})) m.Post("/delete", repo.DeleteBranchPost) m.Post("/restore", repo.RestoreBranchPost) @@ -1325,45 +1338,42 @@ func registerRoutes(m *web.Router) { }, context.RepoMustNotBeArchived(), reqRepoCodeWriter, repo.MustBeNotEmpty) m.Combo("/fork").Get(repo.Fork).Post(web.Bind(forms.CreateRepoForm{}), repo.ForkPost) - }, reqSignIn, context.RepoAssignment, reqRepoCodeReader) + }, reqSignIn, context.RepoAssignment, reqUnitCodeReader) // end "/{username}/{reponame}": repo code m.Group("/{username}/{reponame}", func() { // repo tags m.Group("/tags", func() { - m.Get("", repo.TagsList) - m.Get("/list", repo.GetTagList) + m.Get("", context.RepoRefByDefaultBranch() /* for the "commits" tab */, repo.TagsList) m.Get(".rss", feedEnabled, repo.TagsListFeedRSS) m.Get(".atom", feedEnabled, repo.TagsListFeedAtom) - }, ctxDataSet("EnableFeed", setting.Other.EnableFeed), - repo.MustBeNotEmpty, context.RepoRefByType(context.RepoRefTag, context.RepoRefByTypeOptions{IgnoreNotExistErr: true})) - m.Post("/tags/delete", repo.DeleteTag, reqSignIn, - repo.MustBeNotEmpty, context.RepoMustNotBeArchived(), reqRepoCodeWriter, context.RepoRef()) - }, optSignIn, context.RepoAssignment, reqRepoCodeReader) + m.Get("/list", repo.GetTagList) + }, ctxDataSet("EnableFeed", setting.Other.EnableFeed)) + m.Post("/tags/delete", reqSignIn, reqRepoCodeWriter, context.RepoMustNotBeArchived(), repo.DeleteTag) + }, optSignIn, context.RepoAssignment, repo.MustBeNotEmpty, reqUnitCodeReader) // end "/{username}/{reponame}": repo tags m.Group("/{username}/{reponame}", func() { // repo releases m.Group("/releases", func() { m.Get("", repo.Releases) - m.Get("/tag/*", repo.SingleRelease) - m.Get("/latest", repo.LatestRelease) m.Get(".rss", feedEnabled, repo.ReleasesFeedRSS) m.Get(".atom", feedEnabled, repo.ReleasesFeedAtom) - }, ctxDataSet("EnableFeed", setting.Other.EnableFeed), - repo.MustBeNotEmpty, context.RepoRefByType(context.RepoRefTag, context.RepoRefByTypeOptions{IgnoreNotExistErr: true})) - m.Get("/releases/attachments/{uuid}", repo.MustBeNotEmpty, repo.GetAttachment) - m.Get("/releases/download/{vTag}/{fileName}", repo.MustBeNotEmpty, repo.RedirectDownload) + m.Get("/tag/*", repo.SingleRelease) + m.Get("/latest", repo.LatestRelease) + }, ctxDataSet("EnableFeed", setting.Other.EnableFeed)) + m.Get("/releases/attachments/{uuid}", repo.GetAttachment) + m.Get("/releases/download/{vTag}/{fileName}", repo.RedirectDownload) m.Group("/releases", func() { m.Get("/new", repo.NewRelease) m.Post("/new", web.Bind(forms.NewReleaseForm{}), repo.NewReleasePost) m.Post("/delete", repo.DeleteRelease) m.Post("/attachments", repo.UploadReleaseAttachment) m.Post("/attachments/remove", repo.DeleteAttachment) - }, reqSignIn, repo.MustBeNotEmpty, context.RepoMustNotBeArchived(), reqRepoReleaseWriter, context.RepoRef()) + }, reqSignIn, context.RepoMustNotBeArchived(), reqRepoReleaseWriter, context.RepoRef()) m.Group("/releases", func() { m.Get("/edit/*", repo.EditRelease) m.Post("/edit/*", web.Bind(forms.EditReleaseForm{}), repo.EditReleasePost) - }, reqSignIn, repo.MustBeNotEmpty, context.RepoMustNotBeArchived(), reqRepoReleaseWriter, repo.CommitInfoCache) - }, optSignIn, context.RepoAssignment, reqRepoReleaseReader) + }, reqSignIn, context.RepoMustNotBeArchived(), reqRepoReleaseWriter, repo.CommitInfoCache) + }, optSignIn, context.RepoAssignment, repo.MustBeNotEmpty, reqRepoReleaseReader) // end "/{username}/{reponame}": repo releases m.Group("/{username}/{reponame}", func() { // to maintain compatibility with old attachments @@ -1413,6 +1423,7 @@ func registerRoutes(m *web.Router) { m.Post("/disable", reqRepoAdmin, actions.DisableWorkflowFile) m.Post("/enable", reqRepoAdmin, actions.EnableWorkflowFile) m.Post("/run", reqRepoActionsWriter, actions.Run) + m.Get("/workflow-dispatch-inputs", reqRepoActionsWriter, actions.WorkflowDispatchInputs) m.Group("/runs/{run}", func() { m.Combo(""). @@ -1428,55 +1439,60 @@ func registerRoutes(m *web.Router) { m.Post("/cancel", reqRepoActionsWriter, actions.Cancel) m.Post("/approve", reqRepoActionsWriter, actions.Approve) m.Get("/artifacts/{artifact_name}", actions.ArtifactsDownloadView) - m.Delete("/artifacts/{artifact_name}", actions.ArtifactsDeleteView) + m.Delete("/artifacts/{artifact_name}", reqRepoActionsWriter, actions.ArtifactsDeleteView) m.Post("/rerun", reqRepoActionsWriter, actions.Rerun) }) m.Group("/workflows/{workflow_name}", func() { m.Get("/badge.svg", actions.GetWorkflowBadge) }) - }, optSignIn, context.RepoAssignment, reqRepoActionsReader, actions.MustEnableActions) + }, optSignIn, context.RepoAssignment, repo.MustBeNotEmpty, reqRepoActionsReader, actions.MustEnableActions) // end "/{username}/{reponame}/actions" m.Group("/{username}/{reponame}/wiki", func() { m.Combo(""). Get(repo.Wiki). - Post(context.RepoMustNotBeArchived(), reqSignIn, reqRepoWikiWriter, web.Bind(forms.NewWikiForm{}), repo.WikiPost) + Post(context.RepoMustNotBeArchived(), reqSignIn, reqUnitWikiWriter, web.Bind(forms.NewWikiForm{}), repo.WikiPost) m.Combo("/*"). Get(repo.Wiki). - Post(context.RepoMustNotBeArchived(), reqSignIn, reqRepoWikiWriter, web.Bind(forms.NewWikiForm{}), repo.WikiPost) + Post(context.RepoMustNotBeArchived(), reqSignIn, reqUnitWikiWriter, web.Bind(forms.NewWikiForm{}), repo.WikiPost) m.Get("/blob_excerpt/{sha}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.ExcerptBlob) m.Get("/commit/{sha:[a-f0-9]{7,64}}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.Diff) m.Get("/commit/{sha:[a-f0-9]{7,64}}.{ext:patch|diff}", repo.RawDiff) m.Get("/raw/*", repo.WikiRaw) - }, optSignIn, context.RepoAssignment, repo.MustEnableWiki, reqRepoWikiReader, func(ctx *context.Context) { + }, optSignIn, context.RepoAssignment, repo.MustEnableWiki, reqUnitWikiReader, func(ctx *context.Context) { ctx.Data["PageIsWiki"] = true - ctx.Data["CloneButtonOriginLink"] = ctx.Repo.Repository.WikiCloneLink() + ctx.Data["CloneButtonOriginLink"] = ctx.Repo.Repository.WikiCloneLink(ctx, ctx.Doer) }) // end "/{username}/{reponame}/wiki" m.Group("/{username}/{reponame}/activity", func() { + // activity has its own permission checks m.Get("", repo.Activity) m.Get("/{period}", repo.Activity) - m.Group("/contributors", func() { - m.Get("", repo.Contributors) - m.Get("/data", repo.ContributorsData) - }) - m.Group("/code-frequency", func() { - m.Get("", repo.CodeFrequency) - m.Get("/data", repo.CodeFrequencyData) - }) - m.Group("/recent-commits", func() { - m.Get("", repo.RecentCommits) - m.Get("/data", repo.RecentCommitsData) - }) + + m.Group("", func() { + m.Group("/contributors", func() { + m.Get("", repo.Contributors) + m.Get("/data", repo.ContributorsData) + }) + m.Group("/code-frequency", func() { + m.Get("", repo.CodeFrequency) + m.Get("/data", repo.CodeFrequencyData) + }) + m.Group("/recent-commits", func() { + m.Get("", repo.RecentCommits) + m.Get("/data", repo.RecentCommitsData) + }) + }, reqUnitCodeReader) }, - optSignIn, context.RepoAssignment, context.RequireRepoReaderOr(unit.TypePullRequests, unit.TypeIssues, unit.TypeReleases), - context.RepoRef(), repo.MustBeNotEmpty, + optSignIn, context.RepoAssignment, repo.MustBeNotEmpty, + context.RequireUnitReader(unit.TypeCode, unit.TypeIssues, unit.TypePullRequests, unit.TypeReleases), ) // end "/{username}/{reponame}/activity" m.Group("/{username}/{reponame}", func() { - m.Group("/pulls/{index}", func() { + m.Get("/{type:pulls}", repo.Issues) + m.Group("/{type:pulls}/{index}", func() { m.Get("", repo.SetWhitespaceBehavior, repo.GetPullDiffStats, repo.ViewIssue) m.Get(".diff", repo.DownloadPullDiff) m.Get(".patch", repo.DownloadPullPatch) @@ -1501,14 +1517,14 @@ func registerRoutes(m *web.Router) { }, context.RepoMustNotBeArchived()) }) }) - }, optSignIn, context.RepoAssignment, repo.MustAllowPulls, reqRepoPullsReader) + }, optSignIn, context.RepoAssignment, repo.MustAllowPulls, reqUnitPullsReader) // end "/{username}/{reponame}/pulls/{index}": repo pull request m.Group("/{username}/{reponame}", func() { m.Group("/activity_author_data", func() { m.Get("", repo.ActivityAuthors) m.Get("/{period}", repo.ActivityAuthors) - }, context.RepoRef(), repo.MustBeNotEmpty) + }, repo.MustBeNotEmpty) m.Group("/archive", func() { m.Get("/*", repo.Download) @@ -1517,46 +1533,43 @@ func registerRoutes(m *web.Router) { m.Group("/branches", func() { m.Get("/list", repo.GetBranchesList) - m.Get("", repo.Branches) - }, repo.MustBeNotEmpty, context.RepoRef()) + m.Get("", context.RepoRefByDefaultBranch() /* for the "commits" tab */, repo.Branches) + }, repo.MustBeNotEmpty) m.Group("/media", func() { - m.Get("/branch/*", context.RepoRefByType(context.RepoRefBranch), repo.SingleDownloadOrLFS) - m.Get("/tag/*", context.RepoRefByType(context.RepoRefTag), repo.SingleDownloadOrLFS) - m.Get("/commit/*", context.RepoRefByType(context.RepoRefCommit), repo.SingleDownloadOrLFS) - m.Get("/blob/{sha}", context.RepoRefByType(context.RepoRefBlob), repo.DownloadByIDOrLFS) - // "/*" route is deprecated, and kept for backward compatibility - m.Get("/*", context.RepoRefByType(context.RepoRefUnknown), repo.SingleDownloadOrLFS) + m.Get("/blob/{sha}", repo.DownloadByIDOrLFS) + m.Get("/branch/*", context.RepoRefByType(git.RefTypeBranch), repo.SingleDownloadOrLFS) + m.Get("/tag/*", context.RepoRefByType(git.RefTypeTag), repo.SingleDownloadOrLFS) + m.Get("/commit/*", context.RepoRefByType(git.RefTypeCommit), repo.SingleDownloadOrLFS) + m.Get("/*", context.RepoRefByType(""), repo.SingleDownloadOrLFS) // "/*" route is deprecated, and kept for backward compatibility }, repo.MustBeNotEmpty) m.Group("/raw", func() { - m.Get("/branch/*", context.RepoRefByType(context.RepoRefBranch), repo.SingleDownload) - m.Get("/tag/*", context.RepoRefByType(context.RepoRefTag), repo.SingleDownload) - m.Get("/commit/*", context.RepoRefByType(context.RepoRefCommit), repo.SingleDownload) - m.Get("/blob/{sha}", context.RepoRefByType(context.RepoRefBlob), repo.DownloadByID) - // "/*" route is deprecated, and kept for backward compatibility - m.Get("/*", context.RepoRefByType(context.RepoRefUnknown), repo.SingleDownload) + m.Get("/blob/{sha}", repo.DownloadByID) + m.Get("/branch/*", context.RepoRefByType(git.RefTypeBranch), repo.SingleDownload) + m.Get("/tag/*", context.RepoRefByType(git.RefTypeTag), repo.SingleDownload) + m.Get("/commit/*", context.RepoRefByType(git.RefTypeCommit), repo.SingleDownload) + m.Get("/*", context.RepoRefByType(""), repo.SingleDownload) // "/*" route is deprecated, and kept for backward compatibility }, repo.MustBeNotEmpty) m.Group("/render", func() { - m.Get("/branch/*", context.RepoRefByType(context.RepoRefBranch), repo.RenderFile) - m.Get("/tag/*", context.RepoRefByType(context.RepoRefTag), repo.RenderFile) - m.Get("/commit/*", context.RepoRefByType(context.RepoRefCommit), repo.RenderFile) - m.Get("/blob/{sha}", context.RepoRefByType(context.RepoRefBlob), repo.RenderFile) + m.Get("/branch/*", context.RepoRefByType(git.RefTypeBranch), repo.RenderFile) + m.Get("/tag/*", context.RepoRefByType(git.RefTypeTag), repo.RenderFile) + m.Get("/commit/*", context.RepoRefByType(git.RefTypeCommit), repo.RenderFile) + m.Get("/blob/{sha}", repo.RenderFile) }, repo.MustBeNotEmpty) m.Group("/commits", func() { - m.Get("/branch/*", context.RepoRefByType(context.RepoRefBranch), repo.RefCommits) - m.Get("/tag/*", context.RepoRefByType(context.RepoRefTag), repo.RefCommits) - m.Get("/commit/*", context.RepoRefByType(context.RepoRefCommit), repo.RefCommits) - // "/*" route is deprecated, and kept for backward compatibility - m.Get("/*", context.RepoRefByType(context.RepoRefUnknown), repo.RefCommits) + m.Get("/branch/*", context.RepoRefByType(git.RefTypeBranch), repo.RefCommits) + m.Get("/tag/*", context.RepoRefByType(git.RefTypeTag), repo.RefCommits) + m.Get("/commit/*", context.RepoRefByType(git.RefTypeCommit), repo.RefCommits) + m.Get("/*", context.RepoRefByType(""), repo.RefCommits) // "/*" route is deprecated, and kept for backward compatibility }, repo.MustBeNotEmpty) m.Group("/blame", func() { - m.Get("/branch/*", context.RepoRefByType(context.RepoRefBranch), repo.RefBlame) - m.Get("/tag/*", context.RepoRefByType(context.RepoRefTag), repo.RefBlame) - m.Get("/commit/*", context.RepoRefByType(context.RepoRefCommit), repo.RefBlame) + m.Get("/branch/*", context.RepoRefByType(git.RefTypeBranch), repo.RefBlame) + m.Get("/tag/*", context.RepoRefByType(git.RefTypeTag), repo.RefBlame) + m.Get("/commit/*", context.RepoRefByType(git.RefTypeCommit), repo.RefBlame) }, repo.MustBeNotEmpty) m.Get("/blob_excerpt/{sha}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.ExcerptBlob) @@ -1565,31 +1578,37 @@ func registerRoutes(m *web.Router) { m.Get("/graph", repo.Graph) m.Get("/commit/{sha:([a-f0-9]{7,64})$}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.Diff) m.Get("/commit/{sha:([a-f0-9]{7,64})$}/load-branches-and-tags", repo.LoadBranchesAndTags) - m.Get("/cherry-pick/{sha:([a-f0-9]{7,64})$}", repo.SetEditorconfigIfExists, repo.CherryPick) - }, repo.MustBeNotEmpty, context.RepoRef()) - m.Get("/rss/branch/*", context.RepoRefByType(context.RepoRefBranch), feedEnabled, feed.RenderBranchFeed) - m.Get("/atom/branch/*", context.RepoRefByType(context.RepoRefBranch), feedEnabled, feed.RenderBranchFeed) + // FIXME: this route `/cherry-pick/{sha}` doesn't seem useful or right, the new code always uses `/_cherrypick/` which could handle branch name correctly + m.Get("/cherry-pick/{sha:([a-f0-9]{7,64})$}", repo.SetEditorconfigIfExists, context.RepoRefByDefaultBranch(), repo.CherryPick) + }, repo.MustBeNotEmpty) + + m.Get("/rss/branch/*", context.RepoRefByType(git.RefTypeBranch), feedEnabled, feed.RenderBranchFeed) + m.Get("/atom/branch/*", context.RepoRefByType(git.RefTypeBranch), feedEnabled, feed.RenderBranchFeed) m.Group("/src", func() { - m.Get("/branch/*", context.RepoRefByType(context.RepoRefBranch), repo.Home) - m.Get("/tag/*", context.RepoRefByType(context.RepoRefTag), repo.Home) - m.Get("/commit/*", context.RepoRefByType(context.RepoRefCommit), repo.Home) - m.Get("/*", context.RepoRefByType(context.RepoRefUnknown), repo.Home) // "/*" route is deprecated, and kept for backward compatibility + m.Get("", func(ctx *context.Context) { ctx.Redirect(ctx.Repo.RepoLink) }) // there is no "{owner}/{repo}/src" page, so redirect to "{owner}/{repo}" to avoid 404 + m.Get("/branch/*", context.RepoRefByType(git.RefTypeBranch), repo.Home) + m.Get("/tag/*", context.RepoRefByType(git.RefTypeTag), repo.Home) + m.Get("/commit/*", context.RepoRefByType(git.RefTypeCommit), repo.Home) + m.Get("/*", context.RepoRefByType(""), repo.Home) // "/*" route is deprecated, and kept for backward compatibility }, repo.SetEditorconfigIfExists) + m.Get("/tree/*", repo.RedirectRepoTreeToSrc) // redirect "/owner/repo/tree/*" requests to "/owner/repo/src/*" m.Get("/forks", context.RepoRef(), repo.Forks) m.Get("/commit/{sha:([a-f0-9]{7,64})}.{ext:patch|diff}", repo.MustBeNotEmpty, repo.RawDiff) - m.Post("/lastcommit/*", context.RepoRefByType(context.RepoRefCommit), repo.LastCommit) - }, optSignIn, context.RepoAssignment, reqRepoCodeReader) + m.Post("/lastcommit/*", context.RepoRefByType(git.RefTypeCommit), repo.LastCommit) + }, optSignIn, context.RepoAssignment, reqUnitCodeReader) // end "/{username}/{reponame}": repo code m.Group("/{username}/{reponame}", func() { - m.Get("/stars", repo.Stars) + m.Get("/stars", starsEnabled, repo.Stars) m.Get("/watchers", repo.Watchers) - m.Get("/search", reqRepoCodeReader, repo.Search) - m.Post("/action/{action}", reqSignIn, repo.Action) - }, optSignIn, context.RepoAssignment, context.RepoRef()) + m.Get("/search", reqUnitCodeReader, repo.Search) + m.Post("/action/{action:star|unstar}", reqSignIn, starsEnabled, repo.ActionStar) + m.Post("/action/{action:watch|unwatch}", reqSignIn, repo.ActionWatch) + m.Post("/action/{action:accept_transfer|reject_transfer}", reqSignIn, repo.ActionTransfer) + }, optSignIn, context.RepoAssignment) common.AddOwnerRepoGitLFSRoutes(m, optSignInIgnoreCsrf, lfsServerEnabled) // "/{username}/{reponame}/{lfs-paths}": git-lfs support @@ -1619,7 +1638,7 @@ func registerRoutes(m *web.Router) { m.NotFound(func(w http.ResponseWriter, req *http.Request) { ctx := context.GetWebContext(req) - routing.UpdateFuncInfo(ctx, routing.GetFuncInfo(ctx.NotFound, "WebNotFound")) + defer routing.RecordFuncInfo(ctx, routing.GetFuncInfo(ctx.NotFound, "WebNotFound"))() ctx.NotFound("", nil) }) } diff --git a/services/actions/cleanup.go b/services/actions/cleanup.go index 1223ebcab63..ee1d1677133 100644 --- a/services/actions/cleanup.go +++ b/services/actions/cleanup.go @@ -52,9 +52,9 @@ func cleanExpiredArtifacts(taskCtx context.Context) error { } if err := storage.ActionsArtifacts.Delete(artifact.StoragePath); err != nil { log.Error("Cannot delete artifact %d: %v", artifact.ID, err) - continue + // go on } - log.Info("Artifact %d set expired", artifact.ID) + log.Info("Artifact %d is deleted (due to expiration)", artifact.ID) } return nil } @@ -76,9 +76,9 @@ func cleanNeedDeleteArtifacts(taskCtx context.Context) error { } if err := storage.ActionsArtifacts.Delete(artifact.StoragePath); err != nil { log.Error("Cannot delete artifact %d: %v", artifact.ID, err) - continue + // go on } - log.Info("Artifact %d set deleted", artifact.ID) + log.Info("Artifact %d is deleted (due to pending deletion)", artifact.ID) } if len(artifacts) < deleteArtifactBatchSize { log.Debug("No more artifacts pending deletion") @@ -103,8 +103,7 @@ func CleanupLogs(ctx context.Context) error { for _, task := range tasks { if err := actions_module.RemoveLogs(ctx, task.LogInStorage, task.LogFilename); err != nil { log.Error("Failed to remove log %s (in storage %v) of task %v: %v", task.LogFilename, task.LogInStorage, task.ID, err) - // do not return error here, continue to next task - continue + // do not return error here, go on } task.LogIndexes = nil // clear log indexes since it's a heavy field task.LogExpired = true diff --git a/services/actions/commit_status.go b/services/actions/commit_status.go index 7f52c9d31b5..94ab89a3b7d 100644 --- a/services/actions/commit_status.go +++ b/services/actions/commit_status.go @@ -54,7 +54,13 @@ func createCommitStatus(ctx context.Context, job *actions_model.ActionRunJob) er return fmt.Errorf("head commit is missing in event payload") } sha = payload.HeadCommit.ID - case webhook_module.HookEventPullRequest, webhook_module.HookEventPullRequestSync: + case // pull_request + webhook_module.HookEventPullRequest, + webhook_module.HookEventPullRequestSync, + webhook_module.HookEventPullRequestAssign, + webhook_module.HookEventPullRequestLabel, + webhook_module.HookEventPullRequestReviewRequest, + webhook_module.HookEventPullRequestMilestone: if run.TriggerEvent == actions_module.GithubEventPullRequestTarget { event = "pull_request_target" } else { diff --git a/services/actions/context.go b/services/actions/context.go new file mode 100644 index 00000000000..d14728fae46 --- /dev/null +++ b/services/actions/context.go @@ -0,0 +1,161 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + "context" + "fmt" + + actions_model "code.gitea.io/gitea/models/actions" + "code.gitea.io/gitea/models/db" + actions_module "code.gitea.io/gitea/modules/actions" + "code.gitea.io/gitea/modules/container" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/setting" +) + +// GenerateGiteaContext generate the gitea context without token and gitea_runtime_token +// job can be nil when generating a context for parsing workflow-level expressions +func GenerateGiteaContext(run *actions_model.ActionRun, job *actions_model.ActionRunJob) map[string]any { + event := map[string]any{} + _ = json.Unmarshal([]byte(run.EventPayload), &event) + + baseRef := "" + headRef := "" + ref := run.Ref + sha := run.CommitSHA + if pullPayload, err := run.GetPullRequestEventPayload(); err == nil && pullPayload.PullRequest != nil && pullPayload.PullRequest.Base != nil && pullPayload.PullRequest.Head != nil { + baseRef = pullPayload.PullRequest.Base.Ref + headRef = pullPayload.PullRequest.Head.Ref + + // if the TriggerEvent is pull_request_target, ref and sha need to be set according to the base of pull request + // In GitHub's documentation, ref should be the branch or tag that triggered workflow. But when the TriggerEvent is pull_request_target, + // the ref will be the base branch. + if run.TriggerEvent == actions_module.GithubEventPullRequestTarget { + ref = git.BranchPrefix + pullPayload.PullRequest.Base.Name + sha = pullPayload.PullRequest.Base.Sha + } + } + + refName := git.RefName(ref) + + gitContext := map[string]any{ + // standard contexts, see https://docs.github.com/en/actions/learn-github-actions/contexts#github-context + "action": "", // string, The name of the action currently running, or the id of a step. GitHub removes special characters, and uses the name __run when the current step runs a script without an id. If you use the same action more than once in the same job, the name will include a suffix with the sequence number with underscore before it. For example, the first script you run will have the name __run, and the second script will be named __run_2. Similarly, the second invocation of actions/checkout will be actionscheckout2. + "action_path": "", // string, The path where an action is located. This property is only supported in composite actions. You can use this path to access files located in the same repository as the action. + "action_ref": "", // string, For a step executing an action, this is the ref of the action being executed. For example, v2. + "action_repository": "", // string, For a step executing an action, this is the owner and repository name of the action. For example, actions/checkout. + "action_status": "", // string, For a composite action, the current result of the composite action. + "actor": run.TriggerUser.Name, // string, The username of the user that triggered the initial workflow run. If the workflow run is a re-run, this value may differ from github.triggering_actor. Any workflow re-runs will use the privileges of github.actor, even if the actor initiating the re-run (github.triggering_actor) has different privileges. + "api_url": setting.AppURL + "api/v1", // string, The URL of the GitHub REST API. + "base_ref": baseRef, // string, The base_ref or target branch of the pull request in a workflow run. This property is only available when the event that triggers a workflow run is either pull_request or pull_request_target. + "env": "", // string, Path on the runner to the file that sets environment variables from workflow commands. This file is unique to the current step and is a different file for each step in a job. For more information, see "Workflow commands for GitHub Actions." + "event": event, // object, The full event webhook payload. You can access individual properties of the event using this context. This object is identical to the webhook payload of the event that triggered the workflow run, and is different for each event. The webhooks for each GitHub Actions event is linked in "Events that trigger workflows." For example, for a workflow run triggered by the push event, this object contains the contents of the push webhook payload. + "event_name": run.TriggerEvent, // string, The name of the event that triggered the workflow run. + "event_path": "", // string, The path to the file on the runner that contains the full event webhook payload. + "graphql_url": "", // string, The URL of the GitHub GraphQL API. + "head_ref": headRef, // string, The head_ref or source branch of the pull request in a workflow run. This property is only available when the event that triggers a workflow run is either pull_request or pull_request_target. + "job": "", // string, The job_id of the current job. + "ref": ref, // string, The fully-formed ref of the branch or tag that triggered the workflow run. For workflows triggered by push, this is the branch or tag ref that was pushed. For workflows triggered by pull_request, this is the pull request merge branch. For workflows triggered by release, this is the release tag created. For other triggers, this is the branch or tag ref that triggered the workflow run. This is only set if a branch or tag is available for the event type. The ref given is fully-formed, meaning that for branches the format is refs/heads/, for pull requests it is refs/pull//merge, and for tags it is refs/tags/. For example, refs/heads/feature-branch-1. + "ref_name": refName.ShortName(), // string, The short ref name of the branch or tag that triggered the workflow run. This value matches the branch or tag name shown on GitHub. For example, feature-branch-1. + "ref_protected": false, // boolean, true if branch protections are configured for the ref that triggered the workflow run. + "ref_type": string(refName.RefType()), // string, The type of ref that triggered the workflow run. Valid values are branch or tag. + "path": "", // string, Path on the runner to the file that sets system PATH variables from workflow commands. This file is unique to the current step and is a different file for each step in a job. For more information, see "Workflow commands for GitHub Actions." + "repository": run.Repo.OwnerName + "/" + run.Repo.Name, // string, The owner and repository name. For example, Codertocat/Hello-World. + "repository_owner": run.Repo.OwnerName, // string, The repository owner's name. For example, Codertocat. + "repositoryUrl": run.Repo.HTMLURL(), // string, The Git URL to the repository. For example, git://github.com/codertocat/hello-world.git. + "retention_days": "", // string, The number of days that workflow run logs and artifacts are kept. + "run_id": "", // string, A unique number for each workflow run within a repository. This number does not change if you re-run the workflow run. + "run_number": fmt.Sprint(run.Index), // string, A unique number for each run of a particular workflow in a repository. This number begins at 1 for the workflow's first run, and increments with each new run. This number does not change if you re-run the workflow run. + "run_attempt": "", // string, A unique number for each attempt of a particular workflow run in a repository. This number begins at 1 for the workflow run's first attempt, and increments with each re-run. + "secret_source": "Actions", // string, The source of a secret used in a workflow. Possible values are None, Actions, Dependabot, or Codespaces. + "server_url": setting.AppURL, // string, The URL of the GitHub server. For example: https://github.com. + "sha": sha, // string, The commit SHA that triggered the workflow. The value of this commit SHA depends on the event that triggered the workflow. For more information, see "Events that trigger workflows." For example, ffac537e6cbbf934b08745a378932722df287a53. + "triggering_actor": "", // string, The username of the user that initiated the workflow run. If the workflow run is a re-run, this value may differ from github.actor. Any workflow re-runs will use the privileges of github.actor, even if the actor initiating the re-run (github.triggering_actor) has different privileges. + "workflow": run.WorkflowID, // string, The name of the workflow. If the workflow file doesn't specify a name, the value of this property is the full path of the workflow file in the repository. + "workspace": "", // string, The default working directory on the runner for steps, and the default location of your repository when using the checkout action. + + // additional contexts + "gitea_default_actions_url": setting.Actions.DefaultActionsURL.URL(), + } + + if job != nil { + gitContext["job"] = job.JobID + gitContext["run_id"] = fmt.Sprint(job.RunID) + gitContext["run_attempt"] = fmt.Sprint(job.Attempt) + } + + return gitContext +} + +type TaskNeed struct { + Result actions_model.Status + Outputs map[string]string +} + +// FindTaskNeeds finds the `needs` for the task by the task's job +func FindTaskNeeds(ctx context.Context, job *actions_model.ActionRunJob) (map[string]*TaskNeed, error) { + if len(job.Needs) == 0 { + return nil, nil + } + needs := container.SetOf(job.Needs...) + + jobs, err := db.Find[actions_model.ActionRunJob](ctx, actions_model.FindRunJobOptions{RunID: job.RunID}) + if err != nil { + return nil, fmt.Errorf("FindRunJobs: %w", err) + } + + jobIDJobs := make(map[string][]*actions_model.ActionRunJob) + for _, job := range jobs { + jobIDJobs[job.JobID] = append(jobIDJobs[job.JobID], job) + } + + ret := make(map[string]*TaskNeed, len(needs)) + for jobID, jobsWithSameID := range jobIDJobs { + if !needs.Contains(jobID) { + continue + } + var jobOutputs map[string]string + for _, job := range jobsWithSameID { + if job.TaskID == 0 || !job.Status.IsDone() { + // it shouldn't happen, or the job has been rerun + continue + } + got, err := actions_model.FindTaskOutputByTaskID(ctx, job.TaskID) + if err != nil { + return nil, fmt.Errorf("FindTaskOutputByTaskID: %w", err) + } + outputs := make(map[string]string, len(got)) + for _, v := range got { + outputs[v.OutputKey] = v.OutputValue + } + if len(jobOutputs) == 0 { + jobOutputs = outputs + } else { + jobOutputs = mergeTwoOutputs(outputs, jobOutputs) + } + } + ret[jobID] = &TaskNeed{ + Outputs: jobOutputs, + Result: actions_model.AggregateJobStatus(jobsWithSameID), + } + } + return ret, nil +} + +// mergeTwoOutputs merges two outputs from two different ActionRunJobs +// Values with the same output name may be overridden. The user should ensure the output names are unique. +// See https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#using-job-outputs-in-a-matrix-job +func mergeTwoOutputs(o1, o2 map[string]string) map[string]string { + ret := make(map[string]string, len(o1)) + for k1, v1 := range o1 { + if len(v1) > 0 { + ret[k1] = v1 + } else { + ret[k1] = o2[k1] + } + } + return ret +} diff --git a/routers/api/actions/runner/utils_test.go b/services/actions/context_test.go similarity index 75% rename from routers/api/actions/runner/utils_test.go rename to services/actions/context_test.go index d7a6f84550f..6ed094b2899 100644 --- a/routers/api/actions/runner/utils_test.go +++ b/services/actions/context_test.go @@ -1,7 +1,7 @@ // Copyright 2024 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package runner +package actions import ( "context" @@ -13,12 +13,13 @@ import ( "github.com/stretchr/testify/assert" ) -func Test_findTaskNeeds(t *testing.T) { +func TestFindTaskNeeds(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) task := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: 51}) + job := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: task.JobID}) - ret, err := findTaskNeeds(context.Background(), task) + ret, err := FindTaskNeeds(context.Background(), job) assert.NoError(t, err) assert.Len(t, ret, 1) assert.Contains(t, ret, "job1") diff --git a/services/actions/init_test.go b/services/actions/init_test.go index 59c321ccd77..7ef07022041 100644 --- a/services/actions/init_test.go +++ b/services/actions/init_test.go @@ -17,9 +17,7 @@ import ( ) func TestMain(m *testing.M) { - unittest.MainTest(m, &unittest.TestOptions{ - FixtureFiles: []string{"action_runner_token.yml"}, - }) + unittest.MainTest(m) os.Exit(m.Run()) } diff --git a/services/actions/notifier.go b/services/actions/notifier.go index a4ebdf9e888..1a23b4e0c5a 100644 --- a/services/actions/notifier.go +++ b/services/actions/notifier.go @@ -58,7 +58,15 @@ func (n *actionsNotifier) NewIssue(ctx context.Context, issue *issues_model.Issu // IssueChangeContent notifies change content of issue func (n *actionsNotifier) IssueChangeContent(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, oldContent string) { ctx = withMethod(ctx, "IssueChangeContent") + n.notifyIssueChangeWithTitleOrContent(ctx, doer, issue) +} +func (n *actionsNotifier) IssueChangeTitle(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, oldTitle string) { + ctx = withMethod(ctx, "IssueChangeTitle") + n.notifyIssueChangeWithTitleOrContent(ctx, doer, issue) +} + +func (n *actionsNotifier) notifyIssueChangeWithTitleOrContent(ctx context.Context, doer *user_model.User, issue *issues_model.Issue) { var err error if err = issue.LoadRepo(ctx); err != nil { log.Error("LoadRepo: %v", err) @@ -555,9 +563,9 @@ func (n *actionsNotifier) CreateRef(ctx context.Context, pusher *user_model.User newNotifyInput(repo, pusher, webhook_module.HookEventCreate). WithRef(refFullName.String()). WithPayload(&api.CreatePayload{ - Ref: refFullName.String(), + Ref: refFullName.String(), // HINT: here is inconsistent with the Webhook's payload: webhook uses ShortName Sha: refID, - RefType: refFullName.RefType(), + RefType: string(refFullName.RefType()), Repo: apiRepo, Sender: apiPusher, }). @@ -572,8 +580,8 @@ func (n *actionsNotifier) DeleteRef(ctx context.Context, pusher *user_model.User newNotifyInput(repo, pusher, webhook_module.HookEventDelete). WithPayload(&api.DeletePayload{ - Ref: refFullName.String(), - RefType: refFullName.RefType(), + Ref: refFullName.String(), // HINT: here is inconsistent with the Webhook's payload: webhook uses ShortName + RefType: string(refFullName.RefType()), PusherType: api.PusherTypeUser, Repo: apiRepo, Sender: apiPusher, diff --git a/services/actions/notifier_helper.go b/services/actions/notifier_helper.go index 323c6a76e42..2d8885dc323 100644 --- a/services/actions/notifier_helper.go +++ b/services/actions/notifier_helper.go @@ -117,7 +117,7 @@ func (input *notifyInput) Notify(ctx context.Context) { func notify(ctx context.Context, input *notifyInput) error { shouldDetectSchedules := input.Event == webhook_module.HookEventPush && input.Ref.BranchName() == input.Repo.DefaultBranch - if input.Doer.IsActions() { + if input.Doer.IsGiteaActions() { // avoiding triggering cyclically, for example: // a comment of an issue will trigger the runner to add a new comment as reply, // and the new comment will trigger the runner again. diff --git a/services/agit/agit.go b/services/agit/agit.go index 83b12dfcdb8..897e825012c 100644 --- a/services/agit/agit.go +++ b/services/agit/agit.go @@ -153,7 +153,7 @@ func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git. OriginalRef: opts.RefFullNames[i], OldOID: objectFormat.EmptyObjectID().String(), NewOID: opts.NewCommitIDs[i], - IsCreatePR: true, + IsCreatePR: false, // AGit always creates a pull request so there is no point in prompting user to create one URL: fmt.Sprintf("%s/pulls/%d", repo.HTMLURL(), pr.Index), ShouldShowMessage: setting.Git.PullRequestPushMessage && repo.AllowsPulls(ctx), HeadBranch: headBranch, diff --git a/services/auth/auth.go b/services/auth/auth.go index 43ff95f0530..7deca9bc3d7 100644 --- a/services/auth/auth.go +++ b/services/auth/auth.go @@ -9,6 +9,7 @@ import ( "net/http" "regexp" "strings" + "sync" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/auth/webauthn" @@ -21,44 +22,88 @@ import ( user_service "code.gitea.io/gitea/services/user" ) +type globalVarsStruct struct { + gitRawOrAttachPathRe *regexp.Regexp + lfsPathRe *regexp.Regexp + archivePathRe *regexp.Regexp + feedPathRe *regexp.Regexp + feedRefPathRe *regexp.Regexp +} + +var globalVars = sync.OnceValue(func() *globalVarsStruct { + return &globalVarsStruct{ + gitRawOrAttachPathRe: regexp.MustCompile(`^/[-.\w]+/[-.\w]+/(?:(?:git-(?:(?:upload)|(?:receive))-pack$)|(?:info/refs$)|(?:HEAD$)|(?:objects/)|(?:raw/)|(?:releases/download/)|(?:attachments/))`), + lfsPathRe: regexp.MustCompile(`^/[-.\w]+/[-.\w]+/info/lfs/`), + archivePathRe: regexp.MustCompile(`^/[-.\w]+/[-.\w]+/archive/`), + feedPathRe: regexp.MustCompile(`^/[-.\w]+(/[-.\w]+)?\.(rss|atom)$`), // "/owner.rss" or "/owner/repo.atom" + feedRefPathRe: regexp.MustCompile(`^/[-.\w]+/[-.\w]+/(rss|atom)/`), // "/owner/repo/rss/branch/..." + } +}) + // Init should be called exactly once when the application starts to allow plugins // to allocate necessary resources func Init() { webauthn.Init() } +type authPathDetector struct { + req *http.Request + vars *globalVarsStruct +} + +func newAuthPathDetector(req *http.Request) *authPathDetector { + return &authPathDetector{req: req, vars: globalVars()} +} + +// isAPIPath returns true if the specified URL is an API path +func (a *authPathDetector) isAPIPath() bool { + return strings.HasPrefix(a.req.URL.Path, "/api/") +} + // isAttachmentDownload check if request is a file download (GET) with URL to an attachment -func isAttachmentDownload(req *http.Request) bool { - return strings.HasPrefix(req.URL.Path, "/attachments/") && req.Method == "GET" +func (a *authPathDetector) isAttachmentDownload() bool { + return strings.HasPrefix(a.req.URL.Path, "/attachments/") && a.req.Method == "GET" +} + +func (a *authPathDetector) isFeedRequest(req *http.Request) bool { + if !setting.Other.EnableFeed { + return false + } + if req.Method != "GET" { + return false + } + return a.vars.feedPathRe.MatchString(req.URL.Path) || a.vars.feedRefPathRe.MatchString(req.URL.Path) } // isContainerPath checks if the request targets the container endpoint -func isContainerPath(req *http.Request) bool { - return strings.HasPrefix(req.URL.Path, "/v2/") +func (a *authPathDetector) isContainerPath() bool { + return strings.HasPrefix(a.req.URL.Path, "/v2/") } -var ( - gitRawOrAttachPathRe = regexp.MustCompile(`^/[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+/(?:(?:git-(?:(?:upload)|(?:receive))-pack$)|(?:info/refs$)|(?:HEAD$)|(?:objects/)|(?:raw/)|(?:releases/download/)|(?:attachments/))`) - lfsPathRe = regexp.MustCompile(`^/[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+/info/lfs/`) - archivePathRe = regexp.MustCompile(`^/[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+/archive/`) -) - -func isGitRawOrAttachPath(req *http.Request) bool { - return gitRawOrAttachPathRe.MatchString(req.URL.Path) +func (a *authPathDetector) isGitRawOrAttachPath() bool { + return a.vars.gitRawOrAttachPathRe.MatchString(a.req.URL.Path) } -func isGitRawOrAttachOrLFSPath(req *http.Request) bool { - if isGitRawOrAttachPath(req) { +func (a *authPathDetector) isGitRawOrAttachOrLFSPath() bool { + if a.isGitRawOrAttachPath() { return true } if setting.LFS.StartServer { - return lfsPathRe.MatchString(req.URL.Path) + return a.vars.lfsPathRe.MatchString(a.req.URL.Path) } return false } -func isArchivePath(req *http.Request) bool { - return archivePathRe.MatchString(req.URL.Path) +func (a *authPathDetector) isArchivePath() bool { + return a.vars.archivePathRe.MatchString(a.req.URL.Path) +} + +func (a *authPathDetector) isAuthenticatedTokenRequest() bool { + switch a.req.URL.Path { + case "/login/oauth/userinfo", "/login/oauth/introspect": + return true + } + return false } // handleSignIn clears existing session variables and stores new ones for the specified user object diff --git a/services/auth/auth_test.go b/services/auth/auth_test.go index 3adaa286644..b8d33961636 100644 --- a/services/auth/auth_test.go +++ b/services/auth/auth_test.go @@ -9,6 +9,9 @@ import ( "testing" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" + + "github.com/stretchr/testify/assert" ) func Test_isGitRawOrLFSPath(t *testing.T) { @@ -90,6 +93,19 @@ func Test_isGitRawOrLFSPath(t *testing.T) { true, }, } + + defer test.MockVariableValue(&setting.LFS.StartServer)() + for _, tt := range tests { + t.Run(tt.path, func(t *testing.T) { + req, _ := http.NewRequest("POST", "http://localhost"+tt.path, nil) + setting.LFS.StartServer = false + assert.Equal(t, tt.want, newAuthPathDetector(req).isGitRawOrAttachOrLFSPath()) + + setting.LFS.StartServer = true + assert.Equal(t, tt.want, newAuthPathDetector(req).isGitRawOrAttachOrLFSPath()) + }) + } + lfsTests := []string{ "/owner/repo/info/lfs/", "/owner/repo/info/lfs/objects/batch", @@ -101,34 +117,39 @@ func Test_isGitRawOrLFSPath(t *testing.T) { "/owner/repo/info/lfs/locks/verify", "/owner/repo/info/lfs/locks/123/unlock", } - - origLFSStartServer := setting.LFS.StartServer - - for _, tt := range tests { - t.Run(tt.path, func(t *testing.T) { - req, _ := http.NewRequest("POST", "http://localhost"+tt.path, nil) - setting.LFS.StartServer = false - if got := isGitRawOrAttachOrLFSPath(req); got != tt.want { - t.Errorf("isGitOrLFSPath() = %v, want %v", got, tt.want) - } - setting.LFS.StartServer = true - if got := isGitRawOrAttachOrLFSPath(req); got != tt.want { - t.Errorf("isGitOrLFSPath() = %v, want %v", got, tt.want) - } - }) - } for _, tt := range lfsTests { t.Run(tt, func(t *testing.T) { req, _ := http.NewRequest("POST", tt, nil) setting.LFS.StartServer = false - if got := isGitRawOrAttachOrLFSPath(req); got != setting.LFS.StartServer { - t.Errorf("isGitOrLFSPath(%q) = %v, want %v, %v", tt, got, setting.LFS.StartServer, gitRawOrAttachPathRe.MatchString(tt)) - } + got := newAuthPathDetector(req).isGitRawOrAttachOrLFSPath() + assert.Equalf(t, setting.LFS.StartServer, got, "isGitOrLFSPath(%q) = %v, want %v, %v", tt, got, setting.LFS.StartServer, globalVars().gitRawOrAttachPathRe.MatchString(tt)) + setting.LFS.StartServer = true - if got := isGitRawOrAttachOrLFSPath(req); got != setting.LFS.StartServer { - t.Errorf("isGitOrLFSPath(%q) = %v, want %v", tt, got, setting.LFS.StartServer) - } + got = newAuthPathDetector(req).isGitRawOrAttachOrLFSPath() + assert.Equalf(t, setting.LFS.StartServer, got, "isGitOrLFSPath(%q) = %v, want %v", tt, got, setting.LFS.StartServer) + }) + } +} + +func Test_isFeedRequest(t *testing.T) { + tests := []struct { + want bool + path string + }{ + {true, "/user.rss"}, + {true, "/user/repo.atom"}, + {false, "/user/repo"}, + {false, "/use/repo/file.rss"}, + + {true, "/org/repo/rss/branch/xxx"}, + {true, "/org/repo/atom/tag/xxx"}, + {false, "/org/repo/branch/main/rss/any"}, + {false, "/org/atom/any"}, + } + for _, tt := range tests { + t.Run(tt.path, func(t *testing.T) { + req, _ := http.NewRequest("GET", "http://localhost"+tt.path, nil) + assert.Equal(t, tt.want, newAuthPathDetector(req).isFeedRequest(req)) }) } - setting.LFS.StartServer = origLFSStartServer } diff --git a/services/auth/basic.go b/services/auth/basic.go index 6a05b2fe530..e22b9e1eb77 100644 --- a/services/auth/basic.go +++ b/services/auth/basic.go @@ -17,7 +17,6 @@ import ( "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" - "code.gitea.io/gitea/modules/web/middleware" ) // Ensure the struct implements the interface. @@ -48,8 +47,10 @@ func (b *Basic) Name() string { // name/token on successful validation. // Returns nil if header is empty or validation fails. func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) (*user_model.User, error) { - // Basic authentication should only fire on API, Download or on Git or LFSPaths - if !middleware.IsAPIPath(req) && !isContainerPath(req) && !isAttachmentDownload(req) && !isGitRawOrAttachOrLFSPath(req) { + // Basic authentication should only fire on API, Feed, Download or on Git or LFSPaths + // Not all feed (rss/atom) clients feature the ability to add cookies or headers, so we need to allow basic auth for feeds + detector := newAuthPathDetector(req) + if !detector.isAPIPath() && !detector.isFeedRequest(req) && !detector.isContainerPath() && !detector.isAttachmentDownload() && !detector.isGitRawOrAttachOrLFSPath() { return nil, nil } diff --git a/services/auth/oauth2.go b/services/auth/oauth2.go index 6f2cadd4abb..66cc6868093 100644 --- a/services/auth/oauth2.go +++ b/services/auth/oauth2.go @@ -16,7 +16,6 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" - "code.gitea.io/gitea/modules/web/middleware" "code.gitea.io/gitea/services/actions" "code.gitea.io/gitea/services/oauth2_provider" ) @@ -162,8 +161,9 @@ func (o *OAuth2) userIDFromToken(ctx context.Context, tokenSHA string, store Dat // Returns nil if verification fails. func (o *OAuth2) Verify(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) (*user_model.User, error) { // These paths are not API paths, but we still want to check for tokens because they maybe in the API returned URLs - if !middleware.IsAPIPath(req) && !isAttachmentDownload(req) && !isAuthenticatedTokenRequest(req) && - !isGitRawOrAttachPath(req) && !isArchivePath(req) { + detector := newAuthPathDetector(req) + if !detector.isAPIPath() && !detector.isAttachmentDownload() && !detector.isAuthenticatedTokenRequest() && + !detector.isGitRawOrAttachPath() && !detector.isArchivePath() { return nil, nil } @@ -190,13 +190,3 @@ func (o *OAuth2) Verify(req *http.Request, w http.ResponseWriter, store DataStor log.Trace("OAuth2 Authorization: Logged in user %-v", user) return user, nil } - -func isAuthenticatedTokenRequest(req *http.Request) bool { - switch req.URL.Path { - case "/login/oauth/userinfo": - fallthrough - case "/login/oauth/introspect": - return true - } - return false -} diff --git a/services/auth/reverseproxy.go b/services/auth/reverseproxy.go index 36b4ef68f42..d6664d738de 100644 --- a/services/auth/reverseproxy.go +++ b/services/auth/reverseproxy.go @@ -12,7 +12,6 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/web/middleware" gouuid "github.com/google/uuid" ) @@ -117,7 +116,8 @@ func (r *ReverseProxy) Verify(req *http.Request, w http.ResponseWriter, store Da } // Make sure requests to API paths, attachment downloads, git and LFS do not create a new session - if !middleware.IsAPIPath(req) && !isAttachmentDownload(req) && !isGitRawOrAttachOrLFSPath(req) { + detector := newAuthPathDetector(req) + if !detector.isAPIPath() && !detector.isAttachmentDownload() && !detector.isGitRawOrAttachOrLFSPath() { if sess != nil && (sess.Get("uid") == nil || sess.Get("uid").(int64) != user.ID) { handleSignIn(w, req, sess, user) } diff --git a/services/auth/sspi.go b/services/auth/sspi.go index 8ac83dcb044..3882740ae37 100644 --- a/services/auth/sspi.go +++ b/services/auth/sspi.go @@ -17,7 +17,6 @@ import ( "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/templates" - "code.gitea.io/gitea/modules/web/middleware" "code.gitea.io/gitea/services/auth/source/sspi" gitea_context "code.gitea.io/gitea/services/context" @@ -120,7 +119,8 @@ func (s *SSPI) Verify(req *http.Request, w http.ResponseWriter, store DataStore, } // Make sure requests to API paths and PWA resources do not create a new session - if !middleware.IsAPIPath(req) && !isAttachmentDownload(req) { + detector := newAuthPathDetector(req) + if !detector.isAPIPath() && !detector.isAttachmentDownload() { handleSignIn(w, req, sess, user) } @@ -155,8 +155,9 @@ func (s *SSPI) shouldAuthenticate(req *http.Request) (shouldAuth bool) { } else if req.FormValue("auth_with_sspi") == "1" { shouldAuth = true } - } else if middleware.IsAPIPath(req) || isAttachmentDownload(req) { - shouldAuth = true + } else { + detector := newAuthPathDetector(req) + shouldAuth = detector.isAPIPath() || detector.isAttachmentDownload() } return shouldAuth } diff --git a/services/automerge/automerge.go b/services/automerge/automerge.go index a1ee204882f..bdb0493ae8c 100644 --- a/services/automerge/automerge.go +++ b/services/automerge/automerge.go @@ -24,6 +24,7 @@ import ( "code.gitea.io/gitea/modules/queue" notify_service "code.gitea.io/gitea/services/notify" pull_service "code.gitea.io/gitea/services/pull" + repo_service "code.gitea.io/gitea/services/repository" ) // prAutoMergeQueue represents a queue to handle update pull request tests @@ -63,9 +64,9 @@ func addToQueue(pr *issues_model.PullRequest, sha string) { } // ScheduleAutoMerge if schedule is false and no error, pull can be merged directly -func ScheduleAutoMerge(ctx context.Context, doer *user_model.User, pull *issues_model.PullRequest, style repo_model.MergeStyle, message string) (scheduled bool, err error) { +func ScheduleAutoMerge(ctx context.Context, doer *user_model.User, pull *issues_model.PullRequest, style repo_model.MergeStyle, message string, deleteBranchAfterMerge bool) (scheduled bool, err error) { err = db.WithTx(ctx, func(ctx context.Context) error { - if err := pull_model.ScheduleAutoMerge(ctx, doer, pull.ID, style, message); err != nil { + if err := pull_model.ScheduleAutoMerge(ctx, doer, pull.ID, style, message, deleteBranchAfterMerge); err != nil { return err } scheduled = true @@ -303,4 +304,10 @@ func handlePullRequestAutoMerge(pullID int64, sha string) { // on the pull request page. But this should not be finished in a bug fix PR which will be backport to release branch. return } + + if pr.Flow == issues_model.PullRequestFlowGithub && scheduledPRM.DeleteBranchAfterMerge { + if err := repo_service.DeleteBranch(ctx, doer, pr.HeadRepo, headGitRepo, pr.HeadBranch, pr); err != nil { + log.Error("DeletePullRequestHeadBranch: %v", err) + } + } } diff --git a/services/context/access_log.go b/services/context/access_log.go index 0926748ac54..001d93a362b 100644 --- a/services/context/access_log.go +++ b/services/context/access_log.go @@ -18,13 +18,14 @@ import ( "code.gitea.io/gitea/modules/web/middleware" ) -type routerLoggerOptions struct { - req *http.Request +type accessLoggerTmplData struct { Identity *string Start *time.Time - ResponseWriter http.ResponseWriter - Ctx map[string]any - RequestID *string + ResponseWriter struct { + Status, Size int + } + Ctx map[string]any + RequestID *string } const keyOfRequestIDInTemplate = ".RequestID" @@ -51,51 +52,65 @@ func parseRequestIDFromRequestHeader(req *http.Request) string { return requestID } +type accessLogRecorder struct { + logger log.BaseLogger + logTemplate *template.Template + needRequestID bool +} + +func (lr *accessLogRecorder) record(start time.Time, respWriter ResponseWriter, req *http.Request) { + var requestID string + if lr.needRequestID { + requestID = parseRequestIDFromRequestHeader(req) + } + + reqHost, _, err := net.SplitHostPort(req.RemoteAddr) + if err != nil { + reqHost = req.RemoteAddr + } + + identity := "-" + data := middleware.GetContextData(req.Context()) + if signedUser, ok := data[middleware.ContextDataKeySignedUser].(*user_model.User); ok { + identity = signedUser.Name + } + buf := bytes.NewBuffer([]byte{}) + tmplData := accessLoggerTmplData{ + Identity: &identity, + Start: &start, + Ctx: map[string]any{ + "RemoteAddr": req.RemoteAddr, + "RemoteHost": reqHost, + "Req": req, + }, + RequestID: &requestID, + } + tmplData.ResponseWriter.Status = respWriter.WrittenStatus() + tmplData.ResponseWriter.Size = respWriter.WrittenSize() + err = lr.logTemplate.Execute(buf, tmplData) + if err != nil { + log.Error("Could not execute access logger template: %v", err.Error()) + } + + lr.logger.Log(1, log.INFO, "%s", buf.String()) +} + +func newAccessLogRecorder() *accessLogRecorder { + return &accessLogRecorder{ + logger: log.GetLogger("access"), + logTemplate: template.Must(template.New("log").Parse(setting.Log.AccessLogTemplate)), + needRequestID: len(setting.Log.RequestIDHeaders) > 0 && strings.Contains(setting.Log.AccessLogTemplate, keyOfRequestIDInTemplate), + } +} + // AccessLogger returns a middleware to log access logger func AccessLogger() func(http.Handler) http.Handler { - logger := log.GetLogger("access") - needRequestID := len(setting.Log.RequestIDHeaders) > 0 && strings.Contains(setting.Log.AccessLogTemplate, keyOfRequestIDInTemplate) - logTemplate, _ := template.New("log").Parse(setting.Log.AccessLogTemplate) + recorder := newAccessLogRecorder() return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { start := time.Now() - - var requestID string - if needRequestID { - requestID = parseRequestIDFromRequestHeader(req) - } - - reqHost, _, err := net.SplitHostPort(req.RemoteAddr) - if err != nil { - reqHost = req.RemoteAddr - } - next.ServeHTTP(w, req) - rw := w.(ResponseWriter) - - identity := "-" - data := middleware.GetContextData(req.Context()) - if signedUser, ok := data[middleware.ContextDataKeySignedUser].(*user_model.User); ok { - identity = signedUser.Name - } - buf := bytes.NewBuffer([]byte{}) - err = logTemplate.Execute(buf, routerLoggerOptions{ - req: req, - Identity: &identity, - Start: &start, - ResponseWriter: rw, - Ctx: map[string]any{ - "RemoteAddr": req.RemoteAddr, - "RemoteHost": reqHost, - "Req": req, - }, - RequestID: &requestID, - }) - if err != nil { - log.Error("Could not execute access logger template: %v", err.Error()) - } - - logger.Info("%s", buf.String()) + recorder.record(start, w.(ResponseWriter), req) }) } } diff --git a/services/context/access_log_test.go b/services/context/access_log_test.go new file mode 100644 index 00000000000..bd3e47e0ccd --- /dev/null +++ b/services/context/access_log_test.go @@ -0,0 +1,71 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package context + +import ( + "fmt" + "net/http" + "net/url" + "testing" + "time" + + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" + + "github.com/stretchr/testify/assert" +) + +type testAccessLoggerMock struct { + logs []string +} + +func (t *testAccessLoggerMock) Log(skip int, level log.Level, format string, v ...any) { + t.logs = append(t.logs, fmt.Sprintf(format, v...)) +} + +func (t *testAccessLoggerMock) GetLevel() log.Level { + return log.INFO +} + +type testAccessLoggerResponseWriterMock struct{} + +func (t testAccessLoggerResponseWriterMock) Header() http.Header { + return nil +} + +func (t testAccessLoggerResponseWriterMock) Before(f func(ResponseWriter)) {} + +func (t testAccessLoggerResponseWriterMock) WriteHeader(statusCode int) {} + +func (t testAccessLoggerResponseWriterMock) Write(bytes []byte) (int, error) { + return 0, nil +} + +func (t testAccessLoggerResponseWriterMock) Flush() {} + +func (t testAccessLoggerResponseWriterMock) WrittenStatus() int { + return http.StatusOK +} + +func (t testAccessLoggerResponseWriterMock) WrittenSize() int { + return 123123 +} + +func TestAccessLogger(t *testing.T) { + setting.Log.AccessLogTemplate = `{{.Ctx.RemoteHost}} - {{.Identity}} {{.Start.Format "[02/Jan/2006:15:04:05 -0700]" }} "{{.Ctx.Req.Method}} {{.Ctx.Req.URL.RequestURI}} {{.Ctx.Req.Proto}}" {{.ResponseWriter.Status}} {{.ResponseWriter.Size}} "{{.Ctx.Req.Referer}}" "{{.Ctx.Req.UserAgent}}"` + recorder := newAccessLogRecorder() + mockLogger := &testAccessLoggerMock{} + recorder.logger = mockLogger + req := &http.Request{ + RemoteAddr: "remote-addr", + Method: "GET", + Proto: "https", + URL: &url.URL{Path: "/path"}, + } + req.Header = http.Header{} + req.Header.Add("Referer", "referer") + req.Header.Add("User-Agent", "user-agent") + recorder.record(time.Date(2000, 1, 2, 3, 4, 5, 0, time.UTC), &testAccessLoggerResponseWriterMock{}, req) + assert.Equal(t, []string{`remote-addr - - [02/Jan/2000:03:04:05 +0000] "GET /path https" 200 123123 "referer" "user-agent"`}, mockLogger.logs) +} diff --git a/services/context/api.go b/services/context/api.go index 7b604c5ea15..bdeff0af634 100644 --- a/services/context/api.go +++ b/services/context/api.go @@ -274,7 +274,7 @@ func ReferencesGitRepo(allowEmpty ...bool) func(ctx *APIContext) { // For API calls. if ctx.Repo.GitRepo == nil { var err error - ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx, ctx.Repo.Repository) + ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx.Repo.Repository) if err != nil { ctx.Error(http.StatusInternalServerError, fmt.Sprintf("Open Repository %v failed", ctx.Repo.Repository.FullName()), err) return @@ -293,8 +293,7 @@ func RepoRefForAPI(next http.Handler) http.Handler { return } - // NOTICE: the "ref" here for internal usage only (e.g. woodpecker) - refName, _ := getRefNameLegacy(ctx.Base, ctx.Repo, ctx.FormTrim("ref")) + refName, _, _ := getRefNameLegacy(ctx.Base, ctx.Repo, ctx.PathParam("*"), ctx.FormTrim("ref")) var err error if ctx.Repo.GitRepo.IsBranchExist(refName) { diff --git a/services/context/base.go b/services/context/base.go index 7a39353e09e..5db84f42a55 100644 --- a/services/context/base.go +++ b/services/context/base.go @@ -4,7 +4,6 @@ package context import ( - "context" "fmt" "html/template" "io" @@ -25,8 +24,7 @@ type BaseContextKeyType struct{} var BaseContextKey BaseContextKeyType type Base struct { - context.Context - reqctx.RequestDataStore + reqctx.RequestContext Resp ResponseWriter Req *http.Request @@ -172,19 +170,19 @@ func (b *Base) TrN(cnt any, key1, keyN string, args ...any) template.HTML { } func NewBaseContext(resp http.ResponseWriter, req *http.Request) *Base { - ds := reqctx.GetRequestDataStore(req.Context()) + reqCtx := reqctx.FromContext(req.Context()) b := &Base{ - Context: req.Context(), - RequestDataStore: ds, - Req: req, - Resp: WrapResponseWriter(resp), - Locale: middleware.Locale(resp, req), - Data: ds.GetData(), + RequestContext: reqCtx, + + Req: req, + Resp: WrapResponseWriter(resp), + Locale: middleware.Locale(resp, req), + Data: reqCtx.GetData(), } b.Req = b.Req.WithContext(b) - ds.SetContextValue(BaseContextKey, b) - ds.SetContextValue(translation.ContextKey, b.Locale) - ds.SetContextValue(httplib.RequestContextKey, b.Req) + reqCtx.SetContextValue(BaseContextKey, b) + reqCtx.SetContextValue(translation.ContextKey, b.Locale) + reqCtx.SetContextValue(httplib.RequestContextKey, b.Req) return b } diff --git a/services/context/base_form.go b/services/context/base_form.go index ddf9734f577..5b8cae9e998 100644 --- a/services/context/base_form.go +++ b/services/context/base_form.go @@ -8,11 +8,16 @@ import ( "strings" "code.gitea.io/gitea/modules/optional" + "code.gitea.io/gitea/modules/util" ) // FormString returns the first value matching the provided key in the form as a string -func (b *Base) FormString(key string) string { - return b.Req.FormValue(key) +func (b *Base) FormString(key string, def ...string) string { + s := b.Req.FormValue(key) + if s == "" { + s = util.OptionalArg(def) + } + return s } // FormStrings returns a string slice for the provided key from the form diff --git a/services/context/context.go b/services/context/context.go index 6715c5663dc..5b16f9be982 100644 --- a/services/context/context.go +++ b/services/context/context.go @@ -165,18 +165,10 @@ func Contexter() func(next http.Handler) http.Handler { ctx.Base.SetContextValue(WebContextKey, ctx) ctx.Csrf = NewCSRFProtector(csrfOpts) - // Get the last flash message from cookie - lastFlashCookie := middleware.GetSiteCookie(ctx.Req, CookieNameFlash) + // get the last flash message from cookie + lastFlashCookie, lastFlashMsg := middleware.GetSiteCookieFlashMessage(ctx, ctx.Req, CookieNameFlash) if vals, _ := url.ParseQuery(lastFlashCookie); len(vals) > 0 { - // store last Flash message into the template data, to render it - ctx.Data["Flash"] = &middleware.Flash{ - DataStore: ctx, - Values: vals, - ErrorMsg: vals.Get("error"), - SuccessMsg: vals.Get("success"), - InfoMsg: vals.Get("info"), - WarningMsg: vals.Get("warning"), - } + ctx.Data["Flash"] = lastFlashMsg // store last Flash message into the template data, to render it } // if there are new messages in the ctx.Flash, write them into cookie diff --git a/services/context/context_model.go b/services/context/context_model.go index 4f70aac5169..3a1776102ff 100644 --- a/services/context/context_model.go +++ b/services/context/context_model.go @@ -3,27 +3,7 @@ package context -import ( - "code.gitea.io/gitea/models/unit" -) - // IsUserSiteAdmin returns true if current user is a site admin func (ctx *Context) IsUserSiteAdmin() bool { return ctx.IsSigned && ctx.Doer.IsAdmin } - -// IsUserRepoAdmin returns true if current user is admin in current repo -func (ctx *Context) IsUserRepoAdmin() bool { - return ctx.Repo.IsAdmin() -} - -// IsUserRepoWriter returns true if current user has write privilege in current repo -func (ctx *Context) IsUserRepoWriter(unitTypes []unit.Type) bool { - for _, unitType := range unitTypes { - if ctx.Repo.CanWrite(unitType) { - return true - } - } - - return false -} diff --git a/services/context/org.go b/services/context/org.go index be87cef7a31..3f73165076f 100644 --- a/services/context/org.go +++ b/services/context/org.go @@ -62,215 +62,193 @@ func GetOrganizationByParams(ctx *Context) { } } -// HandleOrgAssignment handles organization assignment -func HandleOrgAssignment(ctx *Context, args ...bool) { - var ( - requireMember bool - requireOwner bool - requireTeamMember bool - requireTeamAdmin bool - ) - if len(args) >= 1 { - requireMember = args[0] - } - if len(args) >= 2 { - requireOwner = args[1] - } - if len(args) >= 3 { - requireTeamMember = args[2] - } - if len(args) >= 4 { - requireTeamAdmin = args[3] - } +type OrgAssignmentOptions struct { + RequireMember bool + RequireOwner bool + RequireTeamMember bool + RequireTeamAdmin bool +} - var err error - - if ctx.ContextUser == nil { - // if Organization is not defined, get it from params - if ctx.Org.Organization == nil { - GetOrganizationByParams(ctx) - if ctx.Written() { - return +// OrgAssignment returns a middleware to handle organization assignment +func OrgAssignment(opts OrgAssignmentOptions) func(ctx *Context) { + return func(ctx *Context) { + var err error + if ctx.ContextUser == nil { + // if Organization is not defined, get it from params + if ctx.Org.Organization == nil { + GetOrganizationByParams(ctx) + if ctx.Written() { + return + } } - } - } else if ctx.ContextUser.IsOrganization() { - if ctx.Org == nil { - ctx.Org = &Organization{} - } - ctx.Org.Organization = (*organization.Organization)(ctx.ContextUser) - } else { - // ContextUser is an individual User - return - } - - org := ctx.Org.Organization - - // Handle Visibility - if org.Visibility != structs.VisibleTypePublic && !ctx.IsSigned { - // We must be signed in to see limited or private organizations - ctx.NotFound("OrgAssignment", err) - return - } - - if org.Visibility == structs.VisibleTypePrivate { - requireMember = true - } else if ctx.IsSigned && ctx.Doer.IsRestricted { - requireMember = true - } - - ctx.ContextUser = org.AsUser() - ctx.Data["Org"] = org - - // Admin has super access. - if ctx.IsSigned && ctx.Doer.IsAdmin { - ctx.Org.IsOwner = true - ctx.Org.IsMember = true - ctx.Org.IsTeamMember = true - ctx.Org.IsTeamAdmin = true - ctx.Org.CanCreateOrgRepo = true - } else if ctx.IsSigned { - ctx.Org.IsOwner, err = org.IsOwnedBy(ctx, ctx.Doer.ID) - if err != nil { - ctx.ServerError("IsOwnedBy", err) + } else if ctx.ContextUser.IsOrganization() { + ctx.Org.Organization = (*organization.Organization)(ctx.ContextUser) + } else { + // ContextUser is an individual User return } - if ctx.Org.IsOwner { + org := ctx.Org.Organization + + // Handle Visibility + if org.Visibility != structs.VisibleTypePublic && !ctx.IsSigned { + // We must be signed in to see limited or private organizations + ctx.NotFound("OrgAssignment", err) + return + } + + if org.Visibility == structs.VisibleTypePrivate { + opts.RequireMember = true + } else if ctx.IsSigned && ctx.Doer.IsRestricted { + opts.RequireMember = true + } + + ctx.ContextUser = org.AsUser() + ctx.Data["Org"] = org + + // Admin has super access. + if ctx.IsSigned && ctx.Doer.IsAdmin { + ctx.Org.IsOwner = true ctx.Org.IsMember = true ctx.Org.IsTeamMember = true ctx.Org.IsTeamAdmin = true ctx.Org.CanCreateOrgRepo = true + } else if ctx.IsSigned { + ctx.Org.IsOwner, err = org.IsOwnedBy(ctx, ctx.Doer.ID) + if err != nil { + ctx.ServerError("IsOwnedBy", err) + return + } + + if ctx.Org.IsOwner { + ctx.Org.IsMember = true + ctx.Org.IsTeamMember = true + ctx.Org.IsTeamAdmin = true + ctx.Org.CanCreateOrgRepo = true + } else { + ctx.Org.IsMember, err = org.IsOrgMember(ctx, ctx.Doer.ID) + if err != nil { + ctx.ServerError("IsOrgMember", err) + return + } + ctx.Org.CanCreateOrgRepo, err = org.CanCreateOrgRepo(ctx, ctx.Doer.ID) + if err != nil { + ctx.ServerError("CanCreateOrgRepo", err) + return + } + } } else { - ctx.Org.IsMember, err = org.IsOrgMember(ctx, ctx.Doer.ID) - if err != nil { - ctx.ServerError("IsOrgMember", err) - return - } - ctx.Org.CanCreateOrgRepo, err = org.CanCreateOrgRepo(ctx, ctx.Doer.ID) - if err != nil { - ctx.ServerError("CanCreateOrgRepo", err) - return - } + // Fake data. + ctx.Data["SignedUser"] = &user_model.User{} } - } else { - // Fake data. - ctx.Data["SignedUser"] = &user_model.User{} - } - if (requireMember && !ctx.Org.IsMember) || - (requireOwner && !ctx.Org.IsOwner) { - ctx.NotFound("OrgAssignment", err) - return - } - ctx.Data["IsOrganizationOwner"] = ctx.Org.IsOwner - ctx.Data["IsOrganizationMember"] = ctx.Org.IsMember - ctx.Data["IsPackageEnabled"] = setting.Packages.Enabled - ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled - ctx.Data["IsPublicMember"] = func(uid int64) bool { - is, _ := organization.IsPublicMembership(ctx, ctx.Org.Organization.ID, uid) - return is - } - ctx.Data["CanCreateOrgRepo"] = ctx.Org.CanCreateOrgRepo + if (opts.RequireMember && !ctx.Org.IsMember) || (opts.RequireOwner && !ctx.Org.IsOwner) { + ctx.NotFound("OrgAssignment", err) + return + } + ctx.Data["IsOrganizationOwner"] = ctx.Org.IsOwner + ctx.Data["IsOrganizationMember"] = ctx.Org.IsMember + ctx.Data["IsPackageEnabled"] = setting.Packages.Enabled + ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled + ctx.Data["IsPublicMember"] = func(uid int64) bool { + is, _ := organization.IsPublicMembership(ctx, ctx.Org.Organization.ID, uid) + return is + } + ctx.Data["CanCreateOrgRepo"] = ctx.Org.CanCreateOrgRepo - ctx.Org.OrgLink = org.AsUser().OrganisationLink() - ctx.Data["OrgLink"] = ctx.Org.OrgLink + ctx.Org.OrgLink = org.AsUser().OrganisationLink() + ctx.Data["OrgLink"] = ctx.Org.OrgLink - // Member - opts := &organization.FindOrgMembersOpts{ - Doer: ctx.Doer, - OrgID: org.ID, - IsDoerMember: ctx.Org.IsMember, - } - ctx.Data["NumMembers"], err = organization.CountOrgMembers(ctx, opts) - if err != nil { - ctx.ServerError("CountOrgMembers", err) - return - } + // Member + findMembersOpts := &organization.FindOrgMembersOpts{ + Doer: ctx.Doer, + OrgID: org.ID, + IsDoerMember: ctx.Org.IsMember, + } + ctx.Data["NumMembers"], err = organization.CountOrgMembers(ctx, findMembersOpts) + if err != nil { + ctx.ServerError("CountOrgMembers", err) + return + } - // Team. - if ctx.Org.IsMember { - shouldSeeAllTeams := false - if ctx.Org.IsOwner { - shouldSeeAllTeams = true - } else { - teams, err := org.GetUserTeams(ctx, ctx.Doer.ID) - if err != nil { - ctx.ServerError("GetUserTeams", err) - return + // Team. + if ctx.Org.IsMember { + shouldSeeAllTeams := false + if ctx.Org.IsOwner { + shouldSeeAllTeams = true + } else { + teams, err := org.GetUserTeams(ctx, ctx.Doer.ID) + if err != nil { + ctx.ServerError("GetUserTeams", err) + return + } + for _, team := range teams { + if team.IncludesAllRepositories && team.AccessMode >= perm.AccessModeAdmin { + shouldSeeAllTeams = true + break + } + } } - for _, team := range teams { - if team.IncludesAllRepositories && team.AccessMode >= perm.AccessModeAdmin { - shouldSeeAllTeams = true + if shouldSeeAllTeams { + ctx.Org.Teams, err = org.LoadTeams(ctx) + if err != nil { + ctx.ServerError("LoadTeams", err) + return + } + } else { + ctx.Org.Teams, err = org.GetUserTeams(ctx, ctx.Doer.ID) + if err != nil { + ctx.ServerError("GetUserTeams", err) + return + } + } + ctx.Data["NumTeams"] = len(ctx.Org.Teams) + } + + teamName := ctx.PathParam("team") + if len(teamName) > 0 { + teamExists := false + for _, team := range ctx.Org.Teams { + if team.LowerName == strings.ToLower(teamName) { + teamExists = true + ctx.Org.Team = team + ctx.Org.IsTeamMember = true + ctx.Data["Team"] = ctx.Org.Team break } } - } - if shouldSeeAllTeams { - ctx.Org.Teams, err = org.LoadTeams(ctx) - if err != nil { - ctx.ServerError("LoadTeams", err) + + if !teamExists { + ctx.NotFound("OrgAssignment", err) return } - } else { - ctx.Org.Teams, err = org.GetUserTeams(ctx, ctx.Doer.ID) - if err != nil { - ctx.ServerError("GetUserTeams", err) + + ctx.Data["IsTeamMember"] = ctx.Org.IsTeamMember + if opts.RequireTeamMember && !ctx.Org.IsTeamMember { + ctx.NotFound("OrgAssignment", err) + return + } + + ctx.Org.IsTeamAdmin = ctx.Org.Team.IsOwnerTeam() || ctx.Org.Team.AccessMode >= perm.AccessModeAdmin + ctx.Data["IsTeamAdmin"] = ctx.Org.IsTeamAdmin + if opts.RequireTeamAdmin && !ctx.Org.IsTeamAdmin { + ctx.NotFound("OrgAssignment", err) return } } - ctx.Data["NumTeams"] = len(ctx.Org.Teams) - } + ctx.Data["ContextUser"] = ctx.ContextUser - teamName := ctx.PathParam("team") - if len(teamName) > 0 { - teamExists := false - for _, team := range ctx.Org.Teams { - if team.LowerName == strings.ToLower(teamName) { - teamExists = true - ctx.Org.Team = team - ctx.Org.IsTeamMember = true - ctx.Data["Team"] = ctx.Org.Team - break + ctx.Data["CanReadProjects"] = ctx.Org.CanReadUnit(ctx, unit.TypeProjects) + ctx.Data["CanReadPackages"] = ctx.Org.CanReadUnit(ctx, unit.TypePackages) + ctx.Data["CanReadCode"] = ctx.Org.CanReadUnit(ctx, unit.TypeCode) + + ctx.Data["IsFollowing"] = ctx.Doer != nil && user_model.IsFollowing(ctx, ctx.Doer.ID, ctx.ContextUser.ID) + if len(ctx.ContextUser.Description) != 0 { + content, err := markdown.RenderString(markup.NewRenderContext(ctx), ctx.ContextUser.Description) + if err != nil { + ctx.ServerError("RenderString", err) + return } + ctx.Data["RenderedDescription"] = content } - - if !teamExists { - ctx.NotFound("OrgAssignment", err) - return - } - - ctx.Data["IsTeamMember"] = ctx.Org.IsTeamMember - if requireTeamMember && !ctx.Org.IsTeamMember { - ctx.NotFound("OrgAssignment", err) - return - } - - ctx.Org.IsTeamAdmin = ctx.Org.Team.IsOwnerTeam() || ctx.Org.Team.AccessMode >= perm.AccessModeAdmin - ctx.Data["IsTeamAdmin"] = ctx.Org.IsTeamAdmin - if requireTeamAdmin && !ctx.Org.IsTeamAdmin { - ctx.NotFound("OrgAssignment", err) - return - } - } - ctx.Data["ContextUser"] = ctx.ContextUser - - ctx.Data["CanReadProjects"] = ctx.Org.CanReadUnit(ctx, unit.TypeProjects) - ctx.Data["CanReadPackages"] = ctx.Org.CanReadUnit(ctx, unit.TypePackages) - ctx.Data["CanReadCode"] = ctx.Org.CanReadUnit(ctx, unit.TypeCode) - - ctx.Data["IsFollowing"] = ctx.Doer != nil && user_model.IsFollowing(ctx, ctx.Doer.ID, ctx.ContextUser.ID) - if len(ctx.ContextUser.Description) != 0 { - content, err := markdown.RenderString(markup.NewRenderContext(ctx), ctx.ContextUser.Description) - if err != nil { - ctx.ServerError("RenderString", err) - return - } - ctx.Data["RenderedDescription"] = content - } -} - -// OrgAssignment returns a middleware to handle organization assignment -func OrgAssignment(args ...bool) func(ctx *Context) { - return func(ctx *Context) { - HandleOrgAssignment(ctx, args...) } } diff --git a/services/context/permission.go b/services/context/permission.go index 9338587257c..0d69ccc4a40 100644 --- a/services/context/permission.go +++ b/services/context/permission.go @@ -9,31 +9,20 @@ import ( auth_model "code.gitea.io/gitea/models/auth" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" - "code.gitea.io/gitea/modules/log" ) // RequireRepoAdmin returns a middleware for requiring repository admin permission func RequireRepoAdmin() func(ctx *Context) { return func(ctx *Context) { if !ctx.IsSigned || !ctx.Repo.IsAdmin() { - ctx.NotFound(ctx.Req.URL.RequestURI(), nil) + ctx.NotFound("RequireRepoAdmin denies the request", nil) return } } } -// RequireRepoWriter returns a middleware for requiring repository write to the specify unitType -func RequireRepoWriter(unitType unit.Type) func(ctx *Context) { - return func(ctx *Context) { - if !ctx.Repo.CanWrite(unitType) { - ctx.NotFound(ctx.Req.URL.RequestURI(), nil) - return - } - } -} - -// CanEnableEditor checks if the user is allowed to write to the branch of the repo -func CanEnableEditor() func(ctx *Context) { +// CanWriteToBranch checks if the user is allowed to write to the branch of the repo +func CanWriteToBranch() func(ctx *Context) { return func(ctx *Context) { if !ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, ctx.Repo.BranchName) { ctx.NotFound("CanWriteToBranch denies permission", nil) @@ -42,75 +31,30 @@ func CanEnableEditor() func(ctx *Context) { } } -// RequireRepoWriterOr returns a middleware for requiring repository write to one of the unit permission -func RequireRepoWriterOr(unitTypes ...unit.Type) func(ctx *Context) { +// RequireUnitWriter returns a middleware for requiring repository write to one of the unit permission +func RequireUnitWriter(unitTypes ...unit.Type) func(ctx *Context) { return func(ctx *Context) { for _, unitType := range unitTypes { if ctx.Repo.CanWrite(unitType) { return } } - ctx.NotFound(ctx.Req.URL.RequestURI(), nil) + ctx.NotFound("RequireUnitWriter denies the request", nil) } } -// RequireRepoReader returns a middleware for requiring repository read to the specify unitType -func RequireRepoReader(unitType unit.Type) func(ctx *Context) { - return func(ctx *Context) { - if !ctx.Repo.CanRead(unitType) { - if unitType == unit.TypeCode && canWriteAsMaintainer(ctx) { - return - } - if log.IsTrace() { - if ctx.IsSigned { - log.Trace("Permission Denied: User %-v cannot read %-v in Repo %-v\n"+ - "User in Repo has Permissions: %-+v", - ctx.Doer, - unitType, - ctx.Repo.Repository, - ctx.Repo.Permission) - } else { - log.Trace("Permission Denied: Anonymous user cannot read %-v in Repo %-v\n"+ - "Anonymous user in Repo has Permissions: %-+v", - unitType, - ctx.Repo.Repository, - ctx.Repo.Permission) - } - } - ctx.NotFound(ctx.Req.URL.RequestURI(), nil) - return - } - } -} - -// RequireRepoReaderOr returns a middleware for requiring repository write to one of the unit permission -func RequireRepoReaderOr(unitTypes ...unit.Type) func(ctx *Context) { +// RequireUnitReader returns a middleware for requiring repository write to one of the unit permission +func RequireUnitReader(unitTypes ...unit.Type) func(ctx *Context) { return func(ctx *Context) { for _, unitType := range unitTypes { if ctx.Repo.CanRead(unitType) { return } - } - if log.IsTrace() { - var format string - var args []any - if ctx.IsSigned { - format = "Permission Denied: User %-v cannot read [" - args = append(args, ctx.Doer) - } else { - format = "Permission Denied: Anonymous user cannot read [" + if unitType == unit.TypeCode && canWriteAsMaintainer(ctx) { + return } - for _, unit := range unitTypes { - format += "%-v, " - args = append(args, unit) - } - - format = format[:len(format)-2] + "] in Repo %-v\n" + - "User in Repo has Permissions: %-+v" - args = append(args, ctx.Repo.Repository, ctx.Repo.Permission) - log.Trace(format, args...) } - ctx.NotFound(ctx.Req.URL.RequestURI(), nil) + ctx.NotFound("RequireUnitReader denies the request", nil) } } diff --git a/services/context/repo.go b/services/context/repo.go index b537a050362..1cb35b9b837 100644 --- a/services/context/repo.go +++ b/services/context/repo.go @@ -46,22 +46,21 @@ type PullRequest struct { // Repository contains information to operate a repository type Repository struct { access_model.Permission - IsWatching bool - IsViewBranch bool - IsViewTag bool - IsViewCommit bool - Repository *repo_model.Repository - Owner *user_model.User + + Repository *repo_model.Repository + Owner *user_model.User + + RepoLink string + GitRepo *git.Repository + + // RefFullName is the full ref name that the user is viewing + RefFullName git.RefName + BranchName string // it is the RefFullName's short name if its type is "branch" + TreePath string + + // Commit it is always set to the commit for the branch or tag, or just the commit that the user is viewing Commit *git.Commit - Tag *git.Tag - GitRepo *git.Repository - RefName string - BranchName string - TagName string - TreePath string CommitID string - RepoLink string - CloneLink repo_model.CloneLink CommitsCount int64 PullRequest *PullRequest @@ -74,7 +73,7 @@ func (r *Repository) CanWriteToBranch(ctx context.Context, user *user_model.User // CanEnableEditor returns true if repository is editable and user has proper access level. func (r *Repository) CanEnableEditor(ctx context.Context, user *user_model.User) bool { - return r.IsViewBranch && r.CanWriteToBranch(ctx, user, r.BranchName) && r.Repository.CanEnableEditor() && !r.Repository.IsArchived + return r.RefFullName.IsBranch() && r.CanWriteToBranch(ctx, user, r.BranchName) && r.Repository.CanEnableEditor() && !r.Repository.IsArchived } // CanCreateBranch returns true if repository is editable and user has proper access level. @@ -149,7 +148,7 @@ func (r *Repository) CanCommitToBranch(ctx context.Context, doer *user_model.Use }, err } -// CanUseTimetracker returns whether or not a user can use the timetracker. +// CanUseTimetracker returns whether a user can use the timetracker. func (r *Repository) CanUseTimetracker(ctx context.Context, issue *issues_model.Issue, user *user_model.User) bool { // Checking for following: // 1. Is timetracker enabled @@ -169,15 +168,9 @@ func (r *Repository) GetCommitsCount() (int64, error) { if r.Commit == nil { return 0, nil } - var contextName string - if r.IsViewBranch { - contextName = r.BranchName - } else if r.IsViewTag { - contextName = r.TagName - } else { - contextName = r.CommitID - } - return cache.GetInt64(r.Repository.GetCommitsCountCacheKey(contextName, r.IsViewBranch || r.IsViewTag), func() (int64, error) { + contextName := r.RefFullName.ShortName() + isRef := r.RefFullName.IsBranch() || r.RefFullName.IsTag() + return cache.GetInt64(r.Repository.GetCommitsCountCacheKey(contextName, isRef), func() (int64, error) { return r.Commit.CommitsCount() }) } @@ -199,33 +192,13 @@ func (r *Repository) GetCommitGraphsCount(ctx context.Context, hidePRRefs bool, }) } -// BranchNameSubURL sub-URL for the BranchName field -func (r *Repository) BranchNameSubURL() string { - switch { - case r.IsViewBranch: - return "branch/" + util.PathEscapeSegments(r.BranchName) - case r.IsViewTag: - return "tag/" + util.PathEscapeSegments(r.TagName) - case r.IsViewCommit: - return "commit/" + util.PathEscapeSegments(r.CommitID) - } - log.Error("Unknown view type for repo: %v", r) - return "" -} - -// FileExists returns true if a file exists in the given repo branch -func (r *Repository) FileExists(path, branch string) (bool, error) { - if branch == "" { - branch = r.Repository.DefaultBranch - } - commit, err := r.GitRepo.GetBranchCommit(branch) - if err != nil { - return false, err - } - if _, err := commit.GetTreeEntryByPath(path); err != nil { - return false, err - } - return true, nil +// RefTypeNameSubURL makes a sub-url for the current ref (branch/tag/commit) field, for example: +// * "branch/master" +// * "tag/v1.0.0" +// * "commit/123456" +// It is usually used to construct a link like ".../src/{{RefTypeNameSubURL}}/{{PathEscapeSegments TreePath}}" +func (r *Repository) RefTypeNameSubURL() string { + return r.RefFullName.RefWebLinkPath() } // GetEditorconfig returns the .editorconfig definition if found in the @@ -325,9 +298,9 @@ func EarlyResponseForGoGetMeta(ctx *Context) { var cloneURL string if setting.Repository.GoGetCloneURLProtocol == "ssh" { - cloneURL = repo_model.ComposeSSHCloneURL(username, reponame) + cloneURL = repo_model.ComposeSSHCloneURL(ctx.Doer, username, reponame) } else { - cloneURL = repo_model.ComposeHTTPSCloneURL(username, reponame) + cloneURL = repo_model.ComposeHTTPSCloneURL(ctx, username, reponame) } goImportContent := fmt.Sprintf("%s git %s", ComposeGoGetImport(ctx, username, reponame), cloneURL) htmlMeta := fmt.Sprintf(``, html.EscapeString(goImportContent)) @@ -398,33 +371,25 @@ func repoAssignment(ctx *Context, repo *repo_model.Repository) { // RepoAssignment returns a middleware to handle repository assignment func RepoAssignment(ctx *Context) { - if _, repoAssignmentOnce := ctx.Data["repoAssignmentExecuted"]; repoAssignmentOnce { - // FIXME: it should panic in dev/test modes to have a clear behavior - if !setting.IsProd || setting.IsInTesting { - panic("RepoAssignment should not be executed twice") - } - return + if ctx.Data["Repository"] != nil { + setting.PanicInDevOrTesting("RepoAssignment should not be executed twice") } - ctx.Data["repoAssignmentExecuted"] = true - - var ( - owner *user_model.User - err error - ) + var err error userName := ctx.PathParam("username") repoName := ctx.PathParam("reponame") repoName = strings.TrimSuffix(repoName, ".git") if setting.Other.EnableFeed { + ctx.Data["EnableFeed"] = true repoName = strings.TrimSuffix(repoName, ".rss") repoName = strings.TrimSuffix(repoName, ".atom") } // Check if the user is the same as the repository owner if ctx.IsSigned && ctx.Doer.LowerName == strings.ToLower(userName) { - owner = ctx.Doer + ctx.Repo.Owner = ctx.Doer } else { - owner, err = user_model.GetUserByName(ctx, userName) + ctx.Repo.Owner, err = user_model.GetUserByName(ctx, userName) if err != nil { if user_model.IsErrUserNotExist(err) { // go-get does not support redirects @@ -447,10 +412,8 @@ func RepoAssignment(ctx *Context) { return } } - ctx.Repo.Owner = owner - ctx.ContextUser = owner + ctx.ContextUser = ctx.Repo.Owner ctx.Data["ContextUser"] = ctx.ContextUser - ctx.Data["Username"] = ctx.Repo.Owner.Name // redirect link to wiki if strings.HasSuffix(repoName, ".wiki") { @@ -473,10 +436,10 @@ func RepoAssignment(ctx *Context) { } // Get repository. - repo, err := repo_model.GetRepositoryByName(ctx, owner.ID, repoName) + repo, err := repo_model.GetRepositoryByName(ctx, ctx.Repo.Owner.ID, repoName) if err != nil { if repo_model.IsErrRepoNotExist(err) { - redirectRepoID, err := repo_model.LookupRedirect(ctx, owner.ID, repoName) + redirectRepoID, err := repo_model.LookupRedirect(ctx, ctx.Repo.Owner.ID, repoName) if err == nil { RedirectToRepo(ctx.Base, redirectRepoID) } else if repo_model.IsErrRedirectNotExist(err) { @@ -493,7 +456,7 @@ func RepoAssignment(ctx *Context) { } return } - repo.Owner = owner + repo.Owner = ctx.Repo.Owner repoAssignment(ctx, repo) if ctx.Written() { @@ -502,12 +465,7 @@ func RepoAssignment(ctx *Context) { ctx.Repo.RepoLink = repo.Link() ctx.Data["RepoLink"] = ctx.Repo.RepoLink - ctx.Data["RepoRelPath"] = ctx.Repo.Owner.Name + "/" + ctx.Repo.Repository.Name - - if setting.Other.EnableFeed { - ctx.Data["EnableFeed"] = true - ctx.Data["FeedURL"] = ctx.Repo.RepoLink - } + ctx.Data["FeedURL"] = ctx.Repo.RepoLink unit, err := ctx.Repo.Repository.GetUnit(ctx, unit_model.TypeExternalTracker) if err == nil { @@ -534,12 +492,9 @@ func RepoAssignment(ctx *Context) { return } - ctx.Data["Title"] = owner.Name + "/" + repo.Name + ctx.Data["Title"] = repo.Owner.Name + "/" + repo.Name ctx.Data["Repository"] = repo ctx.Data["Owner"] = ctx.Repo.Repository.Owner - ctx.Data["IsRepositoryOwner"] = ctx.Repo.IsOwner() - ctx.Data["IsRepositoryAdmin"] = ctx.Repo.IsAdmin() - ctx.Data["RepoOwnerIsOrganization"] = repo.Owner.IsOrganization() ctx.Data["CanWriteCode"] = ctx.Repo.CanWrite(unit_model.TypeCode) ctx.Data["CanWriteIssues"] = ctx.Repo.CanWrite(unit_model.TypeIssues) ctx.Data["CanWritePulls"] = ctx.Repo.CanWrite(unit_model.TypePullRequests) @@ -564,7 +519,7 @@ func RepoAssignment(ctx *Context) { // If multiple forks are available or if the user can fork to another account, but there is already a fork: open selection dialog ctx.Data["ShowForkModal"] = len(userAndOrgForks) > 1 || (canSignedUserFork && len(userAndOrgForks) > 0) - ctx.Data["RepoCloneLink"] = repo.CloneLink() + ctx.Data["RepoCloneLink"] = repo.CloneLink(ctx, ctx.Doer) cloneButtonShowHTTPS := !setting.Repository.DisableHTTPGit cloneButtonShowSSH := !setting.SSH.Disabled && (ctx.IsSigned || setting.SSH.ExposeAnonymous) @@ -607,7 +562,6 @@ func RepoAssignment(ctx *Context) { // Disable everything when the repo is being created if ctx.Repo.Repository.IsBeingCreated() || ctx.Repo.Repository.IsBroken() { - ctx.Data["BranchName"] = ctx.Repo.Repository.DefaultBranch if !isHomeOrSettings { ctx.Redirect(ctx.Repo.RepoLink) } @@ -615,19 +569,16 @@ func RepoAssignment(ctx *Context) { } if ctx.Repo.GitRepo != nil { - if !setting.IsProd || setting.IsInTesting { - panic("RepoAssignment: GitRepo should be nil") - } + setting.PanicInDevOrTesting("RepoAssignment: GitRepo should be nil") _ = ctx.Repo.GitRepo.Close() ctx.Repo.GitRepo = nil } - ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx, repo) + ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, repo) 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.MarkAsBrokenEmpty() - ctx.Data["BranchName"] = ctx.Repo.Repository.DefaultBranch // Only allow access to base of repo or settings if !isHomeOrSettings { ctx.Redirect(ctx.Repo.RepoLink) @@ -640,7 +591,6 @@ func RepoAssignment(ctx *Context) { // Stop at this point when the repo is empty. if ctx.Repo.Repository.IsEmpty { - ctx.Data["BranchName"] = ctx.Repo.Repository.DefaultBranch return } @@ -666,22 +616,6 @@ func RepoAssignment(ctx *Context) { ctx.Data["BranchesCount"] = branchesTotal - // If no branch is set in the request URL, try to guess a default one. - if len(ctx.Repo.BranchName) == 0 { - if len(ctx.Repo.Repository.DefaultBranch) > 0 && ctx.Repo.GitRepo.IsBranchExist(ctx.Repo.Repository.DefaultBranch) { - ctx.Repo.BranchName = ctx.Repo.Repository.DefaultBranch - } else { - ctx.Repo.BranchName, _ = gitrepo.GetDefaultBranch(ctx, ctx.Repo.Repository) - if ctx.Repo.BranchName == "" { - // If it still can't get a default branch, fall back to default branch from setting. - // Something might be wrong. Either site admin should fix the repo sync or Gitea should fix a potential bug. - ctx.Repo.BranchName = setting.Repository.DefaultBranch - } - } - ctx.Repo.RefName = ctx.Repo.BranchName - } - ctx.Data["BranchName"] = ctx.Repo.BranchName - // People who have push access or have forked repository can propose a new pull request. canPush := ctx.Repo.CanWrite(unit_model.TypeCode) || (ctx.IsSigned && repo_model.HasForkedRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID)) @@ -719,37 +653,24 @@ func RepoAssignment(ctx *Context) { ctx.Data["RepoTransfer"] = repoTransfer if ctx.Doer != nil { - ctx.Data["CanUserAcceptTransfer"] = repoTransfer.CanUserAcceptTransfer(ctx, ctx.Doer) + ctx.Data["CanUserAcceptOrRejectTransfer"] = repoTransfer.CanUserAcceptOrRejectTransfer(ctx, ctx.Doer) } } if ctx.FormString("go-get") == "1" { - ctx.Data["GoGetImport"] = ComposeGoGetImport(ctx, owner.Name, repo.Name) + ctx.Data["GoGetImport"] = ComposeGoGetImport(ctx, repo.Owner.Name, repo.Name) fullURLPrefix := repo.HTMLURL() + "/src/branch/" + util.PathEscapeSegments(ctx.Repo.BranchName) ctx.Data["GoDocDirectory"] = fullURLPrefix + "{/dir}" ctx.Data["GoDocFile"] = fullURLPrefix + "{/dir}/{file}#L{line}" } } -// RepoRefType type of repo reference -type RepoRefType int - -const ( - // RepoRefUnknown is for legacy support, makes the code to "guess" the ref type - RepoRefUnknown RepoRefType = iota - RepoRefBranch - RepoRefTag - RepoRefCommit - RepoRefBlob -) - const headRefName = "HEAD" -// RepoRef handles repository reference names when the ref name is not -// explicitly given func RepoRef() func(*Context) { - // since no ref name is explicitly specified, ok to just use branch - return RepoRefByType(RepoRefBranch) + // old code does: return RepoRefByType(git.RefTypeBranch) + // in most cases, it is an abuse, so we just disable it completely and fix the abuses one by one (if there is anything wrong) + return nil } func getRefNameFromPath(repo *Repository, path string, isExist func(string) bool) string { @@ -765,37 +686,29 @@ func getRefNameFromPath(repo *Repository, path string, isExist func(string) bool return "" } -func getRefNameLegacy(ctx *Base, repo *Repository, optionalExtraRef ...string) (string, RepoRefType) { - extraRef := util.OptionalArg(optionalExtraRef) - reqPath := ctx.PathParam("*") - reqPath = path.Join(extraRef, reqPath) - - if refName := getRefName(ctx, repo, RepoRefBranch); refName != "" { - return refName, RepoRefBranch +func getRefNameLegacy(ctx *Base, repo *Repository, reqPath, extraRef string) (refName string, refType git.RefType, fallbackDefaultBranch bool) { + reqRefPath := path.Join(extraRef, reqPath) + reqRefPathParts := strings.Split(reqRefPath, "/") + if refName := getRefName(ctx, repo, reqRefPath, git.RefTypeBranch); refName != "" { + return refName, git.RefTypeBranch, false } - if refName := getRefName(ctx, repo, RepoRefTag); refName != "" { - return refName, RepoRefTag + if refName := getRefName(ctx, repo, reqRefPath, git.RefTypeTag); refName != "" { + return refName, git.RefTypeTag, false } - - // For legacy support only full commit sha - parts := strings.Split(reqPath, "/") - if git.IsStringLikelyCommitID(git.ObjectFormatFromName(repo.Repository.ObjectFormatName), parts[0]) { + if git.IsStringLikelyCommitID(git.ObjectFormatFromName(repo.Repository.ObjectFormatName), reqRefPathParts[0]) { // FIXME: this logic is different from other types. Ideally, it should also try to GetCommit to check if it exists - repo.TreePath = strings.Join(parts[1:], "/") - return parts[0], RepoRefCommit - } - - if refName := getRefName(ctx, repo, RepoRefBlob); len(refName) > 0 { - return refName, RepoRefBlob + repo.TreePath = strings.Join(reqRefPathParts[1:], "/") + return reqRefPathParts[0], git.RefTypeCommit, false } + // FIXME: the old code falls back to default branch if "ref" doesn't exist, there could be an edge case: + // "README?ref=no-such" would read the README file from the default branch, but the user might expect a 404 repo.TreePath = reqPath - return repo.Repository.DefaultBranch, RepoRefBranch + return repo.Repository.DefaultBranch, git.RefTypeBranch, true } -func getRefName(ctx *Base, repo *Repository, pathType RepoRefType) string { - path := ctx.PathParam("*") - switch pathType { - case RepoRefBranch: +func getRefName(ctx *Base, repo *Repository, path string, refType git.RefType) string { + switch refType { + case git.RefTypeBranch: ref := getRefNameFromPath(repo, path, repo.GitRepo.IsBranchExist) if len(ref) == 0 { // check if ref is HEAD @@ -825,9 +738,9 @@ func getRefName(ctx *Base, repo *Repository, pathType RepoRefType) string { } return ref - case RepoRefTag: + case git.RefTypeTag: return getRefNameFromPath(repo, path, repo.GitRepo.IsTagExist) - case RepoRefCommit: + case git.RefTypeCommit: parts := strings.Split(path, "/") if git.IsStringLikelyCommitID(repo.GetObjectFormat(), parts[0], 7) { // FIXME: this logic is different from other types. Ideally, it should also try to GetCommit to check if it exists @@ -844,111 +757,117 @@ func getRefName(ctx *Base, repo *Repository, pathType RepoRefType) string { repo.TreePath = strings.Join(parts[1:], "/") return commit.ID.String() } - case RepoRefBlob: - _, err := repo.GitRepo.GetBlob(path) - if err != nil { - return "" - } - return path default: - panic(fmt.Sprintf("Unrecognized path type: %v", pathType)) + panic(fmt.Sprintf("Unrecognized ref type: %v", refType)) } return "" } -type RepoRefByTypeOptions struct { - IgnoreNotExistErr bool +func repoRefFullName(typ git.RefType, shortName string) git.RefName { + switch typ { + case git.RefTypeBranch: + return git.RefNameFromBranch(shortName) + case git.RefTypeTag: + return git.RefNameFromTag(shortName) + case git.RefTypeCommit: + return git.RefNameFromCommit(shortName) + default: + setting.PanicInDevOrTesting("Unknown RepoRefType: %v", typ) + return git.RefNameFromBranch("main") // just a dummy result, it shouldn't happen + } +} + +func RepoRefByDefaultBranch() func(*Context) { + return func(ctx *Context) { + ctx.Repo.RefFullName = git.RefNameFromBranch(ctx.Repo.Repository.DefaultBranch) + ctx.Repo.BranchName = ctx.Repo.Repository.DefaultBranch + ctx.Repo.Commit, _ = ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.BranchName) + ctx.Repo.CommitsCount, _ = ctx.Repo.GetCommitsCount() + ctx.Data["RefFullName"] = ctx.Repo.RefFullName + ctx.Data["BranchName"] = ctx.Repo.BranchName + ctx.Data["CommitsCount"] = ctx.Repo.CommitsCount + } } // RepoRefByType handles repository reference name for a specific type // of repository reference -func RepoRefByType(detectRefType RepoRefType, opts ...RepoRefByTypeOptions) func(*Context) { - opt := util.OptionalArg(opts) +func RepoRefByType(detectRefType git.RefType) func(*Context) { return func(ctx *Context) { + var err error refType := detectRefType + if ctx.Repo.Repository.IsBeingCreated() { + return // no git repo, so do nothing, users will see a "migrating" UI provided by "migrate/migrating.tmpl" + } // 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.Repo.RefFullName = git.RefNameFromBranch(ctx.Repo.BranchName) + // these variables are used by the template to "add/upload" new files + ctx.Data["BranchName"] = ctx.Repo.BranchName ctx.Data["TreePath"] = "" return } - var ( - refName string - err error - ) - - if ctx.Repo.GitRepo == nil { - ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx, ctx.Repo.Repository) - if err != nil { - ctx.ServerError(fmt.Sprintf("Open Repository %v failed", ctx.Repo.Repository.FullName()), err) - return - } - } - // Get default branch. - if len(ctx.PathParam("*")) == 0 { - refName = ctx.Repo.Repository.DefaultBranch - if !ctx.Repo.GitRepo.IsBranchExist(refName) { + var refShortName string + reqPath := ctx.PathParam("*") + if reqPath == "" { + refShortName = ctx.Repo.Repository.DefaultBranch + if !ctx.Repo.GitRepo.IsBranchExist(refShortName) { brs, _, err := ctx.Repo.GitRepo.GetBranches(0, 1) if err == nil && len(brs) != 0 { - refName = brs[0].Name + refShortName = brs[0].Name } else if len(brs) == 0 { 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() } } - ctx.Repo.RefName = refName - ctx.Repo.BranchName = refName - ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetBranchCommit(refName) + ctx.Repo.RefFullName = git.RefNameFromBranch(refShortName) + ctx.Repo.BranchName = refShortName + ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetBranchCommit(refShortName) 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.IsViewBranch = true - } else { - guessLegacyPath := refType == RepoRefUnknown + } else { // there is a path in request + guessLegacyPath := refType == "" + fallbackDefaultBranch := false if guessLegacyPath { - refName, refType = getRefNameLegacy(ctx.Base, ctx.Repo) + refShortName, refType, fallbackDefaultBranch = getRefNameLegacy(ctx.Base, ctx.Repo, reqPath, "") } else { - refName = getRefName(ctx.Base, ctx.Repo, refType) + refShortName = getRefName(ctx.Base, ctx.Repo, reqPath, refType) } - ctx.Repo.RefName = refName + ctx.Repo.RefFullName = repoRefFullName(refType, refShortName) isRenamedBranch, has := ctx.Data["IsRenamedBranch"].(bool) if isRenamedBranch && has { renamedBranchName := ctx.Data["RenamedBranchName"].(string) - ctx.Flash.Info(ctx.Tr("repo.branch.renamed", refName, renamedBranchName)) - link := setting.AppSubURL + strings.Replace(ctx.Req.URL.EscapedPath(), util.PathEscapeSegments(refName), util.PathEscapeSegments(renamedBranchName), 1) + ctx.Flash.Info(ctx.Tr("repo.branch.renamed", refShortName, renamedBranchName)) + link := setting.AppSubURL + strings.Replace(ctx.Req.URL.EscapedPath(), util.PathEscapeSegments(refShortName), util.PathEscapeSegments(renamedBranchName), 1) ctx.Redirect(link) return } - if refType == RepoRefBranch && ctx.Repo.GitRepo.IsBranchExist(refName) { - ctx.Repo.IsViewBranch = true - ctx.Repo.BranchName = refName + if refType == git.RefTypeBranch && ctx.Repo.GitRepo.IsBranchExist(refShortName) { + ctx.Repo.BranchName = refShortName + ctx.Repo.RefFullName = git.RefNameFromBranch(refShortName) - ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetBranchCommit(refName) + ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetBranchCommit(refShortName) if err != nil { ctx.ServerError("GetBranchCommit", err) return } ctx.Repo.CommitID = ctx.Repo.Commit.ID.String() - } else if refType == RepoRefTag && ctx.Repo.GitRepo.IsTagExist(refName) { - ctx.Repo.IsViewTag = true - ctx.Repo.TagName = refName + } else if refType == git.RefTypeTag && ctx.Repo.GitRepo.IsTagExist(refShortName) { + ctx.Repo.RefFullName = git.RefNameFromTag(refShortName) - ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetTagCommit(refName) + ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetTagCommit(refShortName) if err != nil { if git.IsErrNotExist(err) { ctx.NotFound("GetTagCommit", err) @@ -958,50 +877,58 @@ func RepoRefByType(detectRefType RepoRefType, opts ...RepoRefByTypeOptions) func return } ctx.Repo.CommitID = ctx.Repo.Commit.ID.String() - } else if git.IsStringLikelyCommitID(ctx.Repo.GetObjectFormat(), refName, 7) { - ctx.Repo.IsViewCommit = true - ctx.Repo.CommitID = refName + } else if git.IsStringLikelyCommitID(ctx.Repo.GetObjectFormat(), refShortName, 7) { + ctx.Repo.RefFullName = git.RefNameFromCommit(refShortName) + ctx.Repo.CommitID = refShortName - ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetCommit(refName) + ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetCommit(refShortName) if err != nil { ctx.NotFound("GetCommit", err) return } // If short commit ID add canonical link header - if len(refName) < ctx.Repo.GetObjectFormat().FullLength() { - canonicalURL := util.URLJoin(httplib.GuessCurrentAppURL(ctx), strings.Replace(ctx.Req.URL.RequestURI(), util.PathEscapeSegments(refName), url.PathEscape(ctx.Repo.Commit.ID.String()), 1)) + if len(refShortName) < ctx.Repo.GetObjectFormat().FullLength() { + canonicalURL := util.URLJoin(httplib.GuessCurrentAppURL(ctx), strings.Replace(ctx.Req.URL.RequestURI(), util.PathEscapeSegments(refShortName), url.PathEscape(ctx.Repo.Commit.ID.String()), 1)) ctx.RespHeader().Set("Link", fmt.Sprintf(`<%s>; rel="canonical"`, canonicalURL)) } } else { - if opt.IgnoreNotExistErr { - return - } - ctx.NotFound("RepoRef invalid repo", fmt.Errorf("branch or tag not exist: %s", refName)) + ctx.NotFound("RepoRef invalid repo", fmt.Errorf("branch or tag not exist: %s", refShortName)) return } if guessLegacyPath { // redirect from old URL scheme to new URL scheme - prefix := strings.TrimPrefix(setting.AppSubURL+strings.ToLower(strings.TrimSuffix(ctx.Req.URL.Path, ctx.PathParam("*"))), strings.ToLower(ctx.Repo.RepoLink)) - redirect := path.Join( - ctx.Repo.RepoLink, - util.PathEscapeSegments(prefix), - ctx.Repo.BranchNameSubURL(), - util.PathEscapeSegments(ctx.Repo.TreePath)) + // * /user2/repo1/commits/master => /user2/repo1/commits/branch/master + // * /user2/repo1/src/master => /user2/repo1/src/branch/master + // * /user2/repo1/src/README.md => /user2/repo1/src/branch/master/README.md (fallback to default branch) + var redirect string + refSubPath := "src" + // remove the "/subpath/owner/repo/" prefix, the names are case-insensitive + remainingLowerPath, cut := strings.CutPrefix(setting.AppSubURL+strings.ToLower(ctx.Req.URL.Path), strings.ToLower(ctx.Repo.RepoLink)+"/") + if cut { + refSubPath, _, _ = strings.Cut(remainingLowerPath, "/") // it could be "src" or "commits" + } + if fallbackDefaultBranch { + redirect = fmt.Sprintf("%s/%s/%s/%s/%s", ctx.Repo.RepoLink, refSubPath, refType, util.PathEscapeSegments(refShortName), ctx.PathParamRaw("*")) + } else { + redirect = fmt.Sprintf("%s/%s/%s/%s", ctx.Repo.RepoLink, refSubPath, refType, ctx.PathParamRaw("*")) + } + if ctx.Req.URL.RawQuery != "" { + redirect += "?" + ctx.Req.URL.RawQuery + } ctx.Redirect(redirect) return } } - ctx.Data["BranchName"] = ctx.Repo.BranchName - ctx.Data["RefName"] = ctx.Repo.RefName - ctx.Data["BranchNameSubURL"] = ctx.Repo.BranchNameSubURL() - ctx.Data["TagName"] = ctx.Repo.TagName - ctx.Data["CommitID"] = ctx.Repo.CommitID + ctx.Data["RefFullName"] = ctx.Repo.RefFullName + ctx.Data["RefTypeNameSubURL"] = ctx.Repo.RefTypeNameSubURL() ctx.Data["TreePath"] = ctx.Repo.TreePath - ctx.Data["IsViewBranch"] = ctx.Repo.IsViewBranch - ctx.Data["IsViewTag"] = ctx.Repo.IsViewTag - ctx.Data["IsViewCommit"] = ctx.Repo.IsViewCommit + + ctx.Data["BranchName"] = ctx.Repo.BranchName + + ctx.Data["CommitID"] = ctx.Repo.CommitID + ctx.Data["CanCreateBranch"] = ctx.Repo.CanCreateBranch() // only used by the branch selector dropdown: AllowCreateNewRef ctx.Repo.CommitsCount, err = ctx.Repo.GetCommitsCount() diff --git a/services/context/response.go b/services/context/response.go index 2f271f211b8..c7368ebc6f0 100644 --- a/services/context/response.go +++ b/services/context/response.go @@ -11,31 +11,29 @@ import ( // ResponseWriter represents a response writer for HTTP type ResponseWriter interface { - http.ResponseWriter - http.Flusher - web_types.ResponseStatusProvider + http.ResponseWriter // provides Header/Write/WriteHeader + http.Flusher // provides Flush + web_types.ResponseStatusProvider // provides WrittenStatus - Before(func(ResponseWriter)) - - Status() int // used by access logger template - Size() int // used by access logger template + Before(fn func(ResponseWriter)) + WrittenSize() int } -var _ ResponseWriter = &Response{} +var _ ResponseWriter = (*Response)(nil) // Response represents a response type Response struct { http.ResponseWriter written int status int - befores []func(ResponseWriter) + beforeFuncs []func(ResponseWriter) beforeExecuted bool } // Write writes bytes to HTTP endpoint func (r *Response) Write(bs []byte) (int, error) { if !r.beforeExecuted { - for _, before := range r.befores { + for _, before := range r.beforeFuncs { before(r) } r.beforeExecuted = true @@ -51,18 +49,14 @@ func (r *Response) Write(bs []byte) (int, error) { return size, nil } -func (r *Response) Status() int { - return r.status -} - -func (r *Response) Size() int { +func (r *Response) WrittenSize() int { return r.written } // WriteHeader write status code func (r *Response) WriteHeader(statusCode int) { if !r.beforeExecuted { - for _, before := range r.befores { + for _, before := range r.beforeFuncs { before(r) } r.beforeExecuted = true @@ -87,17 +81,13 @@ func (r *Response) WrittenStatus() int { // Before allows for a function to be called before the ResponseWriter has been written to. This is // useful for setting headers or any other operations that must happen before a response has been written. -func (r *Response) Before(f func(ResponseWriter)) { - r.befores = append(r.befores, f) +func (r *Response) Before(fn func(ResponseWriter)) { + r.beforeFuncs = append(r.beforeFuncs, fn) } func WrapResponseWriter(resp http.ResponseWriter) *Response { if v, ok := resp.(*Response); ok { return v } - return &Response{ - ResponseWriter: resp, - status: 0, - befores: make([]func(ResponseWriter), 0), - } + return &Response{ResponseWriter: resp} } diff --git a/services/convert/issue.go b/services/convert/issue.go index e3124efd640..37935accca2 100644 --- a/services/convert/issue.go +++ b/services/convert/issue.go @@ -16,6 +16,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/util" ) func ToIssue(ctx context.Context, doer *user_model.User, issue *issues_model.Issue) *api.Issue { @@ -186,7 +187,7 @@ func ToStopWatches(ctx context.Context, sws []*issues_model.Stopwatch) (api.Stop result = append(result, api.StopWatch{ Created: sw.CreatedUnix.AsTime(), Seconds: sw.Seconds(), - Duration: sw.Duration(), + Duration: util.SecToHours(sw.Seconds()), IssueIndex: issue.Index, IssueTitle: issue.Title, RepoOwnerName: repo.OwnerName, diff --git a/services/convert/issue_comment.go b/services/convert/issue_comment.go index b8527ae233c..9ad584a62f7 100644 --- a/services/convert/issue_comment.go +++ b/services/convert/issue_comment.go @@ -74,7 +74,7 @@ func ToTimelineComment(ctx context.Context, repo *repo_model.Repository, c *issu c.Content[0] == '|' { // TimeTracking Comments from v1.21 on store the seconds instead of an formatted string // so we check for the "|" delimiter and convert new to legacy format on demand - c.Content = util.SecToTime(c.Content[1:]) + c.Content = util.SecToHours(c.Content[1:]) } if c.Type == issues_model.CommentTypeChangeTimeEstimate { diff --git a/services/convert/repository.go b/services/convert/repository.go index 88ccd88fcfe..632b6392d57 100644 --- a/services/convert/repository.go +++ b/services/convert/repository.go @@ -33,7 +33,9 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR permissionInRepo.SetUnitsWithDefaultAccessMode(repo.Units, permissionInRepo.AccessMode) } - cloneLink := repo.CloneLink() + // TODO: ideally we should pass "doer" into "ToRepo" to to make CloneLink could generate user-related links + // And passing "doer" in will also fix other FIXMEs in this file. + cloneLink := repo.CloneLinkGeneral(ctx) // no doer at the moment permission := &api.Permission{ Admin: permissionInRepo.AccessMode >= perm.AccessModeAdmin, Push: permissionInRepo.UnitAccessMode(unit_model.TypeCode) >= perm.AccessModeWrite, diff --git a/services/convert/utils.go b/services/convert/utils.go index 5e9d32cc8ed..b59884ec508 100644 --- a/services/convert/utils.go +++ b/services/convert/utils.go @@ -36,6 +36,8 @@ func ToGitServiceType(value string) structs.GitServiceType { return structs.OneDevService case "gitbucket": return structs.GitBucketService + case "codebase": + return structs.CodebaseService case "codecommit": return structs.CodeCommitService default: diff --git a/services/convert/utils_test.go b/services/convert/utils_test.go index 1ac03a30975..a8363ec6bde 100644 --- a/services/convert/utils_test.go +++ b/services/convert/utils_test.go @@ -21,6 +21,8 @@ func TestToGitServiceType(t *testing.T) { typ string enum int }{{ + typ: "trash", enum: 1, + }, { typ: "github", enum: 2, }, { typ: "gitea", enum: 3, @@ -29,7 +31,13 @@ func TestToGitServiceType(t *testing.T) { }, { typ: "gogs", enum: 5, }, { - typ: "trash", enum: 1, + typ: "onedev", enum: 6, + }, { + typ: "gitbucket", enum: 7, + }, { + typ: "codebase", enum: 8, + }, { + typ: "codecommit", enum: 9, }} for _, test := range tc { assert.EqualValues(t, test.enum, ToGitServiceType(test.typ)) diff --git a/services/doctor/dbconsistency_test.go b/services/doctor/dbconsistency_test.go index 4e4ac535b7b..eb427dee737 100644 --- a/services/doctor/dbconsistency_test.go +++ b/services/doctor/dbconsistency_test.go @@ -14,6 +14,7 @@ import ( "code.gitea.io/gitea/modules/log" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestConsistencyCheck(t *testing.T) { @@ -21,9 +22,7 @@ func TestConsistencyCheck(t *testing.T) { idx := slices.IndexFunc(checks, func(check consistencyCheck) bool { return check.Name == "Orphaned OAuth2Application without existing User" }) - if !assert.NotEqual(t, -1, idx) { - return - } + require.NotEqual(t, -1, idx) _ = db.TruncateBeans(db.DefaultContext, &auth.OAuth2Application{}, &user.User{}) _ = db.TruncateBeans(db.DefaultContext, &auth.OAuth2Application{}, &auth.OAuth2Application{}) diff --git a/services/feed/notifier.go b/services/feed/notifier.go index 702eb5ad533..3aaf885c9a5 100644 --- a/services/feed/notifier.go +++ b/services/feed/notifier.go @@ -469,7 +469,7 @@ func (a *actionNotifier) NewRelease(ctx context.Context, rel *repo_model.Release Repo: rel.Repo, IsPrivate: rel.Repo.IsPrivate, Content: rel.Title, - RefName: rel.TagName, // FIXME: use a full ref name? + RefName: git.RefNameFromTag(rel.TagName).String(), // Other functions in this file all use "refFullName.String()" }); err != nil { log.Error("NotifyWatchers: %v", err) } diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go index b14171787e4..70019f3fa90 100644 --- a/services/forms/repo_form.go +++ b/services/forms/repo_form.go @@ -110,41 +110,51 @@ type RepoSettingForm struct { EnablePrune bool // Advanced settings - EnableCode bool - EnableWiki bool - EnableExternalWiki bool - DefaultWikiBranch string - DefaultWikiEveryoneAccess string - ExternalWikiURL string + EnableCode bool + DefaultCodeEveryoneAccess string + + EnableWiki bool + EnableExternalWiki bool + DefaultWikiBranch string + DefaultWikiEveryoneAccess string + ExternalWikiURL string + EnableIssues bool + DefaultIssuesEveryoneAccess string EnableExternalTracker bool ExternalTrackerURL string TrackerURLFormat string TrackerIssueStyle string ExternalTrackerRegexpPattern string EnableCloseIssuesViaCommitInAnyBranch bool - EnableProjects bool - ProjectsMode string - EnableReleases bool - EnablePackages bool - EnablePulls bool - EnableActions bool - PullsIgnoreWhitespace bool - PullsAllowMerge bool - PullsAllowRebase bool - PullsAllowRebaseMerge bool - PullsAllowSquash bool - PullsAllowFastForwardOnly bool - PullsAllowManualMerge bool - PullsDefaultMergeStyle string - EnableAutodetectManualMerge bool - PullsAllowRebaseUpdate bool - DefaultDeleteBranchAfterMerge bool - DefaultAllowMaintainerEdit bool - EnableTimetracker bool - AllowOnlyContributorsToTrackTime bool - EnableIssueDependencies bool - IsArchived bool + + EnableProjects bool + ProjectsMode string + + EnableReleases bool + + EnablePackages bool + + EnablePulls bool + PullsIgnoreWhitespace bool + PullsAllowMerge bool + PullsAllowRebase bool + PullsAllowRebaseMerge bool + PullsAllowSquash bool + PullsAllowFastForwardOnly bool + PullsAllowManualMerge bool + PullsDefaultMergeStyle string + EnableAutodetectManualMerge bool + PullsAllowRebaseUpdate bool + DefaultDeleteBranchAfterMerge bool + DefaultAllowMaintainerEdit bool + EnableTimetracker bool + AllowOnlyContributorsToTrackTime bool + EnableIssueDependencies bool + + EnableActions bool + + IsArchived bool // Signing Settings TrustModel string @@ -209,26 +219,18 @@ type ProtectBranchPriorityForm struct { IDs []int64 } -// __ __ ___. .__ __ -// / \ / \ ____\_ |__ | |__ ____ ____ | | __ -// \ \/\/ // __ \| __ \| | \ / _ \ / _ \| |/ / -// \ /\ ___/| \_\ \ Y ( <_> | <_> ) < -// \__/\ / \___ >___ /___| /\____/ \____/|__|_ \ -// \/ \/ \/ \/ \/ - // WebhookForm form for changing web hook type WebhookForm struct { Events string Create bool Delete bool Fork bool + Push bool Issues bool IssueAssign bool IssueLabel bool IssueMilestone bool IssueComment bool - Release bool - Push bool PullRequest bool PullRequestAssign bool PullRequestLabel bool @@ -239,7 +241,9 @@ type WebhookForm struct { PullRequestReviewRequest bool Wiki bool Repository bool + Release bool Package bool + Status bool Active bool BranchFilter string `binding:"GlobPattern"` AuthorizationHeader string @@ -651,8 +655,8 @@ type NewReleaseForm struct { Target string `form:"tag_target" binding:"Required;MaxSize(255)"` Title string `binding:"MaxSize(255)"` Content string - Draft string - TagOnly string + Draft bool + TagOnly bool Prerelease bool AddTagMsg bool Files []string @@ -717,6 +721,7 @@ type EditRepoFileForm struct { NewBranchName string `binding:"GitRefName;MaxSize(100)"` LastCommit string Signoff bool + CommitEmail string } // Validate validates the fields @@ -752,6 +757,7 @@ type CherryPickForm struct { LastCommit string Revert bool Signoff bool + CommitEmail string } // Validate validates the fields @@ -777,6 +783,7 @@ type UploadRepoFileForm struct { NewBranchName string `binding:"GitRefName;MaxSize(100)"` Files []string Signoff bool + CommitEmail string } // Validate validates the fields @@ -811,6 +818,7 @@ type DeleteRepoFileForm struct { NewBranchName string `binding:"GitRefName;MaxSize(100)"` LastCommit string Signoff bool + CommitEmail string } // Validate validates the fields diff --git a/services/gitdiff/csv_test.go b/services/gitdiff/csv_test.go index c006a7c2bd8..f91e0e9aa72 100644 --- a/services/gitdiff/csv_test.go +++ b/services/gitdiff/csv_test.go @@ -192,23 +192,18 @@ c,d,e`, for n, c := range cases { diff, err := ParsePatch(db.DefaultContext, setting.Git.MaxGitDiffLines, setting.Git.MaxGitDiffLineCharacters, setting.Git.MaxGitDiffFiles, strings.NewReader(c.diff), "") - if err != nil { - t.Errorf("ParsePatch failed: %s", err) - } + assert.NoError(t, err) var baseReader *csv.Reader if len(c.base) > 0 { baseReader, err = csv_module.CreateReaderAndDetermineDelimiter(nil, strings.NewReader(c.base)) - if err != nil { - t.Errorf("CreateReaderAndDetermineDelimiter failed: %s", err) - } + assert.NoError(t, err) } + var headReader *csv.Reader if len(c.head) > 0 { headReader, err = csv_module.CreateReaderAndDetermineDelimiter(nil, strings.NewReader(c.head)) - if err != nil { - t.Errorf("CreateReaderAndDetermineDelimiter failed: %s", err) - } + assert.NoError(t, err) } result, err := CreateCsvDiff(diff.Files[0], baseReader, headReader) diff --git a/services/gitdiff/git_diff_tree.go b/services/gitdiff/git_diff_tree.go new file mode 100644 index 00000000000..8039de145d2 --- /dev/null +++ b/services/gitdiff/git_diff_tree.go @@ -0,0 +1,249 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package gitdiff + +import ( + "bufio" + "context" + "fmt" + "io" + "strconv" + "strings" + + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/log" +) + +type DiffTree struct { + Files []*DiffTreeRecord +} + +type DiffTreeRecord struct { + // Status is one of 'added', 'deleted', 'modified', 'renamed', 'copied', 'typechanged', 'unmerged', 'unknown' + Status string + + // For renames and copies, the percentage of similarity between the source and target of the move/rename. + Score uint8 + + HeadPath string + BasePath string + HeadMode git.EntryMode + BaseMode git.EntryMode + HeadBlobID string + BaseBlobID string +} + +// GetDiffTree returns the list of path of the files that have changed between the two commits. +// If useMergeBase is true, the diff will be calculated using the merge base of the two commits. +// This is the same behavior as using a three-dot diff in git diff. +func GetDiffTree(ctx context.Context, gitRepo *git.Repository, useMergeBase bool, baseSha, headSha string) (*DiffTree, error) { + gitDiffTreeRecords, err := runGitDiffTree(ctx, gitRepo, useMergeBase, baseSha, headSha) + if err != nil { + return nil, err + } + + return &DiffTree{ + Files: gitDiffTreeRecords, + }, nil +} + +func runGitDiffTree(ctx context.Context, gitRepo *git.Repository, useMergeBase bool, baseSha, headSha string) ([]*DiffTreeRecord, error) { + useMergeBase, baseCommitID, headCommitID, err := validateGitDiffTreeArguments(gitRepo, useMergeBase, baseSha, headSha) + if err != nil { + return nil, err + } + + cmd := git.NewCommand(ctx, "diff-tree", "--raw", "-r", "--find-renames", "--root") + if useMergeBase { + cmd.AddArguments("--merge-base") + } + cmd.AddDynamicArguments(baseCommitID, headCommitID) + stdout, _, runErr := cmd.RunStdString(&git.RunOpts{Dir: gitRepo.Path}) + if runErr != nil { + log.Warn("git diff-tree: %v", runErr) + return nil, runErr + } + + return parseGitDiffTree(strings.NewReader(stdout)) +} + +func validateGitDiffTreeArguments(gitRepo *git.Repository, useMergeBase bool, baseSha, headSha string) (shouldUseMergeBase bool, resolvedBaseSha, resolvedHeadSha string, err error) { + // if the head is empty its an error + if headSha == "" { + return false, "", "", fmt.Errorf("headSha is empty") + } + + // if the head commit doesn't exist its and error + headCommit, err := gitRepo.GetCommit(headSha) + if err != nil { + return false, "", "", fmt.Errorf("failed to get commit headSha: %v", err) + } + headCommitID := headCommit.ID.String() + + // if the base is empty we should use the parent of the head commit + if baseSha == "" { + // if the headCommit has no parent we should use an empty commit + // this can happen when we are generating a diff against an orphaned commit + if headCommit.ParentCount() == 0 { + objectFormat, err := gitRepo.GetObjectFormat() + if err != nil { + return false, "", "", err + } + + // We set use merge base to false because we have no base commit + return false, objectFormat.EmptyTree().String(), headCommitID, nil + } + + baseCommit, err := headCommit.Parent(0) + if err != nil { + return false, "", "", fmt.Errorf("baseSha is '', attempted to use parent of commit %s, got error: %v", headCommit.ID.String(), err) + } + return useMergeBase, baseCommit.ID.String(), headCommitID, nil + } + + // try and get the base commit + baseCommit, err := gitRepo.GetCommit(baseSha) + // propagate the error if we couldn't get the base commit + if err != nil { + return useMergeBase, "", "", fmt.Errorf("failed to get base commit %s: %v", baseSha, err) + } + + return useMergeBase, baseCommit.ID.String(), headCommit.ID.String(), nil +} + +func parseGitDiffTree(gitOutput io.Reader) ([]*DiffTreeRecord, error) { + /* + The output of `git diff-tree --raw -r --find-renames` is of the form: + + : \t + + or for renames: + + : \t\t + + See: for more details + */ + results := make([]*DiffTreeRecord, 0) + + lines := bufio.NewScanner(gitOutput) + for lines.Scan() { + line := lines.Text() + + if len(line) == 0 { + continue + } + + record, err := parseGitDiffTreeLine(line) + if err != nil { + return nil, err + } + + results = append(results, record) + } + + if err := lines.Err(); err != nil { + return nil, err + } + + return results, nil +} + +func parseGitDiffTreeLine(line string) (*DiffTreeRecord, error) { + line = strings.TrimPrefix(line, ":") + splitSections := strings.SplitN(line, "\t", 2) + if len(splitSections) < 2 { + return nil, fmt.Errorf("unparsable output for diff-tree --raw: `%s`)", line) + } + + fields := strings.Fields(splitSections[0]) + if len(fields) < 5 { + return nil, fmt.Errorf("unparsable output for diff-tree --raw: `%s`, expected 5 space delimited values got %d)", line, len(fields)) + } + + baseMode, err := git.ParseEntryMode(fields[0]) + if err != nil { + return nil, err + } + + headMode, err := git.ParseEntryMode(fields[1]) + if err != nil { + return nil, err + } + + baseBlobID := fields[2] + headBlobID := fields[3] + + status, score, err := statusFromLetter(fields[4]) + if err != nil { + return nil, fmt.Errorf("unparsable output for diff-tree --raw: %s, error: %s", line, err) + } + + filePaths := strings.Split(splitSections[1], "\t") + + var headPath, basePath string + if status == "renamed" { + if len(filePaths) != 2 { + return nil, fmt.Errorf("unparsable output for diff-tree --raw: `%s`, expected 2 paths found %d", line, len(filePaths)) + } + basePath = filePaths[0] + headPath = filePaths[1] + } else { + basePath = filePaths[0] + headPath = filePaths[0] + } + + return &DiffTreeRecord{ + Status: status, + Score: score, + BaseMode: baseMode, + HeadMode: headMode, + BaseBlobID: baseBlobID, + HeadBlobID: headBlobID, + BasePath: basePath, + HeadPath: headPath, + }, nil +} + +func statusFromLetter(rawStatus string) (status string, score uint8, err error) { + if len(rawStatus) < 1 { + return "", 0, fmt.Errorf("empty status letter") + } + switch rawStatus[0] { + case 'A': + return "added", 0, nil + case 'D': + return "deleted", 0, nil + case 'M': + return "modified", 0, nil + case 'R': + score, err = tryParseStatusScore(rawStatus) + return "renamed", score, err + case 'C': + score, err = tryParseStatusScore(rawStatus) + return "copied", score, err + case 'T': + return "typechanged", 0, nil + case 'U': + return "unmerged", 0, nil + case 'X': + return "unknown", 0, nil + default: + return "", 0, fmt.Errorf("unknown status letter: '%s'", rawStatus) + } +} + +func tryParseStatusScore(rawStatus string) (uint8, error) { + if len(rawStatus) < 2 { + return 0, fmt.Errorf("status score missing") + } + + score, err := strconv.ParseUint(rawStatus[1:], 10, 8) + if err != nil { + return 0, fmt.Errorf("failed to parse status score: %w", err) + } else if score > 100 { + return 0, fmt.Errorf("status score out of range: %d", score) + } + + return uint8(score), nil +} diff --git a/services/gitdiff/git_diff_tree_test.go b/services/gitdiff/git_diff_tree_test.go new file mode 100644 index 00000000000..313d279e95f --- /dev/null +++ b/services/gitdiff/git_diff_tree_test.go @@ -0,0 +1,427 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package gitdiff + +import ( + "strings" + "testing" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/git" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGitDiffTree(t *testing.T) { + test := []struct { + Name string + RepoPath string + BaseSha string + HeadSha string + useMergeBase bool + Expected *DiffTree + }{ + { + Name: "happy path", + RepoPath: "../../modules/git/tests/repos/repo5_pulls", + BaseSha: "72866af952e98d02a73003501836074b286a78f6", + HeadSha: "d8e0bbb45f200e67d9a784ce55bd90821af45ebd", + Expected: &DiffTree{ + Files: []*DiffTreeRecord{ + { + Status: "modified", + HeadPath: "LICENSE", + BasePath: "LICENSE", + HeadMode: git.EntryModeBlob, + BaseMode: git.EntryModeBlob, + HeadBlobID: "ee469963e76ae1bb7ee83d7510df2864e6c8c640", + BaseBlobID: "c996f4725be8fc8c1d1c776e58c97ddc5d03b336", + }, + { + Status: "modified", + HeadPath: "README.md", + BasePath: "README.md", + HeadMode: git.EntryModeBlob, + BaseMode: git.EntryModeBlob, + HeadBlobID: "9dfc0a6257d8eff526f0cfaf6a8ea950f55a9dba", + BaseBlobID: "074e590b8e64898b02beef03ece83f962c94f54c", + }, + }, + }, + }, + { + Name: "first commit (no parent)", + RepoPath: "../../modules/git/tests/repos/repo5_pulls", + HeadSha: "72866af952e98d02a73003501836074b286a78f6", + Expected: &DiffTree{ + Files: []*DiffTreeRecord{ + { + Status: "added", + HeadPath: ".gitignore", + BasePath: ".gitignore", + HeadMode: git.EntryModeBlob, + BaseMode: git.EntryModeNoEntry, + HeadBlobID: "f1c181ec9c5c921245027c6b452ecfc1d3626364", + BaseBlobID: "0000000000000000000000000000000000000000", + }, + { + Status: "added", + HeadPath: "LICENSE", + BasePath: "LICENSE", + HeadMode: git.EntryModeBlob, + BaseMode: git.EntryModeNoEntry, + HeadBlobID: "c996f4725be8fc8c1d1c776e58c97ddc5d03b336", + BaseBlobID: "0000000000000000000000000000000000000000", + }, + { + Status: "added", + HeadPath: "README.md", + BasePath: "README.md", + HeadMode: git.EntryModeBlob, + BaseMode: git.EntryModeNoEntry, + HeadBlobID: "074e590b8e64898b02beef03ece83f962c94f54c", + BaseBlobID: "0000000000000000000000000000000000000000", + }, + }, + }, + }, + { + Name: "first commit (no parent), merge base = true", + RepoPath: "../../modules/git/tests/repos/repo5_pulls", + HeadSha: "72866af952e98d02a73003501836074b286a78f6", + useMergeBase: true, + Expected: &DiffTree{ + Files: []*DiffTreeRecord{ + { + Status: "added", + HeadPath: ".gitignore", + BasePath: ".gitignore", + HeadMode: git.EntryModeBlob, + BaseMode: git.EntryModeNoEntry, + HeadBlobID: "f1c181ec9c5c921245027c6b452ecfc1d3626364", + BaseBlobID: "0000000000000000000000000000000000000000", + }, + { + Status: "added", + HeadPath: "LICENSE", + BasePath: "LICENSE", + HeadMode: git.EntryModeBlob, + BaseMode: git.EntryModeNoEntry, + HeadBlobID: "c996f4725be8fc8c1d1c776e58c97ddc5d03b336", + BaseBlobID: "0000000000000000000000000000000000000000", + }, + { + Status: "added", + HeadPath: "README.md", + BasePath: "README.md", + HeadMode: git.EntryModeBlob, + BaseMode: git.EntryModeNoEntry, + HeadBlobID: "074e590b8e64898b02beef03ece83f962c94f54c", + BaseBlobID: "0000000000000000000000000000000000000000", + }, + }, + }, + }, + { + Name: "base and head same", + RepoPath: "../../modules/git/tests/repos/repo5_pulls", + BaseSha: "ed8f4d2fa5b2420706580d191f5dd50c4e491f3f", + HeadSha: "ed8f4d2fa5b2420706580d191f5dd50c4e491f3f", + Expected: &DiffTree{ + Files: []*DiffTreeRecord{}, + }, + }, + { + Name: "useMergeBase false", + RepoPath: "../../modules/git/tests/repos/repo5_pulls", + BaseSha: "ed8f4d2fa5b2420706580d191f5dd50c4e491f3f", + HeadSha: "111cac04bd7d20301964e27a93698aabb5781b80", // this commit can be found on the update-readme branch + useMergeBase: false, + Expected: &DiffTree{ + Files: []*DiffTreeRecord{ + { + Status: "modified", + HeadPath: "LICENSE", + BasePath: "LICENSE", + HeadMode: git.EntryModeBlob, + BaseMode: git.EntryModeBlob, + HeadBlobID: "c996f4725be8fc8c1d1c776e58c97ddc5d03b336", + BaseBlobID: "ed5119b3c1f45547b6785bc03eac7f87570fa17f", + }, + + { + Status: "modified", + HeadPath: "README.md", + BasePath: "README.md", + HeadMode: git.EntryModeBlob, + BaseMode: git.EntryModeBlob, + HeadBlobID: "fb39771a8865c9a67f2ab9b616c854805664553c", + BaseBlobID: "9dfc0a6257d8eff526f0cfaf6a8ea950f55a9dba", + }, + }, + }, + }, + { + Name: "useMergeBase true", + RepoPath: "../../modules/git/tests/repos/repo5_pulls", + BaseSha: "ed8f4d2fa5b2420706580d191f5dd50c4e491f3f", + HeadSha: "111cac04bd7d20301964e27a93698aabb5781b80", // this commit can be found on the update-readme branch + useMergeBase: true, + Expected: &DiffTree{ + Files: []*DiffTreeRecord{ + { + Status: "modified", + HeadPath: "README.md", + BasePath: "README.md", + HeadMode: git.EntryModeBlob, + BaseMode: git.EntryModeBlob, + HeadBlobID: "fb39771a8865c9a67f2ab9b616c854805664553c", + BaseBlobID: "9dfc0a6257d8eff526f0cfaf6a8ea950f55a9dba", + }, + }, + }, + }, + { + Name: "no base set", + RepoPath: "../../modules/git/tests/repos/repo5_pulls", + HeadSha: "d8e0bbb45f200e67d9a784ce55bd90821af45ebd", // this commit can be found on the update-readme branch + useMergeBase: false, + Expected: &DiffTree{ + Files: []*DiffTreeRecord{ + { + Status: "modified", + HeadPath: "LICENSE", + BasePath: "LICENSE", + HeadMode: git.EntryModeBlob, + BaseMode: git.EntryModeBlob, + HeadBlobID: "ee469963e76ae1bb7ee83d7510df2864e6c8c640", + BaseBlobID: "ed5119b3c1f45547b6785bc03eac7f87570fa17f", + }, + }, + }, + }, + } + + for _, tt := range test { + t.Run(tt.Name, func(t *testing.T) { + gitRepo, err := git.OpenRepository(git.DefaultContext, tt.RepoPath) + assert.NoError(t, err) + defer gitRepo.Close() + + diffPaths, err := GetDiffTree(db.DefaultContext, gitRepo, tt.useMergeBase, tt.BaseSha, tt.HeadSha) + require.NoError(t, err) + + assert.Equal(t, tt.Expected, diffPaths) + }) + } +} + +func TestParseGitDiffTree(t *testing.T) { + test := []struct { + Name string + GitOutput string + Expected []*DiffTreeRecord + }{ + { + Name: "file change", + GitOutput: ":100644 100644 64e43d23bcd08db12563a0a4d84309cadb437e1a 5dbc7792b5bb228647cfcc8dfe65fc649119dedc M\tResources/views/curriculum/edit.blade.php", + Expected: []*DiffTreeRecord{ + { + Status: "modified", + HeadPath: "Resources/views/curriculum/edit.blade.php", + BasePath: "Resources/views/curriculum/edit.blade.php", + HeadMode: git.EntryModeBlob, + BaseMode: git.EntryModeBlob, + HeadBlobID: "5dbc7792b5bb228647cfcc8dfe65fc649119dedc", + BaseBlobID: "64e43d23bcd08db12563a0a4d84309cadb437e1a", + }, + }, + }, + { + Name: "file added", + GitOutput: ":000000 100644 0000000000000000000000000000000000000000 0063162fb403db15ceb0517b34ab782e4e58b619 A\tResources/views/class/index.blade.php", + Expected: []*DiffTreeRecord{ + { + Status: "added", + HeadPath: "Resources/views/class/index.blade.php", + BasePath: "Resources/views/class/index.blade.php", + HeadMode: git.EntryModeBlob, + BaseMode: git.EntryModeNoEntry, + HeadBlobID: "0063162fb403db15ceb0517b34ab782e4e58b619", + BaseBlobID: "0000000000000000000000000000000000000000", + }, + }, + }, + { + Name: "file deleted", + GitOutput: ":100644 000000 bac4286303c8c0017ea2f0a48c561ddcc0330a14 0000000000000000000000000000000000000000 D\tResources/views/classes/index.blade.php", + Expected: []*DiffTreeRecord{ + { + Status: "deleted", + HeadPath: "Resources/views/classes/index.blade.php", + BasePath: "Resources/views/classes/index.blade.php", + HeadMode: git.EntryModeNoEntry, + BaseMode: git.EntryModeBlob, + HeadBlobID: "0000000000000000000000000000000000000000", + BaseBlobID: "bac4286303c8c0017ea2f0a48c561ddcc0330a14", + }, + }, + }, + { + Name: "file renamed", + GitOutput: ":100644 100644 c8a055cfb45cd39747292983ad1797ceab40f5b1 97248f79a90aaf81fe7fd74b33c1cb182dd41783 R087\tDatabase/Seeders/AdminDatabaseSeeder.php\tDatabase/Seeders/AcademicDatabaseSeeder.php", + Expected: []*DiffTreeRecord{ + { + Status: "renamed", + Score: 87, + HeadPath: "Database/Seeders/AcademicDatabaseSeeder.php", + BasePath: "Database/Seeders/AdminDatabaseSeeder.php", + HeadMode: git.EntryModeBlob, + BaseMode: git.EntryModeBlob, + HeadBlobID: "97248f79a90aaf81fe7fd74b33c1cb182dd41783", + BaseBlobID: "c8a055cfb45cd39747292983ad1797ceab40f5b1", + }, + }, + }, + { + Name: "no changes", + GitOutput: ``, + Expected: []*DiffTreeRecord{}, + }, + { + Name: "multiple changes", + GitOutput: ":000000 100644 0000000000000000000000000000000000000000 db736b44533a840981f1f17b7029d0f612b69550 A\tHttp/Controllers/ClassController.php\n" + + ":100644 000000 9a4d2344d4d0145db7c91b3f3e123c74367d4ef4 0000000000000000000000000000000000000000 D\tHttp/Controllers/ClassesController.php\n" + + ":100644 100644 f060d6aede65d423f49e7dc248dfa0d8835ef920 b82c8e39a3602dedadb44669956d6eb5b6a7cc86 M\tHttp/Controllers/ProgramDirectorController.php\n", + Expected: []*DiffTreeRecord{ + { + Status: "added", + HeadPath: "Http/Controllers/ClassController.php", + BasePath: "Http/Controllers/ClassController.php", + HeadMode: git.EntryModeBlob, + BaseMode: git.EntryModeNoEntry, + HeadBlobID: "db736b44533a840981f1f17b7029d0f612b69550", + BaseBlobID: "0000000000000000000000000000000000000000", + }, + { + Status: "deleted", + HeadPath: "Http/Controllers/ClassesController.php", + BasePath: "Http/Controllers/ClassesController.php", + HeadMode: git.EntryModeNoEntry, + BaseMode: git.EntryModeBlob, + HeadBlobID: "0000000000000000000000000000000000000000", + BaseBlobID: "9a4d2344d4d0145db7c91b3f3e123c74367d4ef4", + }, + { + Status: "modified", + HeadPath: "Http/Controllers/ProgramDirectorController.php", + BasePath: "Http/Controllers/ProgramDirectorController.php", + HeadMode: git.EntryModeBlob, + BaseMode: git.EntryModeBlob, + HeadBlobID: "b82c8e39a3602dedadb44669956d6eb5b6a7cc86", + BaseBlobID: "f060d6aede65d423f49e7dc248dfa0d8835ef920", + }, + }, + }, + { + Name: "spaces in file path", + GitOutput: ":000000 100644 0000000000000000000000000000000000000000 db736b44533a840981f1f17b7029d0f612b69550 A\tHttp /Controllers/Class Controller.php\n" + + ":100644 000000 9a4d2344d4d0145db7c91b3f3e123c74367d4ef4 0000000000000000000000000000000000000000 D\tHttp/Cont rollers/Classes Controller.php\n" + + ":100644 100644 f060d6aede65d423f49e7dc248dfa0d8835ef920 b82c8e39a3602dedadb44669956d6eb5b6a7cc86 R010\tHttp/Controllers/Program Director Controller.php\tHttp/Cont rollers/ProgramDirectorController.php\n", + Expected: []*DiffTreeRecord{ + { + Status: "added", + HeadPath: "Http /Controllers/Class Controller.php", + BasePath: "Http /Controllers/Class Controller.php", + HeadMode: git.EntryModeBlob, + BaseMode: git.EntryModeNoEntry, + HeadBlobID: "db736b44533a840981f1f17b7029d0f612b69550", + BaseBlobID: "0000000000000000000000000000000000000000", + }, + { + Status: "deleted", + HeadPath: "Http/Cont rollers/Classes Controller.php", + BasePath: "Http/Cont rollers/Classes Controller.php", + HeadMode: git.EntryModeNoEntry, + BaseMode: git.EntryModeBlob, + HeadBlobID: "0000000000000000000000000000000000000000", + BaseBlobID: "9a4d2344d4d0145db7c91b3f3e123c74367d4ef4", + }, + { + Status: "renamed", + Score: 10, + HeadPath: "Http/Cont rollers/ProgramDirectorController.php", + BasePath: "Http/Controllers/Program Director Controller.php", + HeadMode: git.EntryModeBlob, + BaseMode: git.EntryModeBlob, + HeadBlobID: "b82c8e39a3602dedadb44669956d6eb5b6a7cc86", + BaseBlobID: "f060d6aede65d423f49e7dc248dfa0d8835ef920", + }, + }, + }, + { + Name: "file type changed", + GitOutput: ":100644 120000 344e0ca8aa791cc4164fb0ea645f334fd40d00f0 a7c2973de00bfdc6ca51d315f401b5199fe01dc3 T\twebpack.mix.js", + Expected: []*DiffTreeRecord{ + { + Status: "typechanged", + HeadPath: "webpack.mix.js", + BasePath: "webpack.mix.js", + HeadMode: git.EntryModeSymlink, + BaseMode: git.EntryModeBlob, + HeadBlobID: "a7c2973de00bfdc6ca51d315f401b5199fe01dc3", + BaseBlobID: "344e0ca8aa791cc4164fb0ea645f334fd40d00f0", + }, + }, + }, + } + + for _, tt := range test { + t.Run(tt.Name, func(t *testing.T) { + entries, err := parseGitDiffTree(strings.NewReader(tt.GitOutput)) + assert.NoError(t, err) + assert.Equal(t, tt.Expected, entries) + }) + } +} + +func TestGitDiffTreeErrors(t *testing.T) { + test := []struct { + Name string + RepoPath string + BaseSha string + HeadSha string + }{ + { + Name: "head doesn't exist", + RepoPath: "../../modules/git/tests/repos/repo5_pulls", + BaseSha: "f32b0a9dfd09a60f616f29158f772cedd89942d2", + HeadSha: "asdfasdfasdf", + }, + { + Name: "base doesn't exist", + RepoPath: "../../modules/git/tests/repos/repo5_pulls", + BaseSha: "asdfasdfasdf", + HeadSha: "f32b0a9dfd09a60f616f29158f772cedd89942d2", + }, + { + Name: "head not set", + RepoPath: "../../modules/git/tests/repos/repo5_pulls", + BaseSha: "f32b0a9dfd09a60f616f29158f772cedd89942d2", + }, + } + + for _, tt := range test { + t.Run(tt.Name, func(t *testing.T) { + gitRepo, err := git.OpenRepository(git.DefaultContext, tt.RepoPath) + assert.NoError(t, err) + defer gitRepo.Close() + + diffPaths, err := GetDiffTree(db.DefaultContext, gitRepo, true, tt.BaseSha, tt.HeadSha) + assert.Error(t, err) + assert.Nil(t, diffPaths) + }) + } +} diff --git a/services/gitdiff/gitdiff.go b/services/gitdiff/gitdiff.go index 0b5a855d42f..f046e596782 100644 --- a/services/gitdiff/gitdiff.go +++ b/services/gitdiff/gitdiff.go @@ -360,7 +360,6 @@ type DiffFile struct { IsLFSFile bool IsRenamed bool IsAmbiguous bool - IsSubmodule bool Sections []*DiffSection IsIncomplete bool IsIncompleteLineTooLong bool @@ -372,6 +371,9 @@ type DiffFile struct { Language string Mode string OldMode string + + IsSubmodule bool // if IsSubmodule==true, then there must be a SubmoduleDiffInfo + SubmoduleDiffInfo *SubmoduleDiffInfo } // GetType returns type of diff file. @@ -609,9 +611,8 @@ parsingLoop: if strings.HasPrefix(line, "new mode ") { curFile.Mode = prepareValue(line, "new mode ") } - if strings.HasSuffix(line, " 160000\n") { - curFile.IsSubmodule = true + curFile.IsSubmodule, curFile.SubmoduleDiffInfo = true, &SubmoduleDiffInfo{} } case strings.HasPrefix(line, "rename from "): curFile.IsRenamed = true @@ -646,17 +647,17 @@ parsingLoop: curFile.Mode = prepareValue(line, "new file mode ") } if strings.HasSuffix(line, " 160000\n") { - curFile.IsSubmodule = true + curFile.IsSubmodule, curFile.SubmoduleDiffInfo = true, &SubmoduleDiffInfo{} } case strings.HasPrefix(line, "deleted"): curFile.Type = DiffFileDel curFile.IsDeleted = true if strings.HasSuffix(line, " 160000\n") { - curFile.IsSubmodule = true + curFile.IsSubmodule, curFile.SubmoduleDiffInfo = true, &SubmoduleDiffInfo{} } case strings.HasPrefix(line, "index"): if strings.HasSuffix(line, " 160000\n") { - curFile.IsSubmodule = true + curFile.IsSubmodule, curFile.SubmoduleDiffInfo = true, &SubmoduleDiffInfo{} } case strings.HasPrefix(line, "similarity index 100%"): curFile.Type = DiffFileRename @@ -915,6 +916,13 @@ func parseHunks(ctx context.Context, curFile *DiffFile, maxLines, maxLineCharact } } curSection.Lines = append(curSection.Lines, diffLine) + + // Parse submodule additions + if curFile.SubmoduleDiffInfo != nil { + if ref, found := bytes.CutPrefix(lineBytes, []byte("+Subproject commit ")); found { + curFile.SubmoduleDiffInfo.NewRefID = string(bytes.TrimSpace(ref)) + } + } case '-': curFileLinesCount++ curFile.Deletion++ @@ -936,6 +944,13 @@ func parseHunks(ctx context.Context, curFile *DiffFile, maxLines, maxLineCharact lastLeftIdx = len(curSection.Lines) } curSection.Lines = append(curSection.Lines, diffLine) + + // Parse submodule deletion + if curFile.SubmoduleDiffInfo != nil { + if ref, found := bytes.CutPrefix(lineBytes, []byte("-Subproject commit ")); found { + curFile.SubmoduleDiffInfo.PreviousRefID = string(bytes.TrimSpace(ref)) + } + } case ' ': curFileLinesCount++ if maxLines > -1 && curFileLinesCount >= maxLines { @@ -1121,7 +1136,10 @@ func GetDiff(ctx context.Context, gitRepo *git.Repository, opts *DiffOptions, fi } else { actualBeforeCommitID := opts.BeforeCommitID if len(actualBeforeCommitID) == 0 { - parentCommit, _ := commit.Parent(0) + parentCommit, err := commit.Parent(0) + if err != nil { + return nil, err + } actualBeforeCommitID = parentCommit.ID.String() } @@ -1130,7 +1148,6 @@ func GetDiff(ctx context.Context, gitRepo *git.Repository, opts *DiffOptions, fi AddDynamicArguments(actualBeforeCommitID, opts.AfterCommitID) opts.BeforeCommitID = actualBeforeCommitID - var err error beforeCommit, err = gitRepo.GetCommit(opts.BeforeCommitID) if err != nil { return nil, err @@ -1195,6 +1212,11 @@ func GetDiff(ctx context.Context, gitRepo *git.Repository, opts *DiffOptions, fi } } + // Populate Submodule URLs + if diffFile.SubmoduleDiffInfo != nil { + diffFile.SubmoduleDiffInfo.PopulateURL(diffFile, beforeCommit, commit) + } + if !isVendored.Has() { isVendored = optional.Some(analyze.IsVendor(diffFile.Name)) } diff --git a/services/gitdiff/gitdiff_test.go b/services/gitdiff/gitdiff_test.go index 2351c5da879..ca9b5a6f4e9 100644 --- a/services/gitdiff/gitdiff_test.go +++ b/services/gitdiff/gitdiff_test.go @@ -5,6 +5,7 @@ package gitdiff import ( + "context" "strconv" "strings" "testing" @@ -19,6 +20,7 @@ import ( dmp "github.com/sergi/go-diff/diffmatchpatch" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestDiffToHTML(t *testing.T) { @@ -627,24 +629,25 @@ func TestDiffLine_GetCommentSide(t *testing.T) { } func TestGetDiffRangeWithWhitespaceBehavior(t *testing.T) { - gitRepo, err := git.OpenRepository(git.DefaultContext, "./testdata/academic-module") - if !assert.NoError(t, err) { - return - } + gitRepo, err := git.OpenRepository(context.Background(), "../../modules/git/tests/repos/repo5_pulls") + require.NoError(t, err) + defer gitRepo.Close() for _, behavior := range []git.TrustedCmdArgs{{"-w"}, {"--ignore-space-at-eol"}, {"-b"}, nil} { - diffs, err := GetDiff(db.DefaultContext, gitRepo, + diffs, err := GetDiff(context.Background(), gitRepo, &DiffOptions{ - AfterCommitID: "bd7063cc7c04689c4d082183d32a604ed27a24f9", - BeforeCommitID: "559c156f8e0178b71cb44355428f24001b08fc68", + AfterCommitID: "d8e0bbb45f200e67d9a784ce55bd90821af45ebd", + BeforeCommitID: "72866af952e98d02a73003501836074b286a78f6", MaxLines: setting.Git.MaxGitDiffLines, MaxLineCharacters: setting.Git.MaxGitDiffLineCharacters, - MaxFiles: setting.Git.MaxGitDiffFiles, + MaxFiles: 1, WhitespaceBehavior: behavior, }) - assert.NoError(t, err, "Error when diff with %s", behavior) + require.NoError(t, err, "Error when diff with WhitespaceBehavior=%s", behavior) + assert.True(t, diffs.IsIncomplete) + assert.Len(t, diffs.Files, 1) for _, f := range diffs.Files { - assert.NotEmpty(t, f.Sections, "%s should have sections", f.Name) + assert.NotEmpty(t, f.Sections, "Diff file %q should have sections", f.Name) } } } diff --git a/services/gitdiff/submodule.go b/services/gitdiff/submodule.go new file mode 100644 index 00000000000..02ca666544c --- /dev/null +++ b/services/gitdiff/submodule.go @@ -0,0 +1,65 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package gitdiff + +import ( + "context" + "html/template" + + "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/htmlutil" + "code.gitea.io/gitea/modules/log" +) + +type SubmoduleDiffInfo struct { + SubmoduleName string + SubmoduleFile *git.CommitSubmoduleFile // it might be nil if the submodule is not found or unable to parse + NewRefID string + PreviousRefID string +} + +func (si *SubmoduleDiffInfo) PopulateURL(diffFile *DiffFile, leftCommit, rightCommit *git.Commit) { + si.SubmoduleName = diffFile.Name + submoduleCommit := rightCommit // If the submodule is added or updated, check at the right commit + if diffFile.IsDeleted { + submoduleCommit = leftCommit // If the submodule is deleted, check at the left commit + } + if submoduleCommit == nil { + return + } + + submodule, err := submoduleCommit.GetSubModule(diffFile.GetDiffFileName()) + if err != nil { + log.Error("Unable to PopulateURL for submodule %q: GetSubModule: %v", diffFile.GetDiffFileName(), err) + return // ignore the error, do not cause 500 errors for end users + } + if submodule != nil { + si.SubmoduleFile = git.NewCommitSubmoduleFile(submodule.URL, submoduleCommit.ID.String()) + } +} + +func (si *SubmoduleDiffInfo) CommitRefIDLinkHTML(ctx context.Context, commitID string) template.HTML { + webLink := si.SubmoduleFile.SubmoduleWebLink(ctx, commitID) + if webLink == nil { + return htmlutil.HTMLFormat("%s", base.ShortSha(commitID)) + } + return htmlutil.HTMLFormat(`%s`, webLink.CommitWebLink, base.ShortSha(commitID)) +} + +func (si *SubmoduleDiffInfo) CompareRefIDLinkHTML(ctx context.Context) template.HTML { + webLink := si.SubmoduleFile.SubmoduleWebLink(ctx, si.PreviousRefID, si.NewRefID) + if webLink == nil { + return htmlutil.HTMLFormat("%s...%s", base.ShortSha(si.PreviousRefID), base.ShortSha(si.NewRefID)) + } + return htmlutil.HTMLFormat(`%s...%s`, webLink.CommitWebLink, base.ShortSha(si.PreviousRefID), base.ShortSha(si.NewRefID)) +} + +func (si *SubmoduleDiffInfo) SubmoduleRepoLinkHTML(ctx context.Context) template.HTML { + webLink := si.SubmoduleFile.SubmoduleWebLink(ctx) + if webLink == nil { + return htmlutil.HTMLFormat("%s", si.SubmoduleName) + } + return htmlutil.HTMLFormat(`%s`, webLink.RepoWebLink, si.SubmoduleName) +} diff --git a/services/gitdiff/submodule_test.go b/services/gitdiff/submodule_test.go new file mode 100644 index 00000000000..f0eab5557cf --- /dev/null +++ b/services/gitdiff/submodule_test.go @@ -0,0 +1,236 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package gitdiff + +import ( + "context" + "strings" + "testing" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/setting" + + "github.com/stretchr/testify/assert" +) + +func TestParseSubmoduleInfo(t *testing.T) { + type testcase struct { + name string + gitdiff string + infos map[int]SubmoduleDiffInfo + } + + tests := []testcase{ + { + name: "added", + gitdiff: `diff --git a/.gitmodules b/.gitmodules +new file mode 100644 +index 0000000..4ac13c1 +--- /dev/null ++++ b/.gitmodules +@@ -0,0 +1,3 @@ ++[submodule "gitea-mirror"] ++ path = gitea-mirror ++ url = https://gitea.com/gitea/gitea-mirror +diff --git a/gitea-mirror b/gitea-mirror +new file mode 160000 +index 0000000..68972a9 +--- /dev/null ++++ b/gitea-mirror +@@ -0,0 +1 @@ ++Subproject commit 68972a994719ae5c74e28d8fa82fa27c23399bc8 +`, + infos: map[int]SubmoduleDiffInfo{ + 1: {NewRefID: "68972a994719ae5c74e28d8fa82fa27c23399bc8"}, + }, + }, + { + name: "updated", + gitdiff: `diff --git a/gitea-mirror b/gitea-mirror +index 68972a9..c8ffe77 160000 +--- a/gitea-mirror ++++ b/gitea-mirror +@@ -1 +1 @@ +-Subproject commit 68972a994719ae5c74e28d8fa82fa27c23399bc8 ++Subproject commit c8ffe777cf9c5bb47a38e3e0b3a3b5de6cd8813d +`, + infos: map[int]SubmoduleDiffInfo{ + 0: { + PreviousRefID: "68972a994719ae5c74e28d8fa82fa27c23399bc8", + NewRefID: "c8ffe777cf9c5bb47a38e3e0b3a3b5de6cd8813d", + }, + }, + }, + { + name: "rename", + gitdiff: `diff --git a/.gitmodules b/.gitmodules +index 4ac13c1..0510edd 100644 +--- a/.gitmodules ++++ b/.gitmodules +@@ -1,3 +1,3 @@ + [submodule "gitea-mirror"] +- path = gitea-mirror ++ path = gitea + url = https://gitea.com/gitea/gitea-mirror +diff --git a/gitea-mirror b/gitea +similarity index 100% +rename from gitea-mirror +rename to gitea +`, + }, + { + name: "deleted", + gitdiff: `diff --git a/.gitmodules b/.gitmodules +index 0510edd..e69de29 100644 +--- a/.gitmodules ++++ b/.gitmodules +@@ -1,3 +0,0 @@ +-[submodule "gitea-mirror"] +- path = gitea +- url = https://gitea.com/gitea/gitea-mirror +diff --git a/gitea b/gitea +deleted file mode 160000 +index c8ffe77..0000000 +--- a/gitea ++++ /dev/null +@@ -1 +0,0 @@ +-Subproject commit c8ffe777cf9c5bb47a38e3e0b3a3b5de6cd8813d +`, + infos: map[int]SubmoduleDiffInfo{ + 1: { + PreviousRefID: "c8ffe777cf9c5bb47a38e3e0b3a3b5de6cd8813d", + }, + }, + }, + { + name: "moved and updated", + gitdiff: `diff --git a/.gitmodules b/.gitmodules +index 0510edd..bced3d8 100644 +--- a/.gitmodules ++++ b/.gitmodules +@@ -1,3 +1,3 @@ + [submodule "gitea-mirror"] +- path = gitea ++ path = gitea-1.22 + url = https://gitea.com/gitea/gitea-mirror +diff --git a/gitea b/gitea +deleted file mode 160000 +index c8ffe77..0000000 +--- a/gitea ++++ /dev/null +@@ -1 +0,0 @@ +-Subproject commit c8ffe777cf9c5bb47a38e3e0b3a3b5de6cd8813d +diff --git a/gitea-1.22 b/gitea-1.22 +new file mode 160000 +index 0000000..8eefa1f +--- /dev/null ++++ b/gitea-1.22 +@@ -0,0 +1 @@ ++Subproject commit 8eefa1f6dedf2488db2c9e12c916e8e51f673160 +`, + infos: map[int]SubmoduleDiffInfo{ + 1: { + PreviousRefID: "c8ffe777cf9c5bb47a38e3e0b3a3b5de6cd8813d", + }, + 2: { + NewRefID: "8eefa1f6dedf2488db2c9e12c916e8e51f673160", + }, + }, + }, + { + name: "converted to file", + gitdiff: `diff --git a/.gitmodules b/.gitmodules +index 0510edd..e69de29 100644 +--- a/.gitmodules ++++ b/.gitmodules +@@ -1,3 +0,0 @@ +-[submodule "gitea-mirror"] +- path = gitea +- url = https://gitea.com/gitea/gitea-mirror +diff --git a/gitea b/gitea +deleted file mode 160000 +index c8ffe77..0000000 +--- a/gitea ++++ /dev/null +@@ -1 +0,0 @@ +-Subproject commit c8ffe777cf9c5bb47a38e3e0b3a3b5de6cd8813d +diff --git a/gitea b/gitea +new file mode 100644 +index 0000000..33a9488 +--- /dev/null ++++ b/gitea +@@ -0,0 +1 @@ ++example +`, + infos: map[int]SubmoduleDiffInfo{ + 1: { + PreviousRefID: "c8ffe777cf9c5bb47a38e3e0b3a3b5de6cd8813d", + }, + }, + }, + { + name: "converted to submodule", + gitdiff: `diff --git a/.gitmodules b/.gitmodules +index e69de29..14ee267 100644 +--- a/.gitmodules ++++ b/.gitmodules +@@ -0,0 +1,3 @@ ++[submodule "gitea"] ++ path = gitea ++ url = https://gitea.com/gitea/gitea-mirror +diff --git a/gitea b/gitea +deleted file mode 100644 +index 33a9488..0000000 +--- a/gitea ++++ /dev/null +@@ -1 +0,0 @@ +-example +diff --git a/gitea b/gitea +new file mode 160000 +index 0000000..68972a9 +--- /dev/null ++++ b/gitea +@@ -0,0 +1 @@ ++Subproject commit 68972a994719ae5c74e28d8fa82fa27c23399bc8 +`, + infos: map[int]SubmoduleDiffInfo{ + 2: { + NewRefID: "68972a994719ae5c74e28d8fa82fa27c23399bc8", + }, + }, + }, + } + + for _, testcase := range tests { + testcase := testcase + t.Run(testcase.name, func(t *testing.T) { + diff, err := ParsePatch(db.DefaultContext, setting.Git.MaxGitDiffLines, setting.Git.MaxGitDiffLineCharacters, setting.Git.MaxGitDiffFiles, strings.NewReader(testcase.gitdiff), "") + assert.NoError(t, err) + + for i, expected := range testcase.infos { + actual := diff.Files[i] + assert.NotNil(t, actual) + assert.Equal(t, expected, *actual.SubmoduleDiffInfo) + } + }) + } +} + +func TestSubmoduleInfo(t *testing.T) { + sdi := &SubmoduleDiffInfo{ + SubmoduleName: "name", + PreviousRefID: "aaaa", + NewRefID: "bbbb", + } + ctx := context.Background() + assert.EqualValues(t, "1111", sdi.CommitRefIDLinkHTML(ctx, "1111")) + assert.EqualValues(t, "aaaa...bbbb", sdi.CompareRefIDLinkHTML(ctx)) + assert.EqualValues(t, "name", sdi.SubmoduleRepoLinkHTML(ctx)) + + sdi.SubmoduleFile = git.NewCommitSubmoduleFile("https://github.com/owner/repo", "1234") + assert.EqualValues(t, `1111`, sdi.CommitRefIDLinkHTML(ctx, "1111")) + assert.EqualValues(t, `aaaa...bbbb`, sdi.CompareRefIDLinkHTML(ctx)) + assert.EqualValues(t, `name`, sdi.SubmoduleRepoLinkHTML(ctx)) +} diff --git a/services/gitdiff/testdata/academic-module/config b/services/gitdiff/testdata/academic-module/config deleted file mode 100644 index 1bc26be5146..00000000000 --- a/services/gitdiff/testdata/academic-module/config +++ /dev/null @@ -1,10 +0,0 @@ -[core] - repositoryformatversion = 0 - filemode = true - bare = false - logallrefupdates = true - ignorecase = true - precomposeunicode = true -[branch "master"] - remote = origin - merge = refs/heads/master diff --git a/services/gitdiff/testdata/academic-module/index b/services/gitdiff/testdata/academic-module/index deleted file mode 100644 index e712c906e38..00000000000 Binary files a/services/gitdiff/testdata/academic-module/index and /dev/null differ diff --git a/services/gitdiff/testdata/academic-module/logs/HEAD b/services/gitdiff/testdata/academic-module/logs/HEAD deleted file mode 100644 index 16b2e1c0f60..00000000000 --- a/services/gitdiff/testdata/academic-module/logs/HEAD +++ /dev/null @@ -1 +0,0 @@ -0000000000000000000000000000000000000000 bd7063cc7c04689c4d082183d32a604ed27a24f9 Lunny Xiao 1574829684 +0800 clone: from https://try.gitea.io/shemgp-aiias/academic-module diff --git a/services/gitdiff/testdata/academic-module/logs/refs/heads/master b/services/gitdiff/testdata/academic-module/logs/refs/heads/master deleted file mode 100644 index 16b2e1c0f60..00000000000 --- a/services/gitdiff/testdata/academic-module/logs/refs/heads/master +++ /dev/null @@ -1 +0,0 @@ -0000000000000000000000000000000000000000 bd7063cc7c04689c4d082183d32a604ed27a24f9 Lunny Xiao 1574829684 +0800 clone: from https://try.gitea.io/shemgp-aiias/academic-module diff --git a/services/gitdiff/testdata/academic-module/logs/refs/remotes/origin/HEAD b/services/gitdiff/testdata/academic-module/logs/refs/remotes/origin/HEAD deleted file mode 100644 index 16b2e1c0f60..00000000000 --- a/services/gitdiff/testdata/academic-module/logs/refs/remotes/origin/HEAD +++ /dev/null @@ -1 +0,0 @@ -0000000000000000000000000000000000000000 bd7063cc7c04689c4d082183d32a604ed27a24f9 Lunny Xiao 1574829684 +0800 clone: from https://try.gitea.io/shemgp-aiias/academic-module diff --git a/services/gitdiff/testdata/academic-module/objects/pack/pack-597efbc3613c7ba790e33b178fd9fc1fe17b4245.idx b/services/gitdiff/testdata/academic-module/objects/pack/pack-597efbc3613c7ba790e33b178fd9fc1fe17b4245.idx deleted file mode 100644 index 4d759aa504b..00000000000 Binary files a/services/gitdiff/testdata/academic-module/objects/pack/pack-597efbc3613c7ba790e33b178fd9fc1fe17b4245.idx and /dev/null differ diff --git a/services/gitdiff/testdata/academic-module/objects/pack/pack-597efbc3613c7ba790e33b178fd9fc1fe17b4245.pack b/services/gitdiff/testdata/academic-module/objects/pack/pack-597efbc3613c7ba790e33b178fd9fc1fe17b4245.pack deleted file mode 100644 index 2dc49cfded0..00000000000 Binary files a/services/gitdiff/testdata/academic-module/objects/pack/pack-597efbc3613c7ba790e33b178fd9fc1fe17b4245.pack and /dev/null differ diff --git a/services/gitdiff/testdata/academic-module/packed-refs b/services/gitdiff/testdata/academic-module/packed-refs deleted file mode 100644 index 13b56116508..00000000000 --- a/services/gitdiff/testdata/academic-module/packed-refs +++ /dev/null @@ -1,2 +0,0 @@ -# pack-refs with: peeled fully-peeled sorted -bd7063cc7c04689c4d082183d32a604ed27a24f9 refs/remotes/origin/master diff --git a/services/gitdiff/testdata/academic-module/refs/heads/master b/services/gitdiff/testdata/academic-module/refs/heads/master deleted file mode 100644 index bd2b56eaf45..00000000000 --- a/services/gitdiff/testdata/academic-module/refs/heads/master +++ /dev/null @@ -1 +0,0 @@ -bd7063cc7c04689c4d082183d32a604ed27a24f9 diff --git a/services/gitdiff/testdata/academic-module/refs/remotes/origin/HEAD b/services/gitdiff/testdata/academic-module/refs/remotes/origin/HEAD deleted file mode 100644 index 6efe28fff83..00000000000 --- a/services/gitdiff/testdata/academic-module/refs/remotes/origin/HEAD +++ /dev/null @@ -1 +0,0 @@ -ref: refs/remotes/origin/master diff --git a/services/issue/issue.go b/services/issue/issue.go index 8682d04e364..9cdc7a0c3f1 100644 --- a/services/issue/issue.go +++ b/services/issue/issue.go @@ -250,8 +250,9 @@ func GetRefEndNamesAndURLs(issues []*issues_model.Issue, repoLink string) (map[i issueRefURLs := make(map[int64]string, len(issues)) for _, issue := range issues { if issue.Ref != "" { - issueRefEndNames[issue.ID] = git.RefName(issue.Ref).ShortName() - issueRefURLs[issue.ID] = git.RefURL(repoLink, issue.Ref) + ref := git.RefName(issue.Ref) + issueRefEndNames[issue.ID] = ref.ShortName() + issueRefURLs[issue.ID] = repoLink + "/src/" + ref.RefWebLinkPath() } } return issueRefEndNames, issueRefURLs diff --git a/services/lfs/server.go b/services/lfs/server.go index a77623fdc19..c4866edaab2 100644 --- a/services/lfs/server.go +++ b/services/lfs/server.go @@ -134,7 +134,9 @@ func DownloadHandler(ctx *context.Context) { } contentLength := toByte + 1 - fromByte - ctx.Resp.Header().Set("Content-Length", strconv.FormatInt(contentLength, 10)) + contentLengthStr := strconv.FormatInt(contentLength, 10) + ctx.Resp.Header().Set("Content-Length", contentLengthStr) + ctx.Resp.Header().Set("X-Gitea-LFS-Content-Length", contentLengthStr) // we need this header to make sure it won't be affected by reverse proxy or compression ctx.Resp.Header().Set("Content-Type", "application/octet-stream") filename := ctx.PathParam("filename") diff --git a/services/mailer/mail_issue.go b/services/mailer/mail_issue.go index fab3315be21..e269b1ca1e2 100644 --- a/services/mailer/mail_issue.go +++ b/services/mailer/mail_issue.go @@ -109,7 +109,7 @@ func mailIssueCommentToParticipants(ctx *mailCommentContext, mentions []*user_mo } visited.AddMultiple(ids...) - unfilteredUsers, err := user_model.GetMaileableUsersByIDs(ctx, unfiltered, false) + unfilteredUsers, err := user_model.GetMailableUsersByIDs(ctx, unfiltered, false) if err != nil { return err } diff --git a/services/mailer/mail_release.go b/services/mailer/mail_release.go index 796d63d27ae..31316b0053b 100644 --- a/services/mailer/mail_release.go +++ b/services/mailer/mail_release.go @@ -35,9 +35,9 @@ func MailNewRelease(ctx context.Context, rel *repo_model.Release) { return } - recipients, err := user_model.GetMaileableUsersByIDs(ctx, watcherIDList, false) + recipients, err := user_model.GetMailableUsersByIDs(ctx, watcherIDList, false) if err != nil { - log.Error("user_model.GetMaileableUsersByIDs: %v", err) + log.Error("user_model.GetMailableUsersByIDs: %v", err) return } diff --git a/services/mailer/mail_test.go b/services/mailer/mail_test.go index 185b72f0699..8298ac4a342 100644 --- a/services/mailer/mail_test.go +++ b/services/mailer/mail_test.go @@ -85,7 +85,7 @@ func TestComposeIssueCommentMessage(t *testing.T) { recipients := []*user_model.User{{Name: "Test", Email: "test@gitea.com"}, {Name: "Test2", Email: "test2@gitea.com"}} msgs, err := composeIssueCommentMessages(&mailCommentContext{ - Context: context.TODO(), // TODO: use a correct context + Context: context.TODO(), Issue: issue, Doer: doer, ActionType: activities_model.ActionCommentIssue, Content: fmt.Sprintf("test @%s %s#%d body", doer.Name, issue.Repo.FullName(), issue.Index), Comment: comment, @@ -131,7 +131,7 @@ func TestComposeIssueMessage(t *testing.T) { recipients := []*user_model.User{{Name: "Test", Email: "test@gitea.com"}, {Name: "Test2", Email: "test2@gitea.com"}} msgs, err := composeIssueCommentMessages(&mailCommentContext{ - Context: context.TODO(), // TODO: use a correct context + Context: context.TODO(), Issue: issue, Doer: doer, ActionType: activities_model.ActionCreateIssue, Content: "test body", }, "en-US", recipients, false, "issue create") @@ -178,14 +178,14 @@ func TestTemplateSelection(t *testing.T) { } msg := testComposeIssueCommentMessage(t, &mailCommentContext{ - Context: context.TODO(), // TODO: use a correct context + Context: context.TODO(), Issue: issue, Doer: doer, ActionType: activities_model.ActionCreateIssue, Content: "test body", }, recipients, false, "TestTemplateSelection") expect(t, msg, "issue/new/subject", "issue/new/body") msg = testComposeIssueCommentMessage(t, &mailCommentContext{ - Context: context.TODO(), // TODO: use a correct context + Context: context.TODO(), Issue: issue, Doer: doer, ActionType: activities_model.ActionCommentIssue, Content: "test body", Comment: comment, }, recipients, false, "TestTemplateSelection") @@ -194,14 +194,14 @@ func TestTemplateSelection(t *testing.T) { pull := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2, Repo: repo, Poster: doer}) comment = unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 4, Issue: pull}) msg = testComposeIssueCommentMessage(t, &mailCommentContext{ - Context: context.TODO(), // TODO: use a correct context + Context: context.TODO(), Issue: pull, Doer: doer, ActionType: activities_model.ActionCommentPull, Content: "test body", Comment: comment, }, recipients, false, "TestTemplateSelection") expect(t, msg, "pull/comment/subject", "pull/comment/body") msg = testComposeIssueCommentMessage(t, &mailCommentContext{ - Context: context.TODO(), // TODO: use a correct context + Context: context.TODO(), Issue: issue, Doer: doer, ActionType: activities_model.ActionCloseIssue, Content: "test body", Comment: comment, }, recipients, false, "TestTemplateSelection") @@ -220,7 +220,7 @@ func TestTemplateServices(t *testing.T) { recipients := []*user_model.User{{Name: "Test", Email: "test@gitea.com"}} msg := testComposeIssueCommentMessage(t, &mailCommentContext{ - Context: context.TODO(), // TODO: use a correct context + Context: context.TODO(), Issue: issue, Doer: doer, ActionType: actionType, Content: "test body", Comment: comment, }, recipients, fromMention, "TestTemplateServices") @@ -263,7 +263,7 @@ func testComposeIssueCommentMessage(t *testing.T, ctx *mailCommentContext, recip func TestGenerateAdditionalHeaders(t *testing.T) { doer, _, issue, _ := prepareMailerTest(t) - ctx := &mailCommentContext{Context: context.TODO() /* TODO: use a correct context */, Issue: issue, Doer: doer} + ctx := &mailCommentContext{Context: context.TODO(), Issue: issue, Doer: doer} recipient := &user_model.User{Name: "test", Email: "test@gitea.com"} headers := generateAdditionalHeaders(ctx, "dummy-reason", recipient) @@ -390,9 +390,7 @@ func TestGenerateMessageIDForIssue(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := generateMessageIDForIssue(tt.args.issue, tt.args.comment, tt.args.actionType) - if !strings.HasPrefix(got, tt.prefix) { - t.Errorf("generateMessageIDForIssue() = %v, want %v", got, tt.prefix) - } + assert.True(t, strings.HasPrefix(got, tt.prefix), "%v, want %v", got, tt.prefix) }) } } diff --git a/services/migrations/codebase.go b/services/migrations/codebase.go index 492fc908e90..880dd214975 100644 --- a/services/migrations/codebase.go +++ b/services/migrations/codebase.go @@ -66,7 +66,6 @@ type codebaseUser struct { // from Codebase type CodebaseDownloader struct { base.NullDownloader - ctx context.Context client *http.Client baseURL *url.URL projectURL *url.URL @@ -77,17 +76,11 @@ type CodebaseDownloader struct { commitMap map[string]string } -// SetContext set context -func (d *CodebaseDownloader) SetContext(ctx context.Context) { - d.ctx = ctx -} - // NewCodebaseDownloader creates a new downloader -func NewCodebaseDownloader(ctx context.Context, projectURL *url.URL, project, repoName, username, password string) *CodebaseDownloader { +func NewCodebaseDownloader(_ context.Context, projectURL *url.URL, project, repoName, username, password string) *CodebaseDownloader { baseURL, _ := url.Parse("https://api3.codebasehq.com") downloader := &CodebaseDownloader{ - ctx: ctx, baseURL: baseURL, projectURL: projectURL, project: project, @@ -127,7 +120,7 @@ func (d *CodebaseDownloader) FormatCloneURL(opts base.MigrateOptions, remoteAddr return opts.CloneAddr, nil } -func (d *CodebaseDownloader) callAPI(endpoint string, parameter map[string]string, result any) error { +func (d *CodebaseDownloader) callAPI(ctx context.Context, endpoint string, parameter map[string]string, result any) error { u, err := d.baseURL.Parse(endpoint) if err != nil { return err @@ -141,7 +134,7 @@ func (d *CodebaseDownloader) callAPI(endpoint string, parameter map[string]strin u.RawQuery = query.Encode() } - req, err := http.NewRequestWithContext(d.ctx, "GET", u.String(), nil) + req, err := http.NewRequestWithContext(ctx, "GET", u.String(), nil) if err != nil { return err } @@ -158,7 +151,7 @@ func (d *CodebaseDownloader) callAPI(endpoint string, parameter map[string]strin // GetRepoInfo returns repository information // https://support.codebasehq.com/kb/projects -func (d *CodebaseDownloader) GetRepoInfo() (*base.Repository, error) { +func (d *CodebaseDownloader) GetRepoInfo(ctx context.Context) (*base.Repository, error) { var rawRepository struct { XMLName xml.Name `xml:"repository"` Name string `xml:"name"` @@ -169,6 +162,7 @@ func (d *CodebaseDownloader) GetRepoInfo() (*base.Repository, error) { } err := d.callAPI( + ctx, fmt.Sprintf("/%s/%s", d.project, d.repoName), nil, &rawRepository, @@ -187,7 +181,7 @@ func (d *CodebaseDownloader) GetRepoInfo() (*base.Repository, error) { // GetMilestones returns milestones // https://support.codebasehq.com/kb/tickets-and-milestones/milestones -func (d *CodebaseDownloader) GetMilestones() ([]*base.Milestone, error) { +func (d *CodebaseDownloader) GetMilestones(ctx context.Context) ([]*base.Milestone, error) { var rawMilestones struct { XMLName xml.Name `xml:"ticketing-milestone"` Type string `xml:"type,attr"` @@ -209,6 +203,7 @@ func (d *CodebaseDownloader) GetMilestones() ([]*base.Milestone, error) { } err := d.callAPI( + ctx, fmt.Sprintf("/%s/milestones", d.project), nil, &rawMilestones, @@ -245,7 +240,7 @@ func (d *CodebaseDownloader) GetMilestones() ([]*base.Milestone, error) { // GetLabels returns labels // https://support.codebasehq.com/kb/tickets-and-milestones/statuses-priorities-and-categories -func (d *CodebaseDownloader) GetLabels() ([]*base.Label, error) { +func (d *CodebaseDownloader) GetLabels(ctx context.Context) ([]*base.Label, error) { var rawTypes struct { XMLName xml.Name `xml:"ticketing-types"` Type string `xml:"type,attr"` @@ -259,6 +254,7 @@ func (d *CodebaseDownloader) GetLabels() ([]*base.Label, error) { } err := d.callAPI( + ctx, fmt.Sprintf("/%s/tickets/types", d.project), nil, &rawTypes, @@ -284,7 +280,7 @@ type codebaseIssueContext struct { // GetIssues returns issues, limits are not supported // https://support.codebasehq.com/kb/tickets-and-milestones // https://support.codebasehq.com/kb/tickets-and-milestones/updating-tickets -func (d *CodebaseDownloader) GetIssues(page, perPage int) ([]*base.Issue, bool, error) { +func (d *CodebaseDownloader) GetIssues(ctx context.Context, _, _ int) ([]*base.Issue, bool, error) { var rawIssues struct { XMLName xml.Name `xml:"tickets"` Type string `xml:"type,attr"` @@ -324,6 +320,7 @@ func (d *CodebaseDownloader) GetIssues(page, perPage int) ([]*base.Issue, bool, } err := d.callAPI( + ctx, fmt.Sprintf("/%s/tickets", d.project), nil, &rawIssues, @@ -358,6 +355,7 @@ func (d *CodebaseDownloader) GetIssues(page, perPage int) ([]*base.Issue, bool, } `xml:"ticket-note"` } err := d.callAPI( + ctx, fmt.Sprintf("/%s/tickets/%d/notes", d.project, issue.TicketID.Value), nil, ¬es, @@ -370,7 +368,7 @@ func (d *CodebaseDownloader) GetIssues(page, perPage int) ([]*base.Issue, bool, if len(note.Content) == 0 { continue } - poster := d.tryGetUser(note.UserID.Value) + poster := d.tryGetUser(ctx, note.UserID.Value) comments = append(comments, &base.Comment{ IssueIndex: issue.TicketID.Value, Index: note.ID.Value, @@ -390,7 +388,7 @@ func (d *CodebaseDownloader) GetIssues(page, perPage int) ([]*base.Issue, bool, if issue.Status.TreatAsClosed.Value { state = "closed" } - poster := d.tryGetUser(issue.ReporterID.Value) + poster := d.tryGetUser(ctx, issue.ReporterID.Value) issues = append(issues, &base.Issue{ Title: issue.Summary, Number: issue.TicketID.Value, @@ -419,7 +417,7 @@ func (d *CodebaseDownloader) GetIssues(page, perPage int) ([]*base.Issue, bool, } // GetComments returns comments -func (d *CodebaseDownloader) GetComments(commentable base.Commentable) ([]*base.Comment, bool, error) { +func (d *CodebaseDownloader) GetComments(_ context.Context, commentable base.Commentable) ([]*base.Comment, bool, error) { context, ok := commentable.GetContext().(codebaseIssueContext) if !ok { return nil, false, fmt.Errorf("unexpected context: %+v", commentable.GetContext()) @@ -430,7 +428,7 @@ func (d *CodebaseDownloader) GetComments(commentable base.Commentable) ([]*base. // GetPullRequests returns pull requests // https://support.codebasehq.com/kb/repositories/merge-requests -func (d *CodebaseDownloader) GetPullRequests(page, perPage int) ([]*base.PullRequest, bool, error) { +func (d *CodebaseDownloader) GetPullRequests(ctx context.Context, page, perPage int) ([]*base.PullRequest, bool, error) { var rawMergeRequests struct { XMLName xml.Name `xml:"merge-requests"` Type string `xml:"type,attr"` @@ -443,6 +441,7 @@ func (d *CodebaseDownloader) GetPullRequests(page, perPage int) ([]*base.PullReq } err := d.callAPI( + ctx, fmt.Sprintf("/%s/%s/merge_requests", d.project, d.repoName), map[string]string{ "query": `"Target Project" is "` + d.repoName + `"`, @@ -503,6 +502,7 @@ func (d *CodebaseDownloader) GetPullRequests(page, perPage int) ([]*base.PullReq } `xml:"comments"` } err := d.callAPI( + ctx, fmt.Sprintf("/%s/%s/merge_requests/%d", d.project, d.repoName, mr.ID.Value), nil, &rawMergeRequest, @@ -531,7 +531,7 @@ func (d *CodebaseDownloader) GetPullRequests(page, perPage int) ([]*base.PullReq } continue } - poster := d.tryGetUser(comment.UserID.Value) + poster := d.tryGetUser(ctx, comment.UserID.Value) comments = append(comments, &base.Comment{ IssueIndex: number, Index: comment.ID.Value, @@ -547,7 +547,7 @@ func (d *CodebaseDownloader) GetPullRequests(page, perPage int) ([]*base.PullReq comments = append(comments, &base.Comment{}) } - poster := d.tryGetUser(rawMergeRequest.UserID.Value) + poster := d.tryGetUser(ctx, rawMergeRequest.UserID.Value) pullRequests = append(pullRequests, &base.PullRequest{ Title: rawMergeRequest.Subject, @@ -563,12 +563,12 @@ func (d *CodebaseDownloader) GetPullRequests(page, perPage int) ([]*base.PullReq MergedTime: mergedTime, Head: base.PullRequestBranch{ Ref: rawMergeRequest.SourceRef, - SHA: d.getHeadCommit(rawMergeRequest.SourceRef), + SHA: d.getHeadCommit(ctx, rawMergeRequest.SourceRef), RepoName: d.repoName, }, Base: base.PullRequestBranch{ Ref: rawMergeRequest.TargetRef, - SHA: d.getHeadCommit(rawMergeRequest.TargetRef), + SHA: d.getHeadCommit(ctx, rawMergeRequest.TargetRef), RepoName: d.repoName, }, ForeignIndex: rawMergeRequest.ID.Value, @@ -584,7 +584,7 @@ func (d *CodebaseDownloader) GetPullRequests(page, perPage int) ([]*base.PullReq return pullRequests, true, nil } -func (d *CodebaseDownloader) tryGetUser(userID int64) *codebaseUser { +func (d *CodebaseDownloader) tryGetUser(ctx context.Context, userID int64) *codebaseUser { if len(d.userMap) == 0 { var rawUsers struct { XMLName xml.Name `xml:"users"` @@ -602,6 +602,7 @@ func (d *CodebaseDownloader) tryGetUser(userID int64) *codebaseUser { } err := d.callAPI( + ctx, "/users", nil, &rawUsers, @@ -627,7 +628,7 @@ func (d *CodebaseDownloader) tryGetUser(userID int64) *codebaseUser { return user } -func (d *CodebaseDownloader) getHeadCommit(ref string) string { +func (d *CodebaseDownloader) getHeadCommit(ctx context.Context, ref string) string { commitRef, ok := d.commitMap[ref] if !ok { var rawCommits struct { @@ -638,6 +639,7 @@ func (d *CodebaseDownloader) getHeadCommit(ref string) string { } `xml:"commit"` } err := d.callAPI( + ctx, fmt.Sprintf("/%s/%s/commits/%s", d.project, d.repoName, ref), nil, &rawCommits, diff --git a/services/migrations/codebase_test.go b/services/migrations/codebase_test.go index 68721e06410..ec4da1bff50 100644 --- a/services/migrations/codebase_test.go +++ b/services/migrations/codebase_test.go @@ -30,9 +30,9 @@ func TestCodebaseDownloadRepo(t *testing.T) { if cloneUser != "" { u.User = url.UserPassword(cloneUser, clonePassword) } - + ctx := context.Background() factory := &CodebaseDownloaderFactory{} - downloader, err := factory.New(context.Background(), base.MigrateOptions{ + downloader, err := factory.New(ctx, base.MigrateOptions{ CloneAddr: u.String(), AuthUsername: apiUser, AuthPassword: apiPassword, @@ -40,7 +40,7 @@ func TestCodebaseDownloadRepo(t *testing.T) { if err != nil { t.Fatalf("Error creating Codebase downloader: %v", err) } - repo, err := downloader.GetRepoInfo() + repo, err := downloader.GetRepoInfo(ctx) assert.NoError(t, err) assertRepositoryEqual(t, &base.Repository{ Name: "test", @@ -50,7 +50,7 @@ func TestCodebaseDownloadRepo(t *testing.T) { OriginalURL: cloneAddr, }, repo) - milestones, err := downloader.GetMilestones() + milestones, err := downloader.GetMilestones(ctx) assert.NoError(t, err) assertMilestonesEqual(t, []*base.Milestone{ { @@ -65,11 +65,11 @@ func TestCodebaseDownloadRepo(t *testing.T) { }, }, milestones) - labels, err := downloader.GetLabels() + labels, err := downloader.GetLabels(ctx) assert.NoError(t, err) assert.Len(t, labels, 4) - issues, isEnd, err := downloader.GetIssues(1, 2) + issues, isEnd, err := downloader.GetIssues(ctx, 1, 2) assert.NoError(t, err) assert.True(t, isEnd) assertIssuesEqual(t, []*base.Issue{ @@ -106,7 +106,7 @@ func TestCodebaseDownloadRepo(t *testing.T) { }, }, issues) - comments, _, err := downloader.GetComments(issues[0]) + comments, _, err := downloader.GetComments(ctx, issues[0]) assert.NoError(t, err) assertCommentsEqual(t, []*base.Comment{ { @@ -119,7 +119,7 @@ func TestCodebaseDownloadRepo(t *testing.T) { }, }, comments) - prs, _, err := downloader.GetPullRequests(1, 1) + prs, _, err := downloader.GetPullRequests(ctx, 1, 1) assert.NoError(t, err) assertPullRequestsEqual(t, []*base.PullRequest{ { @@ -144,7 +144,7 @@ func TestCodebaseDownloadRepo(t *testing.T) { }, }, prs) - rvs, err := downloader.GetReviews(prs[0]) + rvs, err := downloader.GetReviews(ctx, prs[0]) assert.NoError(t, err) assert.Empty(t, rvs) } diff --git a/services/migrations/codecommit.go b/services/migrations/codecommit.go index fead527f5b9..c45f9e59437 100644 --- a/services/migrations/codecommit.go +++ b/services/migrations/codecommit.go @@ -62,9 +62,8 @@ func (c *CodeCommitDownloaderFactory) GitServiceType() structs.GitServiceType { return structs.CodeCommitService } -func NewCodeCommitDownloader(ctx context.Context, repoName, baseURL, accessKeyID, secretAccessKey, region string) *CodeCommitDownloader { +func NewCodeCommitDownloader(_ context.Context, repoName, baseURL, accessKeyID, secretAccessKey, region string) *CodeCommitDownloader { downloader := CodeCommitDownloader{ - ctx: ctx, repoName: repoName, baseURL: baseURL, codeCommitClient: codecommit.New(codecommit.Options{ @@ -79,21 +78,15 @@ func NewCodeCommitDownloader(ctx context.Context, repoName, baseURL, accessKeyID // CodeCommitDownloader implements a downloader for AWS CodeCommit type CodeCommitDownloader struct { base.NullDownloader - ctx context.Context codeCommitClient *codecommit.Client repoName string baseURL string allPullRequestIDs []string } -// SetContext set context -func (c *CodeCommitDownloader) SetContext(ctx context.Context) { - c.ctx = ctx -} - // GetRepoInfo returns a repository information -func (c *CodeCommitDownloader) GetRepoInfo() (*base.Repository, error) { - output, err := c.codeCommitClient.GetRepository(c.ctx, &codecommit.GetRepositoryInput{ +func (c *CodeCommitDownloader) GetRepoInfo(ctx context.Context) (*base.Repository, error) { + output, err := c.codeCommitClient.GetRepository(ctx, &codecommit.GetRepositoryInput{ RepositoryName: util.ToPointer(c.repoName), }) if err != nil { @@ -117,14 +110,14 @@ func (c *CodeCommitDownloader) GetRepoInfo() (*base.Repository, error) { } // GetComments returns comments of an issue or PR -func (c *CodeCommitDownloader) GetComments(commentable base.Commentable) ([]*base.Comment, bool, error) { +func (c *CodeCommitDownloader) GetComments(ctx context.Context, commentable base.Commentable) ([]*base.Comment, bool, error) { var ( nextToken *string comments []*base.Comment ) for { - resp, err := c.codeCommitClient.GetCommentsForPullRequest(c.ctx, &codecommit.GetCommentsForPullRequestInput{ + resp, err := c.codeCommitClient.GetCommentsForPullRequest(ctx, &codecommit.GetCommentsForPullRequestInput{ NextToken: nextToken, PullRequestId: util.ToPointer(strconv.FormatInt(commentable.GetForeignIndex(), 10)), }) @@ -155,8 +148,8 @@ func (c *CodeCommitDownloader) GetComments(commentable base.Commentable) ([]*bas } // GetPullRequests returns pull requests according page and perPage -func (c *CodeCommitDownloader) GetPullRequests(page, perPage int) ([]*base.PullRequest, bool, error) { - allPullRequestIDs, err := c.getAllPullRequestIDs() +func (c *CodeCommitDownloader) GetPullRequests(ctx context.Context, page, perPage int) ([]*base.PullRequest, bool, error) { + allPullRequestIDs, err := c.getAllPullRequestIDs(ctx) if err != nil { return nil, false, err } @@ -170,7 +163,7 @@ func (c *CodeCommitDownloader) GetPullRequests(page, perPage int) ([]*base.PullR prs := make([]*base.PullRequest, 0, len(batch)) for _, id := range batch { - output, err := c.codeCommitClient.GetPullRequest(c.ctx, &codecommit.GetPullRequestInput{ + output, err := c.codeCommitClient.GetPullRequest(ctx, &codecommit.GetPullRequestInput{ PullRequestId: util.ToPointer(id), }) if err != nil { @@ -231,7 +224,7 @@ func (c *CodeCommitDownloader) FormatCloneURL(opts MigrateOptions, remoteAddr st return u.String(), nil } -func (c *CodeCommitDownloader) getAllPullRequestIDs() ([]string, error) { +func (c *CodeCommitDownloader) getAllPullRequestIDs(ctx context.Context) ([]string, error) { if len(c.allPullRequestIDs) > 0 { return c.allPullRequestIDs, nil } @@ -242,7 +235,7 @@ func (c *CodeCommitDownloader) getAllPullRequestIDs() ([]string, error) { ) for { - output, err := c.codeCommitClient.ListPullRequests(c.ctx, &codecommit.ListPullRequestsInput{ + output, err := c.codeCommitClient.ListPullRequests(ctx, &codecommit.ListPullRequestsInput{ RepositoryName: util.ToPointer(c.repoName), NextToken: nextToken, }) diff --git a/services/migrations/dump.go b/services/migrations/dump.go index 07812002afa..11efc181630 100644 --- a/services/migrations/dump.go +++ b/services/migrations/dump.go @@ -32,7 +32,6 @@ var _ base.Uploader = &RepositoryDumper{} // RepositoryDumper implements an Uploader to the local directory type RepositoryDumper struct { - ctx context.Context baseDir string repoOwner string repoName string @@ -56,7 +55,6 @@ func NewRepositoryDumper(ctx context.Context, baseDir, repoOwner, repoName strin return nil, err } return &RepositoryDumper{ - ctx: ctx, opts: opts, baseDir: baseDir, repoOwner: repoOwner, @@ -105,7 +103,7 @@ func (g *RepositoryDumper) setURLToken(remoteAddr string) (string, error) { } // CreateRepo creates a repository -func (g *RepositoryDumper) CreateRepo(repo *base.Repository, opts base.MigrateOptions) error { +func (g *RepositoryDumper) CreateRepo(ctx context.Context, repo *base.Repository, opts base.MigrateOptions) error { f, err := os.Create(filepath.Join(g.baseDir, "repo.yml")) if err != nil { return err @@ -149,7 +147,7 @@ func (g *RepositoryDumper) CreateRepo(repo *base.Repository, opts base.MigrateOp return err } - err = git.Clone(g.ctx, remoteAddr, repoPath, git.CloneRepoOptions{ + err = git.Clone(ctx, remoteAddr, repoPath, git.CloneRepoOptions{ Mirror: true, Quiet: true, Timeout: migrateTimeout, @@ -158,19 +156,19 @@ func (g *RepositoryDumper) CreateRepo(repo *base.Repository, opts base.MigrateOp if err != nil { return fmt.Errorf("Clone: %w", err) } - if err := git.WriteCommitGraph(g.ctx, repoPath); err != nil { + if err := git.WriteCommitGraph(ctx, repoPath); err != nil { return err } if opts.Wiki { wikiPath := g.wikiPath() - wikiRemotePath := repository.WikiRemoteURL(g.ctx, remoteAddr) + wikiRemotePath := repository.WikiRemoteURL(ctx, remoteAddr) if len(wikiRemotePath) > 0 { if err := os.MkdirAll(wikiPath, os.ModePerm); err != nil { return fmt.Errorf("Failed to remove %s: %w", wikiPath, err) } - if err := git.Clone(g.ctx, wikiRemotePath, wikiPath, git.CloneRepoOptions{ + if err := git.Clone(ctx, wikiRemotePath, wikiPath, git.CloneRepoOptions{ Mirror: true, Quiet: true, Timeout: migrateTimeout, @@ -181,13 +179,13 @@ func (g *RepositoryDumper) CreateRepo(repo *base.Repository, opts base.MigrateOp if err := os.RemoveAll(wikiPath); err != nil { return fmt.Errorf("Failed to remove %s: %w", wikiPath, err) } - } else if err := git.WriteCommitGraph(g.ctx, wikiPath); err != nil { + } else if err := git.WriteCommitGraph(ctx, wikiPath); err != nil { return err } } } - g.gitRepo, err = git.OpenRepository(g.ctx, g.gitPath()) + g.gitRepo, err = git.OpenRepository(ctx, g.gitPath()) return err } @@ -220,7 +218,7 @@ func (g *RepositoryDumper) Close() { } // CreateTopics creates topics -func (g *RepositoryDumper) CreateTopics(topics ...string) error { +func (g *RepositoryDumper) CreateTopics(_ context.Context, topics ...string) error { f, err := os.Create(filepath.Join(g.baseDir, "topic.yml")) if err != nil { return err @@ -242,7 +240,7 @@ func (g *RepositoryDumper) CreateTopics(topics ...string) error { } // CreateMilestones creates milestones -func (g *RepositoryDumper) CreateMilestones(milestones ...*base.Milestone) error { +func (g *RepositoryDumper) CreateMilestones(_ context.Context, milestones ...*base.Milestone) error { var err error if g.milestoneFile == nil { g.milestoneFile, err = os.Create(filepath.Join(g.baseDir, "milestone.yml")) @@ -264,7 +262,7 @@ func (g *RepositoryDumper) CreateMilestones(milestones ...*base.Milestone) error } // CreateLabels creates labels -func (g *RepositoryDumper) CreateLabels(labels ...*base.Label) error { +func (g *RepositoryDumper) CreateLabels(_ context.Context, labels ...*base.Label) error { var err error if g.labelFile == nil { g.labelFile, err = os.Create(filepath.Join(g.baseDir, "label.yml")) @@ -286,7 +284,7 @@ func (g *RepositoryDumper) CreateLabels(labels ...*base.Label) error { } // CreateReleases creates releases -func (g *RepositoryDumper) CreateReleases(releases ...*base.Release) error { +func (g *RepositoryDumper) CreateReleases(_ context.Context, releases ...*base.Release) error { if g.opts.ReleaseAssets { for _, release := range releases { attachDir := filepath.Join("release_assets", release.TagName) @@ -354,12 +352,12 @@ func (g *RepositoryDumper) CreateReleases(releases ...*base.Release) error { } // SyncTags syncs releases with tags in the database -func (g *RepositoryDumper) SyncTags() error { +func (g *RepositoryDumper) SyncTags(ctx context.Context) error { return nil } // CreateIssues creates issues -func (g *RepositoryDumper) CreateIssues(issues ...*base.Issue) error { +func (g *RepositoryDumper) CreateIssues(_ context.Context, issues ...*base.Issue) error { var err error if g.issueFile == nil { g.issueFile, err = os.Create(filepath.Join(g.baseDir, "issue.yml")) @@ -412,7 +410,7 @@ func (g *RepositoryDumper) encodeItems(number int64, items []any, dir string, it } // CreateComments creates comments of issues -func (g *RepositoryDumper) CreateComments(comments ...*base.Comment) error { +func (g *RepositoryDumper) CreateComments(_ context.Context, comments ...*base.Comment) error { commentsMap := make(map[int64][]any, len(comments)) for _, comment := range comments { commentsMap[comment.IssueIndex] = append(commentsMap[comment.IssueIndex], comment) @@ -421,7 +419,7 @@ func (g *RepositoryDumper) CreateComments(comments ...*base.Comment) error { return g.createItems(g.commentDir(), g.commentFiles, commentsMap) } -func (g *RepositoryDumper) handlePullRequest(pr *base.PullRequest) error { +func (g *RepositoryDumper) handlePullRequest(ctx context.Context, pr *base.PullRequest) error { // SECURITY: this pr must have been ensured safe if !pr.EnsuredSafe { log.Error("PR #%d in %s/%s has not been checked for safety ... We will ignore this.", pr.Number, g.repoOwner, g.repoName) @@ -490,7 +488,7 @@ func (g *RepositoryDumper) handlePullRequest(pr *base.PullRequest) error { if pr.Head.CloneURL == "" || pr.Head.Ref == "" { // Set head information if pr.Head.SHA is available if pr.Head.SHA != "" { - _, _, err = git.NewCommand(g.ctx, "update-ref", "--no-deref").AddDynamicArguments(pr.GetGitRefName(), pr.Head.SHA).RunStdString(&git.RunOpts{Dir: g.gitPath()}) + _, _, err = git.NewCommand(ctx, "update-ref", "--no-deref").AddDynamicArguments(pr.GetGitRefName(), pr.Head.SHA).RunStdString(&git.RunOpts{Dir: g.gitPath()}) if err != nil { log.Error("PR #%d in %s/%s unable to update-ref for pr HEAD: %v", pr.Number, g.repoOwner, g.repoName, err) } @@ -520,7 +518,7 @@ func (g *RepositoryDumper) handlePullRequest(pr *base.PullRequest) error { if !ok { // Set head information if pr.Head.SHA is available if pr.Head.SHA != "" { - _, _, err = git.NewCommand(g.ctx, "update-ref", "--no-deref").AddDynamicArguments(pr.GetGitRefName(), pr.Head.SHA).RunStdString(&git.RunOpts{Dir: g.gitPath()}) + _, _, err = git.NewCommand(ctx, "update-ref", "--no-deref").AddDynamicArguments(pr.GetGitRefName(), pr.Head.SHA).RunStdString(&git.RunOpts{Dir: g.gitPath()}) if err != nil { log.Error("PR #%d in %s/%s unable to update-ref for pr HEAD: %v", pr.Number, g.repoOwner, g.repoName, err) } @@ -555,7 +553,7 @@ func (g *RepositoryDumper) handlePullRequest(pr *base.PullRequest) error { fetchArg = git.BranchPrefix + fetchArg } - _, _, err = git.NewCommand(g.ctx, "fetch", "--no-tags").AddDashesAndList(remote, fetchArg).RunStdString(&git.RunOpts{Dir: g.gitPath()}) + _, _, err = git.NewCommand(ctx, "fetch", "--no-tags").AddDashesAndList(remote, fetchArg).RunStdString(&git.RunOpts{Dir: g.gitPath()}) if err != nil { log.Error("Fetch branch from %s failed: %v", pr.Head.CloneURL, err) // We need to continue here so that the Head.Ref is reset and we attempt to set the gitref for the PR @@ -579,7 +577,7 @@ func (g *RepositoryDumper) handlePullRequest(pr *base.PullRequest) error { pr.Head.SHA = headSha } if pr.Head.SHA != "" { - _, _, err = git.NewCommand(g.ctx, "update-ref", "--no-deref").AddDynamicArguments(pr.GetGitRefName(), pr.Head.SHA).RunStdString(&git.RunOpts{Dir: g.gitPath()}) + _, _, err = git.NewCommand(ctx, "update-ref", "--no-deref").AddDynamicArguments(pr.GetGitRefName(), pr.Head.SHA).RunStdString(&git.RunOpts{Dir: g.gitPath()}) if err != nil { log.Error("unable to set %s as the local head for PR #%d from %s in %s/%s. Error: %v", pr.Head.SHA, pr.Number, pr.Head.Ref, g.repoOwner, g.repoName, err) } @@ -589,7 +587,7 @@ func (g *RepositoryDumper) handlePullRequest(pr *base.PullRequest) error { } // CreatePullRequests creates pull requests -func (g *RepositoryDumper) CreatePullRequests(prs ...*base.PullRequest) error { +func (g *RepositoryDumper) CreatePullRequests(ctx context.Context, prs ...*base.PullRequest) error { var err error if g.pullrequestFile == nil { if err := os.MkdirAll(g.baseDir, os.ModePerm); err != nil { @@ -607,7 +605,7 @@ func (g *RepositoryDumper) CreatePullRequests(prs ...*base.PullRequest) error { count := 0 for i := 0; i < len(prs); i++ { pr := prs[i] - if err := g.handlePullRequest(pr); err != nil { + if err := g.handlePullRequest(ctx, pr); err != nil { log.Error("PR #%d in %s/%s failed - skipping", pr.Number, g.repoOwner, g.repoName, err) continue } @@ -620,7 +618,7 @@ func (g *RepositoryDumper) CreatePullRequests(prs ...*base.PullRequest) error { } // CreateReviews create pull request reviews -func (g *RepositoryDumper) CreateReviews(reviews ...*base.Review) error { +func (g *RepositoryDumper) CreateReviews(_ context.Context, reviews ...*base.Review) error { reviewsMap := make(map[int64][]any, len(reviews)) for _, review := range reviews { reviewsMap[review.IssueIndex] = append(reviewsMap[review.IssueIndex], review) @@ -636,7 +634,7 @@ func (g *RepositoryDumper) Rollback() error { } // Finish when migrating succeed, this will update something. -func (g *RepositoryDumper) Finish() error { +func (g *RepositoryDumper) Finish(_ context.Context) error { return nil } diff --git a/services/migrations/git.go b/services/migrations/git.go index 22ffd5e7658..1ed99499a16 100644 --- a/services/migrations/git.go +++ b/services/migrations/git.go @@ -28,12 +28,8 @@ func NewPlainGitDownloader(ownerName, repoName, remoteURL string) *PlainGitDownl } } -// SetContext set context -func (g *PlainGitDownloader) SetContext(ctx context.Context) { -} - // GetRepoInfo returns a repository information -func (g *PlainGitDownloader) GetRepoInfo() (*base.Repository, error) { +func (g *PlainGitDownloader) GetRepoInfo(_ context.Context) (*base.Repository, error) { // convert github repo to stand Repo return &base.Repository{ Owner: g.ownerName, @@ -43,6 +39,6 @@ func (g *PlainGitDownloader) GetRepoInfo() (*base.Repository, error) { } // GetTopics return empty string slice -func (g PlainGitDownloader) GetTopics() ([]string, error) { +func (g PlainGitDownloader) GetTopics(_ context.Context) ([]string, error) { return []string{}, nil } diff --git a/services/migrations/gitea_downloader.go b/services/migrations/gitea_downloader.go index 272bf02e11b..f92f318293d 100644 --- a/services/migrations/gitea_downloader.go +++ b/services/migrations/gitea_downloader.go @@ -67,7 +67,6 @@ func (f *GiteaDownloaderFactory) GitServiceType() structs.GitServiceType { // GiteaDownloader implements a Downloader interface to get repository information's type GiteaDownloader struct { base.NullDownloader - ctx context.Context client *gitea_sdk.Client baseURL string repoOwner string @@ -114,7 +113,6 @@ func NewGiteaDownloader(ctx context.Context, baseURL, repoPath, username, passwo } return &GiteaDownloader{ - ctx: ctx, client: giteaClient, baseURL: baseURL, repoOwner: path[0], @@ -124,11 +122,6 @@ func NewGiteaDownloader(ctx context.Context, baseURL, repoPath, username, passwo }, nil } -// SetContext set context -func (g *GiteaDownloader) SetContext(ctx context.Context) { - g.ctx = ctx -} - // String implements Stringer func (g *GiteaDownloader) String() string { return fmt.Sprintf("migration from gitea server %s %s/%s", g.baseURL, g.repoOwner, g.repoName) @@ -142,7 +135,7 @@ func (g *GiteaDownloader) LogString() string { } // GetRepoInfo returns a repository information -func (g *GiteaDownloader) GetRepoInfo() (*base.Repository, error) { +func (g *GiteaDownloader) GetRepoInfo(_ context.Context) (*base.Repository, error) { if g == nil { return nil, errors.New("error: GiteaDownloader is nil") } @@ -164,19 +157,19 @@ func (g *GiteaDownloader) GetRepoInfo() (*base.Repository, error) { } // GetTopics return gitea topics -func (g *GiteaDownloader) GetTopics() ([]string, error) { +func (g *GiteaDownloader) GetTopics(_ context.Context) ([]string, error) { topics, _, err := g.client.ListRepoTopics(g.repoOwner, g.repoName, gitea_sdk.ListRepoTopicsOptions{}) return topics, err } // GetMilestones returns milestones -func (g *GiteaDownloader) GetMilestones() ([]*base.Milestone, error) { +func (g *GiteaDownloader) GetMilestones(ctx context.Context) ([]*base.Milestone, error) { milestones := make([]*base.Milestone, 0, g.maxPerPage) for i := 1; ; i++ { // make sure gitea can shutdown gracefully select { - case <-g.ctx.Done(): + case <-ctx.Done(): return nil, nil default: } @@ -235,13 +228,13 @@ func (g *GiteaDownloader) convertGiteaLabel(label *gitea_sdk.Label) *base.Label } // GetLabels returns labels -func (g *GiteaDownloader) GetLabels() ([]*base.Label, error) { +func (g *GiteaDownloader) GetLabels(ctx context.Context) ([]*base.Label, error) { labels := make([]*base.Label, 0, g.maxPerPage) for i := 1; ; i++ { // make sure gitea can shutdown gracefully select { - case <-g.ctx.Done(): + case <-ctx.Done(): return nil, nil default: } @@ -323,13 +316,13 @@ func (g *GiteaDownloader) convertGiteaRelease(rel *gitea_sdk.Release) *base.Rele } // GetReleases returns releases -func (g *GiteaDownloader) GetReleases() ([]*base.Release, error) { +func (g *GiteaDownloader) GetReleases(ctx context.Context) ([]*base.Release, error) { releases := make([]*base.Release, 0, g.maxPerPage) for i := 1; ; i++ { // make sure gitea can shutdown gracefully select { - case <-g.ctx.Done(): + case <-ctx.Done(): return nil, nil default: } @@ -395,7 +388,7 @@ func (g *GiteaDownloader) getCommentReactions(commentID int64) ([]*base.Reaction } // GetIssues returns issues according start and limit -func (g *GiteaDownloader) GetIssues(page, perPage int) ([]*base.Issue, bool, error) { +func (g *GiteaDownloader) GetIssues(_ context.Context, page, perPage int) ([]*base.Issue, bool, error) { if perPage > g.maxPerPage { perPage = g.maxPerPage } @@ -458,13 +451,13 @@ func (g *GiteaDownloader) GetIssues(page, perPage int) ([]*base.Issue, bool, err } // GetComments returns comments according issueNumber -func (g *GiteaDownloader) GetComments(commentable base.Commentable) ([]*base.Comment, bool, error) { +func (g *GiteaDownloader) GetComments(ctx context.Context, commentable base.Commentable) ([]*base.Comment, bool, error) { allComments := make([]*base.Comment, 0, g.maxPerPage) for i := 1; ; i++ { // make sure gitea can shutdown gracefully select { - case <-g.ctx.Done(): + case <-ctx.Done(): return nil, false, nil default: } @@ -504,7 +497,7 @@ func (g *GiteaDownloader) GetComments(commentable base.Commentable) ([]*base.Com } // GetPullRequests returns pull requests according page and perPage -func (g *GiteaDownloader) GetPullRequests(page, perPage int) ([]*base.PullRequest, bool, error) { +func (g *GiteaDownloader) GetPullRequests(_ context.Context, page, perPage int) ([]*base.PullRequest, bool, error) { if perPage > g.maxPerPage { perPage = g.maxPerPage } @@ -624,7 +617,7 @@ func (g *GiteaDownloader) GetPullRequests(page, perPage int) ([]*base.PullReques } // GetReviews returns pull requests review -func (g *GiteaDownloader) GetReviews(reviewable base.Reviewable) ([]*base.Review, error) { +func (g *GiteaDownloader) GetReviews(ctx context.Context, reviewable base.Reviewable) ([]*base.Review, error) { if err := g.client.CheckServerVersionConstraint(">=1.12"); err != nil { log.Info("GiteaDownloader: instance to old, skip GetReviews") return nil, nil @@ -635,7 +628,7 @@ func (g *GiteaDownloader) GetReviews(reviewable base.Reviewable) ([]*base.Review for i := 1; ; i++ { // make sure gitea can shutdown gracefully select { - case <-g.ctx.Done(): + case <-ctx.Done(): return nil, nil default: } diff --git a/services/migrations/gitea_downloader_test.go b/services/migrations/gitea_downloader_test.go index c37c70947e3..3dccc4017e0 100644 --- a/services/migrations/gitea_downloader_test.go +++ b/services/migrations/gitea_downloader_test.go @@ -14,6 +14,7 @@ import ( base "code.gitea.io/gitea/modules/migration" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestGiteaDownloadRepo(t *testing.T) { @@ -27,16 +28,12 @@ func TestGiteaDownloadRepo(t *testing.T) { if err != nil || resp.StatusCode != http.StatusOK { t.Skipf("Can't reach https://gitea.com, skipping %s", t.Name()) } + ctx := context.Background() + downloader, err := NewGiteaDownloader(ctx, "https://gitea.com", "gitea/test_repo", "", "", giteaToken) + require.NoError(t, err, "NewGiteaDownloader error occur") + require.NotNil(t, downloader, "NewGiteaDownloader is nil") - downloader, err := NewGiteaDownloader(context.Background(), "https://gitea.com", "gitea/test_repo", "", "", giteaToken) - if downloader == nil { - t.Fatal("NewGitlabDownloader is nil") - } - if !assert.NoError(t, err) { - t.Fatal("NewGitlabDownloader error occur") - } - - repo, err := downloader.GetRepoInfo() + repo, err := downloader.GetRepoInfo(ctx) assert.NoError(t, err) assertRepositoryEqual(t, &base.Repository{ Name: "test_repo", @@ -48,12 +45,12 @@ func TestGiteaDownloadRepo(t *testing.T) { DefaultBranch: "master", }, repo) - topics, err := downloader.GetTopics() + topics, err := downloader.GetTopics(ctx) assert.NoError(t, err) sort.Strings(topics) assert.EqualValues(t, []string{"ci", "gitea", "migration", "test"}, topics) - labels, err := downloader.GetLabels() + labels, err := downloader.GetLabels(ctx) assert.NoError(t, err) assertLabelsEqual(t, []*base.Label{ { @@ -83,7 +80,7 @@ func TestGiteaDownloadRepo(t *testing.T) { }, }, labels) - milestones, err := downloader.GetMilestones() + milestones, err := downloader.GetMilestones(ctx) assert.NoError(t, err) assertMilestonesEqual(t, []*base.Milestone{ { @@ -103,7 +100,7 @@ func TestGiteaDownloadRepo(t *testing.T) { }, }, milestones) - releases, err := downloader.GetReleases() + releases, err := downloader.GetReleases(ctx) assert.NoError(t, err) assertReleasesEqual(t, []*base.Release{ { @@ -134,13 +131,13 @@ func TestGiteaDownloadRepo(t *testing.T) { }, }, releases) - issues, isEnd, err := downloader.GetIssues(1, 50) + issues, isEnd, err := downloader.GetIssues(ctx, 1, 50) assert.NoError(t, err) assert.True(t, isEnd) assert.Len(t, issues, 7) assert.EqualValues(t, "open", issues[0].State) - issues, isEnd, err = downloader.GetIssues(3, 2) + issues, isEnd, err = downloader.GetIssues(ctx, 3, 2) assert.NoError(t, err) assert.False(t, isEnd) @@ -197,7 +194,7 @@ func TestGiteaDownloadRepo(t *testing.T) { }, }, issues) - comments, _, err := downloader.GetComments(&base.Issue{Number: 4, ForeignIndex: 4}) + comments, _, err := downloader.GetComments(ctx, &base.Issue{Number: 4, ForeignIndex: 4}) assert.NoError(t, err) assertCommentsEqual(t, []*base.Comment{ { @@ -220,11 +217,11 @@ func TestGiteaDownloadRepo(t *testing.T) { }, }, comments) - prs, isEnd, err := downloader.GetPullRequests(1, 50) + prs, isEnd, err := downloader.GetPullRequests(ctx, 1, 50) assert.NoError(t, err) assert.True(t, isEnd) assert.Len(t, prs, 6) - prs, isEnd, err = downloader.GetPullRequests(1, 3) + prs, isEnd, err = downloader.GetPullRequests(ctx, 1, 3) assert.NoError(t, err) assert.False(t, isEnd) assert.Len(t, prs, 3) @@ -262,7 +259,7 @@ func TestGiteaDownloadRepo(t *testing.T) { PatchURL: "https://gitea.com/gitea/test_repo/pulls/12.patch", }, prs[1]) - reviews, err := downloader.GetReviews(&base.Issue{Number: 7, ForeignIndex: 7}) + reviews, err := downloader.GetReviews(ctx, &base.Issue{Number: 7, ForeignIndex: 7}) assert.NoError(t, err) assertReviewsEqual(t, []*base.Review{ { diff --git a/services/migrations/gitea_uploader.go b/services/migrations/gitea_uploader.go index 9e06b77b66c..eb16d6cb424 100644 --- a/services/migrations/gitea_uploader.go +++ b/services/migrations/gitea_uploader.go @@ -41,7 +41,6 @@ var _ base.Uploader = &GiteaLocalUploader{} // GiteaLocalUploader implements an Uploader to gitea sites type GiteaLocalUploader struct { - ctx context.Context doer *user_model.User repoOwner string repoName string @@ -58,9 +57,8 @@ type GiteaLocalUploader struct { } // NewGiteaLocalUploader creates an gitea Uploader via gitea API v1 -func NewGiteaLocalUploader(ctx context.Context, doer *user_model.User, repoOwner, repoName string) *GiteaLocalUploader { +func NewGiteaLocalUploader(_ context.Context, doer *user_model.User, repoOwner, repoName string) *GiteaLocalUploader { return &GiteaLocalUploader{ - ctx: ctx, doer: doer, repoOwner: repoOwner, repoName: repoName, @@ -93,15 +91,15 @@ func (g *GiteaLocalUploader) MaxBatchInsertSize(tp string) int { } // CreateRepo creates a repository -func (g *GiteaLocalUploader) CreateRepo(repo *base.Repository, opts base.MigrateOptions) error { - owner, err := user_model.GetUserByName(g.ctx, g.repoOwner) +func (g *GiteaLocalUploader) CreateRepo(ctx context.Context, repo *base.Repository, opts base.MigrateOptions) error { + owner, err := user_model.GetUserByName(ctx, g.repoOwner) if err != nil { return err } var r *repo_model.Repository if opts.MigrateToRepoID <= 0 { - r, err = repo_service.CreateRepositoryDirectly(g.ctx, g.doer, owner, repo_service.CreateRepoOptions{ + r, err = repo_service.CreateRepositoryDirectly(ctx, g.doer, owner, repo_service.CreateRepoOptions{ Name: g.repoName, Description: repo.Description, OriginalURL: repo.OriginalURL, @@ -111,7 +109,7 @@ func (g *GiteaLocalUploader) CreateRepo(repo *base.Repository, opts base.Migrate Status: repo_model.RepositoryBeingMigrated, }) } else { - r, err = repo_model.GetRepositoryByID(g.ctx, opts.MigrateToRepoID) + r, err = repo_model.GetRepositoryByID(ctx, opts.MigrateToRepoID) } if err != nil { return err @@ -119,7 +117,7 @@ func (g *GiteaLocalUploader) CreateRepo(repo *base.Repository, opts base.Migrate r.DefaultBranch = repo.DefaultBranch r.Description = repo.Description - r, err = repo_service.MigrateRepositoryGitData(g.ctx, owner, r, base.MigrateOptions{ + r, err = repo_service.MigrateRepositoryGitData(ctx, owner, r, base.MigrateOptions{ RepoName: g.repoName, Description: repo.Description, OriginalURL: repo.OriginalURL, @@ -139,7 +137,7 @@ func (g *GiteaLocalUploader) CreateRepo(repo *base.Repository, opts base.Migrate if err != nil { return err } - g.gitRepo, err = gitrepo.OpenRepository(g.ctx, g.repo) + g.gitRepo, err = gitrepo.OpenRepository(ctx, g.repo) if err != nil { return err } @@ -150,7 +148,7 @@ func (g *GiteaLocalUploader) CreateRepo(repo *base.Repository, opts base.Migrate return err } g.repo.ObjectFormatName = objectFormat.Name() - return repo_model.UpdateRepositoryCols(g.ctx, g.repo, "object_format_name") + return repo_model.UpdateRepositoryCols(ctx, g.repo, "object_format_name") } // Close closes this uploader @@ -161,7 +159,7 @@ func (g *GiteaLocalUploader) Close() { } // CreateTopics creates topics -func (g *GiteaLocalUploader) CreateTopics(topics ...string) error { +func (g *GiteaLocalUploader) CreateTopics(ctx context.Context, topics ...string) error { // Ignore topics too long for the db c := 0 for _, topic := range topics { @@ -173,11 +171,11 @@ func (g *GiteaLocalUploader) CreateTopics(topics ...string) error { c++ } topics = topics[:c] - return repo_model.SaveTopics(g.ctx, g.repo.ID, topics...) + return repo_model.SaveTopics(ctx, g.repo.ID, topics...) } // CreateMilestones creates milestones -func (g *GiteaLocalUploader) CreateMilestones(milestones ...*base.Milestone) error { +func (g *GiteaLocalUploader) CreateMilestones(ctx context.Context, milestones ...*base.Milestone) error { mss := make([]*issues_model.Milestone, 0, len(milestones)) for _, milestone := range milestones { var deadline timeutil.TimeStamp @@ -216,7 +214,7 @@ func (g *GiteaLocalUploader) CreateMilestones(milestones ...*base.Milestone) err mss = append(mss, &ms) } - err := issues_model.InsertMilestones(g.ctx, mss...) + err := issues_model.InsertMilestones(ctx, mss...) if err != nil { return err } @@ -228,7 +226,7 @@ func (g *GiteaLocalUploader) CreateMilestones(milestones ...*base.Milestone) err } // CreateLabels creates labels -func (g *GiteaLocalUploader) CreateLabels(labels ...*base.Label) error { +func (g *GiteaLocalUploader) CreateLabels(ctx context.Context, labels ...*base.Label) error { lbs := make([]*issues_model.Label, 0, len(labels)) for _, l := range labels { if color, err := label.NormalizeColor(l.Color); err != nil { @@ -247,7 +245,7 @@ func (g *GiteaLocalUploader) CreateLabels(labels ...*base.Label) error { }) } - err := issues_model.NewLabels(g.ctx, lbs...) + err := issues_model.NewLabels(ctx, lbs...) if err != nil { return err } @@ -258,7 +256,7 @@ func (g *GiteaLocalUploader) CreateLabels(labels ...*base.Label) error { } // CreateReleases creates releases -func (g *GiteaLocalUploader) CreateReleases(releases ...*base.Release) error { +func (g *GiteaLocalUploader) CreateReleases(ctx context.Context, releases ...*base.Release) error { rels := make([]*repo_model.Release, 0, len(releases)) for _, release := range releases { if release.Created.IsZero() { @@ -292,7 +290,7 @@ func (g *GiteaLocalUploader) CreateReleases(releases ...*base.Release) error { CreatedUnix: timeutil.TimeStamp(release.Created.Unix()), } - if err := g.remapUser(release, &rel); err != nil { + if err := g.remapUser(ctx, release, &rel); err != nil { return err } @@ -361,16 +359,16 @@ func (g *GiteaLocalUploader) CreateReleases(releases ...*base.Release) error { rels = append(rels, &rel) } - return repo_model.InsertReleases(g.ctx, rels...) + return repo_model.InsertReleases(ctx, rels...) } // SyncTags syncs releases with tags in the database -func (g *GiteaLocalUploader) SyncTags() error { - return repo_module.SyncReleasesWithTags(g.ctx, g.repo, g.gitRepo) +func (g *GiteaLocalUploader) SyncTags(ctx context.Context) error { + return repo_module.SyncReleasesWithTags(ctx, g.repo, g.gitRepo) } // CreateIssues creates issues -func (g *GiteaLocalUploader) CreateIssues(issues ...*base.Issue) error { +func (g *GiteaLocalUploader) CreateIssues(ctx context.Context, issues ...*base.Issue) error { iss := make([]*issues_model.Issue, 0, len(issues)) for _, issue := range issues { var labels []*issues_model.Label @@ -419,7 +417,7 @@ func (g *GiteaLocalUploader) CreateIssues(issues ...*base.Issue) error { UpdatedUnix: timeutil.TimeStamp(issue.Updated.Unix()), } - if err := g.remapUser(issue, &is); err != nil { + if err := g.remapUser(ctx, issue, &is); err != nil { return err } @@ -432,7 +430,7 @@ func (g *GiteaLocalUploader) CreateIssues(issues ...*base.Issue) error { Type: reaction.Content, CreatedUnix: timeutil.TimeStampNow(), } - if err := g.remapUser(reaction, &res); err != nil { + if err := g.remapUser(ctx, reaction, &res); err != nil { return err } is.Reactions = append(is.Reactions, &res) @@ -441,7 +439,7 @@ func (g *GiteaLocalUploader) CreateIssues(issues ...*base.Issue) error { } if len(iss) > 0 { - if err := issues_model.InsertIssues(g.ctx, iss...); err != nil { + if err := issues_model.InsertIssues(ctx, iss...); err != nil { return err } @@ -454,7 +452,7 @@ func (g *GiteaLocalUploader) CreateIssues(issues ...*base.Issue) error { } // CreateComments creates comments of issues -func (g *GiteaLocalUploader) CreateComments(comments ...*base.Comment) error { +func (g *GiteaLocalUploader) CreateComments(ctx context.Context, comments ...*base.Comment) error { cms := make([]*issues_model.Comment, 0, len(comments)) for _, comment := range comments { var issue *issues_model.Issue @@ -513,7 +511,7 @@ func (g *GiteaLocalUploader) CreateComments(comments ...*base.Comment) error { default: } - if err := g.remapUser(comment, &cm); err != nil { + if err := g.remapUser(ctx, comment, &cm); err != nil { return err } @@ -523,7 +521,7 @@ func (g *GiteaLocalUploader) CreateComments(comments ...*base.Comment) error { Type: reaction.Content, CreatedUnix: timeutil.TimeStampNow(), } - if err := g.remapUser(reaction, &res); err != nil { + if err := g.remapUser(ctx, reaction, &res); err != nil { return err } cm.Reactions = append(cm.Reactions, &res) @@ -535,35 +533,35 @@ func (g *GiteaLocalUploader) CreateComments(comments ...*base.Comment) error { if len(cms) == 0 { return nil } - return issues_model.InsertIssueComments(g.ctx, cms) + return issues_model.InsertIssueComments(ctx, cms) } // CreatePullRequests creates pull requests -func (g *GiteaLocalUploader) CreatePullRequests(prs ...*base.PullRequest) error { +func (g *GiteaLocalUploader) CreatePullRequests(ctx context.Context, prs ...*base.PullRequest) error { gprs := make([]*issues_model.PullRequest, 0, len(prs)) for _, pr := range prs { - gpr, err := g.newPullRequest(pr) + gpr, err := g.newPullRequest(ctx, pr) if err != nil { return err } - if err := g.remapUser(pr, gpr.Issue); err != nil { + if err := g.remapUser(ctx, pr, gpr.Issue); err != nil { return err } gprs = append(gprs, gpr) } - if err := issues_model.InsertPullRequests(g.ctx, gprs...); err != nil { + if err := issues_model.InsertPullRequests(ctx, gprs...); err != nil { return err } for _, pr := range gprs { g.issues[pr.Issue.Index] = pr.Issue - pull.AddToTaskQueue(g.ctx, pr) + pull.AddToTaskQueue(ctx, pr) } return nil } -func (g *GiteaLocalUploader) updateGitForPullRequest(pr *base.PullRequest) (head string, err error) { +func (g *GiteaLocalUploader) updateGitForPullRequest(ctx context.Context, pr *base.PullRequest) (head string, err error) { // SECURITY: this pr must have been must have been ensured safe if !pr.EnsuredSafe { log.Error("PR #%d in %s/%s has not been checked for safety.", pr.Number, g.repoOwner, g.repoName) @@ -664,7 +662,7 @@ func (g *GiteaLocalUploader) updateGitForPullRequest(pr *base.PullRequest) (head fetchArg = git.BranchPrefix + fetchArg } - _, _, err = git.NewCommand(g.ctx, "fetch", "--no-tags").AddDashesAndList(remote, fetchArg).RunStdString(&git.RunOpts{Dir: g.repo.RepoPath()}) + _, _, err = git.NewCommand(ctx, "fetch", "--no-tags").AddDashesAndList(remote, fetchArg).RunStdString(&git.RunOpts{Dir: g.repo.RepoPath()}) if err != nil { log.Error("Fetch branch from %s failed: %v", pr.Head.CloneURL, err) return head, nil @@ -683,7 +681,7 @@ func (g *GiteaLocalUploader) updateGitForPullRequest(pr *base.PullRequest) (head pr.Head.SHA = headSha } - _, _, err = git.NewCommand(g.ctx, "update-ref", "--no-deref").AddDynamicArguments(pr.GetGitRefName(), pr.Head.SHA).RunStdString(&git.RunOpts{Dir: g.repo.RepoPath()}) + _, _, err = git.NewCommand(ctx, "update-ref", "--no-deref").AddDynamicArguments(pr.GetGitRefName(), pr.Head.SHA).RunStdString(&git.RunOpts{Dir: g.repo.RepoPath()}) if err != nil { return "", err } @@ -700,13 +698,13 @@ func (g *GiteaLocalUploader) updateGitForPullRequest(pr *base.PullRequest) (head // The SHA is empty log.Warn("Empty reference, no pull head for PR #%d in %s/%s", pr.Number, g.repoOwner, g.repoName) } else { - _, _, err = git.NewCommand(g.ctx, "rev-list", "--quiet", "-1").AddDynamicArguments(pr.Head.SHA).RunStdString(&git.RunOpts{Dir: g.repo.RepoPath()}) + _, _, err = git.NewCommand(ctx, "rev-list", "--quiet", "-1").AddDynamicArguments(pr.Head.SHA).RunStdString(&git.RunOpts{Dir: g.repo.RepoPath()}) if err != nil { // Git update-ref remove bad references with a relative path log.Warn("Deprecated local head %s for PR #%d in %s/%s, removing %s", pr.Head.SHA, pr.Number, g.repoOwner, g.repoName, pr.GetGitRefName()) } else { // set head information - _, _, err = git.NewCommand(g.ctx, "update-ref", "--no-deref").AddDynamicArguments(pr.GetGitRefName(), pr.Head.SHA).RunStdString(&git.RunOpts{Dir: g.repo.RepoPath()}) + _, _, err = git.NewCommand(ctx, "update-ref", "--no-deref").AddDynamicArguments(pr.GetGitRefName(), pr.Head.SHA).RunStdString(&git.RunOpts{Dir: g.repo.RepoPath()}) if err != nil { log.Error("unable to set %s as the local head for PR #%d from %s in %s/%s. Error: %v", pr.Head.SHA, pr.Number, pr.Head.Ref, g.repoOwner, g.repoName, err) } @@ -716,7 +714,7 @@ func (g *GiteaLocalUploader) updateGitForPullRequest(pr *base.PullRequest) (head return head, nil } -func (g *GiteaLocalUploader) newPullRequest(pr *base.PullRequest) (*issues_model.PullRequest, error) { +func (g *GiteaLocalUploader) newPullRequest(ctx context.Context, pr *base.PullRequest) (*issues_model.PullRequest, error) { var labels []*issues_model.Label for _, label := range pr.Labels { lb, ok := g.labels[label.Name] @@ -727,7 +725,7 @@ func (g *GiteaLocalUploader) newPullRequest(pr *base.PullRequest) (*issues_model milestoneID := g.milestones[pr.Milestone] - head, err := g.updateGitForPullRequest(pr) + head, err := g.updateGitForPullRequest(ctx, pr) if err != nil { return nil, fmt.Errorf("updateGitForPullRequest: %w", err) } @@ -779,7 +777,7 @@ func (g *GiteaLocalUploader) newPullRequest(pr *base.PullRequest) (*issues_model UpdatedUnix: timeutil.TimeStamp(pr.Updated.Unix()), } - if err := g.remapUser(pr, &issue); err != nil { + if err := g.remapUser(ctx, pr, &issue); err != nil { return nil, err } @@ -789,7 +787,7 @@ func (g *GiteaLocalUploader) newPullRequest(pr *base.PullRequest) (*issues_model Type: reaction.Content, CreatedUnix: timeutil.TimeStampNow(), } - if err := g.remapUser(reaction, &res); err != nil { + if err := g.remapUser(ctx, reaction, &res); err != nil { return nil, err } issue.Reactions = append(issue.Reactions, &res) @@ -839,7 +837,7 @@ func convertReviewState(state string) issues_model.ReviewType { } // CreateReviews create pull request reviews of currently migrated issues -func (g *GiteaLocalUploader) CreateReviews(reviews ...*base.Review) error { +func (g *GiteaLocalUploader) CreateReviews(ctx context.Context, reviews ...*base.Review) error { cms := make([]*issues_model.Review, 0, len(reviews)) for _, review := range reviews { var issue *issues_model.Issue @@ -860,7 +858,7 @@ func (g *GiteaLocalUploader) CreateReviews(reviews ...*base.Review) error { UpdatedUnix: timeutil.TimeStamp(review.CreatedAt.Unix()), } - if err := g.remapUser(review, &cm); err != nil { + if err := g.remapUser(ctx, review, &cm); err != nil { return err } @@ -870,7 +868,7 @@ func (g *GiteaLocalUploader) CreateReviews(reviews ...*base.Review) error { pr, ok := g.prCache[issue.ID] if !ok { var err error - pr, err = issues_model.GetPullRequestByIssueIDWithNoAttributes(g.ctx, issue.ID) + pr, err = issues_model.GetPullRequestByIssueIDWithNoAttributes(ctx, issue.ID) if err != nil { return err } @@ -940,7 +938,7 @@ func (g *GiteaLocalUploader) CreateReviews(reviews ...*base.Review) error { UpdatedUnix: timeutil.TimeStamp(comment.UpdatedAt.Unix()), } - if err := g.remapUser(review, &c); err != nil { + if err := g.remapUser(ctx, review, &c); err != nil { return err } @@ -948,7 +946,7 @@ func (g *GiteaLocalUploader) CreateReviews(reviews ...*base.Review) error { } } - return issues_model.InsertReviews(g.ctx, cms) + return issues_model.InsertReviews(ctx, cms) } // Rollback when migrating failed, this will rollback all the changes. @@ -962,31 +960,31 @@ func (g *GiteaLocalUploader) Rollback() error { } // Finish when migrating success, this will do some status update things. -func (g *GiteaLocalUploader) Finish() error { +func (g *GiteaLocalUploader) Finish(ctx context.Context) error { if g.repo == nil || g.repo.ID <= 0 { return ErrRepoNotCreated } // update issue_index - if err := issues_model.RecalculateIssueIndexForRepo(g.ctx, g.repo.ID); err != nil { + if err := issues_model.RecalculateIssueIndexForRepo(ctx, g.repo.ID); err != nil { return err } - if err := models.UpdateRepoStats(g.ctx, g.repo.ID); err != nil { + if err := models.UpdateRepoStats(ctx, g.repo.ID); err != nil { return err } g.repo.Status = repo_model.RepositoryReady - return repo_model.UpdateRepositoryCols(g.ctx, g.repo, "status") + return repo_model.UpdateRepositoryCols(ctx, g.repo, "status") } -func (g *GiteaLocalUploader) remapUser(source user_model.ExternalUserMigrated, target user_model.ExternalUserRemappable) error { +func (g *GiteaLocalUploader) remapUser(ctx context.Context, source user_model.ExternalUserMigrated, target user_model.ExternalUserRemappable) error { var userID int64 var err error if g.sameApp { - userID, err = g.remapLocalUser(source) + userID, err = g.remapLocalUser(ctx, source) } else { - userID, err = g.remapExternalUser(source) + userID, err = g.remapExternalUser(ctx, source) } if err != nil { return err @@ -998,10 +996,10 @@ func (g *GiteaLocalUploader) remapUser(source user_model.ExternalUserMigrated, t return target.RemapExternalUser(source.GetExternalName(), source.GetExternalID(), g.doer.ID) } -func (g *GiteaLocalUploader) remapLocalUser(source user_model.ExternalUserMigrated) (int64, error) { +func (g *GiteaLocalUploader) remapLocalUser(ctx context.Context, source user_model.ExternalUserMigrated) (int64, error) { userid, ok := g.userMap[source.GetExternalID()] if !ok { - name, err := user_model.GetUserNameByID(g.ctx, source.GetExternalID()) + name, err := user_model.GetUserNameByID(ctx, source.GetExternalID()) if err != nil { return 0, err } @@ -1016,10 +1014,10 @@ func (g *GiteaLocalUploader) remapLocalUser(source user_model.ExternalUserMigrat return userid, nil } -func (g *GiteaLocalUploader) remapExternalUser(source user_model.ExternalUserMigrated) (userid int64, err error) { +func (g *GiteaLocalUploader) remapExternalUser(ctx context.Context, source user_model.ExternalUserMigrated) (userid int64, err error) { userid, ok := g.userMap[source.GetExternalID()] if !ok { - userid, err = user_model.GetUserIDByExternalUserID(g.ctx, g.gitServiceType.Name(), fmt.Sprintf("%d", source.GetExternalID())) + userid, err = user_model.GetUserIDByExternalUserID(ctx, g.gitServiceType.Name(), fmt.Sprintf("%d", source.GetExternalID())) if err != nil { log.Error("GetUserIDByExternalUserID: %v", err) return 0, err diff --git a/services/migrations/gitea_uploader_test.go b/services/migrations/gitea_uploader_test.go index f2379dadf8f..18d11715976 100644 --- a/services/migrations/gitea_uploader_test.go +++ b/services/migrations/gitea_uploader_test.go @@ -132,8 +132,9 @@ func TestGiteaUploadRemapLocalUser(t *testing.T) { doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + ctx := context.Background() repoName := "migrated" - uploader := NewGiteaLocalUploader(context.Background(), doer, doer.Name, repoName) + uploader := NewGiteaLocalUploader(ctx, doer, doer.Name, repoName) // call remapLocalUser uploader.sameApp = true @@ -150,7 +151,7 @@ func TestGiteaUploadRemapLocalUser(t *testing.T) { // target := repo_model.Release{} uploader.userMap = make(map[int64]int64) - err := uploader.remapUser(&source, &target) + err := uploader.remapUser(ctx, &source, &target) assert.NoError(t, err) assert.EqualValues(t, doer.ID, target.GetUserID()) @@ -161,7 +162,7 @@ func TestGiteaUploadRemapLocalUser(t *testing.T) { source.PublisherID = user.ID target = repo_model.Release{} uploader.userMap = make(map[int64]int64) - err = uploader.remapUser(&source, &target) + err = uploader.remapUser(ctx, &source, &target) assert.NoError(t, err) assert.EqualValues(t, doer.ID, target.GetUserID()) @@ -172,7 +173,7 @@ func TestGiteaUploadRemapLocalUser(t *testing.T) { source.PublisherName = user.Name target = repo_model.Release{} uploader.userMap = make(map[int64]int64) - err = uploader.remapUser(&source, &target) + err = uploader.remapUser(ctx, &source, &target) assert.NoError(t, err) assert.EqualValues(t, user.ID, target.GetUserID()) } @@ -180,9 +181,9 @@ func TestGiteaUploadRemapLocalUser(t *testing.T) { func TestGiteaUploadRemapExternalUser(t *testing.T) { unittest.PrepareTestEnv(t) doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) - + ctx := context.Background() repoName := "migrated" - uploader := NewGiteaLocalUploader(context.Background(), doer, doer.Name, repoName) + uploader := NewGiteaLocalUploader(ctx, doer, doer.Name, repoName) uploader.gitServiceType = structs.GiteaService // call remapExternalUser uploader.sameApp = false @@ -200,7 +201,7 @@ func TestGiteaUploadRemapExternalUser(t *testing.T) { // uploader.userMap = make(map[int64]int64) target := repo_model.Release{} - err := uploader.remapUser(&source, &target) + err := uploader.remapUser(ctx, &source, &target) assert.NoError(t, err) assert.EqualValues(t, doer.ID, target.GetUserID()) @@ -223,7 +224,7 @@ func TestGiteaUploadRemapExternalUser(t *testing.T) { // uploader.userMap = make(map[int64]int64) target = repo_model.Release{} - err = uploader.remapUser(&source, &target) + err = uploader.remapUser(ctx, &source, &target) assert.NoError(t, err) assert.EqualValues(t, linkedUser.ID, target.GetUserID()) } @@ -301,11 +302,12 @@ func TestGiteaUploadUpdateGitForPullRequest(t *testing.T) { assert.NoError(t, err) toRepoName := "migrated" - uploader := NewGiteaLocalUploader(context.Background(), fromRepoOwner, fromRepoOwner.Name, toRepoName) + ctx := context.Background() + uploader := NewGiteaLocalUploader(ctx, fromRepoOwner, fromRepoOwner.Name, toRepoName) uploader.gitServiceType = structs.GiteaService assert.NoError(t, repo_service.Init(context.Background())) - assert.NoError(t, uploader.CreateRepo(&base.Repository{ + assert.NoError(t, uploader.CreateRepo(ctx, &base.Repository{ Description: "description", OriginalURL: fromRepo.RepoPath(), CloneURL: fromRepo.RepoPath(), @@ -505,7 +507,7 @@ func TestGiteaUploadUpdateGitForPullRequest(t *testing.T) { testCase.pr.EnsuredSafe = true - head, err := uploader.updateGitForPullRequest(&testCase.pr) + head, err := uploader.updateGitForPullRequest(ctx, &testCase.pr) assert.NoError(t, err) assert.EqualValues(t, testCase.head, head) diff --git a/services/migrations/github.go b/services/migrations/github.go index 604ab84b396..b00d6ed27f6 100644 --- a/services/migrations/github.go +++ b/services/migrations/github.go @@ -64,7 +64,6 @@ func (f *GithubDownloaderV3Factory) GitServiceType() structs.GitServiceType { // from github via APIv3 type GithubDownloaderV3 struct { base.NullDownloader - ctx context.Context clients []*github.Client baseURL string repoOwner string @@ -79,12 +78,11 @@ type GithubDownloaderV3 struct { } // NewGithubDownloaderV3 creates a github Downloader via github v3 API -func NewGithubDownloaderV3(ctx context.Context, baseURL, userName, password, token, repoOwner, repoName string) *GithubDownloaderV3 { +func NewGithubDownloaderV3(_ context.Context, baseURL, userName, password, token, repoOwner, repoName string) *GithubDownloaderV3 { downloader := GithubDownloaderV3{ userName: userName, baseURL: baseURL, password: password, - ctx: ctx, repoOwner: repoOwner, repoName: repoName, maxPerPage: 100, @@ -141,12 +139,7 @@ func (g *GithubDownloaderV3) addClient(client *http.Client, baseURL string) { g.rates = append(g.rates, nil) } -// SetContext set context -func (g *GithubDownloaderV3) SetContext(ctx context.Context) { - g.ctx = ctx -} - -func (g *GithubDownloaderV3) waitAndPickClient() { +func (g *GithubDownloaderV3) waitAndPickClient(ctx context.Context) { var recentIdx int var maxRemaining int for i := 0; i < len(g.clients); i++ { @@ -160,13 +153,13 @@ func (g *GithubDownloaderV3) waitAndPickClient() { for g.rates[g.curClientIdx] != nil && g.rates[g.curClientIdx].Remaining <= GithubLimitRateRemaining { timer := time.NewTimer(time.Until(g.rates[g.curClientIdx].Reset.Time)) select { - case <-g.ctx.Done(): + case <-ctx.Done(): timer.Stop() return case <-timer.C: } - err := g.RefreshRate() + err := g.RefreshRate(ctx) if err != nil { log.Error("g.getClient().RateLimit.Get: %s", err) } @@ -174,8 +167,8 @@ func (g *GithubDownloaderV3) waitAndPickClient() { } // RefreshRate update the current rate (doesn't count in rate limit) -func (g *GithubDownloaderV3) RefreshRate() error { - rates, _, err := g.getClient().RateLimit.Get(g.ctx) +func (g *GithubDownloaderV3) RefreshRate(ctx context.Context) error { + rates, _, err := g.getClient().RateLimit.Get(ctx) if err != nil { // if rate limit is not enabled, ignore it if strings.Contains(err.Error(), "404") { @@ -198,9 +191,9 @@ func (g *GithubDownloaderV3) setRate(rate *github.Rate) { } // GetRepoInfo returns a repository information -func (g *GithubDownloaderV3) GetRepoInfo() (*base.Repository, error) { - g.waitAndPickClient() - gr, resp, err := g.getClient().Repositories.Get(g.ctx, g.repoOwner, g.repoName) +func (g *GithubDownloaderV3) GetRepoInfo(ctx context.Context) (*base.Repository, error) { + g.waitAndPickClient(ctx) + gr, resp, err := g.getClient().Repositories.Get(ctx, g.repoOwner, g.repoName) if err != nil { return nil, err } @@ -219,9 +212,9 @@ func (g *GithubDownloaderV3) GetRepoInfo() (*base.Repository, error) { } // GetTopics return github topics -func (g *GithubDownloaderV3) GetTopics() ([]string, error) { - g.waitAndPickClient() - r, resp, err := g.getClient().Repositories.Get(g.ctx, g.repoOwner, g.repoName) +func (g *GithubDownloaderV3) GetTopics(ctx context.Context) ([]string, error) { + g.waitAndPickClient(ctx) + r, resp, err := g.getClient().Repositories.Get(ctx, g.repoOwner, g.repoName) if err != nil { return nil, err } @@ -230,12 +223,12 @@ func (g *GithubDownloaderV3) GetTopics() ([]string, error) { } // GetMilestones returns milestones -func (g *GithubDownloaderV3) GetMilestones() ([]*base.Milestone, error) { +func (g *GithubDownloaderV3) GetMilestones(ctx context.Context) ([]*base.Milestone, error) { perPage := g.maxPerPage milestones := make([]*base.Milestone, 0, perPage) for i := 1; ; i++ { - g.waitAndPickClient() - ms, resp, err := g.getClient().Issues.ListMilestones(g.ctx, g.repoOwner, g.repoName, + g.waitAndPickClient(ctx) + ms, resp, err := g.getClient().Issues.ListMilestones(ctx, g.repoOwner, g.repoName, &github.MilestoneListOptions{ State: "all", ListOptions: github.ListOptions{ @@ -279,12 +272,12 @@ func convertGithubLabel(label *github.Label) *base.Label { } // GetLabels returns labels -func (g *GithubDownloaderV3) GetLabels() ([]*base.Label, error) { +func (g *GithubDownloaderV3) GetLabels(ctx context.Context) ([]*base.Label, error) { perPage := g.maxPerPage labels := make([]*base.Label, 0, perPage) for i := 1; ; i++ { - g.waitAndPickClient() - ls, resp, err := g.getClient().Issues.ListLabels(g.ctx, g.repoOwner, g.repoName, + g.waitAndPickClient(ctx) + ls, resp, err := g.getClient().Issues.ListLabels(ctx, g.repoOwner, g.repoName, &github.ListOptions{ Page: i, PerPage: perPage, @@ -304,7 +297,7 @@ func (g *GithubDownloaderV3) GetLabels() ([]*base.Label, error) { return labels, nil } -func (g *GithubDownloaderV3) convertGithubRelease(rel *github.RepositoryRelease) *base.Release { +func (g *GithubDownloaderV3) convertGithubRelease(ctx context.Context, rel *github.RepositoryRelease) *base.Release { // GitHub allows commitish to be a reference. // In this case, we need to remove the prefix, i.e. convert "refs/heads/main" to "main". targetCommitish := strings.TrimPrefix(rel.GetTargetCommitish(), git.BranchPrefix) @@ -339,12 +332,12 @@ func (g *GithubDownloaderV3) convertGithubRelease(rel *github.RepositoryRelease) Created: asset.CreatedAt.Time, Updated: asset.UpdatedAt.Time, DownloadFunc: func() (io.ReadCloser, error) { - g.waitAndPickClient() - readCloser, redirectURL, err := g.getClient().Repositories.DownloadReleaseAsset(g.ctx, g.repoOwner, g.repoName, assetID, nil) + g.waitAndPickClient(ctx) + readCloser, redirectURL, err := g.getClient().Repositories.DownloadReleaseAsset(ctx, g.repoOwner, g.repoName, assetID, nil) if err != nil { return nil, err } - if err := g.RefreshRate(); err != nil { + if err := g.RefreshRate(ctx); err != nil { log.Error("g.getClient().RateLimits: %s", err) } @@ -364,13 +357,13 @@ func (g *GithubDownloaderV3) convertGithubRelease(rel *github.RepositoryRelease) return io.NopCloser(strings.NewReader(redirectURL)), nil } - g.waitAndPickClient() - req, err := http.NewRequestWithContext(g.ctx, "GET", redirectURL, nil) + g.waitAndPickClient(ctx) + req, err := http.NewRequestWithContext(ctx, "GET", redirectURL, nil) if err != nil { return nil, err } resp, err := httpClient.Do(req) - err1 := g.RefreshRate() + err1 := g.RefreshRate(ctx) if err1 != nil { log.Error("g.RefreshRate(): %s", err1) } @@ -385,12 +378,12 @@ func (g *GithubDownloaderV3) convertGithubRelease(rel *github.RepositoryRelease) } // GetReleases returns releases -func (g *GithubDownloaderV3) GetReleases() ([]*base.Release, error) { +func (g *GithubDownloaderV3) GetReleases(ctx context.Context) ([]*base.Release, error) { perPage := g.maxPerPage releases := make([]*base.Release, 0, perPage) for i := 1; ; i++ { - g.waitAndPickClient() - ls, resp, err := g.getClient().Repositories.ListReleases(g.ctx, g.repoOwner, g.repoName, + g.waitAndPickClient(ctx) + ls, resp, err := g.getClient().Repositories.ListReleases(ctx, g.repoOwner, g.repoName, &github.ListOptions{ Page: i, PerPage: perPage, @@ -401,7 +394,7 @@ func (g *GithubDownloaderV3) GetReleases() ([]*base.Release, error) { g.setRate(&resp.Rate) for _, release := range ls { - releases = append(releases, g.convertGithubRelease(release)) + releases = append(releases, g.convertGithubRelease(ctx, release)) } if len(ls) < perPage { break @@ -411,7 +404,7 @@ func (g *GithubDownloaderV3) GetReleases() ([]*base.Release, error) { } // GetIssues returns issues according start and limit -func (g *GithubDownloaderV3) GetIssues(page, perPage int) ([]*base.Issue, bool, error) { +func (g *GithubDownloaderV3) GetIssues(ctx context.Context, page, perPage int) ([]*base.Issue, bool, error) { if perPage > g.maxPerPage { perPage = g.maxPerPage } @@ -426,8 +419,8 @@ func (g *GithubDownloaderV3) GetIssues(page, perPage int) ([]*base.Issue, bool, } allIssues := make([]*base.Issue, 0, perPage) - g.waitAndPickClient() - issues, resp, err := g.getClient().Issues.ListByRepo(g.ctx, g.repoOwner, g.repoName, opt) + g.waitAndPickClient(ctx) + issues, resp, err := g.getClient().Issues.ListByRepo(ctx, g.repoOwner, g.repoName, opt) if err != nil { return nil, false, fmt.Errorf("error while listing repos: %w", err) } @@ -447,8 +440,8 @@ func (g *GithubDownloaderV3) GetIssues(page, perPage int) ([]*base.Issue, bool, var reactions []*base.Reaction if !g.SkipReactions { for i := 1; ; i++ { - g.waitAndPickClient() - res, resp, err := g.getClient().Reactions.ListIssueReactions(g.ctx, g.repoOwner, g.repoName, issue.GetNumber(), &github.ListOptions{ + g.waitAndPickClient(ctx) + res, resp, err := g.getClient().Reactions.ListIssueReactions(ctx, g.repoOwner, g.repoName, issue.GetNumber(), &github.ListOptions{ Page: i, PerPage: perPage, }) @@ -503,12 +496,12 @@ func (g *GithubDownloaderV3) SupportGetRepoComments() bool { } // GetComments returns comments according issueNumber -func (g *GithubDownloaderV3) GetComments(commentable base.Commentable) ([]*base.Comment, bool, error) { - comments, err := g.getComments(commentable) +func (g *GithubDownloaderV3) GetComments(ctx context.Context, commentable base.Commentable) ([]*base.Comment, bool, error) { + comments, err := g.getComments(ctx, commentable) return comments, false, err } -func (g *GithubDownloaderV3) getComments(commentable base.Commentable) ([]*base.Comment, error) { +func (g *GithubDownloaderV3) getComments(ctx context.Context, commentable base.Commentable) ([]*base.Comment, error) { var ( allComments = make([]*base.Comment, 0, g.maxPerPage) created = "created" @@ -522,8 +515,8 @@ func (g *GithubDownloaderV3) getComments(commentable base.Commentable) ([]*base. }, } for { - g.waitAndPickClient() - comments, resp, err := g.getClient().Issues.ListComments(g.ctx, g.repoOwner, g.repoName, int(commentable.GetForeignIndex()), opt) + g.waitAndPickClient(ctx) + comments, resp, err := g.getClient().Issues.ListComments(ctx, g.repoOwner, g.repoName, int(commentable.GetForeignIndex()), opt) if err != nil { return nil, fmt.Errorf("error while listing repos: %w", err) } @@ -533,8 +526,8 @@ func (g *GithubDownloaderV3) getComments(commentable base.Commentable) ([]*base. var reactions []*base.Reaction if !g.SkipReactions { for i := 1; ; i++ { - g.waitAndPickClient() - res, resp, err := g.getClient().Reactions.ListIssueCommentReactions(g.ctx, g.repoOwner, g.repoName, comment.GetID(), &github.ListOptions{ + g.waitAndPickClient(ctx) + res, resp, err := g.getClient().Reactions.ListIssueCommentReactions(ctx, g.repoOwner, g.repoName, comment.GetID(), &github.ListOptions{ Page: i, PerPage: g.maxPerPage, }) @@ -576,7 +569,7 @@ func (g *GithubDownloaderV3) getComments(commentable base.Commentable) ([]*base. } // GetAllComments returns repository comments according page and perPageSize -func (g *GithubDownloaderV3) GetAllComments(page, perPage int) ([]*base.Comment, bool, error) { +func (g *GithubDownloaderV3) GetAllComments(ctx context.Context, page, perPage int) ([]*base.Comment, bool, error) { var ( allComments = make([]*base.Comment, 0, perPage) created = "created" @@ -594,8 +587,8 @@ func (g *GithubDownloaderV3) GetAllComments(page, perPage int) ([]*base.Comment, }, } - g.waitAndPickClient() - comments, resp, err := g.getClient().Issues.ListComments(g.ctx, g.repoOwner, g.repoName, 0, opt) + g.waitAndPickClient(ctx) + comments, resp, err := g.getClient().Issues.ListComments(ctx, g.repoOwner, g.repoName, 0, opt) if err != nil { return nil, false, fmt.Errorf("error while listing repos: %w", err) } @@ -608,8 +601,8 @@ func (g *GithubDownloaderV3) GetAllComments(page, perPage int) ([]*base.Comment, var reactions []*base.Reaction if !g.SkipReactions { for i := 1; ; i++ { - g.waitAndPickClient() - res, resp, err := g.getClient().Reactions.ListIssueCommentReactions(g.ctx, g.repoOwner, g.repoName, comment.GetID(), &github.ListOptions{ + g.waitAndPickClient(ctx) + res, resp, err := g.getClient().Reactions.ListIssueCommentReactions(ctx, g.repoOwner, g.repoName, comment.GetID(), &github.ListOptions{ Page: i, PerPage: g.maxPerPage, }) @@ -648,7 +641,7 @@ func (g *GithubDownloaderV3) GetAllComments(page, perPage int) ([]*base.Comment, } // GetPullRequests returns pull requests according page and perPage -func (g *GithubDownloaderV3) GetPullRequests(page, perPage int) ([]*base.PullRequest, bool, error) { +func (g *GithubDownloaderV3) GetPullRequests(ctx context.Context, page, perPage int) ([]*base.PullRequest, bool, error) { if perPage > g.maxPerPage { perPage = g.maxPerPage } @@ -662,8 +655,8 @@ func (g *GithubDownloaderV3) GetPullRequests(page, perPage int) ([]*base.PullReq }, } allPRs := make([]*base.PullRequest, 0, perPage) - g.waitAndPickClient() - prs, resp, err := g.getClient().PullRequests.List(g.ctx, g.repoOwner, g.repoName, opt) + g.waitAndPickClient(ctx) + prs, resp, err := g.getClient().PullRequests.List(ctx, g.repoOwner, g.repoName, opt) if err != nil { return nil, false, fmt.Errorf("error while listing repos: %w", err) } @@ -679,8 +672,8 @@ func (g *GithubDownloaderV3) GetPullRequests(page, perPage int) ([]*base.PullReq var reactions []*base.Reaction if !g.SkipReactions { for i := 1; ; i++ { - g.waitAndPickClient() - res, resp, err := g.getClient().Reactions.ListIssueReactions(g.ctx, g.repoOwner, g.repoName, pr.GetNumber(), &github.ListOptions{ + g.waitAndPickClient(ctx) + res, resp, err := g.getClient().Reactions.ListIssueReactions(ctx, g.repoOwner, g.repoName, pr.GetNumber(), &github.ListOptions{ Page: i, PerPage: perPage, }) @@ -702,7 +695,7 @@ func (g *GithubDownloaderV3) GetPullRequests(page, perPage int) ([]*base.PullReq } // download patch and saved as tmp file - g.waitAndPickClient() + g.waitAndPickClient(ctx) allPRs = append(allPRs, &base.PullRequest{ Title: pr.GetTitle(), @@ -759,15 +752,15 @@ func convertGithubReview(r *github.PullRequestReview) *base.Review { } } -func (g *GithubDownloaderV3) convertGithubReviewComments(cs []*github.PullRequestComment) ([]*base.ReviewComment, error) { +func (g *GithubDownloaderV3) convertGithubReviewComments(ctx context.Context, cs []*github.PullRequestComment) ([]*base.ReviewComment, error) { rcs := make([]*base.ReviewComment, 0, len(cs)) for _, c := range cs { // get reactions var reactions []*base.Reaction if !g.SkipReactions { for i := 1; ; i++ { - g.waitAndPickClient() - res, resp, err := g.getClient().Reactions.ListPullRequestCommentReactions(g.ctx, g.repoOwner, g.repoName, c.GetID(), &github.ListOptions{ + g.waitAndPickClient(ctx) + res, resp, err := g.getClient().Reactions.ListPullRequestCommentReactions(ctx, g.repoOwner, g.repoName, c.GetID(), &github.ListOptions{ Page: i, PerPage: g.maxPerPage, }) @@ -806,7 +799,7 @@ func (g *GithubDownloaderV3) convertGithubReviewComments(cs []*github.PullReques } // GetReviews returns pull requests review -func (g *GithubDownloaderV3) GetReviews(reviewable base.Reviewable) ([]*base.Review, error) { +func (g *GithubDownloaderV3) GetReviews(ctx context.Context, reviewable base.Reviewable) ([]*base.Review, error) { allReviews := make([]*base.Review, 0, g.maxPerPage) if g.SkipReviews { return allReviews, nil @@ -816,8 +809,8 @@ func (g *GithubDownloaderV3) GetReviews(reviewable base.Reviewable) ([]*base.Rev } // Get approve/request change reviews for { - g.waitAndPickClient() - reviews, resp, err := g.getClient().PullRequests.ListReviews(g.ctx, g.repoOwner, g.repoName, int(reviewable.GetForeignIndex()), opt) + g.waitAndPickClient(ctx) + reviews, resp, err := g.getClient().PullRequests.ListReviews(ctx, g.repoOwner, g.repoName, int(reviewable.GetForeignIndex()), opt) if err != nil { return nil, fmt.Errorf("error while listing repos: %w", err) } @@ -830,14 +823,14 @@ func (g *GithubDownloaderV3) GetReviews(reviewable base.Reviewable) ([]*base.Rev PerPage: g.maxPerPage, } for { - g.waitAndPickClient() - reviewComments, resp, err := g.getClient().PullRequests.ListReviewComments(g.ctx, g.repoOwner, g.repoName, int(reviewable.GetForeignIndex()), review.GetID(), opt2) + g.waitAndPickClient(ctx) + reviewComments, resp, err := g.getClient().PullRequests.ListReviewComments(ctx, g.repoOwner, g.repoName, int(reviewable.GetForeignIndex()), review.GetID(), opt2) if err != nil { return nil, fmt.Errorf("error while listing repos: %w", err) } g.setRate(&resp.Rate) - cs, err := g.convertGithubReviewComments(reviewComments) + cs, err := g.convertGithubReviewComments(ctx, reviewComments) if err != nil { return nil, err } @@ -856,8 +849,8 @@ func (g *GithubDownloaderV3) GetReviews(reviewable base.Reviewable) ([]*base.Rev } // Get requested reviews for { - g.waitAndPickClient() - reviewers, resp, err := g.getClient().PullRequests.ListReviewers(g.ctx, g.repoOwner, g.repoName, int(reviewable.GetForeignIndex()), opt) + g.waitAndPickClient(ctx) + reviewers, resp, err := g.getClient().PullRequests.ListReviewers(ctx, g.repoOwner, g.repoName, int(reviewable.GetForeignIndex()), opt) if err != nil { return nil, fmt.Errorf("error while listing repos: %w", err) } diff --git a/services/migrations/github_test.go b/services/migrations/github_test.go index 2b89e6dc0fd..899f9fe52c4 100644 --- a/services/migrations/github_test.go +++ b/services/migrations/github_test.go @@ -21,11 +21,12 @@ func TestGitHubDownloadRepo(t *testing.T) { if token == "" { t.Skip("Skipping GitHub migration test because GITHUB_READ_TOKEN is empty") } - downloader := NewGithubDownloaderV3(context.Background(), "https://github.com", "", "", token, "go-gitea", "test_repo") - err := downloader.RefreshRate() + ctx := context.Background() + downloader := NewGithubDownloaderV3(ctx, "https://github.com", "", "", token, "go-gitea", "test_repo") + err := downloader.RefreshRate(ctx) assert.NoError(t, err) - repo, err := downloader.GetRepoInfo() + repo, err := downloader.GetRepoInfo(ctx) assert.NoError(t, err) assertRepositoryEqual(t, &base.Repository{ Name: "test_repo", @@ -36,11 +37,11 @@ func TestGitHubDownloadRepo(t *testing.T) { DefaultBranch: "master", }, repo) - topics, err := downloader.GetTopics() + topics, err := downloader.GetTopics(ctx) assert.NoError(t, err) assert.Contains(t, topics, "gitea") - milestones, err := downloader.GetMilestones() + milestones, err := downloader.GetMilestones(ctx) assert.NoError(t, err) assertMilestonesEqual(t, []*base.Milestone{ { @@ -63,7 +64,7 @@ func TestGitHubDownloadRepo(t *testing.T) { }, }, milestones) - labels, err := downloader.GetLabels() + labels, err := downloader.GetLabels(ctx) assert.NoError(t, err) assertLabelsEqual(t, []*base.Label{ { @@ -113,7 +114,7 @@ func TestGitHubDownloadRepo(t *testing.T) { }, }, labels) - releases, err := downloader.GetReleases() + releases, err := downloader.GetReleases(ctx) assert.NoError(t, err) assertReleasesEqual(t, []*base.Release{ { @@ -129,7 +130,7 @@ func TestGitHubDownloadRepo(t *testing.T) { }, releases) // downloader.GetIssues() - issues, isEnd, err := downloader.GetIssues(1, 2) + issues, isEnd, err := downloader.GetIssues(ctx, 1, 2) assert.NoError(t, err) assert.False(t, isEnd) assertIssuesEqual(t, []*base.Issue{ @@ -218,7 +219,7 @@ func TestGitHubDownloadRepo(t *testing.T) { }, issues) // downloader.GetComments() - comments, _, err := downloader.GetComments(&base.Issue{Number: 2, ForeignIndex: 2}) + comments, _, err := downloader.GetComments(ctx, &base.Issue{Number: 2, ForeignIndex: 2}) assert.NoError(t, err) assertCommentsEqual(t, []*base.Comment{ { @@ -248,7 +249,7 @@ func TestGitHubDownloadRepo(t *testing.T) { }, comments) // downloader.GetPullRequests() - prs, _, err := downloader.GetPullRequests(1, 2) + prs, _, err := downloader.GetPullRequests(ctx, 1, 2) assert.NoError(t, err) assertPullRequestsEqual(t, []*base.PullRequest{ { @@ -338,7 +339,7 @@ func TestGitHubDownloadRepo(t *testing.T) { }, }, prs) - reviews, err := downloader.GetReviews(&base.PullRequest{Number: 3, ForeignIndex: 3}) + reviews, err := downloader.GetReviews(ctx, &base.PullRequest{Number: 3, ForeignIndex: 3}) assert.NoError(t, err) assertReviewsEqual(t, []*base.Review{ { @@ -370,7 +371,7 @@ func TestGitHubDownloadRepo(t *testing.T) { }, }, reviews) - reviews, err = downloader.GetReviews(&base.PullRequest{Number: 4, ForeignIndex: 4}) + reviews, err = downloader.GetReviews(ctx, &base.PullRequest{Number: 4, ForeignIndex: 4}) assert.NoError(t, err) assertReviewsEqual(t, []*base.Review{ { diff --git a/services/migrations/gitlab.go b/services/migrations/gitlab.go index 295bc7c29f5..efc5b960cfc 100644 --- a/services/migrations/gitlab.go +++ b/services/migrations/gitlab.go @@ -21,7 +21,7 @@ import ( base "code.gitea.io/gitea/modules/migration" "code.gitea.io/gitea/modules/structs" - "github.com/xanzy/go-gitlab" + gitlab "gitlab.com/gitlab-org/api/client-go" ) var ( @@ -80,7 +80,6 @@ func (r *gitlabIIDResolver) generatePullRequestNumber(mrIID int) int64 { // because Gitlab has individual Issue and Pull Request numbers. type GitlabDownloader struct { base.NullDownloader - ctx context.Context client *gitlab.Client baseURL string repoID int @@ -143,7 +142,6 @@ func NewGitlabDownloader(ctx context.Context, baseURL, repoPath, username, passw } return &GitlabDownloader{ - ctx: ctx, client: gitlabClient, baseURL: baseURL, repoID: gr.ID, @@ -164,14 +162,9 @@ func (g *GitlabDownloader) LogString() string { return fmt.Sprintf("", g.baseURL, g.repoID, g.repoName) } -// SetContext set context -func (g *GitlabDownloader) SetContext(ctx context.Context) { - g.ctx = ctx -} - // GetRepoInfo returns a repository information -func (g *GitlabDownloader) GetRepoInfo() (*base.Repository, error) { - gr, _, err := g.client.Projects.GetProject(g.repoID, nil, nil, gitlab.WithContext(g.ctx)) +func (g *GitlabDownloader) GetRepoInfo(ctx context.Context) (*base.Repository, error) { + gr, _, err := g.client.Projects.GetProject(g.repoID, nil, nil, gitlab.WithContext(ctx)) if err != nil { return nil, err } @@ -207,8 +200,8 @@ func (g *GitlabDownloader) GetRepoInfo() (*base.Repository, error) { } // GetTopics return gitlab topics -func (g *GitlabDownloader) GetTopics() ([]string, error) { - gr, _, err := g.client.Projects.GetProject(g.repoID, nil, nil, gitlab.WithContext(g.ctx)) +func (g *GitlabDownloader) GetTopics(ctx context.Context) ([]string, error) { + gr, _, err := g.client.Projects.GetProject(g.repoID, nil, nil, gitlab.WithContext(ctx)) if err != nil { return nil, err } @@ -216,7 +209,7 @@ func (g *GitlabDownloader) GetTopics() ([]string, error) { } // GetMilestones returns milestones -func (g *GitlabDownloader) GetMilestones() ([]*base.Milestone, error) { +func (g *GitlabDownloader) GetMilestones(ctx context.Context) ([]*base.Milestone, error) { perPage := g.maxPerPage state := "all" milestones := make([]*base.Milestone, 0, perPage) @@ -227,7 +220,7 @@ func (g *GitlabDownloader) GetMilestones() ([]*base.Milestone, error) { Page: i, PerPage: perPage, }, - }, nil, gitlab.WithContext(g.ctx)) + }, nil, gitlab.WithContext(ctx)) if err != nil { return nil, err } @@ -288,14 +281,14 @@ func (g *GitlabDownloader) normalizeColor(val string) string { } // GetLabels returns labels -func (g *GitlabDownloader) GetLabels() ([]*base.Label, error) { +func (g *GitlabDownloader) GetLabels(ctx context.Context) ([]*base.Label, error) { perPage := g.maxPerPage labels := make([]*base.Label, 0, perPage) for i := 1; ; i++ { ls, _, err := g.client.Labels.ListLabels(g.repoID, &gitlab.ListLabelsOptions{ListOptions: gitlab.ListOptions{ Page: i, PerPage: perPage, - }}, nil, gitlab.WithContext(g.ctx)) + }}, nil, gitlab.WithContext(ctx)) if err != nil { return nil, err } @@ -314,7 +307,7 @@ func (g *GitlabDownloader) GetLabels() ([]*base.Label, error) { return labels, nil } -func (g *GitlabDownloader) convertGitlabRelease(rel *gitlab.Release) *base.Release { +func (g *GitlabDownloader) convertGitlabRelease(ctx context.Context, rel *gitlab.Release) *base.Release { var zero int r := &base.Release{ TagName: rel.TagName, @@ -337,7 +330,7 @@ func (g *GitlabDownloader) convertGitlabRelease(rel *gitlab.Release) *base.Relea Size: &zero, DownloadCount: &zero, DownloadFunc: func() (io.ReadCloser, error) { - link, _, err := g.client.ReleaseLinks.GetReleaseLink(g.repoID, rel.TagName, assetID, gitlab.WithContext(g.ctx)) + link, _, err := g.client.ReleaseLinks.GetReleaseLink(g.repoID, rel.TagName, assetID, gitlab.WithContext(ctx)) if err != nil { return nil, err } @@ -351,7 +344,7 @@ func (g *GitlabDownloader) convertGitlabRelease(rel *gitlab.Release) *base.Relea if err != nil { return nil, err } - req = req.WithContext(g.ctx) + req = req.WithContext(ctx) resp, err := httpClient.Do(req) if err != nil { return nil, err @@ -366,7 +359,7 @@ func (g *GitlabDownloader) convertGitlabRelease(rel *gitlab.Release) *base.Relea } // GetReleases returns releases -func (g *GitlabDownloader) GetReleases() ([]*base.Release, error) { +func (g *GitlabDownloader) GetReleases(ctx context.Context) ([]*base.Release, error) { perPage := g.maxPerPage releases := make([]*base.Release, 0, perPage) for i := 1; ; i++ { @@ -375,13 +368,13 @@ func (g *GitlabDownloader) GetReleases() ([]*base.Release, error) { Page: i, PerPage: perPage, }, - }, nil, gitlab.WithContext(g.ctx)) + }, nil, gitlab.WithContext(ctx)) if err != nil { return nil, err } for _, release := range ls { - releases = append(releases, g.convertGitlabRelease(release)) + releases = append(releases, g.convertGitlabRelease(ctx, release)) } if len(ls) < perPage { break @@ -397,7 +390,7 @@ type gitlabIssueContext struct { // GetIssues returns issues according start and limit // // Note: issue label description and colors are not supported by the go-gitlab library at this time -func (g *GitlabDownloader) GetIssues(page, perPage int) ([]*base.Issue, bool, error) { +func (g *GitlabDownloader) GetIssues(ctx context.Context, page, perPage int) ([]*base.Issue, bool, error) { state := "all" sort := "asc" @@ -416,7 +409,7 @@ func (g *GitlabDownloader) GetIssues(page, perPage int) ([]*base.Issue, bool, er allIssues := make([]*base.Issue, 0, perPage) - issues, _, err := g.client.Issues.ListProjectIssues(g.repoID, opt, nil, gitlab.WithContext(g.ctx)) + issues, _, err := g.client.Issues.ListProjectIssues(g.repoID, opt, nil, gitlab.WithContext(ctx)) if err != nil { return nil, false, fmt.Errorf("error while listing issues: %w", err) } @@ -436,7 +429,7 @@ func (g *GitlabDownloader) GetIssues(page, perPage int) ([]*base.Issue, bool, er var reactions []*gitlab.AwardEmoji awardPage := 1 for { - awards, _, err := g.client.AwardEmoji.ListIssueAwardEmoji(g.repoID, issue.IID, &gitlab.ListAwardEmojiOptions{Page: awardPage, PerPage: perPage}, gitlab.WithContext(g.ctx)) + awards, _, err := g.client.AwardEmoji.ListIssueAwardEmoji(g.repoID, issue.IID, &gitlab.ListAwardEmojiOptions{Page: awardPage, PerPage: perPage}, gitlab.WithContext(ctx)) if err != nil { return nil, false, fmt.Errorf("error while listing issue awards: %w", err) } @@ -477,7 +470,7 @@ func (g *GitlabDownloader) GetIssues(page, perPage int) ([]*base.Issue, bool, er // GetComments returns comments according issueNumber // TODO: figure out how to transfer comment reactions -func (g *GitlabDownloader) GetComments(commentable base.Commentable) ([]*base.Comment, bool, error) { +func (g *GitlabDownloader) GetComments(ctx context.Context, commentable base.Commentable) ([]*base.Comment, bool, error) { context, ok := commentable.GetContext().(gitlabIssueContext) if !ok { return nil, false, fmt.Errorf("unexpected context: %+v", commentable.GetContext()) @@ -495,12 +488,12 @@ func (g *GitlabDownloader) GetComments(commentable base.Commentable) ([]*base.Co comments, resp, err = g.client.Discussions.ListIssueDiscussions(g.repoID, int(commentable.GetForeignIndex()), &gitlab.ListIssueDiscussionsOptions{ Page: page, PerPage: g.maxPerPage, - }, nil, gitlab.WithContext(g.ctx)) + }, nil, gitlab.WithContext(ctx)) } else { comments, resp, err = g.client.Discussions.ListMergeRequestDiscussions(g.repoID, int(commentable.GetForeignIndex()), &gitlab.ListMergeRequestDiscussionsOptions{ Page: page, PerPage: g.maxPerPage, - }, nil, gitlab.WithContext(g.ctx)) + }, nil, gitlab.WithContext(ctx)) } if err != nil { @@ -528,14 +521,14 @@ func (g *GitlabDownloader) GetComments(commentable base.Commentable) ([]*base.Co Page: page, PerPage: g.maxPerPage, }, - }, nil, gitlab.WithContext(g.ctx)) + }, nil, gitlab.WithContext(ctx)) } else { stateEvents, resp, err = g.client.ResourceStateEvents.ListIssueStateEvents(g.repoID, int(commentable.GetForeignIndex()), &gitlab.ListStateEventsOptions{ ListOptions: gitlab.ListOptions{ Page: page, PerPage: g.maxPerPage, }, - }, nil, gitlab.WithContext(g.ctx)) + }, nil, gitlab.WithContext(ctx)) } if err != nil { return nil, false, fmt.Errorf("error while listing state events: %v %w", g.repoID, err) @@ -604,7 +597,7 @@ func (g *GitlabDownloader) convertNoteToComment(localIndex int64, note *gitlab.N } // GetPullRequests returns pull requests according page and perPage -func (g *GitlabDownloader) GetPullRequests(page, perPage int) ([]*base.PullRequest, bool, error) { +func (g *GitlabDownloader) GetPullRequests(ctx context.Context, page, perPage int) ([]*base.PullRequest, bool, error) { if perPage > g.maxPerPage { perPage = g.maxPerPage } @@ -620,7 +613,7 @@ func (g *GitlabDownloader) GetPullRequests(page, perPage int) ([]*base.PullReque allPRs := make([]*base.PullRequest, 0, perPage) - prs, _, err := g.client.MergeRequests.ListProjectMergeRequests(g.repoID, opt, nil, gitlab.WithContext(g.ctx)) + prs, _, err := g.client.MergeRequests.ListProjectMergeRequests(g.repoID, opt, nil, gitlab.WithContext(ctx)) if err != nil { return nil, false, fmt.Errorf("error while listing merge requests: %w", err) } @@ -673,7 +666,7 @@ func (g *GitlabDownloader) GetPullRequests(page, perPage int) ([]*base.PullReque var reactions []*gitlab.AwardEmoji awardPage := 1 for { - awards, _, err := g.client.AwardEmoji.ListMergeRequestAwardEmoji(g.repoID, pr.IID, &gitlab.ListAwardEmojiOptions{Page: awardPage, PerPage: perPage}, gitlab.WithContext(g.ctx)) + awards, _, err := g.client.AwardEmoji.ListMergeRequestAwardEmoji(g.repoID, pr.IID, &gitlab.ListAwardEmojiOptions{Page: awardPage, PerPage: perPage}, gitlab.WithContext(ctx)) if err != nil { return nil, false, fmt.Errorf("error while listing merge requests awards: %w", err) } @@ -733,8 +726,8 @@ func (g *GitlabDownloader) GetPullRequests(page, perPage int) ([]*base.PullReque } // GetReviews returns pull requests review -func (g *GitlabDownloader) GetReviews(reviewable base.Reviewable) ([]*base.Review, error) { - approvals, resp, err := g.client.MergeRequestApprovals.GetConfiguration(g.repoID, int(reviewable.GetForeignIndex()), gitlab.WithContext(g.ctx)) +func (g *GitlabDownloader) GetReviews(ctx context.Context, reviewable base.Reviewable) ([]*base.Review, error) { + approvals, resp, err := g.client.MergeRequestApprovals.GetConfiguration(g.repoID, int(reviewable.GetForeignIndex()), gitlab.WithContext(ctx)) if err != nil { if resp != nil && resp.StatusCode == http.StatusNotFound { log.Error(fmt.Sprintf("GitlabDownloader: while migrating a error occurred: '%s'", err.Error())) diff --git a/services/migrations/gitlab_test.go b/services/migrations/gitlab_test.go index eccfc4def1f..223a3b86d76 100644 --- a/services/migrations/gitlab_test.go +++ b/services/migrations/gitlab_test.go @@ -17,7 +17,7 @@ import ( base "code.gitea.io/gitea/modules/migration" "github.com/stretchr/testify/assert" - "github.com/xanzy/go-gitlab" + gitlab "gitlab.com/gitlab-org/api/client-go" ) func TestGitlabDownloadRepo(t *testing.T) { @@ -31,12 +31,12 @@ func TestGitlabDownloadRepo(t *testing.T) { if err != nil || resp.StatusCode != http.StatusOK { t.Skipf("Can't access test repo, skipping %s", t.Name()) } - - downloader, err := NewGitlabDownloader(context.Background(), "https://gitlab.com", "gitea/test_repo", "", "", gitlabPersonalAccessToken) + ctx := context.Background() + downloader, err := NewGitlabDownloader(ctx, "https://gitlab.com", "gitea/test_repo", "", "", gitlabPersonalAccessToken) if err != nil { t.Fatalf("NewGitlabDownloader is nil: %v", err) } - repo, err := downloader.GetRepoInfo() + repo, err := downloader.GetRepoInfo(ctx) assert.NoError(t, err) // Repo Owner is blank in Gitlab Group repos assertRepositoryEqual(t, &base.Repository{ @@ -48,12 +48,12 @@ func TestGitlabDownloadRepo(t *testing.T) { DefaultBranch: "master", }, repo) - topics, err := downloader.GetTopics() + topics, err := downloader.GetTopics(ctx) assert.NoError(t, err) assert.Len(t, topics, 2) assert.EqualValues(t, []string{"migration", "test"}, topics) - milestones, err := downloader.GetMilestones() + milestones, err := downloader.GetMilestones(ctx) assert.NoError(t, err) assertMilestonesEqual(t, []*base.Milestone{ { @@ -71,7 +71,7 @@ func TestGitlabDownloadRepo(t *testing.T) { }, }, milestones) - labels, err := downloader.GetLabels() + labels, err := downloader.GetLabels(ctx) assert.NoError(t, err) assertLabelsEqual(t, []*base.Label{ { @@ -112,7 +112,7 @@ func TestGitlabDownloadRepo(t *testing.T) { }, }, labels) - releases, err := downloader.GetReleases() + releases, err := downloader.GetReleases(ctx) assert.NoError(t, err) assertReleasesEqual(t, []*base.Release{ { @@ -126,7 +126,7 @@ func TestGitlabDownloadRepo(t *testing.T) { }, }, releases) - issues, isEnd, err := downloader.GetIssues(1, 2) + issues, isEnd, err := downloader.GetIssues(ctx, 1, 2) assert.NoError(t, err) assert.False(t, isEnd) @@ -214,7 +214,7 @@ func TestGitlabDownloadRepo(t *testing.T) { }, }, issues) - comments, _, err := downloader.GetComments(&base.Issue{ + comments, _, err := downloader.GetComments(ctx, &base.Issue{ Number: 2, ForeignIndex: 2, Context: gitlabIssueContext{IsMergeRequest: false}, @@ -255,7 +255,7 @@ func TestGitlabDownloadRepo(t *testing.T) { }, }, comments) - prs, _, err := downloader.GetPullRequests(1, 1) + prs, _, err := downloader.GetPullRequests(ctx, 1, 1) assert.NoError(t, err) assertPullRequestsEqual(t, []*base.PullRequest{ { @@ -304,7 +304,7 @@ func TestGitlabDownloadRepo(t *testing.T) { }, }, prs) - rvs, err := downloader.GetReviews(&base.PullRequest{Number: 1, ForeignIndex: 1}) + rvs, err := downloader.GetReviews(ctx, &base.PullRequest{Number: 1, ForeignIndex: 1}) assert.NoError(t, err) assertReviewsEqual(t, []*base.Review{ { @@ -323,7 +323,7 @@ func TestGitlabDownloadRepo(t *testing.T) { }, }, rvs) - rvs, err = downloader.GetReviews(&base.PullRequest{Number: 2, ForeignIndex: 2}) + rvs, err = downloader.GetReviews(ctx, &base.PullRequest{Number: 2, ForeignIndex: 2}) assert.NoError(t, err) assertReviewsEqual(t, []*base.Review{ { @@ -423,9 +423,8 @@ func TestGitlabGetReviews(t *testing.T) { defer gitlabClientMockTeardown(server) repoID := 1324 - + ctx := context.Background() downloader := &GitlabDownloader{ - ctx: context.Background(), client: client, repoID: repoID, } @@ -465,7 +464,7 @@ func TestGitlabGetReviews(t *testing.T) { mux.HandleFunc(fmt.Sprintf("/api/v4/projects/%d/merge_requests/%d/approvals", testCase.repoID, testCase.prID), mock) id := int64(testCase.prID) - rvs, err := downloader.GetReviews(&base.Issue{Number: id, ForeignIndex: id}) + rvs, err := downloader.GetReviews(ctx, &base.Issue{Number: id, ForeignIndex: id}) assert.NoError(t, err) assertReviewsEqual(t, []*base.Review{&review}, rvs) } diff --git a/services/migrations/gogs.go b/services/migrations/gogs.go index 72c52d180b9..a4f84dbf72c 100644 --- a/services/migrations/gogs.go +++ b/services/migrations/gogs.go @@ -13,7 +13,6 @@ import ( "code.gitea.io/gitea/modules/log" base "code.gitea.io/gitea/modules/migration" - "code.gitea.io/gitea/modules/proxy" "code.gitea.io/gitea/modules/structs" "github.com/gogs/go-gogs-client" @@ -60,16 +59,14 @@ func (f *GogsDownloaderFactory) GitServiceType() structs.GitServiceType { // from gogs via API type GogsDownloader struct { base.NullDownloader - ctx context.Context - client *gogs.Client baseURL string repoOwner string repoName string userName string password string + token string openIssuesFinished bool openIssuesPages int - transport http.RoundTripper } // String implements Stringer @@ -84,53 +81,45 @@ func (g *GogsDownloader) LogString() string { return fmt.Sprintf("", g.baseURL, g.repoOwner, g.repoName) } -// SetContext set context -func (g *GogsDownloader) SetContext(ctx context.Context) { - g.ctx = ctx -} - // NewGogsDownloader creates a gogs Downloader via gogs API -func NewGogsDownloader(ctx context.Context, baseURL, userName, password, token, repoOwner, repoName string) *GogsDownloader { +func NewGogsDownloader(_ context.Context, baseURL, userName, password, token, repoOwner, repoName string) *GogsDownloader { downloader := GogsDownloader{ - ctx: ctx, baseURL: baseURL, userName: userName, password: password, + token: token, repoOwner: repoOwner, repoName: repoName, } - - var client *gogs.Client - if len(token) != 0 { - client = gogs.NewClient(baseURL, token) - downloader.userName = token - } else { - transport := NewMigrationHTTPTransport() - transport.Proxy = func(req *http.Request) (*url.URL, error) { - req.SetBasicAuth(userName, password) - return proxy.Proxy()(req) - } - downloader.transport = transport - - client = gogs.NewClient(baseURL, "") - client.SetHTTPClient(&http.Client{ - Transport: &downloader, - }) - } - - downloader.client = client return &downloader } -// RoundTrip wraps the provided request within this downloader's context and passes it to our internal http.Transport. -// This implements http.RoundTripper and makes the gogs client requests cancellable even though it is not cancellable itself -func (g *GogsDownloader) RoundTrip(req *http.Request) (*http.Response, error) { - return g.transport.RoundTrip(req.WithContext(g.ctx)) +type roundTripperFunc func(req *http.Request) (*http.Response, error) + +func (rt roundTripperFunc) RoundTrip(r *http.Request) (*http.Response, error) { + return rt(r) +} + +func (g *GogsDownloader) client(ctx context.Context) *gogs.Client { + // Gogs client lacks the context support, so we use a custom transport + // Then each request uses a dedicated client with its own context + httpTransport := NewMigrationHTTPTransport() + gogsClient := gogs.NewClient(g.baseURL, g.token) + gogsClient.SetHTTPClient(&http.Client{ + Transport: roundTripperFunc(func(req *http.Request) (*http.Response, error) { + if g.password != "" { + // Gogs client lacks the support for basic auth, this is the only way to set it + req.SetBasicAuth(g.userName, g.password) + } + return httpTransport.RoundTrip(req.WithContext(ctx)) + }), + }) + return gogsClient } // GetRepoInfo returns a repository information -func (g *GogsDownloader) GetRepoInfo() (*base.Repository, error) { - gr, err := g.client.GetRepo(g.repoOwner, g.repoName) +func (g *GogsDownloader) GetRepoInfo(ctx context.Context) (*base.Repository, error) { + gr, err := g.client(ctx).GetRepo(g.repoOwner, g.repoName) if err != nil { return nil, err } @@ -148,11 +137,11 @@ func (g *GogsDownloader) GetRepoInfo() (*base.Repository, error) { } // GetMilestones returns milestones -func (g *GogsDownloader) GetMilestones() ([]*base.Milestone, error) { +func (g *GogsDownloader) GetMilestones(ctx context.Context) ([]*base.Milestone, error) { perPage := 100 milestones := make([]*base.Milestone, 0, perPage) - ms, err := g.client.ListRepoMilestones(g.repoOwner, g.repoName) + ms, err := g.client(ctx).ListRepoMilestones(g.repoOwner, g.repoName) if err != nil { return nil, err } @@ -171,10 +160,10 @@ func (g *GogsDownloader) GetMilestones() ([]*base.Milestone, error) { } // GetLabels returns labels -func (g *GogsDownloader) GetLabels() ([]*base.Label, error) { +func (g *GogsDownloader) GetLabels(ctx context.Context) ([]*base.Label, error) { perPage := 100 labels := make([]*base.Label, 0, perPage) - ls, err := g.client.ListRepoLabels(g.repoOwner, g.repoName) + ls, err := g.client(ctx).ListRepoLabels(g.repoOwner, g.repoName) if err != nil { return nil, err } @@ -187,7 +176,7 @@ func (g *GogsDownloader) GetLabels() ([]*base.Label, error) { } // GetIssues returns issues according start and limit, perPage is not supported -func (g *GogsDownloader) GetIssues(page, _ int) ([]*base.Issue, bool, error) { +func (g *GogsDownloader) GetIssues(ctx context.Context, page, _ int) ([]*base.Issue, bool, error) { var state string if g.openIssuesFinished { state = string(gogs.STATE_CLOSED) @@ -197,7 +186,7 @@ func (g *GogsDownloader) GetIssues(page, _ int) ([]*base.Issue, bool, error) { g.openIssuesPages = page } - issues, isEnd, err := g.getIssues(page, state) + issues, isEnd, err := g.getIssues(ctx, page, state) if err != nil { return nil, false, err } @@ -212,10 +201,10 @@ func (g *GogsDownloader) GetIssues(page, _ int) ([]*base.Issue, bool, error) { return issues, false, nil } -func (g *GogsDownloader) getIssues(page int, state string) ([]*base.Issue, bool, error) { +func (g *GogsDownloader) getIssues(ctx context.Context, page int, state string) ([]*base.Issue, bool, error) { allIssues := make([]*base.Issue, 0, 10) - issues, err := g.client.ListRepoIssues(g.repoOwner, g.repoName, gogs.ListIssueOption{ + issues, err := g.client(ctx).ListRepoIssues(g.repoOwner, g.repoName, gogs.ListIssueOption{ Page: page, State: state, }) @@ -234,10 +223,10 @@ func (g *GogsDownloader) getIssues(page int, state string) ([]*base.Issue, bool, } // GetComments returns comments according issueNumber -func (g *GogsDownloader) GetComments(commentable base.Commentable) ([]*base.Comment, bool, error) { +func (g *GogsDownloader) GetComments(ctx context.Context, commentable base.Commentable) ([]*base.Comment, bool, error) { allComments := make([]*base.Comment, 0, 100) - comments, err := g.client.ListIssueComments(g.repoOwner, g.repoName, commentable.GetForeignIndex()) + comments, err := g.client(ctx).ListIssueComments(g.repoOwner, g.repoName, commentable.GetForeignIndex()) if err != nil { return nil, false, fmt.Errorf("error while listing repos: %w", err) } @@ -261,7 +250,7 @@ func (g *GogsDownloader) GetComments(commentable base.Commentable) ([]*base.Comm } // GetTopics return repository topics -func (g *GogsDownloader) GetTopics() ([]string, error) { +func (g *GogsDownloader) GetTopics(_ context.Context) ([]string, error) { return []string{}, nil } diff --git a/services/migrations/gogs_test.go b/services/migrations/gogs_test.go index 610af183de2..91c36bdcc6d 100644 --- a/services/migrations/gogs_test.go +++ b/services/migrations/gogs_test.go @@ -28,9 +28,9 @@ func TestGogsDownloadRepo(t *testing.T) { t.Skipf("visit test repo failed, ignored") return } - - downloader := NewGogsDownloader(context.Background(), "https://try.gogs.io", "", "", gogsPersonalAccessToken, "lunnytest", "TESTREPO") - repo, err := downloader.GetRepoInfo() + ctx := context.Background() + downloader := NewGogsDownloader(ctx, "https://try.gogs.io", "", "", gogsPersonalAccessToken, "lunnytest", "TESTREPO") + repo, err := downloader.GetRepoInfo(ctx) assert.NoError(t, err) assertRepositoryEqual(t, &base.Repository{ @@ -42,7 +42,7 @@ func TestGogsDownloadRepo(t *testing.T) { DefaultBranch: "master", }, repo) - milestones, err := downloader.GetMilestones() + milestones, err := downloader.GetMilestones(ctx) assert.NoError(t, err) assertMilestonesEqual(t, []*base.Milestone{ { @@ -51,7 +51,7 @@ func TestGogsDownloadRepo(t *testing.T) { }, }, milestones) - labels, err := downloader.GetLabels() + labels, err := downloader.GetLabels(ctx) assert.NoError(t, err) assertLabelsEqual(t, []*base.Label{ { @@ -85,7 +85,7 @@ func TestGogsDownloadRepo(t *testing.T) { }, labels) // downloader.GetIssues() - issues, isEnd, err := downloader.GetIssues(1, 8) + issues, isEnd, err := downloader.GetIssues(ctx, 1, 8) assert.NoError(t, err) assert.False(t, isEnd) assertIssuesEqual(t, []*base.Issue{ @@ -110,7 +110,7 @@ func TestGogsDownloadRepo(t *testing.T) { }, issues) // downloader.GetComments() - comments, _, err := downloader.GetComments(&base.Issue{Number: 1, ForeignIndex: 1}) + comments, _, err := downloader.GetComments(ctx, &base.Issue{Number: 1, ForeignIndex: 1}) assert.NoError(t, err) assertCommentsEqual(t, []*base.Comment{ { @@ -134,6 +134,6 @@ func TestGogsDownloadRepo(t *testing.T) { }, comments) // downloader.GetPullRequests() - _, _, err = downloader.GetPullRequests(1, 3) + _, _, err = downloader.GetPullRequests(ctx, 1, 3) assert.Error(t, err) } diff --git a/services/migrations/migrate.go b/services/migrations/migrate.go index 51b22d6111b..8319fd541b5 100644 --- a/services/migrations/migrate.go +++ b/services/migrations/migrate.go @@ -176,12 +176,12 @@ func newDownloader(ctx context.Context, ownerName string, opts base.MigrateOptio // migrateRepository will download information and then upload it to Uploader, this is a simple // process for small repository. For a big repository, save all the data to disk // before upload is better -func migrateRepository(_ context.Context, doer *user_model.User, downloader base.Downloader, uploader base.Uploader, opts base.MigrateOptions, messenger base.Messenger) error { +func migrateRepository(ctx context.Context, doer *user_model.User, downloader base.Downloader, uploader base.Uploader, opts base.MigrateOptions, messenger base.Messenger) error { if messenger == nil { messenger = base.NilMessenger } - repo, err := downloader.GetRepoInfo() + repo, err := downloader.GetRepoInfo(ctx) if err != nil { if !base.IsErrNotSupported(err) { return err @@ -220,14 +220,14 @@ func migrateRepository(_ context.Context, doer *user_model.User, downloader base log.Trace("migrating git data from %s", repo.CloneURL) messenger("repo.migrate.migrating_git") - if err = uploader.CreateRepo(repo, opts); err != nil { + if err = uploader.CreateRepo(ctx, repo, opts); err != nil { return err } defer uploader.Close() log.Trace("migrating topics") messenger("repo.migrate.migrating_topics") - topics, err := downloader.GetTopics() + topics, err := downloader.GetTopics(ctx) if err != nil { if !base.IsErrNotSupported(err) { return err @@ -235,7 +235,7 @@ func migrateRepository(_ context.Context, doer *user_model.User, downloader base log.Warn("migrating topics is not supported, ignored") } if len(topics) != 0 { - if err = uploader.CreateTopics(topics...); err != nil { + if err = uploader.CreateTopics(ctx, topics...); err != nil { return err } } @@ -243,7 +243,7 @@ func migrateRepository(_ context.Context, doer *user_model.User, downloader base if opts.Milestones { log.Trace("migrating milestones") messenger("repo.migrate.migrating_milestones") - milestones, err := downloader.GetMilestones() + milestones, err := downloader.GetMilestones(ctx) if err != nil { if !base.IsErrNotSupported(err) { return err @@ -256,7 +256,7 @@ func migrateRepository(_ context.Context, doer *user_model.User, downloader base msBatchSize = len(milestones) } - if err := uploader.CreateMilestones(milestones[:msBatchSize]...); err != nil { + if err := uploader.CreateMilestones(ctx, milestones[:msBatchSize]...); err != nil { return err } milestones = milestones[msBatchSize:] @@ -266,7 +266,7 @@ func migrateRepository(_ context.Context, doer *user_model.User, downloader base if opts.Labels { log.Trace("migrating labels") messenger("repo.migrate.migrating_labels") - labels, err := downloader.GetLabels() + labels, err := downloader.GetLabels(ctx) if err != nil { if !base.IsErrNotSupported(err) { return err @@ -280,7 +280,7 @@ func migrateRepository(_ context.Context, doer *user_model.User, downloader base lbBatchSize = len(labels) } - if err := uploader.CreateLabels(labels[:lbBatchSize]...); err != nil { + if err := uploader.CreateLabels(ctx, labels[:lbBatchSize]...); err != nil { return err } labels = labels[lbBatchSize:] @@ -290,7 +290,7 @@ func migrateRepository(_ context.Context, doer *user_model.User, downloader base if opts.Releases { log.Trace("migrating releases") messenger("repo.migrate.migrating_releases") - releases, err := downloader.GetReleases() + releases, err := downloader.GetReleases(ctx) if err != nil { if !base.IsErrNotSupported(err) { return err @@ -304,14 +304,14 @@ func migrateRepository(_ context.Context, doer *user_model.User, downloader base relBatchSize = len(releases) } - if err = uploader.CreateReleases(releases[:relBatchSize]...); err != nil { + if err = uploader.CreateReleases(ctx, releases[:relBatchSize]...); err != nil { return err } releases = releases[relBatchSize:] } // Once all releases (if any) are inserted, sync any remaining non-release tags - if err = uploader.SyncTags(); err != nil { + if err = uploader.SyncTags(ctx); err != nil { return err } } @@ -329,7 +329,7 @@ func migrateRepository(_ context.Context, doer *user_model.User, downloader base issueBatchSize := uploader.MaxBatchInsertSize("issue") for i := 1; ; i++ { - issues, isEnd, err := downloader.GetIssues(i, issueBatchSize) + issues, isEnd, err := downloader.GetIssues(ctx, i, issueBatchSize) if err != nil { if !base.IsErrNotSupported(err) { return err @@ -338,7 +338,7 @@ func migrateRepository(_ context.Context, doer *user_model.User, downloader base break } - if err := uploader.CreateIssues(issues...); err != nil { + if err := uploader.CreateIssues(ctx, issues...); err != nil { return err } @@ -346,7 +346,7 @@ func migrateRepository(_ context.Context, doer *user_model.User, downloader base allComments := make([]*base.Comment, 0, commentBatchSize) for _, issue := range issues { log.Trace("migrating issue %d's comments", issue.Number) - comments, _, err := downloader.GetComments(issue) + comments, _, err := downloader.GetComments(ctx, issue) if err != nil { if !base.IsErrNotSupported(err) { return err @@ -357,7 +357,7 @@ func migrateRepository(_ context.Context, doer *user_model.User, downloader base allComments = append(allComments, comments...) if len(allComments) >= commentBatchSize { - if err = uploader.CreateComments(allComments[:commentBatchSize]...); err != nil { + if err = uploader.CreateComments(ctx, allComments[:commentBatchSize]...); err != nil { return err } @@ -366,7 +366,7 @@ func migrateRepository(_ context.Context, doer *user_model.User, downloader base } if len(allComments) > 0 { - if err = uploader.CreateComments(allComments...); err != nil { + if err = uploader.CreateComments(ctx, allComments...); err != nil { return err } } @@ -383,7 +383,7 @@ func migrateRepository(_ context.Context, doer *user_model.User, downloader base messenger("repo.migrate.migrating_pulls") prBatchSize := uploader.MaxBatchInsertSize("pullrequest") for i := 1; ; i++ { - prs, isEnd, err := downloader.GetPullRequests(i, prBatchSize) + prs, isEnd, err := downloader.GetPullRequests(ctx, i, prBatchSize) if err != nil { if !base.IsErrNotSupported(err) { return err @@ -392,7 +392,7 @@ func migrateRepository(_ context.Context, doer *user_model.User, downloader base break } - if err := uploader.CreatePullRequests(prs...); err != nil { + if err := uploader.CreatePullRequests(ctx, prs...); err != nil { return err } @@ -402,7 +402,7 @@ func migrateRepository(_ context.Context, doer *user_model.User, downloader base allComments := make([]*base.Comment, 0, commentBatchSize) for _, pr := range prs { log.Trace("migrating pull request %d's comments", pr.Number) - comments, _, err := downloader.GetComments(pr) + comments, _, err := downloader.GetComments(ctx, pr) if err != nil { if !base.IsErrNotSupported(err) { return err @@ -413,14 +413,14 @@ func migrateRepository(_ context.Context, doer *user_model.User, downloader base allComments = append(allComments, comments...) if len(allComments) >= commentBatchSize { - if err = uploader.CreateComments(allComments[:commentBatchSize]...); err != nil { + if err = uploader.CreateComments(ctx, allComments[:commentBatchSize]...); err != nil { return err } allComments = allComments[commentBatchSize:] } } if len(allComments) > 0 { - if err = uploader.CreateComments(allComments...); err != nil { + if err = uploader.CreateComments(ctx, allComments...); err != nil { return err } } @@ -429,7 +429,7 @@ func migrateRepository(_ context.Context, doer *user_model.User, downloader base // migrate reviews allReviews := make([]*base.Review, 0, reviewBatchSize) for _, pr := range prs { - reviews, err := downloader.GetReviews(pr) + reviews, err := downloader.GetReviews(ctx, pr) if err != nil { if !base.IsErrNotSupported(err) { return err @@ -441,14 +441,14 @@ func migrateRepository(_ context.Context, doer *user_model.User, downloader base allReviews = append(allReviews, reviews...) if len(allReviews) >= reviewBatchSize { - if err = uploader.CreateReviews(allReviews[:reviewBatchSize]...); err != nil { + if err = uploader.CreateReviews(ctx, allReviews[:reviewBatchSize]...); err != nil { return err } allReviews = allReviews[reviewBatchSize:] } } if len(allReviews) > 0 { - if err = uploader.CreateReviews(allReviews...); err != nil { + if err = uploader.CreateReviews(ctx, allReviews...); err != nil { return err } } @@ -463,12 +463,12 @@ func migrateRepository(_ context.Context, doer *user_model.User, downloader base if opts.Comments && supportAllComments { log.Trace("migrating comments") for i := 1; ; i++ { - comments, isEnd, err := downloader.GetAllComments(i, commentBatchSize) + comments, isEnd, err := downloader.GetAllComments(ctx, i, commentBatchSize) if err != nil { return err } - if err := uploader.CreateComments(comments...); err != nil { + if err := uploader.CreateComments(ctx, comments...); err != nil { return err } @@ -478,7 +478,7 @@ func migrateRepository(_ context.Context, doer *user_model.User, downloader base } } - return uploader.Finish() + return uploader.Finish(ctx) } // Init migrations service diff --git a/services/migrations/onedev.go b/services/migrations/onedev.go index e2f7b771f36..4ce35dd12e7 100644 --- a/services/migrations/onedev.go +++ b/services/migrations/onedev.go @@ -71,7 +71,6 @@ type onedevUser struct { // from OneDev type OneDevDownloader struct { base.NullDownloader - ctx context.Context client *http.Client baseURL *url.URL repoName string @@ -81,15 +80,9 @@ type OneDevDownloader struct { milestoneMap map[int64]string } -// SetContext set context -func (d *OneDevDownloader) SetContext(ctx context.Context) { - d.ctx = ctx -} - // NewOneDevDownloader creates a new downloader -func NewOneDevDownloader(ctx context.Context, baseURL *url.URL, username, password, repoName string) *OneDevDownloader { +func NewOneDevDownloader(_ context.Context, baseURL *url.URL, username, password, repoName string) *OneDevDownloader { downloader := &OneDevDownloader{ - ctx: ctx, baseURL: baseURL, repoName: repoName, client: &http.Client{ @@ -121,7 +114,7 @@ func (d *OneDevDownloader) LogString() string { return fmt.Sprintf("", d.baseURL, d.repoID, d.repoName) } -func (d *OneDevDownloader) callAPI(endpoint string, parameter map[string]string, result any) error { +func (d *OneDevDownloader) callAPI(ctx context.Context, endpoint string, parameter map[string]string, result any) error { u, err := d.baseURL.Parse(endpoint) if err != nil { return err @@ -135,7 +128,7 @@ func (d *OneDevDownloader) callAPI(endpoint string, parameter map[string]string, u.RawQuery = query.Encode() } - req, err := http.NewRequestWithContext(d.ctx, "GET", u.String(), nil) + req, err := http.NewRequestWithContext(ctx, "GET", u.String(), nil) if err != nil { return err } @@ -151,7 +144,7 @@ func (d *OneDevDownloader) callAPI(endpoint string, parameter map[string]string, } // GetRepoInfo returns repository information -func (d *OneDevDownloader) GetRepoInfo() (*base.Repository, error) { +func (d *OneDevDownloader) GetRepoInfo(ctx context.Context) (*base.Repository, error) { info := make([]struct { ID int64 `json:"id"` Name string `json:"name"` @@ -159,6 +152,7 @@ func (d *OneDevDownloader) GetRepoInfo() (*base.Repository, error) { }, 0, 1) err := d.callAPI( + ctx, "/api/projects", map[string]string{ "query": `"Name" is "` + d.repoName + `"`, @@ -194,7 +188,7 @@ func (d *OneDevDownloader) GetRepoInfo() (*base.Repository, error) { } // GetMilestones returns milestones -func (d *OneDevDownloader) GetMilestones() ([]*base.Milestone, error) { +func (d *OneDevDownloader) GetMilestones(ctx context.Context) ([]*base.Milestone, error) { rawMilestones := make([]struct { ID int64 `json:"id"` Name string `json:"name"` @@ -209,6 +203,7 @@ func (d *OneDevDownloader) GetMilestones() ([]*base.Milestone, error) { offset := 0 for { err := d.callAPI( + ctx, endpoint, map[string]string{ "offset": strconv.Itoa(offset), @@ -243,7 +238,7 @@ func (d *OneDevDownloader) GetMilestones() ([]*base.Milestone, error) { } // GetLabels returns labels -func (d *OneDevDownloader) GetLabels() ([]*base.Label, error) { +func (d *OneDevDownloader) GetLabels(_ context.Context) ([]*base.Label, error) { return []*base.Label{ { Name: "Bug", @@ -277,7 +272,7 @@ type onedevIssueContext struct { } // GetIssues returns issues -func (d *OneDevDownloader) GetIssues(page, perPage int) ([]*base.Issue, bool, error) { +func (d *OneDevDownloader) GetIssues(ctx context.Context, page, perPage int) ([]*base.Issue, bool, error) { rawIssues := make([]struct { ID int64 `json:"id"` Number int64 `json:"number"` @@ -289,6 +284,7 @@ func (d *OneDevDownloader) GetIssues(page, perPage int) ([]*base.Issue, bool, er }, 0, perPage) err := d.callAPI( + ctx, "/api/issues", map[string]string{ "query": `"Project" is "` + d.repoName + `"`, @@ -308,6 +304,7 @@ func (d *OneDevDownloader) GetIssues(page, perPage int) ([]*base.Issue, bool, er Value string `json:"value"` }, 0, 10) err := d.callAPI( + ctx, fmt.Sprintf("/api/issues/%d/fields", issue.ID), nil, &fields, @@ -329,6 +326,7 @@ func (d *OneDevDownloader) GetIssues(page, perPage int) ([]*base.Issue, bool, er Name string `json:"name"` }, 0, 10) err = d.callAPI( + ctx, fmt.Sprintf("/api/issues/%d/milestones", issue.ID), nil, &milestones, @@ -345,7 +343,7 @@ func (d *OneDevDownloader) GetIssues(page, perPage int) ([]*base.Issue, bool, er if state == "released" { state = "closed" } - poster := d.tryGetUser(issue.SubmitterID) + poster := d.tryGetUser(ctx, issue.SubmitterID) issues = append(issues, &base.Issue{ Title: issue.Title, Number: issue.Number, @@ -370,7 +368,7 @@ func (d *OneDevDownloader) GetIssues(page, perPage int) ([]*base.Issue, bool, er } // GetComments returns comments -func (d *OneDevDownloader) GetComments(commentable base.Commentable) ([]*base.Comment, bool, error) { +func (d *OneDevDownloader) GetComments(ctx context.Context, commentable base.Commentable) ([]*base.Comment, bool, error) { context, ok := commentable.GetContext().(onedevIssueContext) if !ok { return nil, false, fmt.Errorf("unexpected context: %+v", commentable.GetContext()) @@ -391,6 +389,7 @@ func (d *OneDevDownloader) GetComments(commentable base.Commentable) ([]*base.Co } err := d.callAPI( + ctx, endpoint, nil, &rawComments, @@ -412,6 +411,7 @@ func (d *OneDevDownloader) GetComments(commentable base.Commentable) ([]*base.Co } err = d.callAPI( + ctx, endpoint, nil, &rawChanges, @@ -425,7 +425,7 @@ func (d *OneDevDownloader) GetComments(commentable base.Commentable) ([]*base.Co if len(comment.Content) == 0 { continue } - poster := d.tryGetUser(comment.UserID) + poster := d.tryGetUser(ctx, comment.UserID) comments = append(comments, &base.Comment{ IssueIndex: commentable.GetLocalIndex(), Index: comment.ID, @@ -450,7 +450,7 @@ func (d *OneDevDownloader) GetComments(commentable base.Commentable) ([]*base.Co continue } - poster := d.tryGetUser(change.UserID) + poster := d.tryGetUser(ctx, change.UserID) comments = append(comments, &base.Comment{ IssueIndex: commentable.GetLocalIndex(), PosterID: poster.ID, @@ -466,7 +466,7 @@ func (d *OneDevDownloader) GetComments(commentable base.Commentable) ([]*base.Co } // GetPullRequests returns pull requests -func (d *OneDevDownloader) GetPullRequests(page, perPage int) ([]*base.PullRequest, bool, error) { +func (d *OneDevDownloader) GetPullRequests(ctx context.Context, page, perPage int) ([]*base.PullRequest, bool, error) { rawPullRequests := make([]struct { ID int64 `json:"id"` Number int64 `json:"number"` @@ -484,6 +484,7 @@ func (d *OneDevDownloader) GetPullRequests(page, perPage int) ([]*base.PullReque }, 0, perPage) err := d.callAPI( + ctx, "/api/pull-requests", map[string]string{ "query": `"Target Project" is "` + d.repoName + `"`, @@ -505,6 +506,7 @@ func (d *OneDevDownloader) GetPullRequests(page, perPage int) ([]*base.PullReque MergeCommitHash string `json:"mergeCommitHash"` } err := d.callAPI( + ctx, fmt.Sprintf("/api/pull-requests/%d/merge-preview", pr.ID), nil, &mergePreview, @@ -525,7 +527,7 @@ func (d *OneDevDownloader) GetPullRequests(page, perPage int) ([]*base.PullReque mergedTime = pr.CloseInfo.Date } } - poster := d.tryGetUser(pr.SubmitterID) + poster := d.tryGetUser(ctx, pr.SubmitterID) number := pr.Number + d.maxIssueIndex pullRequests = append(pullRequests, &base.PullRequest{ @@ -562,7 +564,7 @@ func (d *OneDevDownloader) GetPullRequests(page, perPage int) ([]*base.PullReque } // GetReviews returns pull requests reviews -func (d *OneDevDownloader) GetReviews(reviewable base.Reviewable) ([]*base.Review, error) { +func (d *OneDevDownloader) GetReviews(ctx context.Context, reviewable base.Reviewable) ([]*base.Review, error) { rawReviews := make([]struct { ID int64 `json:"id"` UserID int64 `json:"userId"` @@ -574,6 +576,7 @@ func (d *OneDevDownloader) GetReviews(reviewable base.Reviewable) ([]*base.Revie }, 0, 100) err := d.callAPI( + ctx, fmt.Sprintf("/api/pull-requests/%d/reviews", reviewable.GetForeignIndex()), nil, &rawReviews, @@ -596,7 +599,7 @@ func (d *OneDevDownloader) GetReviews(reviewable base.Reviewable) ([]*base.Revie } } - poster := d.tryGetUser(review.UserID) + poster := d.tryGetUser(ctx, review.UserID) reviews = append(reviews, &base.Review{ IssueIndex: reviewable.GetLocalIndex(), ReviewerID: poster.ID, @@ -610,14 +613,15 @@ func (d *OneDevDownloader) GetReviews(reviewable base.Reviewable) ([]*base.Revie } // GetTopics return repository topics -func (d *OneDevDownloader) GetTopics() ([]string, error) { +func (d *OneDevDownloader) GetTopics(_ context.Context) ([]string, error) { return []string{}, nil } -func (d *OneDevDownloader) tryGetUser(userID int64) *onedevUser { +func (d *OneDevDownloader) tryGetUser(ctx context.Context, userID int64) *onedevUser { user, ok := d.userMap[userID] if !ok { err := d.callAPI( + ctx, fmt.Sprintf("/api/users/%d", userID), nil, &user, diff --git a/services/migrations/onedev_test.go b/services/migrations/onedev_test.go index 48412fec64a..0a4b05446df 100644 --- a/services/migrations/onedev_test.go +++ b/services/migrations/onedev_test.go @@ -22,11 +22,12 @@ func TestOneDevDownloadRepo(t *testing.T) { } u, _ := url.Parse("https://code.onedev.io") - downloader := NewOneDevDownloader(context.Background(), u, "", "", "go-gitea-test_repo") + ctx := context.Background() + downloader := NewOneDevDownloader(ctx, u, "", "", "go-gitea-test_repo") if err != nil { t.Fatalf("NewOneDevDownloader is nil: %v", err) } - repo, err := downloader.GetRepoInfo() + repo, err := downloader.GetRepoInfo(ctx) assert.NoError(t, err) assertRepositoryEqual(t, &base.Repository{ Name: "go-gitea-test_repo", @@ -36,7 +37,7 @@ func TestOneDevDownloadRepo(t *testing.T) { OriginalURL: "https://code.onedev.io/projects/go-gitea-test_repo", }, repo) - milestones, err := downloader.GetMilestones() + milestones, err := downloader.GetMilestones(ctx) assert.NoError(t, err) deadline := time.Unix(1620086400, 0) assertMilestonesEqual(t, []*base.Milestone{ @@ -51,11 +52,11 @@ func TestOneDevDownloadRepo(t *testing.T) { }, }, milestones) - labels, err := downloader.GetLabels() + labels, err := downloader.GetLabels(ctx) assert.NoError(t, err) assert.Len(t, labels, 6) - issues, isEnd, err := downloader.GetIssues(1, 2) + issues, isEnd, err := downloader.GetIssues(ctx, 1, 2) assert.NoError(t, err) assert.False(t, isEnd) assertIssuesEqual(t, []*base.Issue{ @@ -94,7 +95,7 @@ func TestOneDevDownloadRepo(t *testing.T) { }, }, issues) - comments, _, err := downloader.GetComments(&base.Issue{ + comments, _, err := downloader.GetComments(ctx, &base.Issue{ Number: 4, ForeignIndex: 398, Context: onedevIssueContext{IsPullRequest: false}, @@ -110,7 +111,7 @@ func TestOneDevDownloadRepo(t *testing.T) { }, }, comments) - prs, _, err := downloader.GetPullRequests(1, 1) + prs, _, err := downloader.GetPullRequests(ctx, 1, 1) assert.NoError(t, err) assertPullRequestsEqual(t, []*base.PullRequest{ { @@ -136,7 +137,7 @@ func TestOneDevDownloadRepo(t *testing.T) { }, }, prs) - rvs, err := downloader.GetReviews(&base.PullRequest{Number: 5, ForeignIndex: 186}) + rvs, err := downloader.GetReviews(ctx, &base.PullRequest{Number: 5, ForeignIndex: 186}) assert.NoError(t, err) assertReviewsEqual(t, []*base.Review{ { diff --git a/services/migrations/restore.go b/services/migrations/restore.go index fd337b22c7a..56862859350 100644 --- a/services/migrations/restore.go +++ b/services/migrations/restore.go @@ -18,7 +18,6 @@ import ( // RepositoryRestorer implements an Downloader from the local directory type RepositoryRestorer struct { base.NullDownloader - ctx context.Context baseDir string repoOwner string repoName string @@ -26,13 +25,12 @@ type RepositoryRestorer struct { } // NewRepositoryRestorer creates a repository restorer which could restore repository from a dumped folder -func NewRepositoryRestorer(ctx context.Context, baseDir, owner, repoName string, validation bool) (*RepositoryRestorer, error) { +func NewRepositoryRestorer(_ context.Context, baseDir, owner, repoName string, validation bool) (*RepositoryRestorer, error) { baseDir, err := filepath.Abs(baseDir) if err != nil { return nil, err } return &RepositoryRestorer{ - ctx: ctx, baseDir: baseDir, repoOwner: owner, repoName: repoName, @@ -48,11 +46,6 @@ func (r *RepositoryRestorer) reviewDir() string { return filepath.Join(r.baseDir, "reviews") } -// SetContext set context -func (r *RepositoryRestorer) SetContext(ctx context.Context) { - r.ctx = ctx -} - func (r *RepositoryRestorer) getRepoOptions() (map[string]string, error) { p := filepath.Join(r.baseDir, "repo.yml") bs, err := os.ReadFile(p) @@ -69,7 +62,7 @@ func (r *RepositoryRestorer) getRepoOptions() (map[string]string, error) { } // GetRepoInfo returns a repository information -func (r *RepositoryRestorer) GetRepoInfo() (*base.Repository, error) { +func (r *RepositoryRestorer) GetRepoInfo(_ context.Context) (*base.Repository, error) { opts, err := r.getRepoOptions() if err != nil { return nil, err @@ -89,7 +82,7 @@ func (r *RepositoryRestorer) GetRepoInfo() (*base.Repository, error) { } // GetTopics return github topics -func (r *RepositoryRestorer) GetTopics() ([]string, error) { +func (r *RepositoryRestorer) GetTopics(_ context.Context) ([]string, error) { p := filepath.Join(r.baseDir, "topic.yml") topics := struct { @@ -112,7 +105,7 @@ func (r *RepositoryRestorer) GetTopics() ([]string, error) { } // GetMilestones returns milestones -func (r *RepositoryRestorer) GetMilestones() ([]*base.Milestone, error) { +func (r *RepositoryRestorer) GetMilestones(_ context.Context) ([]*base.Milestone, error) { milestones := make([]*base.Milestone, 0, 10) p := filepath.Join(r.baseDir, "milestone.yml") err := base.Load(p, &milestones, r.validation) @@ -127,7 +120,7 @@ func (r *RepositoryRestorer) GetMilestones() ([]*base.Milestone, error) { } // GetReleases returns releases -func (r *RepositoryRestorer) GetReleases() ([]*base.Release, error) { +func (r *RepositoryRestorer) GetReleases(_ context.Context) ([]*base.Release, error) { releases := make([]*base.Release, 0, 10) p := filepath.Join(r.baseDir, "release.yml") _, err := os.Stat(p) @@ -158,7 +151,7 @@ func (r *RepositoryRestorer) GetReleases() ([]*base.Release, error) { } // GetLabels returns labels -func (r *RepositoryRestorer) GetLabels() ([]*base.Label, error) { +func (r *RepositoryRestorer) GetLabels(_ context.Context) ([]*base.Label, error) { labels := make([]*base.Label, 0, 10) p := filepath.Join(r.baseDir, "label.yml") _, err := os.Stat(p) @@ -182,7 +175,7 @@ func (r *RepositoryRestorer) GetLabels() ([]*base.Label, error) { } // GetIssues returns issues according start and limit -func (r *RepositoryRestorer) GetIssues(page, perPage int) ([]*base.Issue, bool, error) { +func (r *RepositoryRestorer) GetIssues(_ context.Context, _, _ int) ([]*base.Issue, bool, error) { issues := make([]*base.Issue, 0, 10) p := filepath.Join(r.baseDir, "issue.yml") err := base.Load(p, &issues, r.validation) @@ -196,7 +189,7 @@ func (r *RepositoryRestorer) GetIssues(page, perPage int) ([]*base.Issue, bool, } // GetComments returns comments according issueNumber -func (r *RepositoryRestorer) GetComments(commentable base.Commentable) ([]*base.Comment, bool, error) { +func (r *RepositoryRestorer) GetComments(_ context.Context, commentable base.Commentable) ([]*base.Comment, bool, error) { comments := make([]*base.Comment, 0, 10) p := filepath.Join(r.commentDir(), fmt.Sprintf("%d.yml", commentable.GetForeignIndex())) _, err := os.Stat(p) @@ -220,7 +213,7 @@ func (r *RepositoryRestorer) GetComments(commentable base.Commentable) ([]*base. } // GetPullRequests returns pull requests according page and perPage -func (r *RepositoryRestorer) GetPullRequests(page, perPage int) ([]*base.PullRequest, bool, error) { +func (r *RepositoryRestorer) GetPullRequests(_ context.Context, page, perPage int) ([]*base.PullRequest, bool, error) { pulls := make([]*base.PullRequest, 0, 10) p := filepath.Join(r.baseDir, "pull_request.yml") _, err := os.Stat(p) @@ -248,7 +241,7 @@ func (r *RepositoryRestorer) GetPullRequests(page, perPage int) ([]*base.PullReq } // GetReviews returns pull requests review -func (r *RepositoryRestorer) GetReviews(reviewable base.Reviewable) ([]*base.Review, error) { +func (r *RepositoryRestorer) GetReviews(ctx context.Context, reviewable base.Reviewable) ([]*base.Review, error) { reviews := make([]*base.Review, 0, 10) p := filepath.Join(r.reviewDir(), fmt.Sprintf("%d.yml", reviewable.GetForeignIndex())) _, err := os.Stat(p) diff --git a/services/mirror/mirror_pull.go b/services/mirror/mirror_pull.go index 22d380e8e68..24605cfae05 100644 --- a/services/mirror/mirror_pull.go +++ b/services/mirror/mirror_pull.go @@ -32,7 +32,7 @@ const gitShortEmptySha = "0000000" // UpdateAddress writes new address to Git repository and database func UpdateAddress(ctx context.Context, m *repo_model.Mirror, addr string) error { - u, err := giturl.Parse(addr) + u, err := giturl.ParseGitURL(addr) if err != nil { return fmt.Errorf("invalid addr: %v", err) } @@ -41,13 +41,13 @@ func UpdateAddress(ctx context.Context, m *repo_model.Mirror, addr string) error repoPath := m.GetRepository(ctx).RepoPath() // Remove old remote _, _, err = git.NewCommand(ctx, "remote", "rm").AddDynamicArguments(remoteName).RunStdString(&git.RunOpts{Dir: repoPath}) - if err != nil && !strings.HasPrefix(err.Error(), "exit status 128 - fatal: No such remote ") { + if err != nil && !git.IsRemoteNotExistError(err) { return err } cmd := git.NewCommand(ctx, "remote", "add").AddDynamicArguments(remoteName).AddArguments("--mirror=fetch").AddDynamicArguments(addr) _, _, err = cmd.RunStdString(&git.RunOpts{Dir: repoPath}) - if err != nil && !strings.HasPrefix(err.Error(), "exit status 128 - fatal: No such remote ") { + if err != nil && !git.IsRemoteNotExistError(err) { return err } @@ -56,13 +56,13 @@ func UpdateAddress(ctx context.Context, m *repo_model.Mirror, addr string) error wikiRemotePath := repo_module.WikiRemoteURL(ctx, addr) // Remove old remote of wiki _, _, err = git.NewCommand(ctx, "remote", "rm").AddDynamicArguments(remoteName).RunStdString(&git.RunOpts{Dir: wikiPath}) - if err != nil && !strings.HasPrefix(err.Error(), "exit status 128 - fatal: No such remote ") { + if err != nil && !git.IsRemoteNotExistError(err) { return err } cmd = git.NewCommand(ctx, "remote", "add").AddDynamicArguments(remoteName).AddArguments("--mirror=fetch").AddDynamicArguments(wikiRemotePath) _, _, err = cmd.RunStdString(&git.RunOpts{Dir: wikiPath}) - if err != nil && !strings.HasPrefix(err.Error(), "exit status 128 - fatal: No such remote ") { + if err != nil && !git.IsRemoteNotExistError(err) { return err } } @@ -87,6 +87,7 @@ type mirrorSyncResult struct { /* // * [new tag] v0.1.8 -> v0.1.8 // * [new branch] master -> origin/master +// * [new ref] refs/pull/2/head -> refs/pull/2/head" // - [deleted] (none) -> origin/test // delete a branch // - [deleted] (none) -> 1 // delete a tag // 957a993..a87ba5f test -> origin/test @@ -117,6 +118,11 @@ func parseRemoteUpdateOutput(output, remoteName string) []*mirrorSyncResult { refName: git.RefNameFromBranch(refName), oldCommitID: gitShortEmptySha, }) + case strings.HasPrefix(lines[i], " * [new ref]"): // new reference + results = append(results, &mirrorSyncResult{ + refName: git.RefName(refName), + oldCommitID: gitShortEmptySha, + }) case strings.HasPrefix(lines[i], " - "): // Delete reference isTag := !strings.HasPrefix(refName, remoteName+"/") var refFullName git.RefName @@ -159,8 +165,15 @@ func parseRemoteUpdateOutput(output, remoteName string) []*mirrorSyncResult { log.Error("Expect two SHAs but not what found: %q", lines[i]) continue } + var refFullName git.RefName + if strings.HasPrefix(refName, "refs/") { + refFullName = git.RefName(refName) + } else { + refFullName = git.RefNameFromBranch(strings.TrimPrefix(refName, remoteName+"/")) + } + results = append(results, &mirrorSyncResult{ - refName: git.RefNameFromBranch(strings.TrimPrefix(refName, remoteName+"/")), + refName: refFullName, oldCommitID: shas[0], newCommitID: shas[1], }) diff --git a/services/notify/notify.go b/services/notify/notify.go index 3b5f24340bb..c97d0fcbaf0 100644 --- a/services/notify/notify.go +++ b/services/notify/notify.go @@ -272,9 +272,9 @@ func MigrateRepository(ctx context.Context, doer, u *user_model.User, repo *repo } // TransferRepository notifies create repository to notifiers -func TransferRepository(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, newOwnerName string) { +func TransferRepository(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, oldOwnerName string) { for _, notifier := range notifiers { - notifier.TransferRepository(ctx, doer, repo, newOwnerName) + notifier.TransferRepository(ctx, doer, repo, oldOwnerName) } } diff --git a/services/packages/arch/repository.go b/services/packages/arch/repository.go index 6731d9a1ac7..7fb4222cf61 100644 --- a/services/packages/arch/repository.go +++ b/services/packages/arch/repository.go @@ -26,9 +26,9 @@ import ( "code.gitea.io/gitea/modules/util" packages_service "code.gitea.io/gitea/services/packages" - "github.com/keybase/go-crypto/openpgp" - "github.com/keybase/go-crypto/openpgp/armor" - "github.com/keybase/go-crypto/openpgp/packet" + "github.com/ProtonMail/go-crypto/openpgp" + "github.com/ProtonMail/go-crypto/openpgp/armor" + "github.com/ProtonMail/go-crypto/openpgp/packet" ) const ( diff --git a/services/packages/cargo/index.go b/services/packages/cargo/index.go index e8a8313625d..88a463e4c61 100644 --- a/services/packages/cargo/index.go +++ b/services/packages/cargo/index.go @@ -11,7 +11,6 @@ import ( "io" "path" "strconv" - "time" packages_model "code.gitea.io/gitea/models/packages" repo_model "code.gitea.io/gitea/models/repo" @@ -296,8 +295,13 @@ func alterRepositoryContent(ctx context.Context, doer *user_model.User, repo *re return err } - now := time.Now() - commitHash, err := t.CommitTreeWithDate(lastCommitID, doer, doer, treeHash, commitMessage, false, now, now) + commitOpts := &files_service.CommitTreeUserOptions{ + ParentCommitID: lastCommitID, + TreeHash: treeHash, + CommitMessage: commitMessage, + DoerUser: doer, + } + commitHash, err := t.CommitTree(commitOpts) if err != nil { return err } diff --git a/services/packages/debian/repository.go b/services/packages/debian/repository.go index 13e98a820e5..34b52b45cfd 100644 --- a/services/packages/debian/repository.go +++ b/services/packages/debian/repository.go @@ -23,10 +23,10 @@ import ( "code.gitea.io/gitea/modules/util" packages_service "code.gitea.io/gitea/services/packages" - "github.com/keybase/go-crypto/openpgp" - "github.com/keybase/go-crypto/openpgp/armor" - "github.com/keybase/go-crypto/openpgp/clearsign" - "github.com/keybase/go-crypto/openpgp/packet" + "github.com/ProtonMail/go-crypto/openpgp" + "github.com/ProtonMail/go-crypto/openpgp/armor" + "github.com/ProtonMail/go-crypto/openpgp/clearsign" + "github.com/ProtonMail/go-crypto/openpgp/packet" "github.com/ulikunitz/xz" ) diff --git a/services/packages/packages.go b/services/packages/packages.go index 55351afce29..bd1d460fd3b 100644 --- a/services/packages/packages.go +++ b/services/packages/packages.go @@ -132,12 +132,11 @@ func createPackageAndVersion(ctx context.Context, pvci *PackageCreationInfo, all } var err error if p, err = packages_model.TryInsertPackage(ctx, p); err != nil { - if err == packages_model.ErrDuplicatePackage { - packageCreated = false - } else { + if !errors.Is(err, packages_model.ErrDuplicatePackage) { log.Error("Error inserting package: %v", err) return nil, false, err } + packageCreated = false } if packageCreated { @@ -163,11 +162,10 @@ func createPackageAndVersion(ctx context.Context, pvci *PackageCreationInfo, all MetadataJSON: string(metadataJSON), } if pv, err = packages_model.GetOrInsertVersion(ctx, pv); err != nil { - if err == packages_model.ErrDuplicatePackageVersion { + if errors.Is(err, packages_model.ErrDuplicatePackageVersion) && allowDuplicate { versionCreated = false - } - if err != packages_model.ErrDuplicatePackageVersion || !allowDuplicate { - log.Error("Error inserting package: %v", err) + } else { + log.Error("Error inserting package: %v", err) // other error, or disallowing duplicates return nil, false, err } } @@ -433,7 +431,7 @@ func GetOrCreateInternalPackageVersion(ctx context.Context, ownerID int64, packa } var err error if p, err = packages_model.TryInsertPackage(ctx, p); err != nil { - if err != packages_model.ErrDuplicatePackage { + if !errors.Is(err, packages_model.ErrDuplicatePackage) { log.Error("Error inserting package: %v", err) return err } diff --git a/services/projects/issue.go b/services/projects/issue.go index db1621a39f9..6ca0f168060 100644 --- a/services/projects/issue.go +++ b/services/projects/issue.go @@ -55,22 +55,29 @@ func MoveIssuesOnProjectColumn(ctx context.Context, doer *user_model.User, colum continue } - _, err = db.Exec(ctx, "UPDATE `project_issue` SET project_board_id=?, sorting=? WHERE issue_id=?", column.ID, sorting, issueID) + projectColumnID, err := curIssue.ProjectColumnID(ctx) if err != nil { return err } - // add timeline to issue - if _, err := issues_model.CreateComment(ctx, &issues_model.CreateCommentOptions{ - Type: issues_model.CommentTypeProjectColumn, - Doer: doer, - Repo: curIssue.Repo, - Issue: curIssue, - ProjectID: column.ProjectID, - ProjectTitle: project.Title, - ProjectColumnID: column.ID, - ProjectColumnTitle: column.Title, - }); err != nil { + if projectColumnID != column.ID { + // add timeline to issue + if _, err := issues_model.CreateComment(ctx, &issues_model.CreateCommentOptions{ + Type: issues_model.CommentTypeProjectColumn, + Doer: doer, + Repo: curIssue.Repo, + Issue: curIssue, + ProjectID: column.ProjectID, + ProjectTitle: project.Title, + ProjectColumnID: column.ID, + ProjectColumnTitle: column.Title, + }); err != nil { + return err + } + } + + _, err = db.Exec(ctx, "UPDATE `project_issue` SET project_board_id=?, sorting=? WHERE issue_id=?", column.ID, sorting, issueID) + if err != nil { return err } } diff --git a/services/pull/check.go b/services/pull/check.go index f7aa8eb3695..e1adc3ca3bf 100644 --- a/services/pull/check.go +++ b/services/pull/check.go @@ -282,9 +282,6 @@ func manuallyMerged(ctx context.Context, pr *issues_model.PullRequest) bool { return false } - pr.MergedCommitID = commit.ID.String() - pr.MergedUnix = timeutil.TimeStamp(commit.Author.When.Unix()) - pr.Status = issues_model.PullRequestStatusManuallyMerged merger, _ := user_model.GetUserByEmail(ctx, commit.Author.Email) // When the commit author is unknown set the BaseRepo owner as merger @@ -297,10 +294,8 @@ func manuallyMerged(ctx context.Context, pr *issues_model.PullRequest) bool { } merger = pr.BaseRepo.Owner } - pr.Merger = merger - pr.MergerID = merger.ID - if merged, err := SetMerged(ctx, pr); err != nil { + if merged, err := SetMerged(ctx, pr, commit.ID.String(), timeutil.TimeStamp(commit.Author.When.Unix()), merger, issues_model.PullRequestStatusManuallyMerged); err != nil { log.Error("%-v setMerged : %v", pr, err) return false } else if !merged { diff --git a/services/pull/merge.go b/services/pull/merge.go index 879fe5a5408..9c909ef7958 100644 --- a/services/pull/merge.go +++ b/services/pull/merge.go @@ -17,6 +17,7 @@ import ( git_model "code.gitea.io/gitea/models/git" issues_model "code.gitea.io/gitea/models/issues" access_model "code.gitea.io/gitea/models/perm/access" + pull_model "code.gitea.io/gitea/models/pull" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" @@ -632,14 +633,8 @@ func MergedManually(ctx context.Context, pr *issues_model.PullRequest, doer *use return fmt.Errorf("Wrong commit ID") } - pr.MergedCommitID = commitID - pr.MergedUnix = timeutil.TimeStamp(commit.Author.When.Unix()) - pr.Status = issues_model.PullRequestStatusManuallyMerged - pr.Merger = doer - pr.MergerID = doer.ID - var merged bool - if merged, err = SetMerged(ctx, pr); err != nil { + if merged, err = SetMerged(ctx, pr, commitID, timeutil.TimeStamp(commit.Author.When.Unix()), doer, issues_model.PullRequestStatusManuallyMerged); err != nil { return err } else if !merged { return fmt.Errorf("SetMerged failed") @@ -658,41 +653,35 @@ func MergedManually(ctx context.Context, pr *issues_model.PullRequest, doer *use } // SetMerged sets a pull request to merged and closes the corresponding issue -func SetMerged(ctx context.Context, pr *issues_model.PullRequest) (bool, error) { +func SetMerged(ctx context.Context, pr *issues_model.PullRequest, mergedCommitID string, mergedTimeStamp timeutil.TimeStamp, merger *user_model.User, mergeStatus issues_model.PullRequestStatus) (bool, error) { if pr.HasMerged { return false, fmt.Errorf("PullRequest[%d] already merged", pr.Index) } + + pr.HasMerged = true + pr.MergedCommitID = mergedCommitID + pr.MergedUnix = mergedTimeStamp + pr.Merger = merger + pr.MergerID = merger.ID + pr.Status = mergeStatus + // reset the conflicted files as there cannot be any if we're merged + pr.ConflictedFiles = []string{} + if pr.MergedCommitID == "" || pr.MergedUnix == 0 || pr.Merger == nil { return false, fmt.Errorf("unable to merge PullRequest[%d], some required fields are empty", pr.Index) } - pr.HasMerged = true - sess := db.GetEngine(ctx) - - if _, err := sess.Exec("UPDATE `issue` SET `repo_id` = `repo_id` WHERE `id` = ?", pr.IssueID); err != nil { - return false, err - } - - if _, err := sess.Exec("UPDATE `pull_request` SET `issue_id` = `issue_id` WHERE `id` = ?", pr.ID); err != nil { + ctx, committer, err := db.TxContext(ctx) + if err != nil { return false, err } + defer committer.Close() pr.Issue = nil if err := pr.LoadIssue(ctx); err != nil { return false, err } - if tmpPr, err := issues_model.GetPullRequestByID(ctx, pr.ID); err != nil { - return false, err - } else if tmpPr.HasMerged { - if pr.Issue.IsClosed { - return false, nil - } - return false, fmt.Errorf("PullRequest[%d] already merged but it's associated issue [%d] is not closed", pr.Index, pr.IssueID) - } else if pr.Issue.IsClosed { - return false, fmt.Errorf("PullRequest[%d] already closed", pr.Index) - } - if err := pr.Issue.LoadRepo(ctx); err != nil { return false, err } @@ -701,16 +690,28 @@ func SetMerged(ctx context.Context, pr *issues_model.PullRequest) (bool, error) return false, err } - if _, err := issues_model.ChangeIssueStatus(ctx, pr.Issue, pr.Merger, true, true); err != nil { + // Removing an auto merge pull and ignore if not exist + if err := pull_model.DeleteScheduledAutoMerge(ctx, pr.ID); err != nil && !db.IsErrNotExist(err) { + return false, fmt.Errorf("DeleteScheduledAutoMerge[%d]: %v", pr.ID, err) + } + + // Set issue as closed + if _, err := issues_model.SetIssueAsClosed(ctx, pr.Issue, pr.Merger, true); err != nil { return false, fmt.Errorf("ChangeIssueStatus: %w", err) } - // reset the conflicted files as there cannot be any if we're merged - pr.ConflictedFiles = []string{} - // We need to save all of the data used to compute this merge as it may have already been changed by TestPatch. FIXME: need to set some state to prevent TestPatch from running whilst we are merging. - if _, err := sess.Where("id = ?", pr.ID).Cols("has_merged, status, merge_base, merged_commit_id, merger_id, merged_unix, conflicted_files").Update(pr); err != nil { + if cnt, err := db.GetEngine(ctx).Where("id = ?", pr.ID). + And("has_merged = ?", false). + Cols("has_merged, status, merge_base, merged_commit_id, merger_id, merged_unix, conflicted_files"). + Update(pr); err != nil { return false, fmt.Errorf("failed to update pr[%d]: %w", pr.ID, err) + } else if cnt != 1 { + return false, issues_model.ErrIssueAlreadyChanged + } + + if err := committer.Commit(); err != nil { + return false, err } return true, nil diff --git a/services/pull/pull.go b/services/pull/pull.go index 86a55c77cdc..a6673c2b752 100644 --- a/services/pull/pull.go +++ b/services/pull/pull.go @@ -6,6 +6,7 @@ package pull import ( "bytes" "context" + "errors" "fmt" "io" "os" @@ -283,6 +284,7 @@ func ChangeTargetBranch(ctx context.Context, pr *issues_model.PullRequest, doer ID: pr.Issue.ID, RepoID: pr.Issue.RepoID, Index: pr.Issue.Index, + IsPull: true, } } @@ -653,33 +655,9 @@ func UpdateRef(ctx context.Context, pr *issues_model.PullRequest) (err error) { return err } -type errlist []error - -func (errs errlist) Error() string { - if len(errs) > 0 { - var buf strings.Builder - for i, err := range errs { - if i > 0 { - buf.WriteString(", ") - } - buf.WriteString(err.Error()) - } - return buf.String() - } - return "" -} - -// RetargetChildrenOnMerge retarget children pull requests on merge if possible -func RetargetChildrenOnMerge(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest) error { - if setting.Repository.PullRequest.RetargetChildrenOnMerge && pr.BaseRepoID == pr.HeadRepoID { - return RetargetBranchPulls(ctx, doer, pr.HeadRepoID, pr.HeadBranch, pr.BaseBranch) - } - return nil -} - -// RetargetBranchPulls change target branch for all pull requests whose base branch is the branch +// retargetBranchPulls change target branch for all pull requests whose base branch is the branch // Both branch and targetBranch must be in the same repo (for security reasons) -func RetargetBranchPulls(ctx context.Context, doer *user_model.User, repoID int64, branch, targetBranch string) error { +func retargetBranchPulls(ctx context.Context, doer *user_model.User, repoID int64, branch, targetBranch string) error { prs, err := issues_model.GetUnmergedPullRequestsByBaseInfo(ctx, repoID, branch) if err != nil { return err @@ -689,7 +667,7 @@ func RetargetBranchPulls(ctx context.Context, doer *user_model.User, repoID int6 return err } - var errs errlist + var errs []error for _, pr := range prs { if err = pr.Issue.LoadRepo(ctx); err != nil { errs = append(errs, err) @@ -699,40 +677,75 @@ func RetargetBranchPulls(ctx context.Context, doer *user_model.User, repoID int6 errs = append(errs, err) } } - - if len(errs) > 0 { - return errs - } - return nil + return errors.Join(errs...) } -// CloseBranchPulls close all the pull requests who's head branch is the branch -func CloseBranchPulls(ctx context.Context, doer *user_model.User, repoID int64, branch string) error { - prs, err := issues_model.GetUnmergedPullRequestsByHeadInfo(ctx, repoID, branch) +// AdjustPullsCausedByBranchDeleted close all the pull requests who's head branch is the branch +// Or Close all the plls who's base branch is the branch if setting.Repository.PullRequest.RetargetChildrenOnMerge is false. +// If it's true, Retarget all these pulls to the default branch. +func AdjustPullsCausedByBranchDeleted(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, branch string) error { + // branch as head branch + prs, err := issues_model.GetUnmergedPullRequestsByHeadInfo(ctx, repo.ID, branch) if err != nil { return err } - prs2, err := issues_model.GetUnmergedPullRequestsByBaseInfo(ctx, repoID, branch) - if err != nil { - return err - } - - prs = append(prs, prs2...) if err := issues_model.PullRequestList(prs).LoadAttributes(ctx); err != nil { return err } + issues_model.PullRequestList(prs).SetHeadRepo(repo) + if err := issues_model.PullRequestList(prs).LoadRepositories(ctx); err != nil { + return err + } - var errs errlist + var errs []error for _, pr := range prs { - if err = issue_service.CloseIssue(ctx, pr.Issue, doer, ""); err != nil && !issues_model.IsErrPullWasClosed(err) && !issues_model.IsErrDependenciesLeft(err) { + if err = issue_service.CloseIssue(ctx, pr.Issue, doer, ""); err != nil && !issues_model.IsErrIssueIsClosed(err) && !issues_model.IsErrDependenciesLeft(err) { errs = append(errs, err) } + if err == nil { + if err := issues_model.AddDeletePRBranchComment(ctx, doer, pr.BaseRepo, pr.Issue.ID, pr.HeadBranch); err != nil { + log.Error("AddDeletePRBranchComment: %v", err) + errs = append(errs, err) + } + } } - if len(errs) > 0 { - return errs + + if setting.Repository.PullRequest.RetargetChildrenOnMerge { + if err := retargetBranchPulls(ctx, doer, repo.ID, branch, repo.DefaultBranch); err != nil { + log.Error("retargetBranchPulls failed: %v", err) + errs = append(errs, err) + } + return errors.Join(errs...) } - return nil + + // branch as base branch + prs, err = issues_model.GetUnmergedPullRequestsByBaseInfo(ctx, repo.ID, branch) + if err != nil { + return err + } + + if err := issues_model.PullRequestList(prs).LoadAttributes(ctx); err != nil { + return err + } + issues_model.PullRequestList(prs).SetBaseRepo(repo) + if err := issues_model.PullRequestList(prs).LoadRepositories(ctx); err != nil { + return err + } + + errs = nil + for _, pr := range prs { + if err = issues_model.AddDeletePRBranchComment(ctx, doer, pr.BaseRepo, pr.Issue.ID, pr.BaseBranch); err != nil { + log.Error("AddDeletePRBranchComment: %v", err) + errs = append(errs, err) + } + if err == nil { + if err = issue_service.CloseIssue(ctx, pr.Issue, doer, ""); err != nil && !issues_model.IsErrIssueIsClosed(err) && !issues_model.IsErrDependenciesLeft(err) { + errs = append(errs, err) + } + } + } + return errors.Join(errs...) } // CloseRepoBranchesPulls close all pull requests which head branches are in the given repository, but only whose base repo is not in the given repository @@ -742,7 +755,7 @@ func CloseRepoBranchesPulls(ctx context.Context, doer *user_model.User, repo *re return err } - var errs errlist + var errs []error for _, branch := range branches { prs, err := issues_model.GetUnmergedPullRequestsByHeadInfo(ctx, repo.ID, branch.Name) if err != nil { @@ -759,16 +772,13 @@ func CloseRepoBranchesPulls(ctx context.Context, doer *user_model.User, repo *re if pr.BaseRepoID == repo.ID { continue } - if err = issue_service.CloseIssue(ctx, pr.Issue, doer, ""); err != nil && !issues_model.IsErrPullWasClosed(err) { + if err = issue_service.CloseIssue(ctx, pr.Issue, doer, ""); err != nil && !issues_model.IsErrIssueIsClosed(err) { errs = append(errs, err) } } } - if len(errs) > 0 { - return errs - } - return nil + return errors.Join(errs...) } var commitMessageTrailersPattern = regexp.MustCompile(`(?:^|\n\n)(?:[\w-]+[ \t]*:[^\n]+\n*(?:[ \t]+[^\n]+\n*)*)+$`) diff --git a/services/repository/archiver/archiver.go b/services/repository/archiver/archiver.go index e1addbed335..d39acc080d5 100644 --- a/services/repository/archiver/archiver.go +++ b/services/repository/archiver/archiver.go @@ -31,19 +31,20 @@ import ( // handle elsewhere. type ArchiveRequest struct { RepoID int64 - refName string Type git.ArchiveType CommitID string + + archiveRefShortName string // the ref short name to download the archive, for example: "master", "v1.0.0", "commit id" } // ErrUnknownArchiveFormat request archive format is not supported type ErrUnknownArchiveFormat struct { - RequestFormat string + RequestNameType string } // Error implements error func (err ErrUnknownArchiveFormat) Error() string { - return fmt.Sprintf("unknown format: %s", err.RequestFormat) + return fmt.Sprintf("unknown format: %s", err.RequestNameType) } // Is implements error @@ -54,12 +55,12 @@ func (ErrUnknownArchiveFormat) Is(err error) bool { // RepoRefNotFoundError is returned when a requested reference (commit, tag) was not found. type RepoRefNotFoundError struct { - RefName string + RefShortName string } // Error implements error. func (e RepoRefNotFoundError) Error() string { - return fmt.Sprintf("unrecognized repository reference: %s", e.RefName) + return fmt.Sprintf("unrecognized repository reference: %s", e.RefShortName) } func (e RepoRefNotFoundError) Is(err error) bool { @@ -67,43 +68,23 @@ func (e RepoRefNotFoundError) Is(err error) bool { return ok } -func ParseFileName(uri string) (ext string, tp git.ArchiveType, err error) { - switch { - case strings.HasSuffix(uri, ".zip"): - ext = ".zip" - tp = git.ZIP - case strings.HasSuffix(uri, ".tar.gz"): - ext = ".tar.gz" - tp = git.TARGZ - case strings.HasSuffix(uri, ".bundle"): - ext = ".bundle" - tp = git.BUNDLE - default: - return "", 0, ErrUnknownArchiveFormat{RequestFormat: uri} - } - return ext, tp, nil -} - // NewRequest creates an archival request, based on the URI. The // resulting ArchiveRequest is suitable for being passed to Await() // if it's determined that the request still needs to be satisfied. -func NewRequest(repoID int64, repo *git.Repository, refName string, fileType git.ArchiveType) (*ArchiveRequest, error) { - if fileType < git.ZIP || fileType > git.BUNDLE { - return nil, ErrUnknownArchiveFormat{RequestFormat: fileType.String()} - } - - r := &ArchiveRequest{ - RepoID: repoID, - refName: refName, - Type: fileType, +func NewRequest(repoID int64, repo *git.Repository, archiveRefExt string) (*ArchiveRequest, error) { + // here the archiveRefShortName is not a clear ref, it could be a tag, branch or commit id + archiveRefShortName, archiveType := git.SplitArchiveNameType(archiveRefExt) + if archiveType == git.ArchiveUnknown { + return nil, ErrUnknownArchiveFormat{archiveRefExt} } // Get corresponding commit. - commitID, err := repo.ConvertToGitID(r.refName) + commitID, err := repo.ConvertToGitID(archiveRefShortName) if err != nil { - return nil, RepoRefNotFoundError{RefName: r.refName} + return nil, RepoRefNotFoundError{RefShortName: archiveRefShortName} } + r := &ArchiveRequest{RepoID: repoID, archiveRefShortName: archiveRefShortName, Type: archiveType} r.CommitID = commitID.String() return r, nil } @@ -111,11 +92,11 @@ func NewRequest(repoID int64, repo *git.Repository, refName string, fileType git // GetArchiveName returns the name of the caller, based on the ref used by the // caller to create this request. func (aReq *ArchiveRequest) GetArchiveName() string { - return strings.ReplaceAll(aReq.refName, "/", "-") + "." + aReq.Type.String() + return strings.ReplaceAll(aReq.archiveRefShortName, "/", "-") + "." + aReq.Type.String() } // Await awaits the completion of an ArchiveRequest. If the archive has -// already been prepared the method returns immediately. Otherwise an archiver +// already been prepared the method returns immediately. Otherwise, an archiver // process will be started and its completion awaited. On success the returned // RepoArchiver may be used to download the archive. Note that even if the // context is cancelled/times out a started archiver will still continue to run @@ -208,8 +189,8 @@ func doArchive(ctx context.Context, r *ArchiveRequest) (*repo_model.RepoArchiver rd, w := io.Pipe() defer func() { - w.Close() - rd.Close() + _ = w.Close() + _ = rd.Close() }() done := make(chan error, 1) // Ensure that there is some capacity which will ensure that the goroutine below can always finish repo, err := repo_model.GetRepositoryByID(ctx, archiver.RepoID) @@ -230,7 +211,7 @@ func doArchive(ctx context.Context, r *ArchiveRequest) (*repo_model.RepoArchiver } }() - if archiver.Type == git.BUNDLE { + if archiver.Type == git.ArchiveBundle { err = gitRepo.CreateBundle( ctx, archiver.CommitID, diff --git a/services/repository/archiver/archiver_test.go b/services/repository/archiver/archiver_test.go index 1d0c6e513d8..522f90558a7 100644 --- a/services/repository/archiver/archiver_test.go +++ b/services/repository/archiver/archiver_test.go @@ -9,7 +9,6 @@ import ( "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/unittest" - "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/services/contexttest" _ "code.gitea.io/gitea/models/actions" @@ -31,47 +30,47 @@ func TestArchive_Basic(t *testing.T) { contexttest.LoadGitRepo(t, ctx) defer ctx.Repo.GitRepo.Close() - bogusReq, err := NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit, git.ZIP) + bogusReq, err := NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit+".zip") assert.NoError(t, err) assert.NotNil(t, bogusReq) assert.EqualValues(t, firstCommit+".zip", bogusReq.GetArchiveName()) // Check a series of bogus requests. // Step 1, valid commit with a bad extension. - bogusReq, err = NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit, 100) + bogusReq, err = NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit+".unknown") assert.Error(t, err) assert.Nil(t, bogusReq) // Step 2, missing commit. - bogusReq, err = NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, "dbffff", git.ZIP) + bogusReq, err = NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, "dbffff.zip") assert.Error(t, err) assert.Nil(t, bogusReq) // Step 3, doesn't look like branch/tag/commit. - bogusReq, err = NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, "db", git.ZIP) + bogusReq, err = NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, "db.zip") assert.Error(t, err) assert.Nil(t, bogusReq) - bogusReq, err = NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, "master", git.ZIP) + bogusReq, err = NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, "master.zip") assert.NoError(t, err) assert.NotNil(t, bogusReq) assert.EqualValues(t, "master.zip", bogusReq.GetArchiveName()) - bogusReq, err = NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, "test/archive", git.ZIP) + bogusReq, err = NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, "test/archive.zip") assert.NoError(t, err) assert.NotNil(t, bogusReq) assert.EqualValues(t, "test-archive.zip", bogusReq.GetArchiveName()) // Now two valid requests, firstCommit with valid extensions. - zipReq, err := NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit, git.ZIP) + zipReq, err := NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit+".zip") assert.NoError(t, err) assert.NotNil(t, zipReq) - tgzReq, err := NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit, git.TARGZ) + tgzReq, err := NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit+".tar.gz") assert.NoError(t, err) assert.NotNil(t, tgzReq) - secondReq, err := NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, secondCommit, git.ZIP) + secondReq, err := NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, secondCommit+".bundle") assert.NoError(t, err) assert.NotNil(t, secondReq) @@ -91,7 +90,7 @@ func TestArchive_Basic(t *testing.T) { // Sleep two seconds to make sure the queue doesn't change. time.Sleep(2 * time.Second) - zipReq2, err := NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit, git.ZIP) + zipReq2, err := NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit+".zip") assert.NoError(t, err) // This zipReq should match what's sitting in the queue, as we haven't // let it release yet. From the consumer's point of view, this looks like @@ -106,12 +105,12 @@ func TestArchive_Basic(t *testing.T) { // Now we'll submit a request and TimedWaitForCompletion twice, before and // after we release it. We should trigger both the timeout and non-timeout // cases. - timedReq, err := NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, secondCommit, git.TARGZ) + timedReq, err := NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, secondCommit+".tar.gz") assert.NoError(t, err) assert.NotNil(t, timedReq) doArchive(db.DefaultContext, timedReq) - zipReq2, err = NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit, git.ZIP) + zipReq2, err = NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit+".zip") assert.NoError(t, err) // Now, we're guaranteed to have released the original zipReq from the queue. // Ensure that we don't get handed back the released entry somehow, but they @@ -129,6 +128,6 @@ func TestArchive_Basic(t *testing.T) { } func TestErrUnknownArchiveFormat(t *testing.T) { - err := ErrUnknownArchiveFormat{RequestFormat: "master"} + err := ErrUnknownArchiveFormat{RequestNameType: "xxx"} assert.ErrorIs(t, err, ErrUnknownArchiveFormat{}) } diff --git a/services/repository/branch.go b/services/repository/branch.go index 4967e46a892..432596328a3 100644 --- a/services/repository/branch.go +++ b/services/repository/branch.go @@ -26,6 +26,7 @@ import ( "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/queue" repo_module "code.gitea.io/gitea/modules/repository" + "code.gitea.io/gitea/modules/reqctx" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" webhook_module "code.gitea.io/gitea/modules/webhook" @@ -416,6 +417,29 @@ func RenameBranch(ctx context.Context, repo *repo_model.Repository, doer *user_m return "from_not_exist", nil } + perm, err := access_model.GetUserRepoPermission(ctx, repo, doer) + if err != nil { + return "", err + } + + isDefault := from == repo.DefaultBranch + if isDefault && !perm.IsAdmin() { + return "", repo_model.ErrUserDoesNotHaveAccessToRepo{ + UserID: doer.ID, + RepoName: repo.LowerName, + } + } + + // If from == rule name, admins are allowed to modify them. + if protectedBranch, err := git_model.GetProtectedBranchRuleByName(ctx, repo.ID, from); err != nil { + return "", err + } else if protectedBranch != nil && !perm.IsAdmin() { + return "", repo_model.ErrUserDoesNotHaveAccessToRepo{ + UserID: doer.ID, + RepoName: repo.LowerName, + } + } + if err := git_model.RenameBranch(ctx, repo, from, to, func(ctx context.Context, isDefault bool) error { err2 := gitRepo.RenameBranch(from, to) if err2 != nil { @@ -489,7 +513,7 @@ func CanDeleteBranch(ctx context.Context, repo *repo_model.Repository, branchNam } // DeleteBranch delete branch -func DeleteBranch(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, gitRepo *git.Repository, branchName string) error { +func DeleteBranch(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, gitRepo *git.Repository, branchName string, pr *issues_model.PullRequest) error { err := repo.MustNotBeArchived() if err != nil { return err @@ -522,6 +546,11 @@ func DeleteBranch(ctx context.Context, doer *user_model.User, repo *repo_model.R if err := issues_model.DeleteIssueDevLinkByBranchName(ctx, repo.ID, branchName); err != nil { return err } + if pr != nil { + if err := issues_model.AddDeletePRBranchComment(ctx, doer, pr.BaseRepo, pr.Issue.ID, pr.HeadBranch); err != nil { + return fmt.Errorf("DeleteBranch: %v", err) + } + } return gitRepo.DeleteBranch(branchName, git.DeleteBranchOptions{ Force: true, @@ -640,3 +669,72 @@ func SetRepoDefaultBranch(ctx context.Context, repo *repo_model.Repository, gitR return nil } + +// BranchDivergingInfo contains the information about the divergence of a head branch to the base branch. +type BranchDivergingInfo struct { + // whether the base branch contains new commits which are not in the head branch + BaseHasNewCommits bool + + // behind/after are number of commits that the head branch is behind/after the base branch, it's 0 if it's unable to calculate. + // there could be a case that BaseHasNewCommits=true while the behind/after are both 0 (unable to calculate). + HeadCommitsBehind int + HeadCommitsAhead int +} + +// GetBranchDivergingInfo returns the information about the divergence of a patch branch to the base branch. +func GetBranchDivergingInfo(ctx reqctx.RequestContext, baseRepo *repo_model.Repository, baseBranch string, headRepo *repo_model.Repository, headBranch string) (*BranchDivergingInfo, error) { + headGitBranch, err := git_model.GetBranch(ctx, headRepo.ID, headBranch) + if err != nil { + return nil, err + } + if headGitBranch.IsDeleted { + return nil, git_model.ErrBranchNotExist{ + BranchName: headBranch, + } + } + baseGitBranch, err := git_model.GetBranch(ctx, baseRepo.ID, baseBranch) + if err != nil { + return nil, err + } + if baseGitBranch.IsDeleted { + return nil, git_model.ErrBranchNotExist{ + BranchName: baseBranch, + } + } + + info := &BranchDivergingInfo{} + if headGitBranch.CommitID == baseGitBranch.CommitID { + return info, nil + } + + // if the fork repo has new commits, this call will fail because they are not in the base repo + // exit status 128 - fatal: Invalid symmetric difference expression aaaaaaaaaaaa...bbbbbbbbbbbb + // so at the moment, we first check the update time, then check whether the fork branch has base's head + diff, err := git.GetDivergingCommits(ctx, baseRepo.RepoPath(), baseGitBranch.CommitID, headGitBranch.CommitID) + if err != nil { + info.BaseHasNewCommits = baseGitBranch.UpdatedUnix > headGitBranch.UpdatedUnix + if headRepo.IsFork && info.BaseHasNewCommits { + return info, nil + } + // if the base's update time is before the fork, check whether the base's head is in the fork + headGitRepo, err := gitrepo.RepositoryFromRequestContextOrOpen(ctx, headRepo) + if err != nil { + return nil, err + } + headCommit, err := headGitRepo.GetCommit(headGitBranch.CommitID) + if err != nil { + return nil, err + } + baseCommitID, err := git.NewIDFromString(baseGitBranch.CommitID) + if err != nil { + return nil, err + } + hasPreviousCommit, _ := headCommit.HasPreviousCommit(baseCommitID) + info.BaseHasNewCommits = !hasPreviousCommit + return info, nil + } + + info.HeadCommitsBehind, info.HeadCommitsAhead = diff.Behind, diff.Ahead + info.BaseHasNewCommits = info.HeadCommitsBehind > 0 + return info, nil +} diff --git a/services/repository/create.go b/services/repository/create.go index a3199f2a40a..23aacd6f958 100644 --- a/services/repository/create.go +++ b/services/repository/create.go @@ -79,7 +79,7 @@ func prepareRepoCommit(ctx context.Context, repo *repo_model.Repository, tmpDir, return fmt.Errorf("GetRepoInitFile[%s]: %w", opts.Readme, err) } - cloneLink := repo.CloneLink() + cloneLink := repo.CloneLink(ctx, nil /* no doer so do not generate user-related SSH link */) match := map[string]string{ "Name": repo.Name, "Description": repo.Description, diff --git a/services/repository/files/cherry_pick.go b/services/repository/files/cherry_pick.go index 10545e9e038..34572838039 100644 --- a/services/repository/files/cherry_pick.go +++ b/services/repository/files/cherry_pick.go @@ -32,15 +32,13 @@ func (err ErrCommitIDDoesNotMatch) Error() string { return fmt.Sprintf("file CommitID does not match [given: %s, expected: %s]", err.GivenCommitID, err.CurrentCommitID) } -// CherryPick cherrypicks or reverts a commit to the given repository +// CherryPick cherry-picks or reverts a commit to the given repository func CherryPick(ctx context.Context, repo *repo_model.Repository, doer *user_model.User, revert bool, opts *ApplyDiffPatchOptions) (*structs.FileResponse, error) { if err := opts.Validate(ctx, repo, doer); err != nil { return nil, err } message := strings.TrimSpace(opts.Message) - author, committer := GetAuthorAndCommitterUsers(opts.Author, opts.Committer, doer) - t, err := NewTemporaryUploadRepository(ctx, repo) if err != nil { log.Error("NewTemporaryUploadRepository failed: %v", err) @@ -112,12 +110,21 @@ func CherryPick(ctx context.Context, repo *repo_model.Repository, doer *user_mod } // Now commit the tree - var commitHash string - if opts.Dates != nil { - commitHash, err = t.CommitTreeWithDate("HEAD", author, committer, treeHash, message, opts.Signoff, opts.Dates.Author, opts.Dates.Committer) - } else { - commitHash, err = t.CommitTree("HEAD", author, committer, treeHash, message, opts.Signoff) + commitOpts := &CommitTreeUserOptions{ + ParentCommitID: "HEAD", + TreeHash: treeHash, + CommitMessage: message, + SignOff: opts.Signoff, + DoerUser: doer, + AuthorIdentity: opts.Author, + AuthorTime: nil, + CommitterIdentity: opts.Committer, + CommitterTime: nil, } + if opts.Dates != nil { + commitOpts.AuthorTime, commitOpts.CommitterTime = &opts.Dates.Author, &opts.Dates.Committer + } + commitHash, err := t.CommitTree(commitOpts) if err != nil { return nil, err } diff --git a/services/repository/files/file.go b/services/repository/files/file.go index d7ca8e79e5b..2caa1b49469 100644 --- a/services/repository/files/file.go +++ b/services/repository/files/file.go @@ -11,7 +11,6 @@ import ( "time" repo_model "code.gitea.io/gitea/models/repo" - user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" @@ -111,51 +110,6 @@ func GetFileCommitResponse(repo *repo_model.Repository, commit *git.Commit) (*ap return fileCommit, nil } -// GetAuthorAndCommitterUsers Gets the author and committer user objects from the IdentityOptions -func GetAuthorAndCommitterUsers(author, committer *IdentityOptions, doer *user_model.User) (authorUser, committerUser *user_model.User) { - // Committer and author are optional. If they are not the doer (not same email address) - // then we use bogus User objects for them to store their FullName and Email. - // If only one of the two are provided, we set both of them to it. - // If neither are provided, both are the doer. - if committer != nil && committer.Email != "" { - if doer != nil && strings.EqualFold(doer.Email, committer.Email) { - committerUser = doer // the committer is the doer, so will use their user object - if committer.Name != "" { - committerUser.FullName = committer.Name - } - } else { - committerUser = &user_model.User{ - FullName: committer.Name, - Email: committer.Email, - } - } - } - if author != nil && author.Email != "" { - if doer != nil && strings.EqualFold(doer.Email, author.Email) { - authorUser = doer // the author is the doer, so will use their user object - if authorUser.Name != "" { - authorUser.FullName = author.Name - } - } else { - authorUser = &user_model.User{ - FullName: author.Name, - Email: author.Email, - } - } - } - if authorUser == nil { - if committerUser != nil { - authorUser = committerUser // No valid author was given so use the committer - } else if doer != nil { - authorUser = doer // No valid author was given and no valid committer so use the doer - } - } - if committerUser == nil { - committerUser = authorUser // No valid committer so use the author as the committer (was set to a valid user above) - } - return authorUser, committerUser -} - // ErrFilenameInvalid represents a "FilenameInvalid" kind of error. type ErrFilenameInvalid struct { Path string diff --git a/services/repository/files/patch.go b/services/repository/files/patch.go index 38c17b4073d..78c275f01c8 100644 --- a/services/repository/files/patch.go +++ b/services/repository/files/patch.go @@ -126,8 +126,6 @@ func ApplyDiffPatch(ctx context.Context, repo *repo_model.Repository, doer *user message := strings.TrimSpace(opts.Message) - author, committer := GetAuthorAndCommitterUsers(opts.Author, opts.Committer, doer) - t, err := NewTemporaryUploadRepository(ctx, repo) if err != nil { log.Error("NewTemporaryUploadRepository failed: %v", err) @@ -187,12 +185,21 @@ func ApplyDiffPatch(ctx context.Context, repo *repo_model.Repository, doer *user } // Now commit the tree - var commitHash string - if opts.Dates != nil { - commitHash, err = t.CommitTreeWithDate("HEAD", author, committer, treeHash, message, opts.Signoff, opts.Dates.Author, opts.Dates.Committer) - } else { - commitHash, err = t.CommitTree("HEAD", author, committer, treeHash, message, opts.Signoff) + commitOpts := &CommitTreeUserOptions{ + ParentCommitID: "HEAD", + TreeHash: treeHash, + CommitMessage: message, + SignOff: opts.Signoff, + DoerUser: doer, + AuthorIdentity: opts.Author, + AuthorTime: nil, + CommitterIdentity: opts.Committer, + CommitterTime: nil, } + if opts.Dates != nil { + commitOpts.AuthorTime, commitOpts.CommitterTime = &opts.Dates.Author, &opts.Dates.Committer + } + commitHash, err := t.CommitTree(commitOpts) if err != nil { return nil, err } diff --git a/services/repository/files/temp_repo.go b/services/repository/files/temp_repo.go index 138af991f9b..cf1402397b6 100644 --- a/services/repository/files/temp_repo.go +++ b/services/repository/files/temp_repo.go @@ -19,6 +19,7 @@ import ( "code.gitea.io/gitea/modules/log" repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" asymkey_service "code.gitea.io/gitea/services/asymkey" "code.gitea.io/gitea/services/gitdiff" ) @@ -225,15 +226,53 @@ func (t *TemporaryUploadRepository) GetLastCommitByRef(ref string) (string, erro return strings.TrimSpace(stdout), nil } -// CommitTree creates a commit from a given tree for the user with provided message -func (t *TemporaryUploadRepository) CommitTree(parent string, author, committer *user_model.User, treeHash, message string, signoff bool) (string, error) { - return t.CommitTreeWithDate(parent, author, committer, treeHash, message, signoff, time.Now(), time.Now()) +type CommitTreeUserOptions struct { + ParentCommitID string + TreeHash string + CommitMessage string + SignOff bool + + DoerUser *user_model.User + + AuthorIdentity *IdentityOptions // if nil, use doer + AuthorTime *time.Time // if nil, use now + CommitterIdentity *IdentityOptions + CommitterTime *time.Time } -// CommitTreeWithDate creates a commit from a given tree for the user with provided message -func (t *TemporaryUploadRepository) CommitTreeWithDate(parent string, author, committer *user_model.User, treeHash, message string, signoff bool, authorDate, committerDate time.Time) (string, error) { - authorSig := author.NewGitSig() - committerSig := committer.NewGitSig() +func makeGitUserSignature(doer *user_model.User, identity, other *IdentityOptions) *git.Signature { + gitSig := &git.Signature{} + if identity != nil { + gitSig.Name, gitSig.Email = identity.GitUserName, identity.GitUserEmail + } + if other != nil { + gitSig.Name = util.IfZero(gitSig.Name, other.GitUserName) + gitSig.Email = util.IfZero(gitSig.Email, other.GitUserEmail) + } + if gitSig.Name == "" { + gitSig.Name = doer.GitName() + } + if gitSig.Email == "" { + gitSig.Email = doer.GetEmail() + } + return gitSig +} + +// CommitTree creates a commit from a given tree for the user with provided message +func (t *TemporaryUploadRepository) CommitTree(opts *CommitTreeUserOptions) (string, error) { + authorSig := makeGitUserSignature(opts.DoerUser, opts.AuthorIdentity, opts.CommitterIdentity) + committerSig := makeGitUserSignature(opts.DoerUser, opts.CommitterIdentity, opts.AuthorIdentity) + + authorDate := opts.AuthorTime + committerDate := opts.CommitterTime + if authorDate == nil && committerDate == nil { + authorDate = util.ToPointer(time.Now()) + committerDate = authorDate + } else if authorDate == nil { + authorDate = committerDate + } else if committerDate == nil { + committerDate = authorDate + } // Because this may call hooks we should pass in the environment env := append(os.Environ(), @@ -244,21 +283,21 @@ func (t *TemporaryUploadRepository) CommitTreeWithDate(parent string, author, co ) messageBytes := new(bytes.Buffer) - _, _ = messageBytes.WriteString(message) + _, _ = messageBytes.WriteString(opts.CommitMessage) _, _ = messageBytes.WriteString("\n") - cmdCommitTree := git.NewCommand(t.ctx, "commit-tree").AddDynamicArguments(treeHash) - if parent != "" { - cmdCommitTree.AddOptionValues("-p", parent) + cmdCommitTree := git.NewCommand(t.ctx, "commit-tree").AddDynamicArguments(opts.TreeHash) + if opts.ParentCommitID != "" { + cmdCommitTree.AddOptionValues("-p", opts.ParentCommitID) } var sign bool var keyID string var signer *git.Signature - if parent != "" { - sign, keyID, signer, _ = asymkey_service.SignCRUDAction(t.ctx, t.repo.RepoPath(), author, t.basePath, parent) + if opts.ParentCommitID != "" { + sign, keyID, signer, _ = asymkey_service.SignCRUDAction(t.ctx, t.repo.RepoPath(), opts.DoerUser, t.basePath, opts.ParentCommitID) } else { - sign, keyID, signer, _ = asymkey_service.SignInitialCommit(t.ctx, t.repo.RepoPath(), author) + sign, keyID, signer, _ = asymkey_service.SignInitialCommit(t.ctx, t.repo.RepoPath(), opts.DoerUser) } if sign { cmdCommitTree.AddOptionFormat("-S%s", keyID) @@ -279,7 +318,7 @@ func (t *TemporaryUploadRepository) CommitTreeWithDate(parent string, author, co cmdCommitTree.AddArguments("--no-gpg-sign") } - if signoff { + if opts.SignOff { // Signed-off-by _, _ = messageBytes.WriteString("\n") _, _ = messageBytes.WriteString("Signed-off-by: ") diff --git a/services/repository/files/update.go b/services/repository/files/update.go index a2763105b0c..a707ea8bb6f 100644 --- a/services/repository/files/update.go +++ b/services/repository/files/update.go @@ -27,8 +27,8 @@ import ( // IdentityOptions for a person's identity like an author or committer type IdentityOptions struct { - Name string - Email string + GitUserName string // to match "git config user.name" + GitUserEmail string // to match "git config user.email" } // CommitDateOptions store dates for GIT_AUTHOR_DATE and GIT_COMMITTER_DATE @@ -160,8 +160,6 @@ func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use message := strings.TrimSpace(opts.Message) - author, committer := GetAuthorAndCommitterUsers(opts.Author, opts.Committer, doer) - t, err := NewTemporaryUploadRepository(ctx, repo) if err != nil { log.Error("NewTemporaryUploadRepository failed: %v", err) @@ -262,12 +260,21 @@ func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use } // Now commit the tree - var commitHash string - if opts.Dates != nil { - commitHash, err = t.CommitTreeWithDate(opts.LastCommitID, author, committer, treeHash, message, opts.Signoff, opts.Dates.Author, opts.Dates.Committer) - } else { - commitHash, err = t.CommitTree(opts.LastCommitID, author, committer, treeHash, message, opts.Signoff) + commitOpts := &CommitTreeUserOptions{ + ParentCommitID: opts.LastCommitID, + TreeHash: treeHash, + CommitMessage: message, + SignOff: opts.Signoff, + DoerUser: doer, + AuthorIdentity: opts.Author, + AuthorTime: nil, + CommitterIdentity: opts.Committer, + CommitterTime: nil, } + if opts.Dates != nil { + commitOpts.AuthorTime, commitOpts.CommitterTime = &opts.Dates.Author, &opts.Dates.Committer + } + commitHash, err := t.CommitTree(commitOpts) if err != nil { return nil, err } diff --git a/services/repository/files/upload.go b/services/repository/files/upload.go index cbfaf49d131..3c58598427e 100644 --- a/services/repository/files/upload.go +++ b/services/repository/files/upload.go @@ -27,6 +27,8 @@ type UploadRepoFileOptions struct { Message string Files []string // In UUID format. Signoff bool + Author *IdentityOptions + Committer *IdentityOptions } type uploadInfo struct { @@ -128,12 +130,17 @@ func UploadRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use return err } - // make author and committer the doer - author := doer - committer := doer - // Now commit the tree - commitHash, err := t.CommitTree(opts.LastCommitID, author, committer, treeHash, opts.Message, opts.Signoff) + commitOpts := &CommitTreeUserOptions{ + ParentCommitID: opts.LastCommitID, + TreeHash: treeHash, + CommitMessage: opts.Message, + SignOff: opts.Signoff, + DoerUser: doer, + AuthorIdentity: opts.Author, + CommitterIdentity: opts.Committer, + } + commitHash, err := t.CommitTree(commitOpts) if err != nil { return err } diff --git a/services/repository/fork.go b/services/repository/fork.go index cff0b1a4036..8d89c2b0b02 100644 --- a/services/repository/fork.go +++ b/services/repository/fork.go @@ -256,9 +256,11 @@ type findForksOptions struct { } func (opts findForksOptions) ToConds() builder.Cond { - return builder.Eq{"fork_id": opts.RepoID}.And( - repo_model.AccessibleRepositoryCondition(opts.Doer, unit.TypeInvalid), - ) + cond := builder.Eq{"fork_id": opts.RepoID} + if opts.Doer != nil && opts.Doer.IsAdmin { + return cond + } + return cond.And(repo_model.AccessibleRepositoryCondition(opts.Doer, unit.TypeInvalid)) } // FindForks returns all the forks of the repository diff --git a/services/repository/generate.go b/services/repository/generate.go index 24cf9d1b9bf..d5c07e98005 100644 --- a/services/repository/generate.go +++ b/services/repository/generate.go @@ -9,7 +9,6 @@ import ( "context" "fmt" "os" - "path" "path/filepath" "regexp" "strconv" @@ -52,7 +51,7 @@ var defaultTransformers = []transformer{ {Name: "TITLE", Transform: util.ToTitleCase}, } -func generateExpansion(src string, templateRepo, generateRepo *repo_model.Repository, sanitizeFileName bool) string { +func generateExpansion(ctx context.Context, src string, templateRepo, generateRepo *repo_model.Repository, sanitizeFileName bool) string { year, month, day := time.Now().Date() expansions := []expansion{ {Name: "YEAR", Value: strconv.Itoa(year), Transformers: nil}, @@ -67,10 +66,10 @@ func generateExpansion(src string, templateRepo, generateRepo *repo_model.Reposi {Name: "TEMPLATE_OWNER", Value: templateRepo.OwnerName, Transformers: defaultTransformers}, {Name: "REPO_LINK", Value: generateRepo.Link(), Transformers: nil}, {Name: "TEMPLATE_LINK", Value: templateRepo.Link(), Transformers: nil}, - {Name: "REPO_HTTPS_URL", Value: generateRepo.CloneLink().HTTPS, Transformers: nil}, - {Name: "TEMPLATE_HTTPS_URL", Value: templateRepo.CloneLink().HTTPS, Transformers: nil}, - {Name: "REPO_SSH_URL", Value: generateRepo.CloneLink().SSH, Transformers: nil}, - {Name: "TEMPLATE_SSH_URL", Value: templateRepo.CloneLink().SSH, Transformers: nil}, + {Name: "REPO_HTTPS_URL", Value: generateRepo.CloneLinkGeneral(ctx).HTTPS, Transformers: nil}, + {Name: "TEMPLATE_HTTPS_URL", Value: templateRepo.CloneLinkGeneral(ctx).HTTPS, Transformers: nil}, + {Name: "REPO_SSH_URL", Value: generateRepo.CloneLinkGeneral(ctx).SSH, Transformers: nil}, + {Name: "TEMPLATE_SSH_URL", Value: templateRepo.CloneLinkGeneral(ctx).SSH, Transformers: nil}, } expansionMap := make(map[string]string) @@ -123,7 +122,7 @@ func (gt *GiteaTemplate) Globs() []glob.Glob { return gt.globs } -func checkGiteaTemplate(tmpDir string) (*GiteaTemplate, error) { +func readGiteaTemplateFile(tmpDir string) (*GiteaTemplate, error) { gtPath := filepath.Join(tmpDir, ".gitea", "template") if _, err := os.Stat(gtPath); os.IsNotExist(err) { return nil, nil @@ -136,12 +135,55 @@ func checkGiteaTemplate(tmpDir string) (*GiteaTemplate, error) { return nil, err } - gt := &GiteaTemplate{ - Path: gtPath, - Content: content, - } + return &GiteaTemplate{Path: gtPath, Content: content}, nil +} - return gt, nil +func processGiteaTemplateFile(ctx context.Context, tmpDir string, templateRepo, generateRepo *repo_model.Repository, giteaTemplateFile *GiteaTemplate) error { + if err := util.Remove(giteaTemplateFile.Path); err != nil { + return fmt.Errorf("remove .giteatemplate: %w", err) + } + if len(giteaTemplateFile.Globs()) == 0 { + return nil // Avoid walking tree if there are no globs + } + tmpDirSlash := strings.TrimSuffix(filepath.ToSlash(tmpDir), "/") + "/" + return filepath.WalkDir(tmpDirSlash, func(path string, d os.DirEntry, walkErr error) error { + if walkErr != nil { + return walkErr + } + + if d.IsDir() { + return nil + } + + base := strings.TrimPrefix(filepath.ToSlash(path), tmpDirSlash) + for _, g := range giteaTemplateFile.Globs() { + if g.Match(base) { + content, err := os.ReadFile(path) + if err != nil { + return err + } + + generatedContent := []byte(generateExpansion(ctx, string(content), templateRepo, generateRepo, false)) + if err := os.WriteFile(path, generatedContent, 0o644); err != nil { + return err + } + + substPath := filepath.FromSlash(filepath.Join(tmpDirSlash, generateExpansion(ctx, base, templateRepo, generateRepo, true))) + + // Create parent subdirectories if needed or continue silently if it exists + if err = os.MkdirAll(filepath.Dir(substPath), 0o755); err != nil { + return err + } + + // Substitute filename variables + if err = os.Rename(path, substPath); err != nil { + return err + } + break + } + } + return nil + }) // end: WalkDir } func generateRepoCommit(ctx context.Context, repo, templateRepo, generateRepo *repo_model.Repository, tmpDir string) error { @@ -167,81 +209,43 @@ func generateRepoCommit(ctx context.Context, repo, templateRepo, generateRepo *r return fmt.Errorf("git clone: %w", err) } - if err := util.RemoveAll(path.Join(tmpDir, ".git")); err != nil { + // Get active submodules from the template + submodules, err := git.GetTemplateSubmoduleCommits(ctx, tmpDir) + if err != nil { + return fmt.Errorf("GetTemplateSubmoduleCommits: %w", err) + } + + if err = util.RemoveAll(filepath.Join(tmpDir, ".git")); err != nil { return fmt.Errorf("remove git dir: %w", err) } // Variable expansion - gt, err := checkGiteaTemplate(tmpDir) + giteaTemplateFile, err := readGiteaTemplateFile(tmpDir) if err != nil { - return fmt.Errorf("checkGiteaTemplate: %w", err) + return fmt.Errorf("readGiteaTemplateFile: %w", err) } - if gt != nil { - if err := util.Remove(gt.Path); err != nil { - return fmt.Errorf("remove .giteatemplate: %w", err) - } - - // Avoid walking tree if there are no globs - if len(gt.Globs()) > 0 { - tmpDirSlash := strings.TrimSuffix(filepath.ToSlash(tmpDir), "/") + "/" - if err := filepath.WalkDir(tmpDirSlash, func(path string, d os.DirEntry, walkErr error) error { - if walkErr != nil { - return walkErr - } - - if d.IsDir() { - return nil - } - - base := strings.TrimPrefix(filepath.ToSlash(path), tmpDirSlash) - for _, g := range gt.Globs() { - if g.Match(base) { - content, err := os.ReadFile(path) - if err != nil { - return err - } - - if err := os.WriteFile(path, - []byte(generateExpansion(string(content), templateRepo, generateRepo, false)), - 0o644); err != nil { - return err - } - - substPath := filepath.FromSlash(filepath.Join(tmpDirSlash, - generateExpansion(base, templateRepo, generateRepo, true))) - - // Create parent subdirectories if needed or continue silently if it exists - if err := os.MkdirAll(filepath.Dir(substPath), 0o755); err != nil { - return err - } - - // Substitute filename variables - if err := os.Rename(path, substPath); err != nil { - return err - } - - break - } - } - return nil - }); err != nil { - return err - } + if giteaTemplateFile != nil { + err = processGiteaTemplateFile(ctx, tmpDir, templateRepo, generateRepo, giteaTemplateFile) + if err != nil { + return err } } - if err := git.InitRepository(ctx, tmpDir, false, templateRepo.ObjectFormatName); err != nil { + if err = git.InitRepository(ctx, tmpDir, false, templateRepo.ObjectFormatName); err != nil { return err } - repoPath := repo.RepoPath() - if stdout, _, err := git.NewCommand(ctx, "remote", "add", "origin").AddDynamicArguments(repoPath). + if stdout, _, err := git.NewCommand(ctx, "remote", "add", "origin").AddDynamicArguments(repo.RepoPath()). RunStdString(&git.RunOpts{Dir: tmpDir, Env: env}); err != nil { log.Error("Unable to add %v as remote origin to temporary repo to %s: stdout %s\nError: %v", repo, tmpDir, stdout, err) return fmt.Errorf("git remote add: %w", err) } + if err = git.AddTemplateSubmoduleIndexes(ctx, tmpDir, submodules); err != nil { + return fmt.Errorf("failed to add submodules: %v", err) + } + // set default branch based on whether it's specified in the newly generated repo or not defaultBranch := repo.DefaultBranch if strings.TrimSpace(defaultBranch) == "" { diff --git a/modules/gitgraph/graph.go b/services/repository/gitgraph/graph.go similarity index 100% rename from modules/gitgraph/graph.go rename to services/repository/gitgraph/graph.go diff --git a/modules/gitgraph/graph_models.go b/services/repository/gitgraph/graph_models.go similarity index 100% rename from modules/gitgraph/graph_models.go rename to services/repository/gitgraph/graph_models.go diff --git a/modules/gitgraph/graph_test.go b/services/repository/gitgraph/graph_test.go similarity index 98% rename from modules/gitgraph/graph_test.go rename to services/repository/gitgraph/graph_test.go index 18d427acd95..2f647aaf83c 100644 --- a/modules/gitgraph/graph_test.go +++ b/services/repository/gitgraph/graph_test.go @@ -10,6 +10,8 @@ import ( "testing" "code.gitea.io/gitea/modules/git" + + "github.com/stretchr/testify/assert" ) func BenchmarkGetCommitGraph(b *testing.B) { @@ -235,9 +237,7 @@ func TestParseGlyphs(t *testing.T) { } row++ } - if len(parser.availableColors) != 9 { - t.Errorf("Expected 9 colors but have %d", len(parser.availableColors)) - } + assert.Len(t, parser.availableColors, 9) } func TestCommitStringParsing(t *testing.T) { @@ -262,9 +262,7 @@ func TestCommitStringParsing(t *testing.T) { return } - if test.commitMessage != commit.Subject { - t.Errorf("%s does not match %s", test.commitMessage, commit.Subject) - } + assert.Equal(t, test.commitMessage, commit.Subject) }) } } diff --git a/modules/gitgraph/parser.go b/services/repository/gitgraph/parser.go similarity index 100% rename from modules/gitgraph/parser.go rename to services/repository/gitgraph/parser.go diff --git a/services/repository/merge_upstream.go b/services/repository/merge_upstream.go index 85ca8f7e31a..34e01df723b 100644 --- a/services/repository/merge_upstream.go +++ b/services/repository/merge_upstream.go @@ -4,35 +4,38 @@ package repository import ( - "context" + "errors" "fmt" - git_model "code.gitea.io/gitea/models/git" issue_model "code.gitea.io/gitea/models/issues" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" repo_module "code.gitea.io/gitea/modules/repository" + "code.gitea.io/gitea/modules/reqctx" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/services/pull" ) -type UpstreamDivergingInfo struct { - BaseIsNewer bool - CommitsBehind int - CommitsAhead int -} - -func MergeUpstream(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, branch string) (mergeStyle string, err error) { +// MergeUpstream merges the base repository's default branch into the fork repository's current branch. +func MergeUpstream(ctx reqctx.RequestContext, doer *user_model.User, repo *repo_model.Repository, branch string) (mergeStyle string, err error) { if err = repo.MustNotBeArchived(); err != nil { return "", err } if err = repo.GetBaseRepo(ctx); err != nil { return "", err } + divergingInfo, err := GetUpstreamDivergingInfo(ctx, repo, branch) + if err != nil { + return "", err + } + if !divergingInfo.BaseBranchHasNewCommits { + return "up-to-date", nil + } + err = git.Push(ctx, repo.BaseRepo.RepoPath(), git.PushOptions{ Remote: repo.RepoPath(), - Branch: fmt.Sprintf("%s:%s", branch, branch), + Branch: fmt.Sprintf("%s:%s", divergingInfo.BaseBranchName, branch), Env: repo_module.PushingEnvironment(doer, repo), }) if err == nil { @@ -64,7 +67,7 @@ func MergeUpstream(ctx context.Context, doer *user_model.User, repo *repo_model. BaseRepoID: repo.BaseRepo.ID, BaseRepo: repo.BaseRepo, HeadBranch: branch, // maybe HeadCommitID is not needed - BaseBranch: branch, + BaseBranch: divergingInfo.BaseBranchName, } fakeIssue.PullRequest = fakePR err = pull.Update(ctx, fakePR, doer, "merge upstream", false) @@ -74,42 +77,47 @@ func MergeUpstream(ctx context.Context, doer *user_model.User, repo *repo_model. return "merge", nil } -func GetUpstreamDivergingInfo(ctx context.Context, repo *repo_model.Repository, branch string) (*UpstreamDivergingInfo, error) { - if !repo.IsFork { +// UpstreamDivergingInfo is also used in templates, so it needs to search for all references before changing it. +type UpstreamDivergingInfo struct { + BaseBranchName string + BaseBranchHasNewCommits bool + HeadBranchCommitsBehind int +} + +// GetUpstreamDivergingInfo returns the information about the divergence between the fork repository's branch and the base repository's default branch. +func GetUpstreamDivergingInfo(ctx reqctx.RequestContext, forkRepo *repo_model.Repository, forkBranch string) (*UpstreamDivergingInfo, error) { + if !forkRepo.IsFork { return nil, util.NewInvalidArgumentErrorf("repo is not a fork") } - if repo.IsArchived { + if forkRepo.IsArchived { return nil, util.NewInvalidArgumentErrorf("repo is archived") } - if err := repo.GetBaseRepo(ctx); err != nil { + if err := forkRepo.GetBaseRepo(ctx); err != nil { return nil, err } - forkBranch, err := git_model.GetBranch(ctx, repo.ID, branch) - if err != nil { - return nil, err + // Do the best to follow the GitHub's behavior, suppose there is a `branch-a` in fork repo: + // * if `branch-a` exists in base repo: try to sync `base:branch-a` to `fork:branch-a` + // * if `branch-a` doesn't exist in base repo: try to sync `base:main` to `fork:branch-a` + info, err := GetBranchDivergingInfo(ctx, forkRepo.BaseRepo, forkBranch, forkRepo, forkBranch) + if err == nil { + return &UpstreamDivergingInfo{ + BaseBranchName: forkBranch, + BaseBranchHasNewCommits: info.BaseHasNewCommits, + HeadBranchCommitsBehind: info.HeadCommitsBehind, + }, nil } - - baseBranch, err := git_model.GetBranch(ctx, repo.BaseRepo.ID, branch) - if err != nil { - return nil, err + if errors.Is(err, util.ErrNotExist) { + info, err = GetBranchDivergingInfo(ctx, forkRepo.BaseRepo, forkRepo.BaseRepo.DefaultBranch, forkRepo, forkBranch) + if err == nil { + return &UpstreamDivergingInfo{ + BaseBranchName: forkRepo.BaseRepo.DefaultBranch, + BaseBranchHasNewCommits: info.BaseHasNewCommits, + HeadBranchCommitsBehind: info.HeadCommitsBehind, + }, nil + } } - - info := &UpstreamDivergingInfo{} - if forkBranch.CommitID == baseBranch.CommitID { - return info, nil - } - - // TODO: if the fork repo has new commits, this call will fail: - // exit status 128 - fatal: Invalid symmetric difference expression aaaaaaaaaaaa...bbbbbbbbbbbb - // so at the moment, we are not able to handle this case, should be improved in the future - diff, err := git.GetDivergingCommits(ctx, repo.BaseRepo.RepoPath(), baseBranch.CommitID, forkBranch.CommitID) - if err != nil { - info.BaseIsNewer = baseBranch.UpdatedUnix > forkBranch.UpdatedUnix - return info, nil - } - info.CommitsBehind, info.CommitsAhead = diff.Behind, diff.Ahead - return info, nil + return nil, err } diff --git a/services/repository/migrate.go b/services/repository/migrate.go index 7fe63eb5ca7..6f3a87afa3e 100644 --- a/services/repository/migrate.go +++ b/services/repository/migrate.go @@ -8,7 +8,6 @@ import ( "errors" "fmt" "net/http" - "strings" "time" "code.gitea.io/gitea/models/db" @@ -254,10 +253,10 @@ func MigrateRepositoryGitData(ctx context.Context, u *user_model.User, func cleanUpMigrateGitConfig(ctx context.Context, repoPath string) error { cmd := git.NewCommand(ctx, "remote", "rm", "origin") // if the origin does not exist - _, stderr, err := cmd.RunStdString(&git.RunOpts{ + _, _, err := cmd.RunStdString(&git.RunOpts{ Dir: repoPath, }) - if err != nil && !strings.HasPrefix(stderr, "fatal: No such remote") { + if err != nil && !git.IsRemoteNotExistError(err) { return err } return nil @@ -276,7 +275,7 @@ func CleanUpMigrateInfo(ctx context.Context, repo *repo_model.Repository) (*repo } _, _, err := git.NewCommand(ctx, "remote", "rm", "origin").RunStdString(&git.RunOpts{Dir: repoPath}) - if err != nil && !strings.HasPrefix(err.Error(), "exit status 128 - fatal: No such remote ") { + if err != nil && !git.IsRemoteNotExistError(err) { return repo, fmt.Errorf("CleanUpMigrateInfo: %w", err) } diff --git a/services/repository/push.go b/services/repository/push.go index 06ad65e48fb..0ea51f9c072 100644 --- a/services/repository/push.go +++ b/services/repository/push.go @@ -275,7 +275,8 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error { } } else { notify_service.DeleteRef(ctx, pusher, repo, opts.RefFullName) - if err = pull_service.CloseBranchPulls(ctx, pusher, repo.ID, branch); err != nil { + + if err := pull_service.AdjustPullsCausedByBranchDeleted(ctx, pusher, repo, branch); err != nil { // close all related pulls log.Error("close related pull request failed: %v", err) } diff --git a/services/repository/transfer.go b/services/repository/transfer.go index 9ef28ddeb9f..bd3bf326b4f 100644 --- a/services/repository/transfer.go +++ b/services/repository/transfer.go @@ -27,19 +27,8 @@ func getRepoWorkingLockKey(repoID int64) string { return fmt.Sprintf("repo_working_%d", repoID) } -// TransferOwnership transfers all corresponding setting from old user to new one. -func TransferOwnership(ctx context.Context, doer, newOwner *user_model.User, repo *repo_model.Repository, teams []*organization.Team) error { - if err := repo.LoadOwner(ctx); err != nil { - return err - } - for _, team := range teams { - if newOwner.ID != team.OrgID { - return fmt.Errorf("team %d does not belong to organization", team.ID) - } - } - - oldOwner := repo.Owner - +// AcceptTransferOwnership transfers all corresponding setting from old user to new one. +func AcceptTransferOwnership(ctx context.Context, repo *repo_model.Repository, doer *user_model.User) error { releaser, err := globallock.Lock(ctx, getRepoWorkingLockKey(repo.ID)) if err != nil { log.Error("lock.Lock(): %v", err) @@ -47,29 +36,44 @@ func TransferOwnership(ctx context.Context, doer, newOwner *user_model.User, rep } defer releaser() - if err := transferOwnership(ctx, doer, newOwner.Name, repo); err != nil { - return err - } - releaser() - - newRepo, err := repo_model.GetRepositoryByID(ctx, repo.ID) + repoTransfer, err := repo_model.GetPendingRepositoryTransfer(ctx, repo) if err != nil { return err } - for _, team := range teams { - if err := addRepositoryToTeam(ctx, team, newRepo); err != nil { + oldOwnerName := repo.OwnerName + + if err := db.WithTx(ctx, func(ctx context.Context) error { + if err := repoTransfer.LoadAttributes(ctx); err != nil { return err } - } - notify_service.TransferRepository(ctx, doer, repo, oldOwner.Name) + if !repoTransfer.CanUserAcceptOrRejectTransfer(ctx, doer) { + return util.ErrPermissionDenied + } + + if err := repo.LoadOwner(ctx); err != nil { + return err + } + for _, team := range repoTransfer.Teams { + if repoTransfer.Recipient.ID != team.OrgID { + return fmt.Errorf("team %d does not belong to organization", team.ID) + } + } + + return transferOwnership(ctx, repoTransfer.Doer, repoTransfer.Recipient.Name, repo, repoTransfer.Teams) + }); err != nil { + return err + } + releaser() + + notify_service.TransferRepository(ctx, doer, repo, oldOwnerName) return nil } // transferOwnership transfers all corresponding repository items from old user to new one. -func transferOwnership(ctx context.Context, doer *user_model.User, newOwnerName string, repo *repo_model.Repository) (err error) { +func transferOwnership(ctx context.Context, doer *user_model.User, newOwnerName string, repo *repo_model.Repository, teams []*organization.Team) (err error) { repoRenamed := false wikiRenamed := false oldOwnerName := doer.Name @@ -138,7 +142,7 @@ func transferOwnership(ctx context.Context, doer *user_model.User, newOwnerName repo.OwnerName = newOwner.Name // Update repository. - if _, err := sess.ID(repo.ID).Update(repo); err != nil { + if err := repo_model.UpdateRepositoryCols(ctx, repo, "owner_id", "owner_name"); err != nil { return fmt.Errorf("update owner: %w", err) } @@ -174,15 +178,13 @@ func transferOwnership(ctx context.Context, doer *user_model.User, newOwnerName collaboration.UserID = 0 } - // Remove old team-repository relations. if oldOwner.IsOrganization() { + // Remove old team-repository relations. if err := organization.RemoveOrgRepo(ctx, oldOwner.ID, repo.ID); err != nil { return fmt.Errorf("removeOrgRepo: %w", err) } - } - // Remove project's issues that belong to old organization's projects - if oldOwner.IsOrganization() { + // Remove project's issues that belong to old organization's projects projects, err := project_model.GetAllProjectsIDsByOwnerIDAndType(ctx, oldOwner.ID, project_model.TypeOrganization) if err != nil { return fmt.Errorf("Unable to find old org projects: %w", err) @@ -225,15 +227,13 @@ func transferOwnership(ctx context.Context, doer *user_model.User, newOwnerName return fmt.Errorf("watchRepo: %w", err) } - // Remove watch for organization. if oldOwner.IsOrganization() { + // Remove watch for organization. if err := repo_model.WatchRepo(ctx, oldOwner, repo, false); err != nil { return fmt.Errorf("watchRepo [false]: %w", err) } - } - // Delete labels that belong to the old organization and comments that added these labels - if oldOwner.IsOrganization() { + // Delete labels that belong to the old organization and comments that added these labels if _, err := sess.Exec(`DELETE FROM issue_label WHERE issue_label.id IN ( SELECT il_too.id FROM ( SELECT il_too_too.id @@ -261,7 +261,6 @@ func transferOwnership(ctx context.Context, doer *user_model.User, newOwnerName // Rename remote repository to new path and delete local copy. dir := user_model.UserPath(newOwner.Name) - if err := os.MkdirAll(dir, os.ModePerm); err != nil { return fmt.Errorf("Failed to create dir %s: %w", dir, err) } @@ -273,7 +272,6 @@ func transferOwnership(ctx context.Context, doer *user_model.User, newOwnerName // Rename remote wiki repository to new path and delete local copy. wikiPath := repo_model.WikiPath(oldOwner.Name, repo.Name) - if isExist, err := util.IsExist(wikiPath); err != nil { log.Error("Unable to check if %s exists. Error: %v", wikiPath, err) return err @@ -301,6 +299,17 @@ func transferOwnership(ctx context.Context, doer *user_model.User, newOwnerName return fmt.Errorf("repo_model.NewRedirect: %w", err) } + newRepo, err := repo_model.GetRepositoryByID(ctx, repo.ID) + if err != nil { + return err + } + + for _, team := range teams { + if err := addRepositoryToTeam(ctx, team, newRepo); err != nil { + return err + } + } + return committer.Commit() } @@ -343,17 +352,9 @@ func changeRepositoryName(ctx context.Context, repo *repo_model.Repository, newR } } - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return err - } - defer committer.Close() - - if err := repo_model.NewRedirect(ctx, repo.Owner.ID, repo.ID, oldRepoName, newRepoName); err != nil { - return err - } - - return committer.Commit() + return db.WithTx(ctx, func(ctx context.Context) error { + return repo_model.NewRedirect(ctx, repo.Owner.ID, repo.ID, oldRepoName, newRepoName) + }) } // ChangeRepositoryName changes all corresponding setting from old repository name to new one. @@ -387,70 +388,142 @@ func ChangeRepositoryName(ctx context.Context, doer *user_model.User, repo *repo // StartRepositoryTransfer transfer a repo from one owner to a new one. // it make repository into pending transfer state, if doer can not create repo for new owner. func StartRepositoryTransfer(ctx context.Context, doer, newOwner *user_model.User, repo *repo_model.Repository, teams []*organization.Team) error { + releaser, err := globallock.Lock(ctx, getRepoWorkingLockKey(repo.ID)) + if err != nil { + return fmt.Errorf("lock.Lock: %w", err) + } + defer releaser() + if err := repo_model.TestRepositoryReadyForTransfer(repo.Status); err != nil { return err } - // Admin is always allowed to transfer || user transfer repo back to his account - if doer.IsAdmin || doer.ID == newOwner.ID { - return TransferOwnership(ctx, doer, newOwner, repo, teams) - } + var isDirectTransfer bool + oldOwnerName := repo.OwnerName - if user_model.IsUserBlockedBy(ctx, doer, newOwner.ID) { - return user_model.ErrBlockedUser - } + if err := db.WithTx(ctx, func(ctx context.Context) error { + // Admin is always allowed to transfer || user transfer repo back to his account, + // then it will transfer directly without acceptance. + if doer.IsAdmin || doer.ID == newOwner.ID { + isDirectTransfer = true + return transferOwnership(ctx, doer, newOwner.Name, repo, teams) + } - // If new owner is an org and user can create repos he can transfer directly too - if newOwner.IsOrganization() { - allowed, err := organization.CanCreateOrgRepo(ctx, newOwner.ID, doer.ID) + if user_model.IsUserBlockedBy(ctx, doer, newOwner.ID) { + return user_model.ErrBlockedUser + } + + // If new owner is an org and user can create repos he can transfer directly too + if newOwner.IsOrganization() { + allowed, err := organization.CanCreateOrgRepo(ctx, newOwner.ID, doer.ID) + if err != nil { + return err + } + if allowed { + isDirectTransfer = true + return transferOwnership(ctx, doer, newOwner.Name, repo, teams) + } + } + + // In case the new owner would not have sufficient access to the repo, give access rights for read + hasAccess, err := access_model.HasAnyUnitAccess(ctx, newOwner.ID, repo) if err != nil { return err } - if allowed { - return TransferOwnership(ctx, doer, newOwner, repo, teams) + if !hasAccess { + if err := AddOrUpdateCollaborator(ctx, repo, newOwner, perm.AccessModeRead); err != nil { + return err + } } - } - // In case the new owner would not have sufficient access to the repo, give access rights for read - hasAccess, err := access_model.HasAnyUnitAccess(ctx, newOwner.ID, repo) - if err != nil { - return err - } - if !hasAccess { - if err := AddOrUpdateCollaborator(ctx, repo, newOwner, perm.AccessModeRead); err != nil { - return err - } - } - - // Make repo as pending for transfer - repo.Status = repo_model.RepositoryPendingTransfer - if err := repo_model.CreatePendingRepositoryTransfer(ctx, doer, newOwner, repo.ID, teams); err != nil { + // Make repo as pending for transfer + repo.Status = repo_model.RepositoryPendingTransfer + return repo_model.CreatePendingRepositoryTransfer(ctx, doer, newOwner, repo.ID, teams) + }); err != nil { return err } - // notify users who are able to accept / reject transfer - notify_service.RepoPendingTransfer(ctx, doer, newOwner, repo) + if isDirectTransfer { + notify_service.TransferRepository(ctx, doer, repo, oldOwnerName) + } else { + // notify users who are able to accept / reject transfer + notify_service.RepoPendingTransfer(ctx, doer, newOwner, repo) + } return nil } -// CancelRepositoryTransfer marks the repository as ready and remove pending transfer entry, +// RejectRepositoryTransfer marks the repository as ready and remove pending transfer entry, // thus cancel the transfer process. -func CancelRepositoryTransfer(ctx context.Context, repo *repo_model.Repository) error { - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return err - } - defer committer.Close() +// The accepter can reject the transfer. +func RejectRepositoryTransfer(ctx context.Context, repo *repo_model.Repository, doer *user_model.User) error { + return db.WithTx(ctx, func(ctx context.Context) error { + repoTransfer, err := repo_model.GetPendingRepositoryTransfer(ctx, repo) + if err != nil { + return err + } - repo.Status = repo_model.RepositoryReady - if err := repo_model.UpdateRepositoryCols(ctx, repo, "status"); err != nil { - return err - } + if err := repoTransfer.LoadAttributes(ctx); err != nil { + return err + } - if err := repo_model.DeleteRepositoryTransfer(ctx, repo.ID); err != nil { - return err - } + if !repoTransfer.CanUserAcceptOrRejectTransfer(ctx, doer) { + return util.ErrPermissionDenied + } - return committer.Commit() + repo.Status = repo_model.RepositoryReady + if err := repo_model.UpdateRepositoryCols(ctx, repo, "status"); err != nil { + return err + } + + return repo_model.DeleteRepositoryTransfer(ctx, repo.ID) + }) +} + +func canUserCancelTransfer(ctx context.Context, r *repo_model.RepoTransfer, u *user_model.User) bool { + if u.IsAdmin || u.ID == r.DoerID { + return true + } + + if err := r.LoadAttributes(ctx); err != nil { + log.Error("LoadAttributes: %v", err) + return false + } + + if err := r.Repo.LoadOwner(ctx); err != nil { + log.Error("LoadOwner: %v", err) + return false + } + + if !r.Repo.Owner.IsOrganization() { + return r.Repo.OwnerID == u.ID + } + + perm, err := access_model.GetUserRepoPermission(ctx, r.Repo, u) + if err != nil { + log.Error("GetUserRepoPermission: %v", err) + return false + } + return perm.IsOwner() +} + +// CancelRepositoryTransfer cancels the repository transfer process. The sender or +// the users who have admin permission of the original repository can cancel the transfer +func CancelRepositoryTransfer(ctx context.Context, repoTransfer *repo_model.RepoTransfer, doer *user_model.User) error { + return db.WithTx(ctx, func(ctx context.Context) error { + if err := repoTransfer.LoadAttributes(ctx); err != nil { + return err + } + + if !canUserCancelTransfer(ctx, repoTransfer, doer) { + return util.ErrPermissionDenied + } + + repoTransfer.Repo.Status = repo_model.RepositoryReady + if err := repo_model.UpdateRepositoryCols(ctx, repoTransfer.Repo, "status"); err != nil { + return err + } + + return repo_model.DeleteRepositoryTransfer(ctx, repoTransfer.RepoID) + }) } diff --git a/services/repository/transfer_test.go b/services/repository/transfer_test.go index 91722fb8ae1..16a8fb6e1ee 100644 --- a/services/repository/transfer_test.go +++ b/services/repository/transfer_test.go @@ -34,23 +34,26 @@ func TestTransferOwnership(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}) - repo.Owner = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) - assert.NoError(t, TransferOwnership(db.DefaultContext, doer, doer, repo, nil)) + assert.NoError(t, repo.LoadOwner(db.DefaultContext)) + repoTransfer := unittest.AssertExistsAndLoadBean(t, &repo_model.RepoTransfer{ID: 1}) + assert.NoError(t, repoTransfer.LoadAttributes(db.DefaultContext)) + assert.NoError(t, AcceptTransferOwnership(db.DefaultContext, repo, doer)) transferredRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}) - assert.EqualValues(t, 2, transferredRepo.OwnerID) + assert.EqualValues(t, 1, transferredRepo.OwnerID) // repo_transfer.yml id=1 + unittest.AssertNotExistsBean(t, &repo_model.RepoTransfer{ID: 1}) exist, err := util.IsExist(repo_model.RepoPath("org3", "repo3")) assert.NoError(t, err) assert.False(t, exist) - exist, err = util.IsExist(repo_model.RepoPath("user2", "repo3")) + exist, err = util.IsExist(repo_model.RepoPath("user1", "repo3")) assert.NoError(t, err) assert.True(t, exist) unittest.AssertExistsAndLoadBean(t, &activities_model.Action{ OpType: activities_model.ActionTransferRepo, - ActUserID: 2, + ActUserID: 1, RepoID: 3, Content: "org3/repo3", }) @@ -61,10 +64,10 @@ func TestTransferOwnership(t *testing.T) { func TestStartRepositoryTransferSetPermission(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}) + doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) recipient := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5}) - repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}) - repo.Owner = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}) + assert.NoError(t, repo.LoadOwner(db.DefaultContext)) hasAccess, err := access_model.HasAnyUnitAccess(db.DefaultContext, recipient.ID, repo) assert.NoError(t, err) @@ -82,7 +85,7 @@ func TestStartRepositoryTransferSetPermission(t *testing.T) { func TestRepositoryTransfer(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}) + doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}) transfer, err := repo_model.GetPendingRepositoryTransfer(db.DefaultContext, repo) @@ -90,7 +93,7 @@ func TestRepositoryTransfer(t *testing.T) { assert.NotNil(t, transfer) // Cancel transfer - assert.NoError(t, CancelRepositoryTransfer(db.DefaultContext, repo)) + assert.NoError(t, CancelRepositoryTransfer(db.DefaultContext, transfer, doer)) transfer, err = repo_model.GetPendingRepositoryTransfer(db.DefaultContext, repo) assert.Error(t, err) @@ -113,10 +116,12 @@ func TestRepositoryTransfer(t *testing.T) { assert.Error(t, err) assert.True(t, repo_model.IsErrRepoTransferInProgress(err)) - // Unknown user - err = repo_model.CreatePendingRepositoryTransfer(db.DefaultContext, doer, &user_model.User{ID: 1000, LowerName: "user1000"}, repo.ID, nil) + repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}) + // Unknown user, transfer non-existent transfer repo id = 2 + err = repo_model.CreatePendingRepositoryTransfer(db.DefaultContext, doer, &user_model.User{ID: 1000, LowerName: "user1000"}, repo2.ID, nil) assert.Error(t, err) - // Cancel transfer - assert.NoError(t, CancelRepositoryTransfer(db.DefaultContext, repo)) + // Reject transfer + err = RejectRepositoryTransfer(db.DefaultContext, repo2, doer) + assert.True(t, repo_model.IsErrNoPendingTransfer(err)) } diff --git a/services/user/block.go b/services/user/block.go index c24ce5273ca..7727780dfc9 100644 --- a/services/user/block.go +++ b/services/user/block.go @@ -117,10 +117,10 @@ func BlockUser(ctx context.Context, doer, blocker, blockee *user_model.User, not } // cancel each other repository transfers - if err := cancelRepositoryTransfers(ctx, blocker, blockee); err != nil { + if err := cancelRepositoryTransfers(ctx, doer, blocker, blockee); err != nil { return err } - if err := cancelRepositoryTransfers(ctx, blockee, blocker); err != nil { + if err := cancelRepositoryTransfers(ctx, doer, blockee, blocker); err != nil { return err } @@ -192,7 +192,7 @@ func unwatchRepos(ctx context.Context, watcher, repoOwner *user_model.User) erro } } -func cancelRepositoryTransfers(ctx context.Context, sender, recipient *user_model.User) error { +func cancelRepositoryTransfers(ctx context.Context, doer, sender, recipient *user_model.User) error { transfers, err := repo_model.GetPendingRepositoryTransfers(ctx, &repo_model.PendingRepositoryTransferOptions{ SenderID: sender.ID, RecipientID: recipient.ID, @@ -202,12 +202,7 @@ func cancelRepositoryTransfers(ctx context.Context, sender, recipient *user_mode } for _, transfer := range transfers { - repo, err := repo_model.GetRepositoryByID(ctx, transfer.RepoID) - if err != nil { - return err - } - - if err := repo_service.CancelRepositoryTransfer(ctx, repo); err != nil { + if err := repo_service.CancelRepositoryTransfer(ctx, transfer, doer); err != nil { return err } } diff --git a/services/webhook/dingtalk.go b/services/webhook/dingtalk.go index e382f5a9df7..3ea8f50764d 100644 --- a/services/webhook/dingtalk.go +++ b/services/webhook/dingtalk.go @@ -170,6 +170,12 @@ func (dc dingtalkConvertor) Package(p *api.PackagePayload) (DingtalkPayload, err return createDingtalkPayload(text, text, "view package", p.Package.HTMLURL), nil } +func (dc dingtalkConvertor) Status(p *api.CommitStatusPayload) (DingtalkPayload, error) { + text, _ := getStatusPayloadInfo(p, noneLinkFormatter, true) + + return createDingtalkPayload(text, text, "Status Changed", p.TargetURL), nil +} + func createDingtalkPayload(title, text, singleTitle, singleURL string) DingtalkPayload { return DingtalkPayload{ MsgType: "actionCard", @@ -190,3 +196,7 @@ func newDingtalkRequest(_ context.Context, w *webhook_model.Webhook, t *webhook_ var pc payloadConvertor[DingtalkPayload] = dingtalkConvertor{} return newJSONRequest(pc, w, t, true) } + +func init() { + RegisterWebhookRequester(webhook_module.DINGTALK, newDingtalkRequest) +} diff --git a/services/webhook/discord.go b/services/webhook/discord.go index c562d981680..43e5e533bff 100644 --- a/services/webhook/discord.go +++ b/services/webhook/discord.go @@ -265,6 +265,12 @@ func (d discordConvertor) Package(p *api.PackagePayload) (DiscordPayload, error) return d.createPayload(p.Sender, text, "", p.Package.HTMLURL, color), nil } +func (d discordConvertor) Status(p *api.CommitStatusPayload) (DiscordPayload, error) { + text, color := getStatusPayloadInfo(p, noneLinkFormatter, false) + + return d.createPayload(p.Sender, text, "", p.TargetURL, color), nil +} + func newDiscordRequest(_ context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { meta := &DiscordMeta{} if err := json.Unmarshal([]byte(w.Meta), meta); err != nil { @@ -277,6 +283,10 @@ func newDiscordRequest(_ context.Context, w *webhook_model.Webhook, t *webhook_m return newJSONRequest(pc, w, t, true) } +func init() { + RegisterWebhookRequester(webhook_module.DISCORD, newDiscordRequest) +} + func parseHookPullRequestEventType(event webhook_module.HookEventType) (string, error) { switch event { case webhook_module.HookEventPullRequestReviewApproved: diff --git a/services/webhook/feishu.go b/services/webhook/feishu.go index 7ca7d1cf5f3..639118d2a5e 100644 --- a/services/webhook/feishu.go +++ b/services/webhook/feishu.go @@ -166,7 +166,17 @@ func (fc feishuConvertor) Package(p *api.PackagePayload) (FeishuPayload, error) return newFeishuTextPayload(text), nil } +func (fc feishuConvertor) Status(p *api.CommitStatusPayload) (FeishuPayload, error) { + text, _ := getStatusPayloadInfo(p, noneLinkFormatter, true) + + return newFeishuTextPayload(text), nil +} + func newFeishuRequest(_ context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { var pc payloadConvertor[FeishuPayload] = feishuConvertor{} return newJSONRequest(pc, w, t, true) } + +func init() { + RegisterWebhookRequester(webhook_module.FEISHU, newFeishuRequest) +} diff --git a/services/webhook/general.go b/services/webhook/general.go index dde43bb3495..91bf68600fc 100644 --- a/services/webhook/general.go +++ b/services/webhook/general.go @@ -307,6 +307,18 @@ func getPackagePayloadInfo(p *api.PackagePayload, linkFormatter linkFormatter, w return text, color } +func getStatusPayloadInfo(p *api.CommitStatusPayload, linkFormatter linkFormatter, withSender bool) (text string, color int) { + refLink := linkFormatter(p.TargetURL, p.Context+"["+p.SHA+"]:"+p.Description) + + text = fmt.Sprintf("Commit Status changed: %s", refLink) + color = greenColor + if withSender { + text += fmt.Sprintf(" by %s", linkFormatter(setting.AppURL+url.PathEscape(p.Sender.UserName), p.Sender.UserName)) + } + + return text, color +} + // ToHook convert models.Webhook to api.Hook // This function is not part of the convert package to prevent an import cycle func ToHook(repoLink string, w *webhook_model.Webhook) (*api.Hook, error) { diff --git a/services/webhook/general_test.go b/services/webhook/general_test.go index ef1ec7f324c..ec735d785a5 100644 --- a/services/webhook/general_test.go +++ b/services/webhook/general_test.go @@ -319,8 +319,8 @@ func packageTestPayload() *api.PackagePayload { AvatarURL: "http://localhost:3000/user1/avatar", }, Repository: nil, - Organization: &api.User{ - UserName: "org1", + Organization: &api.Organization{ + Name: "org1", AvatarURL: "http://localhost:3000/org1/avatar", }, Package: &api.Package{ diff --git a/services/webhook/matrix.go b/services/webhook/matrix.go index 5e9f808d8b6..ec21712837b 100644 --- a/services/webhook/matrix.go +++ b/services/webhook/matrix.go @@ -24,6 +24,10 @@ import ( webhook_module "code.gitea.io/gitea/modules/webhook" ) +func init() { + RegisterWebhookRequester(webhook_module.MATRIX, newMatrixRequest) +} + func newMatrixRequest(_ context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { meta := &MatrixMeta{} if err := json.Unmarshal([]byte(w.Meta), meta); err != nil { @@ -240,6 +244,13 @@ func (m matrixConvertor) Package(p *api.PackagePayload) (MatrixPayload, error) { return m.newPayload(text) } +func (m matrixConvertor) Status(p *api.CommitStatusPayload) (MatrixPayload, error) { + refLink := htmlLinkFormatter(p.TargetURL, p.Context+"["+p.SHA+"]:"+p.Description) + text := fmt.Sprintf("Commit Status changed: %s", refLink) + + return m.newPayload(text) +} + var urlRegex = regexp.MustCompile(`]*?href="([^">]*?)">(.*?)`) func getMessageBody(htmlText string) string { diff --git a/services/webhook/msteams.go b/services/webhook/msteams.go index 7ef96ffa273..485f695be20 100644 --- a/services/webhook/msteams.go +++ b/services/webhook/msteams.go @@ -303,6 +303,20 @@ func (m msteamsConvertor) Package(p *api.PackagePayload) (MSTeamsPayload, error) ), nil } +func (m msteamsConvertor) Status(p *api.CommitStatusPayload) (MSTeamsPayload, error) { + title, color := getStatusPayloadInfo(p, noneLinkFormatter, false) + + return createMSTeamsPayload( + p.Repo, + p.Sender, + title, + "", + p.TargetURL, + color, + &MSTeamsFact{"CommitStatus:", p.Context}, + ), nil +} + func createMSTeamsPayload(r *api.Repository, s *api.User, title, text, actionTarget string, color int, fact *MSTeamsFact) MSTeamsPayload { facts := make([]MSTeamsFact, 0, 2) if r != nil { @@ -349,3 +363,7 @@ func newMSTeamsRequest(_ context.Context, w *webhook_model.Webhook, t *webhook_m var pc payloadConvertor[MSTeamsPayload] = msteamsConvertor{} return newJSONRequest(pc, w, t, true) } + +func init() { + RegisterWebhookRequester(webhook_module.MSTEAMS, newMSTeamsRequest) +} diff --git a/services/webhook/notifier.go b/services/webhook/notifier.go index a3d5cb34b19..6c691c21f43 100644 --- a/services/webhook/notifier.go +++ b/services/webhook/notifier.go @@ -8,6 +8,7 @@ import ( git_model "code.gitea.io/gitea/models/git" issues_model "code.gitea.io/gitea/models/issues" + "code.gitea.io/gitea/models/organization" packages_model "code.gitea.io/gitea/models/packages" "code.gitea.io/gitea/models/perm" access_model "code.gitea.io/gitea/models/perm/access" @@ -763,12 +764,10 @@ func (m *webhookNotifier) PullRequestReviewRequest(ctx context.Context, doer *us func (m *webhookNotifier) CreateRef(ctx context.Context, pusher *user_model.User, repo *repo_model.Repository, refFullName git.RefName, refID string) { apiPusher := convert.ToUser(ctx, pusher, nil) apiRepo := convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeNone}) - refName := refFullName.ShortName() - if err := PrepareWebhooks(ctx, EventSource{Repository: repo}, webhook_module.HookEventCreate, &api.CreatePayload{ - Ref: refName, // FIXME: should it be a full ref name? + Ref: refFullName.ShortName(), // FIXME: should it be a full ref name? But it will break the existing webhooks? Sha: refID, - RefType: refFullName.RefType(), + RefType: string(refFullName.RefType()), Repo: apiRepo, Sender: apiPusher, }); err != nil { @@ -800,11 +799,9 @@ func (m *webhookNotifier) PullRequestSynchronized(ctx context.Context, doer *use func (m *webhookNotifier) DeleteRef(ctx context.Context, pusher *user_model.User, repo *repo_model.Repository, refFullName git.RefName) { apiPusher := convert.ToUser(ctx, pusher, nil) apiRepo := convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeOwner}) - refName := refFullName.ShortName() - if err := PrepareWebhooks(ctx, EventSource{Repository: repo}, webhook_module.HookEventDelete, &api.DeletePayload{ - Ref: refName, // FIXME: should it be a full ref name? - RefType: refFullName.RefType(), + Ref: refFullName.ShortName(), // FIXME: should it be a full ref name? But it will break the existing webhooks? + RefType: string(refFullName.RefType()), PusherType: api.PusherTypeUser, Repo: apiRepo, Sender: apiPusher, @@ -924,10 +921,16 @@ func notifyPackage(ctx context.Context, sender *user_model.User, pd *packages_mo return } + var org *api.Organization + if pd.Owner.IsOrganization() { + org = convert.ToOrganization(ctx, organization.OrgFromUser(pd.Owner)) + } + if err := PrepareWebhooks(ctx, source, webhook_module.HookEventPackage, &api.PackagePayload{ - Action: action, - Package: apiPackage, - Sender: convert.ToUser(ctx, sender, nil), + Action: action, + Package: apiPackage, + Organization: org, + Sender: convert.ToUser(ctx, sender, nil), }); err != nil { log.Error("PrepareWebhooks: %v", err) } diff --git a/services/webhook/packagist.go b/services/webhook/packagist.go index 4d809ab3a6a..6864fc822ab 100644 --- a/services/webhook/packagist.go +++ b/services/webhook/packagist.go @@ -110,6 +110,10 @@ func (pc packagistConvertor) Package(_ *api.PackagePayload) (PackagistPayload, e return PackagistPayload{}, nil } +func (pc packagistConvertor) Status(_ *api.CommitStatusPayload) (PackagistPayload, error) { + return PackagistPayload{}, nil +} + func newPackagistRequest(_ context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { meta := &PackagistMeta{} if err := json.Unmarshal([]byte(w.Meta), meta); err != nil { @@ -120,3 +124,7 @@ func newPackagistRequest(_ context.Context, w *webhook_model.Webhook, t *webhook } return newJSONRequest(pc, w, t, true) } + +func init() { + RegisterWebhookRequester(webhook_module.PACKAGIST, newPackagistRequest) +} diff --git a/services/webhook/payloader.go b/services/webhook/payloader.go index ab280a25b66..c29ad8ac920 100644 --- a/services/webhook/payloader.go +++ b/services/webhook/payloader.go @@ -28,6 +28,7 @@ type payloadConvertor[T any] interface { Release(*api.ReleasePayload) (T, error) Wiki(*api.WikiPayload) (T, error) Package(*api.PackagePayload) (T, error) + Status(*api.CommitStatusPayload) (T, error) } func convertUnmarshalledJSON[T, P any](convert func(P) (T, error), data []byte) (t T, err error) { @@ -77,6 +78,8 @@ func newPayload[T any](rc payloadConvertor[T], data []byte, event webhook_module return convertUnmarshalledJSON(rc.Wiki, data) case webhook_module.HookEventPackage: return convertUnmarshalledJSON(rc.Package, data) + case webhook_module.HookEventStatus: + return convertUnmarshalledJSON(rc.Status, data) } return t, fmt.Errorf("newPayload unsupported event: %s", event) } diff --git a/services/webhook/slack.go b/services/webhook/slack.go index c905e7a89f4..80ed747fd1f 100644 --- a/services/webhook/slack.go +++ b/services/webhook/slack.go @@ -84,9 +84,9 @@ func SlackLinkFormatter(url, text string) string { // SlackLinkToRef slack-formatter link to a repo ref func SlackLinkToRef(repoURL, ref string) string { // FIXME: SHA1 hardcoded here - url := git.RefURL(repoURL, ref) - refName := git.RefName(ref).ShortName() - return SlackLinkFormatter(url, refName) + refName := git.RefName(ref) + url := repoURL + "/src/" + refName.RefWebLinkPath() + return SlackLinkFormatter(url, refName.ShortName()) } // Create implements payloadConvertor Create method @@ -167,6 +167,12 @@ func (s slackConvertor) Package(p *api.PackagePayload) (SlackPayload, error) { return s.createPayload(text, nil), nil } +func (s slackConvertor) Status(p *api.CommitStatusPayload) (SlackPayload, error) { + text, _ := getStatusPayloadInfo(p, SlackLinkFormatter, true) + + return s.createPayload(text, nil), nil +} + // Push implements payloadConvertor Push method func (s slackConvertor) Push(p *api.PushPayload) (SlackPayload, error) { // n new commits @@ -295,6 +301,10 @@ func newSlackRequest(_ context.Context, w *webhook_model.Webhook, t *webhook_mod return newJSONRequest(pc, w, t, true) } +func init() { + RegisterWebhookRequester(webhook_module.SLACK, newSlackRequest) +} + var slackChannel = regexp.MustCompile(`^#?[a-z0-9_-]{1,80}$`) // IsValidSlackChannel validates a channel name conforms to what slack expects: diff --git a/services/webhook/telegram.go b/services/webhook/telegram.go index e54d6f2947d..485e2d990ba 100644 --- a/services/webhook/telegram.go +++ b/services/webhook/telegram.go @@ -174,6 +174,12 @@ func (t telegramConvertor) Package(p *api.PackagePayload) (TelegramPayload, erro return createTelegramPayloadHTML(text), nil } +func (t telegramConvertor) Status(p *api.CommitStatusPayload) (TelegramPayload, error) { + text, _ := getStatusPayloadInfo(p, htmlLinkFormatter, true) + + return createTelegramPayloadHTML(text), nil +} + func createTelegramPayloadHTML(msgHTML string) TelegramPayload { // https://core.telegram.org/bots/api#formatting-options return TelegramPayload{ @@ -187,3 +193,7 @@ func newTelegramRequest(_ context.Context, w *webhook_model.Webhook, t *webhook_ var pc payloadConvertor[TelegramPayload] = telegramConvertor{} return newJSONRequest(pc, w, t, true) } + +func init() { + RegisterWebhookRequester(webhook_module.TELEGRAM, newTelegramRequest) +} diff --git a/services/webhook/webhook.go b/services/webhook/webhook.go index e0e8fa2fc19..182078b39d4 100644 --- a/services/webhook/webhook.go +++ b/services/webhook/webhook.go @@ -27,16 +27,12 @@ import ( "github.com/gobwas/glob" ) -var webhookRequesters = map[webhook_module.HookType]func(context.Context, *webhook_model.Webhook, *webhook_model.HookTask) (req *http.Request, body []byte, err error){ - webhook_module.SLACK: newSlackRequest, - webhook_module.DISCORD: newDiscordRequest, - webhook_module.DINGTALK: newDingtalkRequest, - webhook_module.TELEGRAM: newTelegramRequest, - webhook_module.MSTEAMS: newMSTeamsRequest, - webhook_module.FEISHU: newFeishuRequest, - webhook_module.MATRIX: newMatrixRequest, - webhook_module.WECHATWORK: newWechatworkRequest, - webhook_module.PACKAGIST: newPackagistRequest, +type Requester func(context.Context, *webhook_model.Webhook, *webhook_model.HookTask) (req *http.Request, body []byte, err error) + +var webhookRequesters = map[webhook_module.HookType]Requester{} + +func RegisterWebhookRequester(hookType webhook_module.HookType, requester Requester) { + webhookRequesters[hookType] = requester } // IsValidHookTaskType returns true if a webhook registered @@ -137,14 +133,8 @@ func PrepareWebhook(ctx context.Context, w *webhook_model.Webhook, event webhook return nil } - for _, e := range w.EventCheckers() { - if event == e.Type { - if !e.Has() { - return nil - } - - break - } + if !w.HasEvent(event) { + return nil } // Avoid sending "0 new commits" to non-integration relevant webhooks (e.g. slack, discord, etc.). diff --git a/services/webhook/wechatwork.go b/services/webhook/wechatwork.go index 1d8c1d7dacd..1c834b4020a 100644 --- a/services/webhook/wechatwork.go +++ b/services/webhook/wechatwork.go @@ -175,7 +175,17 @@ func (wc wechatworkConvertor) Package(p *api.PackagePayload) (WechatworkPayload, return newWechatworkMarkdownPayload(text), nil } +func (wc wechatworkConvertor) Status(p *api.CommitStatusPayload) (WechatworkPayload, error) { + text, _ := getStatusPayloadInfo(p, noneLinkFormatter, true) + + return newWechatworkMarkdownPayload(text), nil +} + func newWechatworkRequest(_ context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { var pc payloadConvertor[WechatworkPayload] = wechatworkConvertor{} return newJSONRequest(pc, w, t, true) } + +func init() { + RegisterWebhookRequester(webhook_module.WECHATWORK, newWechatworkRequest) +} diff --git a/services/wiki/wiki_test.go b/services/wiki/wiki_test.go index 0a18cffa253..e8b89f5e97b 100644 --- a/services/wiki/wiki_test.go +++ b/services/wiki/wiki_test.go @@ -17,6 +17,7 @@ import ( _ "code.gitea.io/gitea/models/actions" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestMain(m *testing.M) { @@ -166,9 +167,8 @@ func TestRepository_AddWikiPage(t *testing.T) { assert.NoError(t, AddWikiPage(git.DefaultContext, doer, repo, webPath, wikiContent, commitMsg)) // Now need to show that the page has been added: gitRepo, err := gitrepo.OpenWikiRepository(git.DefaultContext, repo) - if !assert.NoError(t, err) { - return - } + require.NoError(t, err) + defer gitRepo.Close() masterTree, err := gitRepo.GetTree(repo.DefaultWikiBranch) assert.NoError(t, err) @@ -238,9 +238,8 @@ func TestRepository_DeleteWikiPage(t *testing.T) { // Now need to show that the page has been added: gitRepo, err := gitrepo.OpenWikiRepository(git.DefaultContext, repo) - if !assert.NoError(t, err) { - return - } + require.NoError(t, err) + defer gitRepo.Close() masterTree, err := gitRepo.GetTree(repo.DefaultWikiBranch) assert.NoError(t, err) @@ -253,9 +252,8 @@ func TestPrepareWikiFileName(t *testing.T) { unittest.PrepareTestEnv(t) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) gitRepo, err := gitrepo.OpenWikiRepository(git.DefaultContext, repo) - if !assert.NoError(t, err) { - return - } + require.NoError(t, err) + defer gitRepo.Close() tests := []struct { @@ -307,9 +305,8 @@ func TestPrepareWikiFileName_FirstPage(t *testing.T) { assert.NoError(t, err) gitRepo, err := git.OpenRepository(git.DefaultContext, tmpDir) - if !assert.NoError(t, err) { - return - } + require.NoError(t, err) + defer gitRepo.Close() existence, newWikiPath, err := prepareGitPath(gitRepo, "master", "Home") diff --git a/stylelint.config.js b/stylelint.config.js index 977c35d9d5d..1153bf7308b 100644 --- a/stylelint.config.js +++ b/stylelint.config.js @@ -1,3 +1,5 @@ +// @ts-check +import {defineConfig} from 'stylelint-define-config'; import {fileURLToPath} from 'node:url'; const cssVarFiles = [ @@ -6,8 +8,8 @@ const cssVarFiles = [ fileURLToPath(new URL('web_src/css/themes/theme-gitea-dark.css', import.meta.url)), ]; -/** @type {import('stylelint').Config} */ -export default { +export default defineConfig({ + extends: 'stylelint-config-recommended', plugins: [ 'stylelint-declaration-strict-value', 'stylelint-declaration-block-no-ignored-properties', @@ -67,7 +69,7 @@ export default { '@stylistic/function-comma-space-after': null, '@stylistic/function-comma-space-before': null, '@stylistic/function-max-empty-lines': 0, - '@stylistic/function-parentheses-newline-inside': 'never-multi-line', + '@stylistic/function-parentheses-newline-inside': null, '@stylistic/function-parentheses-space-inside': null, '@stylistic/function-whitespace-after': null, '@stylistic/indentation': 2, @@ -114,134 +116,34 @@ export default { '@stylistic/value-list-comma-space-after': null, '@stylistic/value-list-comma-space-before': null, '@stylistic/value-list-max-empty-lines': 0, - 'alpha-value-notation': null, - 'annotation-no-unknown': true, - 'at-rule-allowed-list': null, - 'at-rule-disallowed-list': null, - 'at-rule-empty-line-before': null, 'at-rule-no-unknown': [true, {ignoreAtRules: ['tailwind']}], 'at-rule-no-vendor-prefix': true, - 'at-rule-property-required-list': null, - 'block-no-empty': true, - 'color-function-notation': null, - 'color-hex-alpha': null, - 'color-hex-length': null, - 'color-named': null, - 'color-no-hex': null, - 'color-no-invalid-hex': true, - 'comment-empty-line-before': null, - 'comment-no-empty': true, - 'comment-pattern': null, - 'comment-whitespace-inside': null, - 'comment-word-disallowed-list': null, 'csstools/value-no-unknown-custom-properties': [true, {importFrom: cssVarFiles}], - 'custom-media-pattern': null, - 'custom-property-empty-line-before': null, - 'custom-property-no-missing-var-function': true, - 'custom-property-pattern': null, - 'declaration-block-no-duplicate-custom-properties': true, 'declaration-block-no-duplicate-properties': [true, {ignore: ['consecutive-duplicates-with-different-values']}], - 'declaration-block-no-redundant-longhand-properties': [true, {ignoreShorthands: ['flex-flow', 'overflow']}], - 'declaration-block-no-shorthand-property-overrides': null, - 'declaration-block-single-line-max-declarations': null, - 'declaration-empty-line-before': null, - 'declaration-no-important': null, - 'declaration-property-max-values': null, - 'declaration-property-unit-allowed-list': null, + 'declaration-block-no-redundant-longhand-properties': [true, {ignoreShorthands: ['flex-flow', 'overflow', 'grid-template']}], + // @ts-expect-error - https://github.com/stylelint-types/stylelint-define-config/issues/1 'declaration-property-unit-disallowed-list': {'line-height': ['em']}, - 'declaration-property-value-allowed-list': null, + // @ts-expect-error - https://github.com/stylelint-types/stylelint-define-config/issues/1 'declaration-property-value-disallowed-list': {'word-break': ['break-word']}, - 'declaration-property-value-no-unknown': true, 'font-family-name-quotes': 'always-where-recommended', - 'font-family-no-duplicate-names': true, - 'font-family-no-missing-generic-family-keyword': true, - 'font-weight-notation': null, - 'function-allowed-list': null, - 'function-calc-no-unspaced-operator': true, - 'function-disallowed-list': null, - 'function-linear-gradient-no-nonstandard-direction': true, 'function-name-case': 'lower', - 'function-no-unknown': true, - 'function-url-no-scheme-relative': null, 'function-url-quotes': 'always', - 'function-url-scheme-allowed-list': null, - 'function-url-scheme-disallowed-list': null, - 'hue-degree-notation': null, 'import-notation': 'string', - 'keyframe-block-no-duplicate-selectors': true, - 'keyframe-declaration-no-important': true, - 'keyframe-selector-notation': null, - 'keyframes-name-pattern': null, - 'length-zero-no-unit': [true, {ignore: ['custom-properties']}, {ignoreFunctions: ['var']}], - 'max-nesting-depth': null, - 'media-feature-name-allowed-list': null, - 'media-feature-name-disallowed-list': null, - 'media-feature-name-no-unknown': true, + 'length-zero-no-unit': [true, {ignore: ['custom-properties'], ignoreFunctions: ['var']}], 'media-feature-name-no-vendor-prefix': true, - 'media-feature-name-unit-allowed-list': null, - 'media-feature-name-value-allowed-list': null, - 'media-feature-name-value-no-unknown': true, - 'media-feature-range-notation': null, - 'media-query-no-invalid': true, - 'named-grid-areas-no-invalid': true, 'no-descending-specificity': null, - 'no-duplicate-at-import-rules': true, - 'no-duplicate-selectors': true, - 'no-empty-source': true, - 'no-invalid-double-slash-comments': true, 'no-invalid-position-at-import-rule': [true, {ignoreAtRules: ['tailwind']}], - 'no-irregular-whitespace': true, 'no-unknown-animations': null, // disabled until stylelint supports multi-file linting 'no-unknown-custom-media': null, // disabled until stylelint supports multi-file linting 'no-unknown-custom-properties': null, // disabled until stylelint supports multi-file linting - 'number-max-precision': null, 'plugin/declaration-block-no-ignored-properties': true, - 'property-allowed-list': null, - 'property-disallowed-list': null, - 'property-no-unknown': true, - 'property-no-vendor-prefix': null, - 'rule-empty-line-before': null, - 'rule-selector-property-disallowed-list': null, - 'scale-unlimited/declaration-strict-value': [['/color$/', 'font-weight'], {ignoreValues: '/^(inherit|transparent|unset|initial|currentcolor|none)$/', ignoreFunctions: false, disableFix: true, expandShorthand: true}], - 'selector-anb-no-unmatchable': true, - 'selector-attribute-name-disallowed-list': null, - 'selector-attribute-operator-allowed-list': null, - 'selector-attribute-operator-disallowed-list': null, + 'scale-unlimited/declaration-strict-value': [['/color$/', 'font-weight'], {ignoreValues: '/^(inherit|transparent|unset|initial|currentcolor|none)$/', ignoreFunctions: true, disableFix: true, expandShorthand: true}], 'selector-attribute-quotes': 'always', - 'selector-class-pattern': null, - 'selector-combinator-allowed-list': null, - 'selector-combinator-disallowed-list': null, - 'selector-disallowed-list': null, - 'selector-id-pattern': null, - 'selector-max-attribute': null, - 'selector-max-class': null, - 'selector-max-combinators': null, - 'selector-max-compound-selectors': null, - 'selector-max-id': null, - 'selector-max-pseudo-class': null, - 'selector-max-specificity': null, - 'selector-max-type': null, - 'selector-max-universal': null, - 'selector-nested-pattern': null, - 'selector-no-qualifying-type': null, 'selector-no-vendor-prefix': true, - 'selector-not-notation': null, - 'selector-pseudo-class-allowed-list': null, - 'selector-pseudo-class-disallowed-list': null, - 'selector-pseudo-class-no-unknown': true, - 'selector-pseudo-element-allowed-list': null, 'selector-pseudo-element-colon-notation': 'double', - 'selector-pseudo-element-disallowed-list': null, - 'selector-pseudo-element-no-unknown': true, 'selector-type-case': 'lower', 'selector-type-no-unknown': [true, {ignore: ['custom-elements']}], 'shorthand-property-no-redundant-values': true, - 'string-no-newline': true, - 'time-min-milliseconds': null, - 'unit-allowed-list': null, - 'unit-disallowed-list': null, - 'unit-no-unknown': true, - 'value-keyword-case': null, 'value-no-vendor-prefix': [true, {ignoreValues: ['box', 'inline-box']}], }, -}; +}); diff --git a/templates/admin/navbar.tmpl b/templates/admin/navbar.tmpl index 4116357d1d2..72584ec799c 100644 --- a/templates/admin/navbar.tmpl +++ b/templates/admin/navbar.tmpl @@ -95,7 +95,7 @@ {{ctx.Locale.Tr "admin.notices"}} -
+
{{ctx.Locale.Tr "admin.monitor"}}
diff --git a/templates/admin/perftrace.tmpl b/templates/admin/perftrace.tmpl new file mode 100644 index 00000000000..2e09f14e46d --- /dev/null +++ b/templates/admin/perftrace.tmpl @@ -0,0 +1,13 @@ +{{template "admin/layout_head" (dict "ctxData" . "pageClass" "admin monitor")}} + +
+ {{template "admin/trace_tabs" .}} + + {{range $record := .PerfTraceRecords}} +
+
{{$record.Content}}
+
+ {{end}} +
+ +{{template "admin/layout_footer" .}} diff --git a/templates/admin/stacktrace-row.tmpl b/templates/admin/stacktrace-row.tmpl index 97c361ff902..db7ed81c793 100644 --- a/templates/admin/stacktrace-row.tmpl +++ b/templates/admin/stacktrace-row.tmpl @@ -17,7 +17,10 @@
{{if or (eq .Process.Type "request") (eq .Process.Type "normal")}} - {{svg "octicon-trash" 16 "text-red"}} + {{svg "octicon-trash" 16 "text-red"}} {{end}}
diff --git a/templates/admin/stacktrace.tmpl b/templates/admin/stacktrace.tmpl index ce03d805557..c5dde6b30c1 100644 --- a/templates/admin/stacktrace.tmpl +++ b/templates/admin/stacktrace.tmpl @@ -1,22 +1,7 @@ {{template "admin/layout_head" (dict "ctxData" . "pageClass" "admin monitor")}}
-
- -
-
- - {{ctx.Locale.Tr "tool.raw_seconds"}} -
-
-
- -
+ {{template "admin/trace_tabs" .}}

{{printf "%d Goroutines" .GoroutineCount}}{{/* Goroutine is non-translatable*/}} @@ -34,15 +19,4 @@ {{end}}

- - {{template "admin/layout_footer" .}} diff --git a/templates/admin/trace_tabs.tmpl b/templates/admin/trace_tabs.tmpl new file mode 100644 index 00000000000..5066c9c41b9 --- /dev/null +++ b/templates/admin/trace_tabs.tmpl @@ -0,0 +1,19 @@ +
+ +
+
+ + {{ctx.Locale.Tr "tool.raw_seconds"}} +
+
+
+ +
diff --git a/templates/admin/user/edit.tmpl b/templates/admin/user/edit.tmpl index 41b00defb45..c04d3326606 100644 --- a/templates/admin/user/edit.tmpl +++ b/templates/admin/user/edit.tmpl @@ -128,16 +128,16 @@ -
+
- +
-
+
- +
{{if not .DisableRegularOrgCreation}} @@ -195,8 +195,7 @@
- - + {{template "shared/avatar_upload_crop" dict "LabelText" (ctx.Locale.Tr "settings.choose_new_avatar")}}
diff --git a/templates/base/head_navbar.tmpl b/templates/base/head_navbar.tmpl index ed7a8d6f243..baf37494b98 100644 --- a/templates/base/head_navbar.tmpl +++ b/templates/base/head_navbar.tmpl @@ -70,7 +70,7 @@ {{svg "octicon-triangle-down"}}