diff --git a/.github/workflows/cron-licenses.yml b/.github/workflows/cron-licenses.yml index c34066d318..12f52289b6 100644 --- a/.github/workflows/cron-licenses.yml +++ b/.github/workflows/cron-licenses.yml @@ -10,8 +10,8 @@ jobs: runs-on: ubuntu-latest if: github.repository == 'go-gitea/gitea' steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 + - uses: actions/checkout@v5 + - uses: actions/setup-go@v6 with: go-version-file: go.mod check-latest: true diff --git a/.github/workflows/cron-translations.yml b/.github/workflows/cron-translations.yml index f1b51debf1..ae2238ad2d 100644 --- a/.github/workflows/cron-translations.yml +++ b/.github/workflows/cron-translations.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest if: github.repository == 'go-gitea/gitea' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: crowdin/github-action@v1 with: upload_sources: true diff --git a/.github/workflows/files-changed.yml b/.github/workflows/files-changed.yml index edceef0092..b21341a277 100644 --- a/.github/workflows/files-changed.yml +++ b/.github/workflows/files-changed.yml @@ -34,7 +34,7 @@ jobs: swagger: ${{ steps.changes.outputs.swagger }} yaml: ${{ steps.changes.outputs.yaml }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: dorny/paths-filter@v3 id: changes with: diff --git a/.github/workflows/pull-compliance.yml b/.github/workflows/pull-compliance.yml index 6f8991ed4e..f73772e934 100644 --- a/.github/workflows/pull-compliance.yml +++ b/.github/workflows/pull-compliance.yml @@ -16,8 +16,8 @@ jobs: needs: files-changed runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 + - uses: actions/checkout@v5 + - uses: actions/setup-go@v6 with: go-version-file: go.mod check-latest: true @@ -31,7 +31,7 @@ jobs: needs: files-changed runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: astral-sh/setup-uv@v6 - run: uv python install 3.12 - uses: pnpm/action-setup@v4 @@ -47,7 +47,7 @@ jobs: needs: files-changed runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: astral-sh/setup-uv@v6 - run: uv python install 3.12 - run: make deps-py @@ -58,7 +58,7 @@ jobs: needs: files-changed runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v5 with: @@ -71,8 +71,8 @@ jobs: needs: files-changed runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 + - uses: actions/checkout@v5 + - uses: actions/setup-go@v6 with: go-version-file: go.mod check-latest: true @@ -83,8 +83,8 @@ jobs: needs: files-changed runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 + - uses: actions/checkout@v5 + - uses: actions/setup-go@v6 with: go-version-file: go.mod check-latest: true @@ -100,8 +100,8 @@ jobs: needs: files-changed runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 + - uses: actions/checkout@v5 + - uses: actions/setup-go@v6 with: go-version-file: go.mod check-latest: true @@ -115,8 +115,8 @@ jobs: needs: files-changed runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 + - uses: actions/checkout@v5 + - uses: actions/setup-go@v6 with: go-version-file: go.mod check-latest: true @@ -128,7 +128,7 @@ jobs: needs: files-changed runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v5 with: @@ -144,8 +144,8 @@ jobs: needs: files-changed runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 + - uses: actions/checkout@v5 + - uses: actions/setup-go@v6 with: go-version-file: go.mod check-latest: true @@ -176,7 +176,7 @@ jobs: needs: files-changed runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v5 with: @@ -189,8 +189,8 @@ jobs: needs: files-changed runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 + - uses: actions/checkout@v5 + - uses: actions/setup-go@v6 with: go-version-file: go.mod check-latest: true diff --git a/.github/workflows/pull-db-tests.yml b/.github/workflows/pull-db-tests.yml index a7ad7ed5c3..21ec76b48e 100644 --- a/.github/workflows/pull-db-tests.yml +++ b/.github/workflows/pull-db-tests.yml @@ -38,8 +38,8 @@ jobs: ports: - "9000:9000" steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 + - uses: actions/checkout@v5 + - uses: actions/setup-go@v6 with: go-version-file: go.mod check-latest: true @@ -66,8 +66,8 @@ jobs: needs: files-changed runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 + - uses: actions/checkout@v5 + - uses: actions/setup-go@v6 with: go-version-file: go.mod check-latest: true @@ -124,8 +124,8 @@ jobs: ports: - 10000:10000 steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 + - uses: actions/checkout@v5 + - uses: actions/setup-go@v6 with: go-version-file: go.mod check-latest: true @@ -177,8 +177,8 @@ jobs: - "587:587" - "993:993" steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 + - uses: actions/checkout@v5 + - uses: actions/setup-go@v6 with: go-version-file: go.mod check-latest: true @@ -217,8 +217,8 @@ jobs: ports: - 10000:10000 steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 + - uses: actions/checkout@v5 + - uses: actions/setup-go@v6 with: go-version-file: go.mod check-latest: true diff --git a/.github/workflows/pull-e2e-tests.yml b/.github/workflows/pull-e2e-tests.yml index 89b32260ca..4f806e93bd 100644 --- a/.github/workflows/pull-e2e-tests.yml +++ b/.github/workflows/pull-e2e-tests.yml @@ -18,8 +18,8 @@ jobs: needs: files-changed runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 + - uses: actions/checkout@v5 + - uses: actions/setup-go@v6 with: go-version-file: go.mod check-latest: true diff --git a/.github/workflows/pull-labeler.yml b/.github/workflows/pull-labeler.yml index 812819b599..d05483e56c 100644 --- a/.github/workflows/pull-labeler.yml +++ b/.github/workflows/pull-labeler.yml @@ -15,6 +15,6 @@ jobs: contents: read pull-requests: write steps: - - uses: actions/labeler@v5 + - uses: actions/labeler@v6 with: sync-labels: true diff --git a/.github/workflows/release-nightly.yml b/.github/workflows/release-nightly.yml index 3d652e4ad8..16ce0fd643 100644 --- a/.github/workflows/release-nightly.yml +++ b/.github/workflows/release-nightly.yml @@ -12,11 +12,11 @@ jobs: nightly-binary: runs-on: namespace-profile-gitea-release-binary steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 # fetch all commits instead of only the last as some branches are long lived and could have many between versions # fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567 - run: git fetch --unshallow --quiet --tags --force - - uses: actions/setup-go@v5 + - uses: actions/setup-go@v6 with: go-version-file: go.mod check-latest: true @@ -61,11 +61,11 @@ jobs: permissions: packages: write # to publish to ghcr.io steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 # fetch all commits instead of only the last as some branches are long lived and could have many between versions # fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567 - run: git fetch --unshallow --quiet --tags --force - - uses: actions/setup-go@v5 + - uses: actions/setup-go@v6 with: go-version-file: go.mod check-latest: true @@ -103,11 +103,11 @@ jobs: permissions: packages: write # to publish to ghcr.io steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 # fetch all commits instead of only the last as some branches are long lived and could have many between versions # fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567 - run: git fetch --unshallow --quiet --tags --force - - uses: actions/setup-go@v5 + - uses: actions/setup-go@v6 with: go-version-file: go.mod check-latest: true diff --git a/.github/workflows/release-tag-rc.yml b/.github/workflows/release-tag-rc.yml index f4776a9ed8..c239ff392b 100644 --- a/.github/workflows/release-tag-rc.yml +++ b/.github/workflows/release-tag-rc.yml @@ -13,11 +13,11 @@ jobs: binary: runs-on: namespace-profile-gitea-release-binary steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 # fetch all commits instead of only the last as some branches are long lived and could have many between versions # fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567 - run: git fetch --unshallow --quiet --tags --force - - uses: actions/setup-go@v5 + - uses: actions/setup-go@v6 with: go-version-file: go.mod check-latest: true @@ -71,7 +71,7 @@ jobs: permissions: packages: write # to publish to ghcr.io steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 # fetch all commits instead of only the last as some branches are long lived and could have many between versions # fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567 - run: git fetch --unshallow --quiet --tags --force @@ -112,7 +112,7 @@ jobs: permissions: packages: write # to publish to ghcr.io steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 # fetch all commits instead of only the last as some branches are long lived and could have many between versions # fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567 - run: git fetch --unshallow --quiet --tags --force diff --git a/.github/workflows/release-tag-version.yml b/.github/workflows/release-tag-version.yml index ad0820f31f..289b0e9d9c 100644 --- a/.github/workflows/release-tag-version.yml +++ b/.github/workflows/release-tag-version.yml @@ -17,11 +17,11 @@ jobs: permissions: packages: write # to publish to ghcr.io steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 # fetch all commits instead of only the last as some branches are long lived and could have many between versions # fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567 - run: git fetch --unshallow --quiet --tags --force - - uses: actions/setup-go@v5 + - uses: actions/setup-go@v6 with: go-version-file: go.mod check-latest: true @@ -75,7 +75,7 @@ jobs: permissions: packages: write # to publish to ghcr.io steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 # fetch all commits instead of only the last as some branches are long lived and could have many between versions # fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567 - run: git fetch --unshallow --quiet --tags --force @@ -118,7 +118,7 @@ jobs: docker-rootless: runs-on: namespace-profile-gitea-release-docker steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 # fetch all commits instead of only the last as some branches are long lived and could have many between versions # fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567 - run: git fetch --unshallow --quiet --tags --force diff --git a/.gitignore b/.gitignore index a580861a51..821b1b8c67 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,9 @@ __debug_bin* # Visual Studio /.vs/ +# mise version managment tool +mise.toml + *.cgo1.go *.cgo2.c _cgo_defun.c @@ -121,4 +124,3 @@ prime/ /AGENT.md /CLAUDE.md /llms.txt - diff --git a/.golangci.yml b/.golangci.yml index 2ad39fbae2..483843bc55 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -153,6 +153,7 @@ linters: text: '(?i)exitAfterDefer:' paths: - node_modules + - .venv - public - web_src - third_party$ @@ -172,6 +173,7 @@ formatters: generated: lax paths: - node_modules + - .venv - public - web_src - third_party$ diff --git a/Dockerfile b/Dockerfile index 78a556497a..b60d94cc47 100644 --- a/Dockerfile +++ b/Dockerfile @@ -26,20 +26,16 @@ WORKDIR ${GOPATH}/src/code.gitea.io/gitea RUN if [ -n "${GITEA_VERSION}" ]; then git checkout "${GITEA_VERSION}"; fi \ && make clean-all build -# Begin env-to-ini build -RUN go build contrib/environment-to-ini/environment-to-ini.go - # Copy local files COPY docker/root /tmp/local # Set permissions RUN chmod 755 /tmp/local/usr/bin/entrypoint \ - /tmp/local/usr/local/bin/gitea \ + /tmp/local/usr/local/bin/* \ /tmp/local/etc/s6/gitea/* \ /tmp/local/etc/s6/openssh/* \ /tmp/local/etc/s6/.s6-svscan/* \ - /go/src/code.gitea.io/gitea/gitea \ - /go/src/code.gitea.io/gitea/environment-to-ini + /go/src/code.gitea.io/gitea/gitea FROM docker.io/library/alpine:3.22 LABEL maintainer="maintainers@gitea.io" @@ -82,4 +78,3 @@ CMD ["/usr/bin/s6-svscan", "/etc/s6"] COPY --from=build-env /tmp/local / COPY --from=build-env /go/src/code.gitea.io/gitea/gitea /app/gitea/gitea -COPY --from=build-env /go/src/code.gitea.io/gitea/environment-to-ini /usr/local/bin/environment-to-ini diff --git a/Dockerfile.rootless b/Dockerfile.rootless index e83c1af33b..f7a0412be2 100644 --- a/Dockerfile.rootless +++ b/Dockerfile.rootless @@ -26,18 +26,12 @@ WORKDIR ${GOPATH}/src/code.gitea.io/gitea RUN if [ -n "${GITEA_VERSION}" ]; then git checkout "${GITEA_VERSION}"; fi \ && make clean-all build -# Begin env-to-ini build -RUN go build contrib/environment-to-ini/environment-to-ini.go - # Copy local files COPY docker/rootless /tmp/local # Set permissions -RUN chmod 755 /tmp/local/usr/local/bin/docker-entrypoint.sh \ - /tmp/local/usr/local/bin/docker-setup.sh \ - /tmp/local/usr/local/bin/gitea \ - /go/src/code.gitea.io/gitea/gitea \ - /go/src/code.gitea.io/gitea/environment-to-ini +RUN chmod 755 /tmp/local/usr/local/bin/* \ + /go/src/code.gitea.io/gitea/gitea FROM docker.io/library/alpine:3.22 LABEL maintainer="maintainers@gitea.io" @@ -71,7 +65,6 @@ RUN chown git:git /var/lib/gitea /etc/gitea COPY --from=build-env /tmp/local / COPY --from=build-env --chown=root:root /go/src/code.gitea.io/gitea/gitea /app/gitea/gitea -COPY --from=build-env --chown=root:root /go/src/code.gitea.io/gitea/environment-to-ini /usr/local/bin/environment-to-ini # git:git USER 1000:1000 diff --git a/Makefile b/Makefile index fc507367e7..e81dab7f6c 100644 --- a/Makefile +++ b/Makefile @@ -31,11 +31,11 @@ XGO_VERSION := go-1.25.x AIR_PACKAGE ?= github.com/air-verse/air@v1 EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/v3/cmd/editorconfig-checker@v3 -GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.9.1 -GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.4.0 +GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.9.2 +GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.5.0 GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.15 MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.7.0 -SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@717e3cb29becaaf00e56953556c6d80f8a01b286 +SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.33.1 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 @@ -258,7 +258,7 @@ clean: ## delete backend and integration files .PHONY: fmt fmt: ## format the Go and template code - @GOFUMPT_PACKAGE=$(GOFUMPT_PACKAGE) $(GO) run build/code-batch-process.go gitea-fmt -w '{file-list}' + @GOFUMPT_PACKAGE=$(GOFUMPT_PACKAGE) $(GO) run tools/code-batch-process.go gitea-fmt -w '{file-list}' $(eval TEMPLATES := $(shell find templates -type f -name '*.tmpl')) @# strip whitespace after '{{' or '(' and before '}}' or ')' unless there is only @# whitespace before it @@ -472,7 +472,7 @@ test\#%: coverage: grep '^\(mode: .*\)\|\(.*:[0-9]\+\.[0-9]\+,[0-9]\+\.[0-9]\+ [0-9]\+ [0-9]\+\)$$' coverage.out > coverage-bodged.out grep '^\(mode: .*\)\|\(.*:[0-9]\+\.[0-9]\+,[0-9]\+\.[0-9]\+ [0-9]\+ [0-9]\+\)$$' integration.coverage.out > integration.coverage-bodged.out - $(GO) run build/gocovmerge.go integration.coverage-bodged.out coverage-bodged.out > coverage.all + $(GO) run tools/gocovmerge.go integration.coverage-bodged.out coverage-bodged.out > coverage.all .PHONY: unit-test-coverage unit-test-coverage: diff --git a/assets/go-licenses.json b/assets/go-licenses.json index 9c19080e24..b105757683 100644 --- a/assets/go-licenses.json +++ b/assets/go-licenses.json @@ -1096,8 +1096,13 @@ }, { "name": "github.com/sorairolake/lzip-go", - "path": "github.com/sorairolake/lzip-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\n---\n\nMIT License\n\nCopyright (c) 2024 Shun Sakai\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" + "path": "github.com/sorairolake/lzip-go/LICENSE-APACHE", + "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/spf13/afero", + "path": "github.com/spf13/afero/LICENSE.txt", + "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" }, { "name": "github.com/ssor/bom", @@ -1225,8 +1230,8 @@ "licenseText": "Copyright (c) 2016-2024 Uber Technologies, Inc.\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": "go4.org", - "path": "go4.org/LICENSE", + "name": "go4.org/readerutil", + "path": "go4.org/readerutil/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\n" }, { diff --git a/cmd/admin.go b/cmd/admin.go index 5c58a40ca2..a01274b90e 100644 --- a/cmd/admin.go +++ b/cmd/admin.go @@ -121,7 +121,7 @@ func runRepoSyncReleases(ctx context.Context, _ *cli.Command) error { } log.Trace("Processing next %d repos of %d", len(repos), count) for _, repo := range repos { - log.Trace("Synchronizing repo %s with path %s", repo.FullName(), repo.RepoPath()) + log.Trace("Synchronizing repo %s with path %s", repo.FullName(), repo.RelativePath()) gitRepo, err := gitrepo.OpenRepository(ctx, repo) if err != nil { log.Warn("OpenRepository: %v", err) @@ -147,7 +147,7 @@ func runRepoSyncReleases(ctx context.Context, _ *cli.Command) error { continue } - log.Trace(" repo %s releases synchronized to tags: from %d to %d", + log.Trace("repo %s releases synchronized to tags: from %d to %d", repo.FullName(), oldnum, count) gitRepo.Close() } diff --git a/cmd/admin_user_create.go b/cmd/admin_user_create.go index cbdb5f90e2..7e5675cf58 100644 --- a/cmd/admin_user_create.go +++ b/cmd/admin_user_create.go @@ -151,6 +151,7 @@ func runCreateUser(ctx context.Context, c *cli.Command) error { if err != nil { return err } + // codeql[disable-next-line=go/clear-text-logging] fmt.Printf("generated random password is '%s'\n", password) } else if userType == user_model.UserTypeIndividual { return errors.New("must set either password or random-password flag") diff --git a/cmd/admin_user_must_change_password.go b/cmd/admin_user_must_change_password.go index 8521853dc1..468d462b74 100644 --- a/cmd/admin_user_must_change_password.go +++ b/cmd/admin_user_must_change_password.go @@ -58,6 +58,7 @@ func runMustChangePassword(ctx context.Context, c *cli.Command) error { return err } + // codeql[disable-next-line=go/clear-text-logging] fmt.Printf("Updated %d users setting MustChangePassword to %t\n", n, mustChangePassword) return nil } diff --git a/cmd/config.go b/cmd/config.go new file mode 100644 index 0000000000..5303b0e1e0 --- /dev/null +++ b/cmd/config.go @@ -0,0 +1,156 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package cmd + +import ( + "context" + "errors" + "fmt" + "os" + + "code.gitea.io/gitea/modules/setting" + + "github.com/urfave/cli/v3" +) + +func cmdConfig() *cli.Command { + subcmdConfigEditIni := &cli.Command{ + Name: "edit-ini", + Usage: "Load an existing INI file, apply environment variables, keep specified keys, and output to a new INI file.", + Description: ` +Help users to edit the Gitea configuration INI file. + +# Keep Specified Keys + +If you need to re-create the configuration file with only a subset of keys, +you can provide an INI template file for the kept keys and use the "--config-keep-keys" flag. +For example, if a helm chart needs to reset the settings and only keep SECRET_KEY, +it can use a template file (only keys take effect, values are ignored): + + [security] + SECRET_KEY= + +$ ./gitea config edit-ini --config app-old.ini --config-keep-keys app-keys.ini --out app-new.ini + +# Map Environment Variables to INI Configuration + +Environment variables of the form "GITEA__section_name__KEY_NAME" +will be mapped to the ini section "[section_name]" and the key +"KEY_NAME" with the value as provided. + +Environment variables of the form "GITEA__section_name__KEY_NAME__FILE" +will be mapped to the ini section "[section_name]" and the key +"KEY_NAME" with the value loaded from the specified file. + +Environment variable keys can only contain characters "0-9A-Z_", +if a section or key name contains dot ".", it needs to be escaped as _0x2E_. +For example, to apply this config: + + [git.config] + foo.bar=val + +$ export GITEA__git_0x2E_config__foo_0x2E_bar=val + +# Put All Together + +$ ./gitea config edit-ini --config app.ini --config-keep-keys app-keys.ini --apply-env {--in-place|--out app-new.ini} +`, + Flags: []cli.Flag{ + // "--config" flag is provided by global flags, and this flag is also used by "environment-to-ini" script wrapper + // "--in-place" is also used by "environment-to-ini" script wrapper for its old behavior: always overwrite the existing config file + &cli.BoolFlag{ + Name: "in-place", + Usage: "Output to the same config file as input. This flag will be ignored if --out is set.", + }, + &cli.StringFlag{ + Name: "config-keep-keys", + Usage: "An INI template file containing keys for keeping. Only the keys defined in the INI template will be kept from old config. If not set, all keys will be kept.", + }, + &cli.BoolFlag{ + Name: "apply-env", + Usage: "Apply all GITEA__* variables from the environment to the config.", + }, + &cli.StringFlag{ + Name: "out", + Usage: "Destination config file to write to.", + }, + }, + Action: runConfigEditIni, + } + + return &cli.Command{ + Name: "config", + Usage: "Manage Gitea configuration", + Commands: []*cli.Command{ + subcmdConfigEditIni, + }, + } +} + +func runConfigEditIni(_ context.Context, c *cli.Command) error { + // the config system may change the environment variables, so get a copy first, to be used later + env := append([]string{}, os.Environ()...) + + // don't use the guessed setting.CustomConf, instead, require the user to provide --config explicitly + if !c.IsSet("config") { + return errors.New("flag is required but not set: --config") + } + configFileIn := c.String("config") + + cfgIn, err := setting.NewConfigProviderFromFile(configFileIn) + if err != nil { + return fmt.Errorf("failed to load config file %q: %v", configFileIn, err) + } + + // determine output config file: use "--out" flag or use "--in-place" flag to overwrite input file + inPlace := c.Bool("in-place") + configFileOut := c.String("out") + if configFileOut == "" { + if !inPlace { + return errors.New("either --in-place or --out must be specified") + } + configFileOut = configFileIn // in-place edit + } + + needWriteOut := configFileOut != configFileIn + + cfgOut := cfgIn + configKeepKeys := c.String("config-keep-keys") + if configKeepKeys != "" { + needWriteOut = true + cfgOut, err = setting.NewConfigProviderFromFile(configKeepKeys) + if err != nil { + return fmt.Errorf("failed to load config-keep-keys template file %q: %v", configKeepKeys, err) + } + + for _, secOut := range cfgOut.Sections() { + for _, keyOut := range secOut.Keys() { + secIn := cfgIn.Section(secOut.Name()) + keyIn := setting.ConfigSectionKey(secIn, keyOut.Name()) + if keyIn != nil { + keyOut.SetValue(keyIn.String()) + } else { + secOut.DeleteKey(keyOut.Name()) + } + } + if len(secOut.Keys()) == 0 { + cfgOut.DeleteSection(secOut.Name()) + } + } + } + + if c.Bool("apply-env") { + if setting.EnvironmentToConfig(cfgOut, env) { + needWriteOut = true + } + } + + if needWriteOut { + err = cfgOut.SaveTo(configFileOut) + if err != nil { + return err + } + } + return nil +} diff --git a/cmd/config_test.go b/cmd/config_test.go new file mode 100644 index 0000000000..d123daa617 --- /dev/null +++ b/cmd/config_test.go @@ -0,0 +1,85 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package cmd + +import ( + "os" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestConfigEdit(t *testing.T) { + tmpDir := t.TempDir() + configOld := tmpDir + "/app-old.ini" + configTemplate := tmpDir + "/app-template.ini" + _ = os.WriteFile(configOld, []byte(` +[sec] +k1=v1 +k2=v2 +`), os.ModePerm) + + _ = os.WriteFile(configTemplate, []byte(` +[sec] +k1=in-template + +[sec2] +k3=v3 +`), os.ModePerm) + + t.Setenv("GITEA__EnV__KeY", "val") + + t.Run("OutputToNewWithEnv", func(t *testing.T) { + configNew := tmpDir + "/app-new.ini" + err := NewMainApp(AppVersion{}).Run(t.Context(), []string{ + "./gitea", "--config", configOld, + "config", "edit-ini", + "--apply-env", + "--config-keep-keys", configTemplate, + "--out", configNew, + }) + require.NoError(t, err) + + // "k1" old value is kept because its key is in the template + // "k2" is removed because it isn't in the template + // "k3" isn't in new config because it isn't in the old config + // [env] is applied from environment variable + data, _ := os.ReadFile(configNew) + require.Equal(t, `[sec] +k1 = v1 + +[env] +KeY = val +`, string(data)) + }) + + t.Run("OutputToExisting(environment-to-ini)", func(t *testing.T) { + // the legacy "environment-to-ini" (now a wrapper script) behavior: + // if no "--out", then "--in-place" must be used to overwrite the existing "--config" file + err := NewMainApp(AppVersion{}).Run(t.Context(), []string{ + "./gitea", "config", "edit-ini", + "--apply-env", + "--config", configOld, + }) + require.ErrorContains(t, err, "either --in-place or --out must be specified") + + // simulate the "environment-to-ini" behavior with "--in-place" + err = NewMainApp(AppVersion{}).Run(t.Context(), []string{ + "./gitea", "config", "edit-ini", + "--in-place", + "--apply-env", + "--config", configOld, + }) + require.NoError(t, err) + + data, _ := os.ReadFile(configOld) + require.Equal(t, `[sec] +k1 = v1 +k2 = v2 + +[env] +KeY = val +`, string(data)) + }) +} diff --git a/cmd/generate.go b/cmd/generate.go index cf491604ef..9cb4cf3917 100644 --- a/cmd/generate.go +++ b/cmd/generate.go @@ -91,6 +91,7 @@ func runGenerateSecretKey(_ context.Context, c *cli.Command) error { return err } + // codeql[disable-next-line=go/clear-text-logging] fmt.Printf("%s", secretKey) if isatty.IsTerminal(os.Stdout.Fd()) { diff --git a/cmd/hook.go b/cmd/hook.go index 2f866dd396..1845ade625 100644 --- a/cmd/hook.go +++ b/cmd/hook.go @@ -186,7 +186,7 @@ Gitea or set your environment appropriately.`, "") userID, _ := strconv.ParseInt(os.Getenv(repo_module.EnvPusherID), 10, 64) prID, _ := strconv.ParseInt(os.Getenv(repo_module.EnvPRID), 10, 64) deployKeyID, _ := strconv.ParseInt(os.Getenv(repo_module.EnvDeployKeyID), 10, 64) - actionPerm, _ := strconv.ParseInt(os.Getenv(repo_module.EnvActionPerm), 10, 64) + actionPerm, _ := strconv.Atoi(os.Getenv(repo_module.EnvActionPerm)) hookOptions := private.HookOptions{ UserID: userID, @@ -196,7 +196,7 @@ Gitea or set your environment appropriately.`, "") GitPushOptions: pushOptions(), PullRequestID: prID, DeployKeyID: deployKeyID, - ActionPerm: int(actionPerm), + ActionPerm: actionPerm, } scanner := bufio.NewScanner(os.Stdin) @@ -313,7 +313,7 @@ func runHookPostReceive(ctx context.Context, c *cli.Command) error { setup(ctx, c.Bool("debug")) // First of all run update-server-info no matter what - if _, _, err := gitcmd.NewCommand("update-server-info").RunStdString(ctx, nil); err != nil { + if _, _, err := gitcmd.NewCommand("update-server-info").RunStdString(ctx); err != nil { return fmt.Errorf("failed to call 'git update-server-info': %w", err) } diff --git a/cmd/main.go b/cmd/main.go index 3fdaf48ed9..3a38d675a1 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -128,6 +128,7 @@ func NewMainApp(appVer AppVersion) *cli.Command { // these sub-commands do not need the config file, and they do not depend on any path or environment variable. subCmdStandalone := []*cli.Command{ + cmdConfig(), cmdCert(), CmdGenerate, CmdDocs, diff --git a/cmd/serv.go b/cmd/serv.go index 76d8c81544..72ca7c4a00 100644 --- a/cmd/serv.go +++ b/cmd/serv.go @@ -13,7 +13,6 @@ import ( "path/filepath" "strconv" "strings" - "time" "unicode" asymkey_model "code.gitea.io/gitea/models/asymkey" @@ -32,7 +31,6 @@ import ( "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/services/lfs" - "github.com/golang-jwt/jwt/v5" "github.com/kballard/go-shellquote" "github.com/urfave/cli/v3" ) @@ -133,27 +131,6 @@ func getAccessMode(verb, lfsVerb string) perm.AccessMode { return perm.AccessModeNone } -func getLFSAuthToken(ctx context.Context, lfsVerb string, results *private.ServCommandResults) (string, error) { - now := time.Now() - claims := lfs.Claims{ - RegisteredClaims: jwt.RegisteredClaims{ - ExpiresAt: jwt.NewNumericDate(now.Add(setting.LFS.HTTPAuthExpiry)), - NotBefore: jwt.NewNumericDate(now), - }, - RepoID: results.RepoID, - Op: lfsVerb, - UserID: results.UserID, - } - token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - - // Sign and get the complete encoded token as a string using the secret - tokenString, err := token.SignedString(setting.LFS.JWTSecretBytes) - if err != nil { - return "", fail(ctx, "Failed to sign JWT Token", "Failed to sign JWT token: %v", err) - } - return "Bearer " + tokenString, nil -} - func runServ(ctx context.Context, c *cli.Command) error { // FIXME: This needs to internationalised setup(ctx, c.Bool("debug")) @@ -283,7 +260,7 @@ func runServ(ctx context.Context, c *cli.Command) error { // LFS SSH protocol if verb == git.CmdVerbLfsTransfer { - token, err := getLFSAuthToken(ctx, lfsVerb, results) + token, err := lfs.GetLFSAuthTokenWithBearer(lfs.AuthTokenOptions{Op: lfsVerb, UserID: results.UserID, RepoID: results.RepoID}) if err != nil { return err } @@ -294,7 +271,7 @@ func runServ(ctx context.Context, c *cli.Command) error { if verb == git.CmdVerbLfsAuthenticate { url := fmt.Sprintf("%s%s/%s.git/info/lfs", setting.AppURL, url.PathEscape(results.OwnerName), url.PathEscape(results.RepoName)) - token, err := getLFSAuthToken(ctx, lfsVerb, results) + token, err := lfs.GetLFSAuthTokenWithBearer(lfs.AuthTokenOptions{Op: lfsVerb, UserID: results.UserID, RepoID: results.RepoID}) if err != nil { return err } diff --git a/cmd/web.go b/cmd/web.go index 4723ddbbdd..6e39db2178 100644 --- a/cmd/web.go +++ b/cmd/web.go @@ -156,7 +156,6 @@ func serveInstall(cmd *cli.Command) error { case <-graceful.GetManager().IsShutdown(): <-graceful.GetManager().Done() log.Info("PID: %d Gitea Web Finished", os.Getpid()) - log.GetManager().Close() return err default: } @@ -231,7 +230,6 @@ func serveInstalled(c *cli.Command) error { err := listen(webRoutes, true) <-graceful.GetManager().Done() log.Info("PID: %d Gitea Web Finished", os.Getpid()) - log.GetManager().Close() return err } diff --git a/contrib/environment-to-ini/README b/contrib/environment-to-ini/README deleted file mode 100644 index f1d3f2ae83..0000000000 --- a/contrib/environment-to-ini/README +++ /dev/null @@ -1,47 +0,0 @@ -Environment To Ini -================== - -Multiple docker users have requested that the Gitea docker is changed -to permit arbitrary configuration via environment variables. - -Gitea needs to use an ini file for configuration because the running -environment that starts the docker may not be the same as that used -by the hooks. An ini file also gives a good default and means that -users do not have to completely provide a full environment. - -With those caveats above, this command provides a generic way of -converting suitably structured environment variables into any ini -value. - -To use the command is very simple just run it and the default gitea -app.ini will be rewritten to take account of the variables provided, -however there are various options to give slightly different -behavior and these can be interrogated with the `-h` option. - -The environment variables should be of the form: - - GITEA__SECTION_NAME__KEY_NAME - -Note, SECTION_NAME in the notation above is case-insensitive. - -Environment variables are usually restricted to a reduced character -set "0-9A-Z_" - in order to allow the setting of sections with -characters outside of that set, they should be escaped as following: -"_0X2E_" for "." and "_0X2D_" for "-". The entire section and key names -can be escaped as a UTF8 byte string if necessary. E.g. to configure: - - """ - ... - [log.console] - COLORIZE=false - STDERR=true - ... - """ - -You would set the environment variables: "GITEA__LOG_0x2E_CONSOLE__COLORIZE=false" -and "GITEA__LOG_0x2E_CONSOLE__STDERR=false". Other examples can be found -on the configuration cheat sheet. - -To build locally, run: - - go build contrib/environment-to-ini/environment-to-ini.go diff --git a/contrib/environment-to-ini/environment-to-ini.go b/contrib/environment-to-ini/environment-to-ini.go deleted file mode 100644 index 5eb576c6fe..0000000000 --- a/contrib/environment-to-ini/environment-to-ini.go +++ /dev/null @@ -1,112 +0,0 @@ -// Copyright 2019 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package main - -import ( - "context" - "os" - - "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/setting" - - "github.com/urfave/cli/v3" -) - -func main() { - app := cli.Command{} - app.Name = "environment-to-ini" - app.Usage = "Use provided environment to update configuration ini" - app.Description = `As a helper to allow docker users to update the gitea configuration - through the environment, this command allows environment variables to - be mapped to values in the ini. - - Environment variables of the form "GITEA__SECTION_NAME__KEY_NAME" - will be mapped to the ini section "[section_name]" and the key - "KEY_NAME" with the value as provided. - - Environment variables of the form "GITEA__SECTION_NAME__KEY_NAME__FILE" - will be mapped to the ini section "[section_name]" and the key - "KEY_NAME" with the value loaded from the specified file. - - Environment variables are usually restricted to a reduced character - set "0-9A-Z_" - in order to allow the setting of sections with - characters outside of that set, they should be escaped as following: - "_0X2E_" for ".". The entire section and key names can be escaped as - a UTF8 byte string if necessary. E.g. to configure: - - """ - ... - [log.console] - COLORIZE=false - STDERR=true - ... - """ - - You would set the environment variables: "GITEA__LOG_0x2E_CONSOLE__COLORIZE=false" - and "GITEA__LOG_0x2E_CONSOLE__STDERR=false". Other examples can be found - on the configuration cheat sheet.` - app.Flags = []cli.Flag{ - &cli.StringFlag{ - Name: "custom-path", - Aliases: []string{"C"}, - Value: setting.CustomPath, - Usage: "Custom path file path", - }, - &cli.StringFlag{ - Name: "config", - Aliases: []string{"c"}, - Value: setting.CustomConf, - Usage: "Custom configuration file path", - }, - &cli.StringFlag{ - Name: "work-path", - Aliases: []string{"w"}, - Value: setting.AppWorkPath, - Usage: "Set the gitea working path", - }, - &cli.StringFlag{ - Name: "out", - Aliases: []string{"o"}, - Value: "", - Usage: "Destination file to write to", - }, - } - app.Action = runEnvironmentToIni - err := app.Run(context.Background(), os.Args) - if err != nil { - log.Fatal("Failed to run app with %s: %v", os.Args, err) - } -} - -func runEnvironmentToIni(_ context.Context, c *cli.Command) error { - // the config system may change the environment variables, so get a copy first, to be used later - env := append([]string{}, os.Environ()...) - setting.InitWorkPathAndCfgProvider(os.Getenv, setting.ArgWorkPathAndCustomConf{ - WorkPath: c.String("work-path"), - CustomPath: c.String("custom-path"), - CustomConf: c.String("config"), - }) - - cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf) - if err != nil { - log.Fatal("Failed to load custom conf '%s': %v", setting.CustomConf, err) - } - - changed := setting.EnvironmentToConfig(cfg, env) - - // try to save the config file - destination := c.String("out") - if len(destination) == 0 { - destination = setting.CustomConf - } - if destination != setting.CustomConf || changed { - log.Info("Settings saved to: %q", destination) - err = cfg.SaveTo(destination) - if err != nil { - return err - } - } - - return nil -} diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index aa2fcee765..5fee78af54 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -1343,6 +1343,10 @@ LEVEL = Info ;; Dont mistake it for Reactions. ;CUSTOM_EMOJIS = gitea, codeberg, gitlab, git, github, gogs ;; +;; Comma separated list of enabled emojis, for example: smile, thumbsup, thumbsdown +;; Leave it empty to enable all emojis. +;ENABLED_EMOJIS = +;; ;; Whether the full name of the users should be shown where possible. If the full name isn't set, the username will be used. ;DEFAULT_SHOW_FULL_NAME = false ;; @@ -2536,7 +2540,19 @@ LEVEL = Info ;; * sanitized: Sanitize the content and render it inside current page, default to only allow a few HTML tags and attributes. Customized sanitizer rules can be defined in [markup.sanitizer.*] . ;; * no-sanitizer: Disable the sanitizer and render the content inside current page. It's **insecure** and may lead to XSS attack if the content contains malicious code. ;; * iframe: Render the content in a separate standalone page and embed it into current page by iframe. The iframe is in sandbox mode with same-origin disabled, and the JS code are safely isolated from parent page. -;RENDER_CONTENT_MODE=sanitized +;RENDER_CONTENT_MODE = sanitized +;; The sandbox applied to the iframe and Content-Security-Policy header when RENDER_CONTENT_MODE is `iframe`. +;; It defaults to a safe set of "allow-*" restrictions (space separated). +;; You can also set it by your requirements or use "disabled" to disable the sandbox completely. +;; When set it, make sure there is no security risk: +;; * PDF-only content: generally safe to use "disabled", and it needs to be "disabled" because PDF only renders with no sandbox. +;; * HTML content with JS: if the "RENDER_COMMAND" can guarantee there is no XSS, then it is safe, otherwise, you need to fine tune the "allow-*" restrictions. +;RENDER_CONTENT_SANDBOX = +;; Whether post-process the rendered HTML content, including: +;; resolve relative links and image sources, recognizing issue/commit references, escaping invisible characters, +;; mentioning users, rendering permlink code blocks, replacing emoji shorthands, etc. +;; By default, this is true when RENDER_CONTENT_MODE is `sanitized`, otherwise false. +;NEED_POST_PROCESS = false ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/docker/root/usr/local/bin/environment-to-ini b/docker/root/usr/local/bin/environment-to-ini new file mode 100644 index 0000000000..bb0c540685 --- /dev/null +++ b/docker/root/usr/local/bin/environment-to-ini @@ -0,0 +1,2 @@ +#!/bin/bash +exec /app/gitea/gitea config edit-ini --in-place --apply-env "$@" diff --git a/docker/rootless/usr/local/bin/environment-to-ini b/docker/rootless/usr/local/bin/environment-to-ini new file mode 100644 index 0000000000..bb0c540685 --- /dev/null +++ b/docker/rootless/usr/local/bin/environment-to-ini @@ -0,0 +1,2 @@ +#!/bin/bash +exec /app/gitea/gitea config edit-ini --in-place --apply-env "$@" diff --git a/eslint.config.ts b/eslint.config.ts index 678a49647c..d9c4bcae3a 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -49,24 +49,20 @@ export default defineConfig([ }, linterOptions: { reportUnusedDisableDirectives: 2, + reportUnusedInlineConfigs: 2, }, plugins: { '@eslint-community/eslint-comments': comments, - // @ts-expect-error '@stylistic': stylistic, '@typescript-eslint': typescriptPlugin.plugin, 'array-func': arrayFunc, // @ts-expect-error -- https://github.com/un-ts/eslint-plugin-import-x/issues/203 'import-x': importPlugin, 'no-use-extend-native': noUseExtendNative, - // @ts-expect-error regexp, - // @ts-expect-error sonarjs, - // @ts-expect-error unicorn, github, - // @ts-expect-error wc, }, settings: { @@ -595,6 +591,7 @@ export default defineConfig([ 'no-unused-vars': [0], // handled by @typescript-eslint/no-unused-vars 'no-use-before-define': [0], // handled by @typescript-eslint/no-use-before-define 'no-use-extend-native/no-use-extend-native': [2], + 'no-useless-assignment': [2], 'no-useless-backreference': [2], 'no-useless-call': [2], 'no-useless-catch': [2], @@ -900,7 +897,6 @@ export default defineConfig([ 'yoda': [2, 'never'], }, }, - // @ts-expect-error { ...playwright.configs['flat/recommended'], files: ['tests/e2e/**'], @@ -916,7 +912,6 @@ export default defineConfig([ }, }, extends: [ - // @ts-expect-error vue.configs['flat/recommended'], // @ts-expect-error vueScopedCss.configs['flat/recommended'], diff --git a/flake.lock b/flake.lock index 16a487ba13..5cb95c1aed 100644 --- a/flake.lock +++ b/flake.lock @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1755186698, - "narHash": "sha256-wNO3+Ks2jZJ4nTHMuks+cxAiVBGNuEBXsT29Bz6HASo=", + "lastModified": 1760038930, + "narHash": "sha256-Oncbh0UmHjSlxO7ErQDM3KM0A5/Znfofj2BSzlHLeVw=", "owner": "nixos", "repo": "nixpkgs", - "rev": "fbcf476f790d8a217c3eab4e12033dc4a0f6d23c", + "rev": "0b4defa2584313f3b781240b29d61f6f9f7e0df3", "type": "github" }, "original": { diff --git a/go.mod b/go.mod index a34771f0a2..81187804a3 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module code.gitea.io/gitea -go 1.25.1 +go 1.25.3 // rfc5280 said: "The serial number is an integer assigned by the CA to each certificate." // But some CAs use negative serial number, just relax the check. related: @@ -35,7 +35,7 @@ require ( github.com/bohde/codel v0.2.0 github.com/buildkite/terminal-to-html/v3 v3.16.8 github.com/caddyserver/certmagic v0.24.0 - github.com/charmbracelet/git-lfs-transfer v0.2.0 + github.com/charmbracelet/git-lfs-transfer v0.1.1-0.20251013092601-6327009efd21 github.com/chi-middleware/proxy v1.1.1 github.com/dimiro1/reply v0.0.0-20200315094148-d0136a4c9e21 github.com/djherbis/buffer v1.2.0 @@ -56,7 +56,7 @@ require ( github.com/go-co-op/gocron v1.37.0 github.com/go-enry/go-enry/v2 v2.9.2 github.com/go-git/go-billy/v5 v5.6.2 - github.com/go-git/go-git/v5 v5.16.2 + github.com/go-git/go-git/v5 v5.16.3 github.com/go-ldap/ldap/v3 v3.4.11 github.com/go-redsync/redsync/v4 v4.13.0 github.com/go-sql-driver/mysql v1.9.3 @@ -84,7 +84,7 @@ require ( github.com/mattn/go-isatty v0.0.20 github.com/mattn/go-sqlite3 v1.14.32 github.com/meilisearch/meilisearch-go v0.33.2 - github.com/mholt/archives v0.1.3 + github.com/mholt/archives v0.0.0-20251009205813-e30ac6010726 github.com/microcosm-cc/bluemonday v1.0.27 github.com/microsoft/go-mssqldb v1.9.3 github.com/minio/minio-go/v7 v7.0.95 @@ -109,20 +109,20 @@ require ( github.com/ulikunitz/xz v0.5.15 github.com/urfave/cli-docs/v3 v3.0.0-alpha6 github.com/urfave/cli/v3 v3.4.1 - github.com/wneessen/go-mail v0.7.1 + github.com/wneessen/go-mail v0.7.2 github.com/xeipuuv/gojsonschema v1.2.0 github.com/yohcop/openid-go v1.0.1 github.com/yuin/goldmark v1.7.13 github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc github.com/yuin/goldmark-meta v1.1.0 gitlab.com/gitlab-org/api/client-go v0.142.4 - golang.org/x/crypto v0.41.0 + golang.org/x/crypto v0.42.0 golang.org/x/image v0.30.0 - golang.org/x/net v0.43.0 + golang.org/x/net v0.44.0 golang.org/x/oauth2 v0.30.0 golang.org/x/sync v0.17.0 - golang.org/x/sys v0.35.0 - golang.org/x/text v0.29.0 + golang.org/x/sys v0.37.0 + golang.org/x/text v0.30.0 google.golang.org/grpc v1.75.0 google.golang.org/protobuf v1.36.8 gopkg.in/ini.v1 v1.67.0 @@ -142,7 +142,7 @@ require ( github.com/DataDog/zstd v1.5.7 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/RoaringBitmap/roaring/v2 v2.10.0 // indirect - github.com/STARRY-S/zip v0.2.1 // indirect + github.com/STARRY-S/zip v0.2.3 // indirect github.com/andybalholm/brotli v1.2.0 // indirect github.com/andybalholm/cascadia v1.3.3 // indirect github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect @@ -172,7 +172,7 @@ require ( github.com/blevesearch/zapx/v16 v16.2.4 // indirect github.com/bmatcuk/doublestar/v4 v4.9.1 // indirect github.com/bodgit/plumbing v1.3.0 // indirect - github.com/bodgit/sevenzip v1.6.0 // indirect + github.com/bodgit/sevenzip v1.6.1 // indirect github.com/bodgit/windows v1.0.1 // indirect github.com/boombuler/barcode v1.1.0 // indirect github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf // indirect @@ -233,14 +233,14 @@ require ( github.com/mikelolasagasti/xz v1.0.1 // indirect github.com/minio/crc64nvme v1.1.1 // indirect github.com/minio/md5-simd v1.1.2 // indirect - github.com/minio/minlz v1.0.0 // indirect + github.com/minio/minlz v1.0.1 // indirect github.com/mitchellh/mapstructure v1.5.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 github.com/mschoch/smat v0.2.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/nwaples/rardecode/v2 v2.1.0 // indirect + github.com/nwaples/rardecode/v2 v2.2.0 // indirect github.com/olekukonko/cat v0.0.0-20250817074551-3280053e4e00 // indirect github.com/olekukonko/errors v1.1.0 // indirect github.com/olekukonko/ll v0.1.0 // indirect @@ -259,7 +259,8 @@ require ( github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/skeema/knownhosts v1.3.1 // indirect - github.com/sorairolake/lzip-go v0.3.5 // indirect + github.com/sorairolake/lzip-go v0.3.8 // indirect + github.com/spf13/afero v1.15.0 // indirect github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect github.com/tinylib/msgp v1.4.0 // indirect github.com/unknwon/com v1.0.1 // indirect @@ -278,9 +279,9 @@ require ( go.uber.org/zap/exp v0.3.0 // indirect go4.org v0.0.0-20230225012048-214862532bf5 // indirect golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b // indirect - golang.org/x/mod v0.27.0 // indirect + golang.org/x/mod v0.28.0 // indirect golang.org/x/time v0.12.0 // indirect - golang.org/x/tools v0.36.0 // indirect + golang.org/x/tools v0.37.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250826171959-ef028d996bc1 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect @@ -295,10 +296,7 @@ replace github.com/jaytaylor/html2text => github.com/Necoro/html2text v0.0.0-202 replace github.com/hashicorp/go-version => github.com/6543/go-version v1.3.1 -replace github.com/nektos/act => gitea.com/gitea/act v0.261.6 - -// TODO: the only difference is in `PutObject`: the fork doesn't use `NewVerifyingReader(r, sha256.New(), oid, expectedSize)`, need to figure out why -replace github.com/charmbracelet/git-lfs-transfer => gitea.com/gitea/git-lfs-transfer v0.2.0 +replace github.com/nektos/act => gitea.com/gitea/act v0.261.7-0.20251003180512-ac6e4b751763 replace git.sr.ht/~mariusor/go-xsd-duration => gitea.com/gitea/go-xsd-duration v0.0.0-20220703122237-02e73435a078 diff --git a/go.sum b/go.sum index 3021dada96..02a710e7f0 100644 --- a/go.sum +++ b/go.sum @@ -31,10 +31,8 @@ dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= -gitea.com/gitea/act v0.261.6 h1:CjZwKOyejonNFDmsXOw3wGm5Vet573hHM6VMLsxtvPY= -gitea.com/gitea/act v0.261.6/go.mod h1:Pg5C9kQY1CEA3QjthjhlrqOC/QOT5NyWNjOjRHw23Ok= -gitea.com/gitea/git-lfs-transfer v0.2.0 h1:baHaNoBSRaeq/xKayEXwiDQtlIjps4Ac/Ll4KqLMB40= -gitea.com/gitea/git-lfs-transfer v0.2.0/go.mod h1:UrXUCm3xLQkq15fu7qlXHUMlrhdlXHoi13KH2Dfiits= +gitea.com/gitea/act v0.261.7-0.20251003180512-ac6e4b751763 h1:ohdxegvslDEllZmRNDqpKun6L4Oq81jNdEDtGgHEV2c= +gitea.com/gitea/act v0.261.7-0.20251003180512-ac6e4b751763/go.mod h1:Pg5C9kQY1CEA3QjthjhlrqOC/QOT5NyWNjOjRHw23Ok= gitea.com/gitea/go-xsd-duration v0.0.0-20220703122237-02e73435a078 h1:BAFmdZpRW7zMQZQDClaCWobRj9uL1MR3MzpCVJvc5s4= gitea.com/gitea/go-xsd-duration v0.0.0-20220703122237-02e73435a078/go.mod h1:g/V2Hjas6Z1UHUp4yIx6bATpNzJ7DYtD0FG3+xARWxs= gitea.com/go-chi/binding v0.0.0-20240430071103-39a851e106ed h1:EZZBtilMLSZNWtHHcgq2mt6NSGhJSZBuduAlinMEmso= @@ -93,8 +91,8 @@ github.com/RoaringBitmap/roaring v0.4.23/go.mod h1:D0gp8kJQgE1A4LQ5wFLggQEyvDi06 github.com/RoaringBitmap/roaring v0.7.1/go.mod h1:jdT9ykXwHFNdJbEtxePexlFYH9LXucApeS0/+/g+p1I= github.com/RoaringBitmap/roaring/v2 v2.10.0 h1:HbJ8Cs71lfCJyvmSptxeMX2PtvOC8yonlU0GQcy2Ak0= github.com/RoaringBitmap/roaring/v2 v2.10.0/go.mod h1:FiJcsfkGje/nZBZgCu0ZxCPOKD/hVXDS2dXi7/eUFE0= -github.com/STARRY-S/zip v0.2.1 h1:pWBd4tuSGm3wtpoqRZZ2EAwOmcHK6XFf7bU9qcJXyFg= -github.com/STARRY-S/zip v0.2.1/go.mod h1:xNvshLODWtC4EJ702g7cTYn13G53o1+X9BWnPFpcWV4= +github.com/STARRY-S/zip v0.2.3 h1:luE4dMvRPDOWQdeDdUxUoZkzUIpTccdKdhHHsQJ1fm4= +github.com/STARRY-S/zip v0.2.3/go.mod h1:lqJ9JdeRipyOQJrYSOtpNAiaesFO6zVDsE8GIGFaoSk= github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.8.0 h1:tgjwQrDH5m6jIYB7kac5IQZmfUzQNseac/e3H4VoCNE= github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.8.0/go.mod h1:1HmmMEVsr+0R1QWahSeMJkjSkq6CYAZu1aIbYSpfJ4o= github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= @@ -193,8 +191,8 @@ github.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTS github.com/bmizerany/perks v0.0.0-20141205001514-d9a9656a3a4b/go.mod h1:ac9efd0D1fsDb3EJvhqgXRbFx7bs2wqZ10HQPeU8U/Q= github.com/bodgit/plumbing v1.3.0 h1:pf9Itz1JOQgn7vEOE7v7nlEfBykYqvUYioC61TwWCFU= github.com/bodgit/plumbing v1.3.0/go.mod h1:JOTb4XiRu5xfnmdnDJo6GmSbSbtSyufrsyZFByMtKEs= -github.com/bodgit/sevenzip v1.6.0 h1:a4R0Wu6/P1o1pP/3VV++aEOcyeBxeO/xE2Y9NSTrr6A= -github.com/bodgit/sevenzip v1.6.0/go.mod h1:zOBh9nJUof7tcrlqJFv1koWRrhz3LbDbUNngkuZxLMc= +github.com/bodgit/sevenzip v1.6.1 h1:kikg2pUMYC9ljU7W9SaqHXhym5HyKm8/M/jd31fYan4= +github.com/bodgit/sevenzip v1.6.1/go.mod h1:GVoYQbEVbOGT8n2pfqCIMRUaRjQ8F9oSqoBEqZh5fQ8= github.com/bodgit/windows v1.0.1 h1:tF7K6KOluPYygXa3Z2594zxlkbKPAOvqr97etrGNIz4= github.com/bodgit/windows v1.0.1/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM= github.com/bohde/codel v0.2.0 h1:fzF7ibgKmCfQbOzQCblmQcwzDRmV7WO7VMLm/hDvD3E= @@ -219,6 +217,8 @@ github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a h1:MISbI8sU/PSK/ github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a/go.mod h1:2GxOXOlEPAMFPfp014mK1SWq8G8BN8o7/dfYqJrVGn8= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/charmbracelet/git-lfs-transfer v0.1.1-0.20251013092601-6327009efd21 h1:2d64+4Jek9vjYwhY93AjbleiVH+AeWvPwPmDi1mfKFQ= +github.com/charmbracelet/git-lfs-transfer v0.1.1-0.20251013092601-6327009efd21/go.mod h1:fNlYtCHWTRC8MofQERZkVUNUWaOvZeTBqHn/amSbKZI= github.com/chi-middleware/proxy v1.1.1 h1:4HaXUp8o2+bhHr1OhVy+VjN0+L7/07JDcn6v7YrTjrQ= github.com/chi-middleware/proxy v1.1.1/go.mod h1:jQwMEJct2tz9VmtCELxvnXoMfa+SOdikvbVJVHv/M+0= github.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs= @@ -339,8 +339,8 @@ github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UN github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU= 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.16.2 h1:fT6ZIOjE5iEnkzKyxTHK1W4HGAsPhqEqiSAssSO77hM= -github.com/go-git/go-git/v5 v5.16.2/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8= +github.com/go-git/go-git/v5 v5.16.3 h1:Z8BtvxZ09bYm/yYNgPKCzgWtaRqDTgIKRgIRHBfU6Z8= +github.com/go-git/go-git/v5 v5.16.3/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= @@ -572,8 +572,8 @@ github.com/meilisearch/meilisearch-go v0.33.2 h1:YgsQSLYhAkRN2ias6I1KNRTjdYCN5w2 github.com/meilisearch/meilisearch-go v0.33.2/go.mod h1:6eOPcQ+OAuwXvnONlfSgfgvr7TIAWM/6OdhcVHg8cF0= github.com/mholt/acmez/v3 v3.1.2 h1:auob8J/0FhmdClQicvJvuDavgd5ezwLBfKuYmynhYzc= github.com/mholt/acmez/v3 v3.1.2/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ= -github.com/mholt/archives v0.1.3 h1:aEAaOtNra78G+TvV5ohmXrJOAzf++dIlYeDW3N9q458= -github.com/mholt/archives v0.1.3/go.mod h1:LUCGp++/IbV/I0Xq4SzcIR6uwgeh2yjnQWamjRQfLTU= +github.com/mholt/archives v0.0.0-20251009205813-e30ac6010726 h1:narluFTg20M5KBwKxedpFiSMkdjQRRNUlpY4uAsKMwk= +github.com/mholt/archives v0.0.0-20251009205813-e30ac6010726/go.mod h1:3TPMmBLPsgszL+1As5zECTuKwKvIfj6YcwWPpeTAXF4= github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= github.com/microsoft/go-mssqldb v1.9.3 h1:hy4p+LDC8LIGvI3JATnLVmBOLMJbmn5X400mr5j0lPs= @@ -588,8 +588,8 @@ github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= github.com/minio/minio-go/v7 v7.0.95 h1:ywOUPg+PebTMTzn9VDsoFJy32ZuARN9zhB+K3IYEvYU= github.com/minio/minio-go/v7 v7.0.95/go.mod h1:wOOX3uxS334vImCNRVyIDdXX9OsXDm89ToynKgqUKlo= -github.com/minio/minlz v1.0.0 h1:Kj7aJZ1//LlTP1DM8Jm7lNKvvJS2m74gyyXXn3+uJWQ= -github.com/minio/minlz v1.0.0/go.mod h1:qT0aEB35q79LLornSzeDH75LBf3aH1MV+jB5w9Wasec= +github.com/minio/minlz v1.0.1 h1:OUZUzXcib8diiX+JYxyRLIdomyZYzHct6EShOKtQY2A= +github.com/minio/minlz v1.0.1/go.mod h1:qT0aEB35q79LLornSzeDH75LBf3aH1MV+jB5w9Wasec= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= @@ -610,8 +610,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/niklasfasching/go-org v1.9.1 h1:/3s4uTPOF06pImGa2Yvlp24yKXZoTYM+nsIlMzfpg/0= github.com/niklasfasching/go-org v1.9.1/go.mod h1:ZAGFFkWvUQcpazmi/8nHqwvARpr1xpb+Es67oUGX/48= -github.com/nwaples/rardecode/v2 v2.1.0 h1:JQl9ZoBPDy+nIZGb1mx8+anfHp/LV3NE2MjMiv0ct/U= -github.com/nwaples/rardecode/v2 v2.1.0/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw= +github.com/nwaples/rardecode/v2 v2.2.0 h1:4ufPGHiNe1rYJxYfehALLjup4Ls3ck42CWwjKiOqu0A= +github.com/nwaples/rardecode/v2 v2.2.0/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= @@ -714,9 +714,11 @@ github.com/smartystreets/assertions v1.1.1/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYl github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s= github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337 h1:WN9BUFbdyOsSH/XohnWpXOlq9NBD5sGAB2FciQMUEe8= github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= -github.com/sorairolake/lzip-go v0.3.5 h1:ms5Xri9o1JBIWvOFAorYtUNik6HI3HgBTkISiqu0Cwg= -github.com/sorairolake/lzip-go v0.3.5/go.mod h1:N0KYq5iWrMXI0ZEXKXaS9hCyOjZUQdBDEIbXfoUwbdk= +github.com/sorairolake/lzip-go v0.3.8 h1:j5Q2313INdTA80ureWYRhX+1K78mUXfMoPZCw/ivWik= +github.com/sorairolake/lzip-go v0.3.8/go.mod h1:JcBqGMV0frlxwrsE9sMWXDjqn3EeVf0/54YPsw66qkU= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= +github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= @@ -729,6 +731,7 @@ github.com/steveyen/gtreap v0.1.0/go.mod h1:kl/5J7XbrOmlIbYIXdRHDDE5QxHqpk0cmkT7 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= @@ -765,8 +768,8 @@ github.com/urfave/cli/v3 v3.4.1/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZ github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ= github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY= github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= -github.com/wneessen/go-mail v0.7.1 h1:rvy63sp14N06/kdGqCYwW8Na5gDCXjTQM1E7So4PuKk= -github.com/wneessen/go-mail v0.7.1/go.mod h1:+TkW6QP3EVkgTEqHtVmnAE/1MRhmzb8Y9/W3pweuS+k= +github.com/wneessen/go-mail v0.7.2 h1:xxPnhZ6IZLSgxShebmZ6DPKh1b6OJcoHfzy7UjOkzS8= +github.com/wneessen/go-mail v0.7.2/go.mod h1:+TkW6QP3EVkgTEqHtVmnAE/1MRhmzb8Y9/W3pweuS+k= 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/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= @@ -837,8 +840,8 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= -golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= -golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= +golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= +golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -875,8 +878,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.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= -golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= +golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= +golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -905,8 +908,8 @@ 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.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= -golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= -golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= +golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= +golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -972,8 +975,8 @@ golang.org/x/sys v0.17.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.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= -golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 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= @@ -984,8 +987,8 @@ golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= -golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= -golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= +golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= +golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -999,8 +1002,8 @@ 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= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= -golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= @@ -1036,8 +1039,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.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= -golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= +golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= +golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= 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= diff --git a/main.go b/main.go index 2c25bac4e3..bc2121b1e7 100644 --- a/main.go +++ b/main.go @@ -44,6 +44,7 @@ func main() { } app := cmd.NewMainApp(cmd.AppVersion{Version: Version, Extra: formatBuiltWith()}) _ = cmd.RunMainApp(app, os.Args...) // all errors should have been handled by the RunMainApp + // flush the queued logs before exiting, it is a MUST, otherwise there will be log loss log.GetManager().Close() } diff --git a/models/actions/run.go b/models/actions/run.go index f5ccba06c2..4da6958e2d 100644 --- a/models/actions/run.go +++ b/models/actions/run.go @@ -16,13 +16,13 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" webhook_module "code.gitea.io/gitea/modules/webhook" - "github.com/nektos/act/pkg/jobparser" "xorm.io/builder" ) @@ -30,7 +30,7 @@ import ( type ActionRun struct { ID int64 Title string - RepoID int64 `xorm:"index unique(repo_index)"` + RepoID int64 `xorm:"unique(repo_index) index(repo_concurrency)"` Repo *repo_model.Repository `xorm:"-"` OwnerID int64 `xorm:"index"` WorkflowID string `xorm:"index"` // the name of workflow file @@ -49,6 +49,9 @@ type ActionRun struct { TriggerEvent string // the trigger event defined in the `on` configuration of the triggered workflow Status Status `xorm:"index"` Version int `xorm:"version default 0"` // Status could be updated concomitantly, so an optimistic lock is needed + RawConcurrency string // raw concurrency + ConcurrencyGroup string `xorm:"index(repo_concurrency) NOT NULL DEFAULT ''"` + ConcurrencyCancel bool `xorm:"NOT NULL DEFAULT FALSE"` // Started and Stopped is used for recording last run time, if rerun happened, they will be reset to 0 Started timeutil.TimeStamp Stopped timeutil.TimeStamp @@ -102,6 +105,15 @@ func (run *ActionRun) PrettyRef() string { return refName.ShortName() } +// RefTooltip return a tooltop of run's ref. For pull request, it's the title of the PR, otherwise it's the ShortName. +func (run *ActionRun) RefTooltip() string { + payload, err := run.GetPullRequestEventPayload() + if err == nil && payload != nil && payload.PullRequest != nil { + return payload.PullRequest.Title + } + return git.RefName(run.Ref).ShortName() +} + // LoadAttributes load Repo TriggerUser if not loaded func (run *ActionRun) LoadAttributes(ctx context.Context) error { if run == nil { @@ -181,7 +193,7 @@ func (run *ActionRun) IsSchedule() bool { return run.ScheduleID > 0 } -func updateRepoRunsNumbers(ctx context.Context, repo *repo_model.Repository) error { +func UpdateRepoRunsNumbers(ctx context.Context, repo *repo_model.Repository) error { _, err := db.GetEngine(ctx).ID(repo.ID). NoAutoTime(). SetExpr("num_action_runs", @@ -238,116 +250,62 @@ func CancelPreviousJobs(ctx context.Context, repoID int64, ref, workflowID strin return cancelledJobs, err } - // Iterate over each job and attempt to cancel it. - for _, job := range jobs { - // Skip jobs that are already in a terminal state (completed, cancelled, etc.). - status := job.Status - if status.IsDone() { - continue - } - - // If the job has no associated task (probably an error), set its status to 'Cancelled' and stop it. - if job.TaskID == 0 { - job.Status = StatusCancelled - job.Stopped = timeutil.TimeStampNow() - - // Update the job's status and stopped time in the database. - n, err := UpdateRunJob(ctx, job, builder.Eq{"task_id": 0}, "status", "stopped") - if err != nil { - return cancelledJobs, err - } - - // If the update affected 0 rows, it means the job has changed in the meantime, so we need to try again. - if n == 0 { - return cancelledJobs, errors.New("job has changed, try again") - } - - cancelledJobs = append(cancelledJobs, job) - // Continue with the next job. - continue - } - - // If the job has an associated task, try to stop the task, effectively cancelling the job. - if err := StopTask(ctx, job.TaskID, StatusCancelled); err != nil { - return cancelledJobs, err - } - cancelledJobs = append(cancelledJobs, job) + cjs, err := CancelJobs(ctx, jobs) + if err != nil { + return cancelledJobs, err } + cancelledJobs = append(cancelledJobs, cjs...) } // Return nil to indicate successful cancellation of all running and waiting jobs. return cancelledJobs, nil } -// InsertRun inserts a run -// The title will be cut off at 255 characters if it's longer than 255 characters. -func InsertRun(ctx context.Context, run *ActionRun, jobs []*jobparser.SingleWorkflow) error { - return db.WithTx(ctx, func(ctx context.Context) error { - index, err := db.GetNextResourceIndex(ctx, "action_run_index", run.RepoID) - if err != nil { - return err - } - run.Index = index - run.Title = util.EllipsisDisplayString(run.Title, 255) - - if err := db.Insert(ctx, run); err != nil { - return err +func CancelJobs(ctx context.Context, jobs []*ActionRunJob) ([]*ActionRunJob, error) { + cancelledJobs := make([]*ActionRunJob, 0, len(jobs)) + // Iterate over each job and attempt to cancel it. + for _, job := range jobs { + // Skip jobs that are already in a terminal state (completed, cancelled, etc.). + status := job.Status + if status.IsDone() { + continue } - if run.Repo == nil { - repo, err := repo_model.GetRepositoryByID(ctx, run.RepoID) + // If the job has no associated task (probably an error), set its status to 'Cancelled' and stop it. + if job.TaskID == 0 { + job.Status = StatusCancelled + job.Stopped = timeutil.TimeStampNow() + + // Update the job's status and stopped time in the database. + n, err := UpdateRunJob(ctx, job, builder.Eq{"task_id": 0}, "status", "stopped") if err != nil { - return err + return cancelledJobs, err } - run.Repo = repo + + // If the update affected 0 rows, it means the job has changed in the meantime + if n == 0 { + log.Error("Failed to cancel job %d because it has changed", job.ID) + continue + } + + cancelledJobs = append(cancelledJobs, job) + // Continue with the next job. + continue } - if err := updateRepoRunsNumbers(ctx, run.Repo); err != nil { - return err + // If the job has an associated task, try to stop the task, effectively cancelling the job. + if err := StopTask(ctx, job.TaskID, StatusCancelled); err != nil { + return cancelledJobs, err } + updatedJob, err := GetRunJobByID(ctx, job.ID) + if err != nil { + return cancelledJobs, fmt.Errorf("get job: %w", err) + } + cancelledJobs = append(cancelledJobs, updatedJob) + } - runJobs := make([]*ActionRunJob, 0, len(jobs)) - var hasWaiting bool - for _, v := range jobs { - id, job := v.Job() - needs := job.Needs() - if err := v.SetJob(id, job.EraseNeeds()); err != nil { - return err - } - payload, _ := v.Marshal() - status := StatusWaiting - if len(needs) > 0 || run.NeedApproval { - status = StatusBlocked - } else { - hasWaiting = true - } - job.Name = util.EllipsisDisplayString(job.Name, 255) - runJobs = append(runJobs, &ActionRunJob{ - RunID: run.ID, - RepoID: run.RepoID, - OwnerID: run.OwnerID, - CommitSHA: run.CommitSHA, - IsForkPullRequest: run.IsForkPullRequest, - Name: job.Name, - WorkflowPayload: payload, - JobID: id, - Needs: needs, - RunsOn: job.RunsOn(), - Status: status, - }) - } - if err := db.Insert(ctx, runJobs); err != nil { - return err - } - - // if there is a job in the waiting status, increase tasks version. - if hasWaiting { - if err := IncreaseTaskVersion(ctx, run.OwnerID, run.RepoID); err != nil { - return err - } - } - return nil - }) + // Return nil to indicate successful cancellation of all running and waiting jobs. + return cancelledJobs, nil } func GetRunByRepoAndID(ctx context.Context, repoID, runID int64) (*ActionRun, error) { @@ -432,7 +390,7 @@ func UpdateRun(ctx context.Context, run *ActionRun, cols ...string) error { if err = run.LoadRepo(ctx); err != nil { return err } - if err := updateRepoRunsNumbers(ctx, run.Repo); err != nil { + if err := UpdateRepoRunsNumbers(ctx, run.Repo); err != nil { return err } } @@ -441,3 +399,59 @@ func UpdateRun(ctx context.Context, run *ActionRun, cols ...string) error { } type ActionRunIndex db.ResourceIndex + +func GetConcurrentRunsAndJobs(ctx context.Context, repoID int64, concurrencyGroup string, status []Status) ([]*ActionRun, []*ActionRunJob, error) { + runs, err := db.Find[ActionRun](ctx, &FindRunOptions{ + RepoID: repoID, + ConcurrencyGroup: concurrencyGroup, + Status: status, + }) + if err != nil { + return nil, nil, fmt.Errorf("find runs: %w", err) + } + + jobs, err := db.Find[ActionRunJob](ctx, &FindRunJobOptions{ + RepoID: repoID, + ConcurrencyGroup: concurrencyGroup, + Statuses: status, + }) + if err != nil { + return nil, nil, fmt.Errorf("find jobs: %w", err) + } + + return runs, jobs, nil +} + +func CancelPreviousJobsByRunConcurrency(ctx context.Context, actionRun *ActionRun) ([]*ActionRunJob, error) { + if actionRun.ConcurrencyGroup == "" { + return nil, nil + } + + var jobsToCancel []*ActionRunJob + + statusFindOption := []Status{StatusWaiting, StatusBlocked} + if actionRun.ConcurrencyCancel { + statusFindOption = append(statusFindOption, StatusRunning) + } + runs, jobs, err := GetConcurrentRunsAndJobs(ctx, actionRun.RepoID, actionRun.ConcurrencyGroup, statusFindOption) + if err != nil { + return nil, fmt.Errorf("find concurrent runs and jobs: %w", err) + } + jobsToCancel = append(jobsToCancel, jobs...) + + // cancel runs in the same concurrency group + for _, run := range runs { + if run.ID == actionRun.ID { + continue + } + jobs, err := db.Find[ActionRunJob](ctx, FindRunJobOptions{ + RunID: run.ID, + }) + if err != nil { + return nil, fmt.Errorf("find run %d jobs: %w", run.ID, err) + } + jobsToCancel = append(jobsToCancel, jobs...) + } + + return CancelJobs(ctx, jobsToCancel) +} diff --git a/models/actions/run_job.go b/models/actions/run_job.go index e7fa21270c..f72a7040e3 100644 --- a/models/actions/run_job.go +++ b/models/actions/run_job.go @@ -14,6 +14,7 @@ import ( "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" + "github.com/nektos/act/pkg/jobparser" "xorm.io/builder" ) @@ -22,23 +23,38 @@ type ActionRunJob struct { ID int64 RunID int64 `xorm:"index"` Run *ActionRun `xorm:"-"` - RepoID int64 `xorm:"index"` + RepoID int64 `xorm:"index(repo_concurrency)"` Repo *repo_model.Repository `xorm:"-"` OwnerID int64 `xorm:"index"` CommitSHA string `xorm:"index"` IsForkPullRequest bool Name string `xorm:"VARCHAR(255)"` Attempt int64 - WorkflowPayload []byte - JobID string `xorm:"VARCHAR(255)"` // job id in workflow, not job's id - Needs []string `xorm:"JSON TEXT"` - RunsOn []string `xorm:"JSON TEXT"` - TaskID int64 // the latest task of the job - Status Status `xorm:"index"` - Started timeutil.TimeStamp - Stopped timeutil.TimeStamp - Created timeutil.TimeStamp `xorm:"created"` - Updated timeutil.TimeStamp `xorm:"updated index"` + + // WorkflowPayload is act/jobparser.SingleWorkflow for act/jobparser.Parse + // it should contain exactly one job with global workflow fields for this model + WorkflowPayload []byte + + JobID string `xorm:"VARCHAR(255)"` // job id in workflow, not job's id + Needs []string `xorm:"JSON TEXT"` + RunsOn []string `xorm:"JSON TEXT"` + TaskID int64 // the latest task of the job + Status Status `xorm:"index"` + + RawConcurrency string // raw concurrency from job YAML's "concurrency" section + + // IsConcurrencyEvaluated is only valid/needed when this job's RawConcurrency is not empty. + // If RawConcurrency can't be evaluated (e.g. depend on other job's outputs or have errors), this field will be false. + // If RawConcurrency has been successfully evaluated, this field will be true, ConcurrencyGroup and ConcurrencyCancel are also set. + IsConcurrencyEvaluated bool + + ConcurrencyGroup string `xorm:"index(repo_concurrency) NOT NULL DEFAULT ''"` // evaluated concurrency.group + ConcurrencyCancel bool `xorm:"NOT NULL DEFAULT FALSE"` // evaluated concurrency.cancel-in-progress + + Started timeutil.TimeStamp + Stopped timeutil.TimeStamp + Created timeutil.TimeStamp `xorm:"created"` + Updated timeutil.TimeStamp `xorm:"updated index"` } func init() { @@ -84,6 +100,24 @@ func (job *ActionRunJob) LoadAttributes(ctx context.Context) error { return job.Run.LoadAttributes(ctx) } +// ParseJob parses the job structure from the ActionRunJob.WorkflowPayload +func (job *ActionRunJob) ParseJob() (*jobparser.Job, error) { + // job.WorkflowPayload is a SingleWorkflow created from an ActionRun's workflow, which exactly contains this job's YAML definition. + // Ideally it shouldn't be called "Workflow", it is just a job with global workflow fields + trigger + parsedWorkflows, err := jobparser.Parse(job.WorkflowPayload) + if err != nil { + return nil, fmt.Errorf("job %d single workflow: unable to parse: %w", job.ID, err) + } else if len(parsedWorkflows) != 1 { + return nil, fmt.Errorf("job %d single workflow: not single workflow", job.ID) + } + _, workflowJob := parsedWorkflows[0].Job() + if workflowJob == nil { + // it shouldn't happen, and since the callers don't check nil, so return an error instead of nil + return nil, util.ErrorWrap(util.ErrNotExist, "job %d single workflow: payload doesn't contain a job", job.ID) + } + return workflowJob, nil +} + func GetRunJobByID(ctx context.Context, id int64) (*ActionRunJob, error) { var job ActionRunJob has, err := db.GetEngine(ctx).Where("id=?", id).Get(&job) @@ -125,7 +159,7 @@ func UpdateRunJob(ctx context.Context, job *ActionRunJob, cond builder.Cond, col return affected, nil } - if affected != 0 && slices.Contains(cols, "status") && job.Status.IsWaiting() { + if slices.Contains(cols, "status") && job.Status.IsWaiting() { // if the status of job changes to waiting again, increase tasks version. if err := IncreaseTaskVersion(ctx, job.OwnerID, job.RepoID); err != nil { return 0, err @@ -197,3 +231,39 @@ func AggregateJobStatus(jobs []*ActionRunJob) Status { return StatusUnknown // it shouldn't happen } } + +func CancelPreviousJobsByJobConcurrency(ctx context.Context, job *ActionRunJob) (jobsToCancel []*ActionRunJob, _ error) { + if job.RawConcurrency == "" { + return nil, nil + } + if !job.IsConcurrencyEvaluated { + return nil, nil + } + if job.ConcurrencyGroup == "" { + return nil, nil + } + + statusFindOption := []Status{StatusWaiting, StatusBlocked} + if job.ConcurrencyCancel { + statusFindOption = append(statusFindOption, StatusRunning) + } + runs, jobs, err := GetConcurrentRunsAndJobs(ctx, job.RepoID, job.ConcurrencyGroup, statusFindOption) + if err != nil { + return nil, fmt.Errorf("find concurrent runs and jobs: %w", err) + } + jobs = slices.DeleteFunc(jobs, func(j *ActionRunJob) bool { return j.ID == job.ID }) + jobsToCancel = append(jobsToCancel, jobs...) + + // cancel runs in the same concurrency group + for _, run := range runs { + jobs, err := db.Find[ActionRunJob](ctx, FindRunJobOptions{ + RunID: run.ID, + }) + if err != nil { + return nil, fmt.Errorf("find run %d jobs: %w", run.ID, err) + } + jobsToCancel = append(jobsToCancel, jobs...) + } + + return CancelJobs(ctx, jobsToCancel) +} diff --git a/models/actions/run_job_list.go b/models/actions/run_job_list.go index 5f7bb62878..10f76d3641 100644 --- a/models/actions/run_job_list.go +++ b/models/actions/run_job_list.go @@ -69,12 +69,13 @@ func (jobs ActionJobList) LoadAttributes(ctx context.Context, withRepo bool) err type FindRunJobOptions struct { db.ListOptions - RunID int64 - RepoID int64 - OwnerID int64 - CommitSHA string - Statuses []Status - UpdatedBefore timeutil.TimeStamp + RunID int64 + RepoID int64 + OwnerID int64 + CommitSHA string + Statuses []Status + UpdatedBefore timeutil.TimeStamp + ConcurrencyGroup string } func (opts FindRunJobOptions) ToConds() builder.Cond { @@ -94,6 +95,12 @@ func (opts FindRunJobOptions) ToConds() builder.Cond { if opts.UpdatedBefore > 0 { cond = cond.And(builder.Lt{"`action_run_job`.updated": opts.UpdatedBefore}) } + if opts.ConcurrencyGroup != "" { + if opts.RepoID == 0 { + panic("Invalid FindRunJobOptions: repo_id is required") + } + cond = cond.And(builder.Eq{"`action_run_job`.concurrency_group": opts.ConcurrencyGroup}) + } return cond } diff --git a/models/actions/run_list.go b/models/actions/run_list.go index 12c55e538e..2628c4712f 100644 --- a/models/actions/run_list.go +++ b/models/actions/run_list.go @@ -64,15 +64,16 @@ func (runs RunList) LoadRepos(ctx context.Context) error { type FindRunOptions struct { db.ListOptions - RepoID int64 - OwnerID int64 - WorkflowID string - Ref string // the commit/tag/… that caused this workflow - TriggerUserID int64 - TriggerEvent webhook_module.HookEventType - Approved bool // not util.OptionalBool, it works only when it's true - Status []Status - CommitSHA string + RepoID int64 + OwnerID int64 + WorkflowID string + Ref string // the commit/tag/… that caused this workflow + TriggerUserID int64 + TriggerEvent webhook_module.HookEventType + Approved bool // not util.OptionalBool, it works only when it's true + Status []Status + ConcurrencyGroup string + CommitSHA string } func (opts FindRunOptions) ToConds() builder.Cond { @@ -101,6 +102,12 @@ func (opts FindRunOptions) ToConds() builder.Cond { if opts.CommitSHA != "" { cond = cond.And(builder.Eq{"`action_run`.commit_sha": opts.CommitSHA}) } + if len(opts.ConcurrencyGroup) > 0 { + if opts.RepoID == 0 { + panic("Invalid FindRunOptions: repo_id is required") + } + cond = cond.And(builder.Eq{"`action_run`.concurrency_group": opts.ConcurrencyGroup}) + } return cond } diff --git a/models/actions/runner.go b/models/actions/runner.go index 81d4249ae0..84398b143b 100644 --- a/models/actions/runner.go +++ b/models/actions/runner.go @@ -14,6 +14,7 @@ import ( repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/shared/types" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" @@ -173,6 +174,13 @@ func (r *ActionRunner) GenerateToken() (err error) { return err } +// CanMatchLabels checks whether the runner's labels can match a job's "runs-on" +// See https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#jobsjob_idruns-on +func (r *ActionRunner) CanMatchLabels(jobRunsOn []string) bool { + runnerLabelSet := container.SetOf(r.AgentLabels...) + return runnerLabelSet.Contains(jobRunsOn...) // match all labels +} + func init() { db.RegisterModel(&ActionRunner{}) } diff --git a/models/actions/task.go b/models/actions/task.go index c1306a8418..8b4ecf28f7 100644 --- a/models/actions/task.go +++ b/models/actions/task.go @@ -13,7 +13,6 @@ import ( auth_model "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/unit" - "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" @@ -21,7 +20,6 @@ import ( runnerv1 "code.gitea.io/actions-proto-go/runner/v1" lru "github.com/hashicorp/golang-lru/v2" - "github.com/nektos/act/pkg/jobparser" "google.golang.org/protobuf/types/known/timestamppb" "xorm.io/builder" ) @@ -246,7 +244,7 @@ func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask var job *ActionRunJob log.Trace("runner labels: %v", runner.AgentLabels) for _, v := range jobs { - if isSubset(runner.AgentLabels, v.RunsOn) { + if runner.CanMatchLabels(v.RunsOn) { job = v break } @@ -278,13 +276,10 @@ func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask return nil, false, err } - parsedWorkflows, err := jobparser.Parse(job.WorkflowPayload) + workflowJob, err := job.ParseJob() if err != nil { - return nil, false, fmt.Errorf("parse workflow of job %d: %w", job.ID, err) - } else if len(parsedWorkflows) != 1 { - return nil, false, fmt.Errorf("workflow of job %d: not single workflow", job.ID) + return nil, false, fmt.Errorf("load job %d: %w", job.ID, err) } - _, workflowJob := parsedWorkflows[0].Job() if _, err := e.Insert(task); err != nil { return nil, false, err @@ -479,20 +474,6 @@ func FindOldTasksToExpire(ctx context.Context, olderThan timeutil.TimeStamp, lim Find(&tasks) } -func isSubset(set, subset []string) bool { - m := make(container.Set[string], len(set)) - for _, v := range set { - m.Add(v) - } - - for _, v := range subset { - if !m.Contains(v) { - return false - } - } - return true -} - func convertTimestamp(timestamp *timestamppb.Timestamp) timeutil.TimeStamp { if timestamp.GetSeconds() == 0 && timestamp.GetNanos() == 0 { return timeutil.TimeStamp(0) diff --git a/models/admin/task.go b/models/admin/task.go index 0541a8ec78..5d2b9bbff6 100644 --- a/models/admin/task.go +++ b/models/admin/task.go @@ -11,6 +11,7 @@ import ( repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/migration" "code.gitea.io/gitea/modules/secret" "code.gitea.io/gitea/modules/setting" @@ -123,17 +124,17 @@ func (task *Task) MigrateConfig() (*migration.MigrateOptions, error) { // decrypt credentials if opts.CloneAddrEncrypted != "" { if opts.CloneAddr, err = secret.DecryptSecret(setting.SecretKey, opts.CloneAddrEncrypted); err != nil { - return nil, err + log.Error("Unable to decrypt CloneAddr, maybe SECRET_KEY is wrong: %v", err) } } if opts.AuthPasswordEncrypted != "" { if opts.AuthPassword, err = secret.DecryptSecret(setting.SecretKey, opts.AuthPasswordEncrypted); err != nil { - return nil, err + log.Error("Unable to decrypt AuthPassword, maybe SECRET_KEY is wrong: %v", err) } } if opts.AuthTokenEncrypted != "" { if opts.AuthToken, err = secret.DecryptSecret(setting.SecretKey, opts.AuthTokenEncrypted); err != nil { - return nil, err + log.Error("Unable to decrypt AuthToken, maybe SECRET_KEY is wrong: %v", err) } } diff --git a/models/asymkey/ssh_key.go b/models/asymkey/ssh_key.go index 87205f0651..d77b5d46a7 100644 --- a/models/asymkey/ssh_key.go +++ b/models/asymkey/ssh_key.go @@ -67,13 +67,6 @@ func (key *PublicKey) OmitEmail() string { return strings.Join(strings.Split(key.Content, " ")[:2], " ") } -// AuthorizedString returns formatted public key string for authorized_keys file. -// -// TODO: Consider dropping this function -func (key *PublicKey) AuthorizedString() string { - return AuthorizedStringForKey(key) -} - func addKey(ctx context.Context, key *PublicKey) (err error) { if len(key.Fingerprint) == 0 { key.Fingerprint, err = CalcFingerprint(key.Content) diff --git a/models/asymkey/ssh_key_authorized_keys.go b/models/asymkey/ssh_key_authorized_keys.go index 2e4cd62e5c..db4730f00a 100644 --- a/models/asymkey/ssh_key_authorized_keys.go +++ b/models/asymkey/ssh_key_authorized_keys.go @@ -17,29 +17,13 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" + + "golang.org/x/crypto/ssh" ) -// _____ __ .__ .__ .___ -// / _ \ __ ___/ |_| |__ ___________|__|_______ ____ __| _/ -// / /_\ \| | \ __\ | \ / _ \_ __ \ \___ // __ \ / __ | -// / | \ | /| | | Y ( <_> ) | \/ |/ /\ ___// /_/ | -// \____|__ /____/ |__| |___| /\____/|__| |__/_____ \\___ >____ | -// \/ \/ \/ \/ \/ -// ____ __. -// | |/ _|____ ___.__. ______ -// | <_/ __ < | |/ ___/ -// | | \ ___/\___ |\___ \ -// |____|__ \___ > ____/____ > -// \/ \/\/ \/ -// -// This file contains functions for creating authorized_keys files -// -// There is a dependence on the database within RegeneratePublicKeys however most of these functions probably belong in a module - -const ( - tplCommentPrefix = `# gitea public key` - tplPublicKey = tplCommentPrefix + "\n" + `command=%s,no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty,no-user-rc,restrict %s` + "\n" -) +// AuthorizedStringCommentPrefix is a magic tag +// some functions like RegeneratePublicKeys needs this tag to skip the keys generated by Gitea, while keep other keys +const AuthorizedStringCommentPrefix = `# gitea public key` var sshOpLocker sync.Mutex @@ -50,17 +34,45 @@ func WithSSHOpLocker(f func() error) error { } // AuthorizedStringForKey creates the authorized keys string appropriate for the provided key -func AuthorizedStringForKey(key *PublicKey) string { +func AuthorizedStringForKey(key *PublicKey) (string, error) { sb := &strings.Builder{} - _ = setting.SSH.AuthorizedKeysCommandTemplateTemplate.Execute(sb, map[string]any{ + _, err := writeAuthorizedStringForKey(key, sb) + return sb.String(), err +} + +// WriteAuthorizedStringForValidKey writes the authorized key for the provided key. If the key is invalid, it does nothing. +func WriteAuthorizedStringForValidKey(key *PublicKey, w io.Writer) error { + validKey, err := writeAuthorizedStringForKey(key, w) + if !validKey { + log.Debug("WriteAuthorizedStringForValidKey: key %s is not valid: %v", key, err) + return nil + } + return err +} + +func writeAuthorizedStringForKey(key *PublicKey, w io.Writer) (keyValid bool, err error) { + const tpl = AuthorizedStringCommentPrefix + "\n" + `command=%s,no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty,no-user-rc,restrict %s %s` + "\n" + pubKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(key.Content)) + if err != nil { + return false, err + } + // now the key is valid, the code below could only return template/IO related errors + sbCmd := &strings.Builder{} + err = setting.SSH.AuthorizedKeysCommandTemplateTemplate.Execute(sbCmd, map[string]any{ "AppPath": util.ShellEscape(setting.AppPath), "AppWorkPath": util.ShellEscape(setting.AppWorkPath), "CustomConf": util.ShellEscape(setting.CustomConf), "CustomPath": util.ShellEscape(setting.CustomPath), "Key": key, }) - - return fmt.Sprintf(tplPublicKey, util.ShellEscape(sb.String()), key.Content) + if err != nil { + return true, err + } + sshCommandEscaped := util.ShellEscape(sbCmd.String()) + sshKeyMarshalled := strings.TrimSpace(string(ssh.MarshalAuthorizedKey(pubKey))) + sshKeyComment := fmt.Sprintf("user-%d", key.OwnerID) + _, err = fmt.Fprintf(w, tpl, sshCommandEscaped, sshKeyMarshalled, sshKeyComment) + return true, err } // appendAuthorizedKeysToFile appends new SSH keys' content to authorized_keys file. @@ -112,7 +124,7 @@ func appendAuthorizedKeysToFile(keys ...*PublicKey) error { if key.Type == KeyTypePrincipal { continue } - if _, err = f.WriteString(key.AuthorizedString()); err != nil { + if err = WriteAuthorizedStringForValidKey(key, f); err != nil { return err } } @@ -120,10 +132,9 @@ func appendAuthorizedKeysToFile(keys ...*PublicKey) error { } // RegeneratePublicKeys regenerates the authorized_keys file -func RegeneratePublicKeys(ctx context.Context, t io.StringWriter) error { +func RegeneratePublicKeys(ctx context.Context, t io.Writer) error { if err := db.GetEngine(ctx).Where("type != ?", KeyTypePrincipal).Iterate(new(PublicKey), func(idx int, bean any) (err error) { - _, err = t.WriteString((bean.(*PublicKey)).AuthorizedString()) - return err + return WriteAuthorizedStringForValidKey(bean.(*PublicKey), t) }); err != nil { return err } @@ -144,11 +155,11 @@ func RegeneratePublicKeys(ctx context.Context, t io.StringWriter) error { scanner := bufio.NewScanner(f) for scanner.Scan() { line := scanner.Text() - if strings.HasPrefix(line, tplCommentPrefix) { + if strings.HasPrefix(line, AuthorizedStringCommentPrefix) { scanner.Scan() continue } - _, err = t.WriteString(line + "\n") + _, err = io.WriteString(t, line+"\n") if err != nil { return err } diff --git a/models/auth/twofactor.go b/models/auth/twofactor.go index 200ce7c7c0..4263495650 100644 --- a/models/auth/twofactor.go +++ b/models/auth/twofactor.go @@ -111,11 +111,11 @@ func (t *TwoFactor) SetSecret(secretString string) error { func (t *TwoFactor) ValidateTOTP(passcode string) (bool, error) { decodedStoredSecret, err := base64.StdEncoding.DecodeString(t.Secret) if err != nil { - return false, err + return false, fmt.Errorf("ValidateTOTP invalid base64: %w", err) } secretBytes, err := secret.AesDecrypt(t.getEncryptionKey(), decodedStoredSecret) if err != nil { - return false, err + return false, fmt.Errorf("ValidateTOTP unable to decrypt (maybe SECRET_KEY is wrong): %w", err) } secretStr := string(secretBytes) return totp.Validate(passcode, secretStr), nil diff --git a/models/fixtures/action_run.yml b/models/fixtures/action_run.yml index 09dfa6cccb..b9688dd5f5 100644 --- a/models/fixtures/action_run.yml +++ b/models/fixtures/action_run.yml @@ -139,3 +139,23 @@ updated: 1683636626 need_approval: 0 approved_by: 0 +- + id: 804 + title: "use a private action" + repo_id: 60 + owner_id: 40 + workflow_id: "run.yaml" + index: 189 + trigger_user_id: 40 + ref: "refs/heads/master" + commit_sha: "6e64b26de7ba966d01d90ecfaf5c7f14ef203e86" + event: "push" + trigger_event: "push" + is_fork_pull_request: 0 + status: 1 + started: 1683636528 + stopped: 1683636626 + created: 1683636108 + updated: 1683636626 + need_approval: 0 + approved_by: 0 diff --git a/models/fixtures/action_run_job.yml b/models/fixtures/action_run_job.yml index 6c06d94aa4..337e83605a 100644 --- a/models/fixtures/action_run_job.yml +++ b/models/fixtures/action_run_job.yml @@ -129,3 +129,17 @@ status: 5 started: 1683636528 stopped: 1683636626 +- + id: 205 + run_id: 804 + repo_id: 6 + owner_id: 10 + commit_sha: 6e64b26de7ba966d01d90ecfaf5c7f14ef203e86 + is_fork_pull_request: 0 + name: job_2 + attempt: 1 + job_id: job_2 + task_id: 48 + status: 1 + started: 1683636528 + stopped: 1683636626 diff --git a/models/fixtures/action_task.yml b/models/fixtures/action_task.yml index c79fb07050..e09fd6f2ec 100644 --- a/models/fixtures/action_task.yml +++ b/models/fixtures/action_task.yml @@ -177,3 +177,23 @@ log_length: 0 log_size: 0 log_expired: 0 +- + id: 55 + job_id: 205 + attempt: 1 + runner_id: 1 + status: 6 # 6 is the status code for "running" + started: 1683636528 + stopped: 1683636626 + repo_id: 6 + owner_id: 10 + commit_sha: 6e64b26de7ba966d01d90ecfaf5c7f14ef203e86 + is_fork_pull_request: 0 + token_hash: b8d3962425466b6709b9ac51446f93260c54afe8e7b6d3686e34f991fb8a8953822b0deed86fe41a103f34bc48dbc478422b + token_salt: ERxJGHvg3I + token_last_eight: 182199eb + log_filename: collaborative-owner-test/1a/49.log + log_in_storage: 1 + log_length: 707 + log_size: 90179 + log_expired: 0 diff --git a/models/fixtures/branch.yml b/models/fixtures/branch.yml index 323374cdd9..a1a1627f45 100644 --- a/models/fixtures/branch.yml +++ b/models/fixtures/branch.yml @@ -217,6 +217,43 @@ - id: 26 repo_id: 10 + name: 'feature/1' + commit_id: '65f1bf27bc3bf70f64657658635e66094edbcb4d' + commit_message: 'Initial commit' + commit_time: 1489950479 + pusher_id: 2 + is_deleted: false + deleted_by_id: 0 + deleted_unix: 0 + +- + id: 27 + repo_id: 1 + name: 'DefaultBranch' + commit_id: '90c1019714259b24fb81711d4416ac0f18667dfa' + commit_message: 'add license' + commit_time: 1709345946 + pusher_id: 1 + is_deleted: false + deleted_by_id: 0 + deleted_unix: 0 + +- + id: 28 + repo_id: 1 + name: 'sub-home-md-img-check' + commit_id: '4649299398e4d39a5c09eb4f534df6f1e1eb87cc' + commit_message: "Test how READMEs render images when found in a subfolder" + commit_time: 1678403550 + pusher_id: 1 + is_deleted: false + deleted_by_id: 0 + deleted_unix: 0 + + +- + id: 29 + repo_id: 10 name: 'develop' commit_id: '65f1bf27bc3bf70f64657658635e66094edbcb4d' commit_message: 'Initial commit' @@ -227,7 +264,7 @@ deleted_unix: 0 - - id: 27 + id: 30 repo_id: 1 name: 'pr-to-update' commit_id: '62fb502a7172d4453f0322a2cc85bddffa57f07a' diff --git a/models/fixtures/repo_unit.yml b/models/fixtures/repo_unit.yml index f6b6252da1..f8bb8ef0d3 100644 --- a/models/fixtures/repo_unit.yml +++ b/models/fixtures/repo_unit.yml @@ -733,3 +733,10 @@ type: 3 config: "{\"IgnoreWhitespaceConflicts\":false,\"AllowMerge\":true,\"AllowRebase\":true,\"AllowRebaseMerge\":true,\"AllowSquash\":true}" created_unix: 946684810 + +- + id: 111 + repo_id: 3 + type: 10 + config: "{}" + created_unix: 946684810 diff --git a/models/git/commit_status.go b/models/git/commit_status.go index e255bca5d0..2ae5937a3d 100644 --- a/models/git/commit_status.go +++ b/models/git/commit_status.go @@ -30,17 +30,21 @@ import ( // CommitStatus holds a single Status of a single Commit type CommitStatus struct { - ID int64 `xorm:"pk autoincr"` - Index int64 `xorm:"INDEX UNIQUE(repo_sha_index)"` - RepoID int64 `xorm:"INDEX UNIQUE(repo_sha_index)"` - Repo *repo_model.Repository `xorm:"-"` - State commitstatus.CommitStatusState `xorm:"VARCHAR(7) NOT NULL"` - SHA string `xorm:"VARCHAR(64) NOT NULL INDEX UNIQUE(repo_sha_index)"` - TargetURL string `xorm:"TEXT"` - Description string `xorm:"TEXT"` - ContextHash string `xorm:"VARCHAR(64) index"` - Context string `xorm:"TEXT"` - Creator *user_model.User `xorm:"-"` + ID int64 `xorm:"pk autoincr"` + Index int64 `xorm:"INDEX UNIQUE(repo_sha_index)"` + RepoID int64 `xorm:"INDEX UNIQUE(repo_sha_index)"` + Repo *repo_model.Repository `xorm:"-"` + State commitstatus.CommitStatusState `xorm:"VARCHAR(7) NOT NULL"` + SHA string `xorm:"VARCHAR(64) NOT NULL INDEX UNIQUE(repo_sha_index)"` + + // TargetURL points to the commit status page reported by a CI system + // If Gitea Actions is used, it is a relative link like "{RepoLink}/actions/runs/{RunID}/jobs{JobID}" + TargetURL string `xorm:"TEXT"` + + Description string `xorm:"TEXT"` + ContextHash string `xorm:"VARCHAR(64) index"` + Context string `xorm:"TEXT"` + Creator *user_model.User `xorm:"-"` CreatorID int64 CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` @@ -211,21 +215,45 @@ func (status *CommitStatus) LocaleString(lang translation.Locale) string { // HideActionsURL set `TargetURL` to an empty string if the status comes from Gitea Actions func (status *CommitStatus) HideActionsURL(ctx context.Context) { + if _, ok := status.cutTargetURLGiteaActionsPrefix(ctx); ok { + status.TargetURL = "" + } +} + +func (status *CommitStatus) cutTargetURLGiteaActionsPrefix(ctx context.Context) (string, bool) { if status.RepoID == 0 { - return + return "", false } if status.Repo == nil { if err := status.loadRepository(ctx); err != nil { log.Error("loadRepository: %v", err) - return + return "", false } } prefix := status.Repo.Link() + "/actions" - if strings.HasPrefix(status.TargetURL, prefix) { - status.TargetURL = "" + return strings.CutPrefix(status.TargetURL, prefix) +} + +// ParseGiteaActionsTargetURL parses the commit status target URL as Gitea Actions link +func (status *CommitStatus) ParseGiteaActionsTargetURL(ctx context.Context) (runID, jobID int64, ok bool) { + s, ok := status.cutTargetURLGiteaActionsPrefix(ctx) + if !ok { + return 0, 0, false } + + parts := strings.Split(s, "/") // expect: /runs/{runID}/jobs/{jobID} + if len(parts) < 5 || parts[1] != "runs" || parts[3] != "jobs" { + return 0, 0, false + } + + runID, err1 := strconv.ParseInt(parts[2], 10, 64) + jobID, err2 := strconv.ParseInt(parts[4], 10, 64) + if err1 != nil || err2 != nil { + return 0, 0, false + } + return runID, jobID, true } // CalcCommitStatus returns commit status state via some status, the commit statues should order by id desc diff --git a/models/git/lfs.go b/models/git/lfs.go index 8bba060ff9..a4ae3e7bee 100644 --- a/models/git/lfs.go +++ b/models/git/lfs.go @@ -8,7 +8,6 @@ import ( "fmt" "code.gitea.io/gitea/models/db" - "code.gitea.io/gitea/models/perm" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" @@ -42,30 +41,6 @@ func (err ErrLFSLockNotExist) Unwrap() error { return util.ErrNotExist } -// ErrLFSUnauthorizedAction represents a "LFSUnauthorizedAction" kind of error. -type ErrLFSUnauthorizedAction struct { - RepoID int64 - UserName string - Mode perm.AccessMode -} - -// IsErrLFSUnauthorizedAction checks if an error is a ErrLFSUnauthorizedAction. -func IsErrLFSUnauthorizedAction(err error) bool { - _, ok := err.(ErrLFSUnauthorizedAction) - return ok -} - -func (err ErrLFSUnauthorizedAction) Error() string { - if err.Mode == perm.AccessModeWrite { - return fmt.Sprintf("User %s doesn't have write access for lfs lock [rid: %d]", err.UserName, err.RepoID) - } - return fmt.Sprintf("User %s doesn't have read access for lfs lock [rid: %d]", err.UserName, err.RepoID) -} - -func (err ErrLFSUnauthorizedAction) Unwrap() error { - return util.ErrPermissionDenied -} - // ErrLFSLockAlreadyExist represents a "LFSLockAlreadyExist" kind of error. type ErrLFSLockAlreadyExist struct { RepoID int64 @@ -93,12 +68,6 @@ type ErrLFSFileLocked struct { UserName string } -// IsErrLFSFileLocked checks if an error is a ErrLFSFileLocked. -func IsErrLFSFileLocked(err error) bool { - _, ok := err.(ErrLFSFileLocked) - return ok -} - func (err ErrLFSFileLocked) Error() string { return fmt.Sprintf("File is lfs locked [repo: %d, locked by: %s, path: %s]", err.RepoID, err.UserName, err.Path) } diff --git a/models/git/lfs_lock.go b/models/git/lfs_lock.go index c5f9a4e6de..184e616915 100644 --- a/models/git/lfs_lock.go +++ b/models/git/lfs_lock.go @@ -11,10 +11,7 @@ import ( "time" "code.gitea.io/gitea/models/db" - "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/models/unit" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" @@ -71,10 +68,6 @@ func (l *LFSLock) LoadOwner(ctx context.Context) error { // CreateLFSLock creates a new lock. func CreateLFSLock(ctx context.Context, repo *repo_model.Repository, lock *LFSLock) (*LFSLock, error) { return db.WithTx2(ctx, func(ctx context.Context) (*LFSLock, error) { - if err := CheckLFSAccessForRepo(ctx, lock.OwnerID, repo, perm.AccessModeWrite); err != nil { - return nil, err - } - lock.Path = util.PathJoinRel(lock.Path) lock.RepoID = repo.ID @@ -165,10 +158,6 @@ func DeleteLFSLockByID(ctx context.Context, id int64, repo *repo_model.Repositor return nil, err } - if err := CheckLFSAccessForRepo(ctx, u.ID, repo, perm.AccessModeWrite); err != nil { - return nil, err - } - if !force && u.ID != lock.OwnerID { return nil, errors.New("user doesn't own lock and force flag is not set") } @@ -180,22 +169,3 @@ func DeleteLFSLockByID(ctx context.Context, id int64, repo *repo_model.Repositor return lock, nil }) } - -// CheckLFSAccessForRepo check needed access mode base on action -func CheckLFSAccessForRepo(ctx context.Context, ownerID int64, repo *repo_model.Repository, mode perm.AccessMode) error { - if ownerID == 0 { - return ErrLFSUnauthorizedAction{repo.ID, "undefined", mode} - } - u, err := user_model.GetUserByID(ctx, ownerID) - if err != nil { - return err - } - perm, err := access_model.GetUserRepoPermission(ctx, repo, u) - if err != nil { - return err - } - if !perm.CanAccess(mode, unit.TypeCode) { - return ErrLFSUnauthorizedAction{repo.ID, u.DisplayName(), mode} - } - return nil -} diff --git a/models/git/protected_branch.go b/models/git/protected_branch.go index 511f7563cf..13e1ced0e1 100644 --- a/models/git/protected_branch.go +++ b/models/git/protected_branch.go @@ -5,7 +5,6 @@ package git import ( "context" - "errors" "fmt" "slices" "strings" @@ -25,7 +24,7 @@ import ( "xorm.io/builder" ) -var ErrBranchIsProtected = errors.New("branch is protected") +var ErrBranchIsProtected = util.ErrorWrap(util.ErrPermissionDenied, "branch is protected") // ProtectedBranch struct type ProtectedBranch struct { diff --git a/models/issues/issue_search.go b/models/issues/issue_search.go index 466e788d6c..049dcc7de8 100644 --- a/models/issues/issue_search.go +++ b/models/issues/issue_search.go @@ -476,7 +476,7 @@ func applySubscribedCondition(sess *xorm.Session, subscriberID int64) { ), builder.Eq{"issue.poster_id": subscriberID}, builder.In("issue.repo_id", builder. - Select("id"). + Select("repo_id"). From("watch"). Where(builder.And(builder.Eq{"user_id": subscriberID}, builder.In("mode", repo_model.WatchModeNormal, repo_model.WatchModeAuto))), diff --git a/models/issues/issue_test.go b/models/issues/issue_test.go index 09fd492667..55a90f50a1 100644 --- a/models/issues/issue_test.go +++ b/models/issues/issue_test.go @@ -197,6 +197,12 @@ func TestIssues(t *testing.T) { }, []int64{2}, }, + { + issues_model.IssuesOptions{ + SubscriberID: 11, + }, + []int64{11, 5, 9, 8, 3, 2, 1}, + }, } { issues, err := issues_model.Issues(t.Context(), &test.Opts) assert.NoError(t, err) diff --git a/models/issues/review_list.go b/models/issues/review_list.go index bbb8c489fa..86b1a2e76e 100644 --- a/models/issues/review_list.go +++ b/models/issues/review_list.go @@ -173,7 +173,7 @@ func GetReviewsByIssueID(ctx context.Context, issueID int64) (latestReviews, mig 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} + countedReivewTypes := []ReviewType{ReviewTypeApprove, ReviewTypeReject, ReviewTypeRequest, ReviewTypeComment} for _, review := range reviews { if review.ReviewerTeamID == 0 && slices.Contains(countedReivewTypes, review.Type) && !review.Dismissed { if review.OriginalAuthorID != 0 { diff --git a/models/issues/review_test.go b/models/issues/review_test.go index 7b8537cc7d..6795ea8e66 100644 --- a/models/issues/review_test.go +++ b/models/issues/review_test.go @@ -122,6 +122,7 @@ func TestGetReviewersByIssueID(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 3}) + user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}) user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) @@ -129,6 +130,12 @@ func TestGetReviewersByIssueID(t *testing.T) { expectedReviews := []*issues_model.Review{} expectedReviews = append(expectedReviews, + &issues_model.Review{ + ID: 5, + Reviewer: user1, + Type: issues_model.ReviewTypeComment, + UpdatedUnix: 946684810, + }, &issues_model.Review{ ID: 7, Reviewer: org3, @@ -167,8 +174,9 @@ func TestGetReviewersByIssueID(t *testing.T) { for _, review := range allReviews { assert.NoError(t, review.LoadReviewer(t.Context())) } - if assert.Len(t, allReviews, 5) { + if assert.Len(t, allReviews, 6) { for i, review := range allReviews { + assert.Equal(t, expectedReviews[i].ID, review.ID) assert.Equal(t, expectedReviews[i].Reviewer, review.Reviewer) assert.Equal(t, expectedReviews[i].Type, review.Type) assert.Equal(t, expectedReviews[i].UpdatedUnix, review.UpdatedUnix) diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index e164317fa9..9917704b41 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -397,7 +397,8 @@ func prepareMigrationTasks() []*migration { newMigration(322, "Extend comment tree_path length limit", v1_25.ExtendCommentTreePathLength), // Gitea 1.25.0 ends at database version 323 - newMigration(323, "Add table issue_dev_link", v1_26.CreateTableIssueDevLink), + newMigration(323, "Add support for actions concurrency", v1_26.AddActionsConcurrency), + newMigration(324, "Add table issue_dev_link", v1_26.CreateTableIssueDevLink), } return preparedMigrations } diff --git a/models/migrations/v1_12/v128.go b/models/migrations/v1_12/v128.go index 34746dcdc4..ff5b12af18 100644 --- a/models/migrations/v1_12/v128.go +++ b/models/migrations/v1_12/v128.go @@ -84,17 +84,17 @@ func FixMergeBase(ctx context.Context, x *xorm.Engine) error { if !pr.HasMerged { var err error - pr.MergeBase, _, err = gitcmd.NewCommand("merge-base").AddDashesAndList(pr.BaseBranch, gitRefName).RunStdString(ctx, &gitcmd.RunOpts{Dir: repoPath}) + pr.MergeBase, _, err = gitcmd.NewCommand("merge-base").AddDashesAndList(pr.BaseBranch, gitRefName).WithDir(repoPath).RunStdString(ctx) if err != nil { var err2 error - pr.MergeBase, _, err2 = gitcmd.NewCommand("rev-parse").AddDynamicArguments(git.BranchPrefix+pr.BaseBranch).RunStdString(ctx, &gitcmd.RunOpts{Dir: repoPath}) + pr.MergeBase, _, err2 = gitcmd.NewCommand("rev-parse").AddDynamicArguments(git.BranchPrefix + pr.BaseBranch).WithDir(repoPath).RunStdString(ctx) if err2 != nil { log.Error("Unable to get merge base for PR ID %d, Index %d in %s/%s. Error: %v & %v", pr.ID, pr.Index, baseRepo.OwnerName, baseRepo.Name, err, err2) continue } } } else { - parentsString, _, err := gitcmd.NewCommand("rev-list", "--parents", "-n", "1").AddDynamicArguments(pr.MergedCommitID).RunStdString(ctx, &gitcmd.RunOpts{Dir: repoPath}) + parentsString, _, err := gitcmd.NewCommand("rev-list", "--parents", "-n", "1").AddDynamicArguments(pr.MergedCommitID).WithDir(repoPath).RunStdString(ctx) if err != nil { log.Error("Unable to get parents for merged PR ID %d, Index %d in %s/%s. Error: %v", pr.ID, pr.Index, baseRepo.OwnerName, baseRepo.Name, err) continue @@ -108,7 +108,7 @@ func FixMergeBase(ctx context.Context, x *xorm.Engine) error { refs = append(refs, gitRefName) cmd := gitcmd.NewCommand("merge-base").AddDashesAndList(refs...) - pr.MergeBase, _, err = cmd.RunStdString(ctx, &gitcmd.RunOpts{Dir: repoPath}) + pr.MergeBase, _, err = cmd.WithDir(repoPath).RunStdString(ctx) if err != nil { log.Error("Unable to get merge base for merged PR ID %d, Index %d in %s/%s. Error: %v", pr.ID, pr.Index, baseRepo.OwnerName, baseRepo.Name, err) continue diff --git a/models/migrations/v1_12/v134.go b/models/migrations/v1_12/v134.go index d31cc3abdb..98bb8dbda7 100644 --- a/models/migrations/v1_12/v134.go +++ b/models/migrations/v1_12/v134.go @@ -80,7 +80,7 @@ func RefixMergeBase(ctx context.Context, x *xorm.Engine) error { gitRefName := fmt.Sprintf("refs/pull/%d/head", pr.Index) - parentsString, _, err := gitcmd.NewCommand("rev-list", "--parents", "-n", "1").AddDynamicArguments(pr.MergedCommitID).RunStdString(ctx, &gitcmd.RunOpts{Dir: repoPath}) + parentsString, _, err := gitcmd.NewCommand("rev-list", "--parents", "-n", "1").AddDynamicArguments(pr.MergedCommitID).WithDir(repoPath).RunStdString(ctx) if err != nil { log.Error("Unable to get parents for merged PR ID %d, Index %d in %s/%s. Error: %v", pr.ID, pr.Index, baseRepo.OwnerName, baseRepo.Name, err) continue @@ -95,7 +95,7 @@ func RefixMergeBase(ctx context.Context, x *xorm.Engine) error { refs = append(refs, gitRefName) cmd := gitcmd.NewCommand("merge-base").AddDashesAndList(refs...) - pr.MergeBase, _, err = cmd.RunStdString(ctx, &gitcmd.RunOpts{Dir: repoPath}) + pr.MergeBase, _, err = cmd.WithDir(repoPath).RunStdString(ctx) if err != nil { log.Error("Unable to get merge base for merged PR ID %d, Index %d in %s/%s. Error: %v", pr.ID, pr.Index, baseRepo.OwnerName, baseRepo.Name, err) continue diff --git a/models/migrations/v1_26/v323.go b/models/migrations/v1_26/v323.go index ed265f88e9..b116f73bf0 100644 --- a/models/migrations/v1_26/v323.go +++ b/models/migrations/v1_26/v323.go @@ -4,19 +4,40 @@ package v1_26 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 - LinkID int64 // branch id in branch table or pull request id - CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` +func AddActionsConcurrency(x *xorm.Engine) error { + type ActionRun struct { + RepoID int64 `xorm:"index(repo_concurrency)"` + RawConcurrency string + ConcurrencyGroup string `xorm:"index(repo_concurrency) NOT NULL DEFAULT ''"` + ConcurrencyCancel bool `xorm:"NOT NULL DEFAULT FALSE"` } - return x.Sync(new(IssueDevLink)) + + if _, err := x.SyncWithOptions(xorm.SyncOptions{ + IgnoreDropIndices: true, + }, new(ActionRun)); err != nil { + return err + } + + if err := x.Sync(new(ActionRun)); err != nil { + return err + } + + type ActionRunJob struct { + RepoID int64 `xorm:"index(repo_concurrency)"` + RawConcurrency string + IsConcurrencyEvaluated bool + ConcurrencyGroup string `xorm:"index(repo_concurrency) NOT NULL DEFAULT ''"` + ConcurrencyCancel bool `xorm:"NOT NULL DEFAULT FALSE"` + } + + if _, err := x.SyncWithOptions(xorm.SyncOptions{ + IgnoreDropIndices: true, + }, new(ActionRunJob)); err != nil { + return err + } + + return nil } diff --git a/models/migrations/v1_26/v324.go b/models/migrations/v1_26/v324.go new file mode 100644 index 0000000000..ed265f88e9 --- /dev/null +++ b/models/migrations/v1_26/v324.go @@ -0,0 +1,22 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_26 + +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 + LinkID int64 // branch id in branch table or pull request id + CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` + } + return x.Sync(new(IssueDevLink)) +} diff --git a/models/migrations/v1_26/v323_test.go b/models/migrations/v1_26/v324_test.go similarity index 100% rename from models/migrations/v1_26/v323_test.go rename to models/migrations/v1_26/v324_test.go diff --git a/models/organization/org.go b/models/organization/org.go index 9ece044d6c..b4d28f5405 100644 --- a/models/organization/org.go +++ b/models/organization/org.go @@ -429,6 +429,10 @@ func HasOrgOrUserVisible(ctx context.Context, orgOrUser, user *user_model.User) return true } + if !setting.Service.RequireSignInViewStrict && orgOrUser.Visibility == structs.VisibleTypePublic { + return true + } + if (orgOrUser.Visibility == structs.VisibleTypePrivate || user.IsRestricted) && !OrgFromUser(orgOrUser).hasMemberWithUserID(ctx, user.ID) { return false } diff --git a/models/organization/org_test.go b/models/organization/org_test.go index e7c4d2f9f7..7a74c5f5fc 100644 --- a/models/organization/org_test.go +++ b/models/organization/org_test.go @@ -13,7 +13,9 @@ import ( repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/test" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -382,6 +384,12 @@ func TestHasOrgVisibleTypePublic(t *testing.T) { assert.True(t, test1) // owner of org assert.True(t, test2) // user not a part of org assert.True(t, test3) // logged out user + + restrictedUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 29, IsRestricted: true}) + require.True(t, restrictedUser.IsRestricted) + assert.True(t, organization.HasOrgOrUserVisible(t.Context(), org.AsUser(), restrictedUser)) + defer test.MockVariableValue(&setting.Service.RequireSignInViewStrict, true)() + assert.False(t, organization.HasOrgOrUserVisible(t.Context(), org.AsUser(), restrictedUser)) } func TestHasOrgVisibleTypeLimited(t *testing.T) { diff --git a/models/perm/access/access.go b/models/perm/access/access.go index 6a0a901f71..6433c4675c 100644 --- a/models/perm/access/access.go +++ b/models/perm/access/access.go @@ -13,6 +13,8 @@ import ( "code.gitea.io/gitea/models/perm" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/structs" "xorm.io/builder" ) @@ -41,7 +43,12 @@ func accessLevel(ctx context.Context, user *user_model.User, repo *repo_model.Re restricted = user.IsRestricted } - if !restricted && !repo.IsPrivate { + if err := repo.LoadOwner(ctx); err != nil { + return mode, err + } + + repoIsFullyPublic := !setting.Service.RequireSignInViewStrict && repo.Owner.Visibility == structs.VisibleTypePublic && !repo.IsPrivate + if (restricted && repoIsFullyPublic) || (!restricted && !repo.IsPrivate) { mode = perm.AccessModeRead } diff --git a/models/perm/access/access_test.go b/models/perm/access/access_test.go index f01993ab4e..15d18b368c 100644 --- a/models/perm/access/access_test.go +++ b/models/perm/access/access_test.go @@ -12,6 +12,7 @@ import ( repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/setting" "github.com/stretchr/testify/assert" ) @@ -51,7 +52,14 @@ func TestAccessLevel(t *testing.T) { assert.NoError(t, err) assert.Equal(t, perm_model.AccessModeNone, level) - // restricted user has no access to a public repo + // restricted user has default access to a public repo if no sign-in is required + setting.Service.RequireSignInViewStrict = false + level, err = access_model.AccessLevel(t.Context(), user29, repo1) + assert.NoError(t, err) + assert.Equal(t, perm_model.AccessModeRead, level) + + // restricted user has no access to a public repo if sign-in is required + setting.Service.RequireSignInViewStrict = true level, err = access_model.AccessLevel(t.Context(), user29, repo1) assert.NoError(t, err) assert.Equal(t, perm_model.AccessModeNone, level) diff --git a/models/perm/access/repo_permission.go b/models/perm/access/repo_permission.go index 678b18442e..ba7544f343 100644 --- a/models/perm/access/repo_permission.go +++ b/models/perm/access/repo_permission.go @@ -5,9 +5,11 @@ package access import ( "context" + "errors" "fmt" "slices" + actions_model "code.gitea.io/gitea/models/actions" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/organization" perm_model "code.gitea.io/gitea/models/perm" @@ -253,6 +255,43 @@ func finalProcessRepoUnitPermission(user *user_model.User, perm *Permission) { } } +// GetActionsUserRepoPermission returns the actions user permissions to the repository +func GetActionsUserRepoPermission(ctx context.Context, repo *repo_model.Repository, actionsUser *user_model.User, taskID int64) (perm Permission, err error) { + if actionsUser.ID != user_model.ActionsUserID { + return perm, errors.New("api GetActionsUserRepoPermission can only be called by the actions user") + } + task, err := actions_model.GetTaskByID(ctx, taskID) + if err != nil { + return perm, err + } + + var accessMode perm_model.AccessMode + if task.RepoID != repo.ID { + taskRepo, exist, err := db.GetByID[repo_model.Repository](ctx, task.RepoID) + if err != nil || !exist { + return perm, err + } + actionsCfg := repo.MustGetUnit(ctx, unit.TypeActions).ActionsConfig() + if !actionsCfg.IsCollaborativeOwner(taskRepo.OwnerID) || !taskRepo.IsPrivate { + // The task repo can access the current repo only if the task repo is private and + // the owner of the task repo is a collaborative owner of the current repo. + // FIXME allow public repo read access if tokenless pull is enabled + return perm, nil + } + accessMode = perm_model.AccessModeRead + } else if task.IsForkPullRequest { + accessMode = perm_model.AccessModeRead + } else { + accessMode = perm_model.AccessModeWrite + } + + if err := repo.LoadUnits(ctx); err != nil { + return perm, err + } + perm.SetUnitsWithDefaultAccessMode(repo.Units, accessMode) + return perm, nil +} + // 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() { diff --git a/modules/git/repo_archive_test.go b/models/repo/archive_test.go similarity index 98% rename from modules/git/repo_archive_test.go rename to models/repo/archive_test.go index ff7e2dfce1..bb6c1bf9bc 100644 --- a/modules/git/repo_archive_test.go +++ b/models/repo/archive_test.go @@ -1,7 +1,7 @@ // Copyright 2025 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package git +package repo import ( "testing" diff --git a/models/repo/archiver.go b/models/repo/archiver.go index d06e94e5ac..4f1b7238d7 100644 --- a/models/repo/archiver.go +++ b/models/repo/archiver.go @@ -11,7 +11,6 @@ import ( "time" "code.gitea.io/gitea/models/db" - "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" @@ -27,11 +26,46 @@ const ( ArchiverReady // it's ready ) +// ArchiveType archive types +type ArchiveType int + +const ( + ArchiveUnknown ArchiveType = iota + ArchiveZip // 1 + ArchiveTarGz // 2 + ArchiveBundle // 3 +) + +// String converts an ArchiveType to string: the extension of the archive file without prefix dot +func (a ArchiveType) String() string { + switch a { + case ArchiveZip: + return "zip" + case ArchiveTarGz: + return "tar.gz" + case ArchiveBundle: + return "bundle" + } + return "unknown" +} + +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 s, ArchiveUnknown +} + // RepoArchiver represents all archivers type RepoArchiver struct { //revive:disable-line:exported - ID int64 `xorm:"pk autoincr"` - RepoID int64 `xorm:"index unique(s)"` - Type git.ArchiveType `xorm:"unique(s)"` + ID int64 `xorm:"pk autoincr"` + RepoID int64 `xorm:"index unique(s)"` + Type ArchiveType `xorm:"unique(s)"` Status ArchiverStatus CommitID string `xorm:"VARCHAR(64) unique(s)"` CreatedUnix timeutil.TimeStamp `xorm:"INDEX NOT NULL created"` @@ -56,15 +90,15 @@ func repoArchiverForRelativePath(relativePath string) (*RepoArchiver, error) { if err != nil { return nil, util.NewInvalidArgumentErrorf("invalid storage path: invalid repo id") } - commitID, archiveType := git.SplitArchiveNameType(parts[2]) - if archiveType == git.ArchiveUnknown { + commitID, archiveType := SplitArchiveNameType(parts[2]) + if archiveType == ArchiveUnknown { return nil, util.NewInvalidArgumentErrorf("invalid storage path: invalid archive type") } return &RepoArchiver{RepoID: repoID, CommitID: commitID, Type: archiveType}, nil } // GetRepoArchiver get an archiver -func GetRepoArchiver(ctx context.Context, repoID int64, tp git.ArchiveType, commitID string) (*RepoArchiver, error) { +func GetRepoArchiver(ctx context.Context, repoID int64, tp ArchiveType, commitID string) (*RepoArchiver, error) { var archiver RepoArchiver has, err := db.GetEngine(ctx).Where("repo_id=?", repoID).And("`type`=?", tp).And("commit_id=?", commitID).Get(&archiver) if err != nil { diff --git a/models/repo/repo.go b/models/repo/repo.go index 8007fb381f..33a12a7d0e 100644 --- a/models/repo/repo.go +++ b/models/repo/repo.go @@ -229,10 +229,6 @@ func RelativePath(ownerName, repoName string) string { return strings.ToLower(ownerName) + "/" + strings.ToLower(repoName) + ".git" } -func RelativeWikiPath(ownerName, repoName string) string { - return strings.ToLower(ownerName) + "/" + strings.ToLower(repoName) + ".wiki.git" -} - // RelativePath should be an unix style path like username/reponame.git func (repo *Repository) RelativePath() string { return RelativePath(repo.OwnerName, repo.Name) @@ -245,12 +241,6 @@ func (sr StorageRepo) RelativePath() string { return string(sr) } -// WikiStorageRepo returns the storage repo for the wiki -// The wiki repository should have the same object format as the code repository -func (repo *Repository) WikiStorageRepo() StorageRepo { - return StorageRepo(RelativeWikiPath(repo.OwnerName, repo.Name)) -} - // SanitizedOriginalURL returns a sanitized OriginalURL func (repo *Repository) SanitizedOriginalURL() string { if repo.OriginalURL == "" { @@ -605,7 +595,7 @@ func (repo *Repository) IsGenerated() bool { // RepoPath returns repository path by given user and repository name. func RepoPath(userName, repoName string) string { //revive:disable-line:exported - return filepath.Join(user_model.UserPath(userName), strings.ToLower(repoName)+".git") + return filepath.Join(setting.RepoRootPath, filepath.Clean(strings.ToLower(userName)), filepath.Clean(strings.ToLower(repoName)+".git")) } // RepoPath returns the repository path diff --git a/models/repo/repo_list.go b/models/repo/repo_list.go index f2cdd2f284..811f83c999 100644 --- a/models/repo/repo_list.go +++ b/models/repo/repo_list.go @@ -642,6 +642,17 @@ func SearchRepositoryIDsByCondition(ctx context.Context, cond builder.Cond) ([]i Find(&repoIDs) } +func userAllPublicRepoCond(cond builder.Cond, orgVisibilityLimit []structs.VisibleType) builder.Cond { + return cond.Or(builder.And( + builder.Eq{"`repository`.is_private": false}, + // Aren't in a private organisation or limited organisation if we're not logged in + builder.NotIn("`repository`.owner_id", builder.Select("id").From("`user`").Where( + builder.And( + builder.Eq{"type": user_model.UserTypeOrganization}, + builder.In("visibility", orgVisibilityLimit)), + )))) +} + // AccessibleRepositoryCondition takes a user a returns a condition for checking if a repository is accessible func AccessibleRepositoryCondition(user *user_model.User, unitType unit.Type) builder.Cond { cond := builder.NewCond() @@ -651,15 +662,8 @@ func AccessibleRepositoryCondition(user *user_model.User, unitType unit.Type) bu if user == nil || user.ID <= 0 { orgVisibilityLimit = append(orgVisibilityLimit, structs.VisibleTypeLimited) } - // 1. Be able to see all non-private repositories that either: - cond = cond.Or(builder.And( - builder.Eq{"`repository`.is_private": false}, - // 2. Aren't in an private organisation or limited organisation if we're not logged in - builder.NotIn("`repository`.owner_id", builder.Select("id").From("`user`").Where( - builder.And( - builder.Eq{"type": user_model.UserTypeOrganization}, - builder.In("visibility", orgVisibilityLimit)), - )))) + // 1. Be able to see all non-private repositories + cond = userAllPublicRepoCond(cond, orgVisibilityLimit) } if user != nil { @@ -683,6 +687,9 @@ func AccessibleRepositoryCondition(user *user_model.User, unitType unit.Type) bu if !user.IsRestricted { // 5. Be able to see all public repos in private organizations that we are an org_user of cond = cond.Or(userOrgPublicRepoCond(user.ID)) + } else if !setting.Service.RequireSignInViewStrict { + orgVisibilityLimit := []structs.VisibleType{structs.VisibleTypePrivate, structs.VisibleTypeLimited} + cond = userAllPublicRepoCond(cond, orgVisibilityLimit) } } diff --git a/models/repo/repo_list_test.go b/models/repo/repo_list_test.go index 6cc0d3155c..943e0c5025 100644 --- a/models/repo/repo_list_test.go +++ b/models/repo/repo_list_test.go @@ -10,9 +10,14 @@ import ( "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/optional" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/test" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func getTestCases() []struct { @@ -182,7 +187,16 @@ func getTestCases() []struct { func TestSearchRepository(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) + t.Run("SearchRepositoryPublic", testSearchRepositoryPublic) + t.Run("SearchRepositoryPublicRestricted", testSearchRepositoryRestricted) + t.Run("SearchRepositoryPrivate", testSearchRepositoryPrivate) + t.Run("SearchRepositoryNonExistingOwner", testSearchRepositoryNonExistingOwner) + t.Run("SearchRepositoryWithInDescription", testSearchRepositoryWithInDescription) + t.Run("SearchRepositoryNotInDescription", testSearchRepositoryNotInDescription) + t.Run("SearchRepositoryCases", testSearchRepositoryCases) +} +func testSearchRepositoryPublic(t *testing.T) { // test search public repository on explore page repos, count, err := repo_model.SearchRepositoryByName(t.Context(), repo_model.SearchRepoOptions{ ListOptions: db.ListOptions{ @@ -211,9 +225,54 @@ func TestSearchRepository(t *testing.T) { assert.NoError(t, err) assert.Equal(t, int64(2), count) assert.Len(t, repos, 2) +} +func testSearchRepositoryRestricted(t *testing.T) { + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + restrictedUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 29, IsRestricted: true}) + + performSearch := func(t *testing.T, user *user_model.User) (publicRepoIDs []int64) { + repos, count, err := repo_model.SearchRepositoryByName(t.Context(), repo_model.SearchRepoOptions{ + ListOptions: db.ListOptions{Page: 1, PageSize: 10000}, + Actor: user, + }) + require.NoError(t, err) + assert.Len(t, repos, int(count)) + for _, repo := range repos { + require.NoError(t, repo.LoadOwner(t.Context())) + if repo.Owner.Visibility == structs.VisibleTypePublic && !repo.IsPrivate { + publicRepoIDs = append(publicRepoIDs, repo.ID) + } + } + return publicRepoIDs + } + + normalPublicRepoIDs := performSearch(t, user2) + require.Greater(t, len(normalPublicRepoIDs), 10) // quite a lot + + t.Run("RestrictedUser-NoSignInRequirement", func(t *testing.T) { + // restricted user can also see public repositories if no "required sign-in" + repoIDs := performSearch(t, restrictedUser) + assert.ElementsMatch(t, normalPublicRepoIDs, repoIDs) + }) + + defer test.MockVariableValue(&setting.Service.RequireSignInViewStrict, true)() + + t.Run("NormalUser-RequiredSignIn", func(t *testing.T) { + // normal user can still see all public repos, not affected by "required sign-in" + repoIDs := performSearch(t, user2) + assert.ElementsMatch(t, normalPublicRepoIDs, repoIDs) + }) + t.Run("RestrictedUser-RequiredSignIn", func(t *testing.T) { + // restricted user can see only their own repo + repoIDs := performSearch(t, restrictedUser) + assert.Equal(t, []int64{4}, repoIDs) + }) +} + +func testSearchRepositoryPrivate(t *testing.T) { // test search private repository on explore page - repos, count, err = repo_model.SearchRepositoryByName(t.Context(), repo_model.SearchRepoOptions{ + repos, count, err := repo_model.SearchRepositoryByName(t.Context(), repo_model.SearchRepoOptions{ ListOptions: db.ListOptions{ Page: 1, PageSize: 10, @@ -242,16 +301,18 @@ func TestSearchRepository(t *testing.T) { assert.NoError(t, err) assert.Equal(t, int64(3), count) assert.Len(t, repos, 3) +} - // Test non existing owner - repos, count, err = repo_model.SearchRepositoryByName(t.Context(), repo_model.SearchRepoOptions{OwnerID: unittest.NonexistentID}) +func testSearchRepositoryNonExistingOwner(t *testing.T) { + repos, count, err := repo_model.SearchRepositoryByName(t.Context(), repo_model.SearchRepoOptions{OwnerID: unittest.NonexistentID}) assert.NoError(t, err) assert.Empty(t, repos) assert.Equal(t, int64(0), count) +} - // Test search within description - repos, count, err = repo_model.SearchRepository(t.Context(), repo_model.SearchRepoOptions{ +func testSearchRepositoryWithInDescription(t *testing.T) { + repos, count, err := repo_model.SearchRepository(t.Context(), repo_model.SearchRepoOptions{ ListOptions: db.ListOptions{ Page: 1, PageSize: 10, @@ -266,9 +327,10 @@ func TestSearchRepository(t *testing.T) { assert.Equal(t, "test_repo_14", repos[0].Name) } assert.Equal(t, int64(1), count) +} - // Test NOT search within description - repos, count, err = repo_model.SearchRepository(t.Context(), repo_model.SearchRepoOptions{ +func testSearchRepositoryNotInDescription(t *testing.T) { + repos, count, err := repo_model.SearchRepository(t.Context(), repo_model.SearchRepoOptions{ ListOptions: db.ListOptions{ Page: 1, PageSize: 10, @@ -281,7 +343,9 @@ func TestSearchRepository(t *testing.T) { assert.NoError(t, err) assert.Empty(t, repos) assert.Equal(t, int64(0), count) +} +func testSearchRepositoryCases(t *testing.T) { testCases := getTestCases() for _, testCase := range testCases { diff --git a/models/repo/repo_unit.go b/models/repo/repo_unit.go index a5207bc22a..ad0bb9d3f8 100644 --- a/models/repo/repo_unit.go +++ b/models/repo/repo_unit.go @@ -170,6 +170,9 @@ func (cfg *PullRequestsConfig) GetDefaultMergeStyle() MergeStyle { type ActionsConfig struct { DisabledWorkflows []string + // CollaborativeOwnerIDs is a list of owner IDs used to share actions from private repos. + // Only workflows from the private repos whose owners are in CollaborativeOwnerIDs can access the current repo's actions. + CollaborativeOwnerIDs []int64 } func (cfg *ActionsConfig) EnableWorkflow(file string) { @@ -192,6 +195,20 @@ func (cfg *ActionsConfig) DisableWorkflow(file string) { cfg.DisabledWorkflows = append(cfg.DisabledWorkflows, file) } +func (cfg *ActionsConfig) AddCollaborativeOwner(ownerID int64) { + if !slices.Contains(cfg.CollaborativeOwnerIDs, ownerID) { + cfg.CollaborativeOwnerIDs = append(cfg.CollaborativeOwnerIDs, ownerID) + } +} + +func (cfg *ActionsConfig) RemoveCollaborativeOwner(ownerID int64) { + cfg.CollaborativeOwnerIDs = util.SliceRemoveAll(cfg.CollaborativeOwnerIDs, ownerID) +} + +func (cfg *ActionsConfig) IsCollaborativeOwner(ownerID int64) bool { + return slices.Contains(cfg.CollaborativeOwnerIDs, ownerID) +} + // FromDB fills up a ActionsConfig from serialized format. func (cfg *ActionsConfig) FromDB(bs []byte) error { return json.UnmarshalHandleDoubleEncode(bs, &cfg) diff --git a/models/repo/upload.go b/models/repo/upload.go index f7d4749842..b9bda8fdbf 100644 --- a/models/repo/upload.go +++ b/models/repo/upload.go @@ -127,16 +127,9 @@ func DeleteUploads(ctx context.Context, uploads ...*Upload) (err error) { for _, upload := range uploads { localPath := upload.LocalPath() - isFile, err := util.IsFile(localPath) - if err != nil { - log.Error("Unable to check if %s is a file. Error: %v", localPath, err) - } - if !isFile { - continue - } - if err := util.Remove(localPath); err != nil { - return fmt.Errorf("remove upload: %w", err) + // just continue, don't fail the whole operation if a file is missing (removed by others) + log.Error("unable to remove upload file %s: %v", localPath, err) } } diff --git a/models/repo/wiki.go b/models/repo/wiki.go index 9f41445bf8..47c8fa43ab 100644 --- a/models/repo/wiki.go +++ b/models/repo/wiki.go @@ -7,7 +7,6 @@ package repo import ( "context" "fmt" - "path/filepath" "strings" user_model "code.gitea.io/gitea/models/user" @@ -76,12 +75,12 @@ func (repo *Repository) WikiCloneLink(ctx context.Context, doer *user_model.User return repo.cloneLink(ctx, doer, repo.Name+".wiki") } -// WikiPath returns wiki data path by given user and repository name. -func WikiPath(userName, repoName string) string { - return filepath.Join(user_model.UserPath(userName), strings.ToLower(repoName)+".wiki.git") +func RelativeWikiPath(ownerName, repoName string) string { + return strings.ToLower(ownerName) + "/" + strings.ToLower(repoName) + ".wiki.git" } -// WikiPath returns wiki data path for given repository. -func (repo *Repository) WikiPath() string { - return WikiPath(repo.OwnerName, repo.Name) +// WikiStorageRepo returns the storage repo for the wiki +// The wiki repository should have the same object format as the code repository +func (repo *Repository) WikiStorageRepo() StorageRepo { + return StorageRepo(RelativeWikiPath(repo.OwnerName, repo.Name)) } diff --git a/models/repo/wiki_test.go b/models/repo/wiki_test.go index 41e53d93d9..636c78009b 100644 --- a/models/repo/wiki_test.go +++ b/models/repo/wiki_test.go @@ -4,12 +4,10 @@ package repo_test import ( - "path/filepath" "testing" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" - "code.gitea.io/gitea/modules/setting" "github.com/stretchr/testify/assert" ) @@ -23,15 +21,10 @@ func TestRepository_WikiCloneLink(t *testing.T) { assert.Equal(t, "https://try.gitea.io/user2/repo1.wiki.git", cloneLink.HTTPS) } -func TestWikiPath(t *testing.T) { +func TestRepository_RelativeWikiPath(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - expected := filepath.Join(setting.RepoRootPath, "user2/repo1.wiki.git") - assert.Equal(t, expected, repo_model.WikiPath("user2", "repo1")) -} -func TestRepository_WikiPath(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) - expected := filepath.Join(setting.RepoRootPath, "user2/repo1.wiki.git") - assert.Equal(t, expected, repo.WikiPath()) + assert.Equal(t, "user2/repo1.wiki.git", repo_model.RelativeWikiPath(repo.OwnerName, repo.Name)) + assert.Equal(t, "user2/repo1.wiki.git", repo.WikiStorageRepo().RelativePath()) } diff --git a/models/secret/secret.go b/models/secret/secret.go index 10a0287dfd..a82a924c39 100644 --- a/models/secret/secret.go +++ b/models/secret/secret.go @@ -178,8 +178,8 @@ func GetSecretsOfTask(ctx context.Context, task *actions_model.ActionTask) (map[ for _, secret := range append(ownerSecrets, repoSecrets...) { v, err := secret_module.DecryptSecret(setting.SecretKey, secret.Data) if err != nil { - log.Error("decrypt secret %v %q: %v", secret.ID, secret.Name, err) - return nil, err + log.Error("Unable to decrypt Actions secret %v %q, maybe SECRET_KEY is wrong: %v", secret.ID, secret.Name, err) + continue } secrets[secret.Name] = v } diff --git a/models/user/search.go b/models/user/search.go index cfd0d011bc..db4b07f64a 100644 --- a/models/user/search.go +++ b/models/user/search.go @@ -6,6 +6,7 @@ package user import ( "context" "fmt" + "slices" "strings" "code.gitea.io/gitea/models/db" @@ -22,7 +23,7 @@ type SearchUserOptions struct { db.ListOptions Keyword string - Type UserType + Types []UserType UID int64 LoginName string // this option should be used only for admin user SourceID int64 // this option should be used only for admin user @@ -43,16 +44,16 @@ type SearchUserOptions struct { func (opts *SearchUserOptions) toSearchQueryBase(ctx context.Context) *xorm.Session { var cond builder.Cond - cond = builder.Eq{"type": opts.Type} + cond = builder.In("type", opts.Types) if opts.IncludeReserved { - switch opts.Type { - case UserTypeIndividual: + switch { + case slices.Contains(opts.Types, UserTypeIndividual): cond = cond.Or(builder.Eq{"type": UserTypeUserReserved}).Or( builder.Eq{"type": UserTypeBot}, ).Or( builder.Eq{"type": UserTypeRemoteUser}, ) - case UserTypeOrganization: + case slices.Contains(opts.Types, UserTypeOrganization): cond = cond.Or(builder.Eq{"type": UserTypeOrganizationReserved}) } } diff --git a/models/user/user.go b/models/user/user.go index 6143992a25..d6e1eec276 100644 --- a/models/user/user.go +++ b/models/user/user.go @@ -249,8 +249,13 @@ func (u *User) MaxCreationLimit() int { } // CanCreateRepoIn checks whether the doer(u) can create a repository in the owner -// NOTE: functions calling this assume a failure due to repository count limit; it ONLY checks the repo number LIMIT, if new checks are added, those functions should be revised +// NOTE: functions calling this assume a failure due to repository count limit, or the owner is not a real user. +// It ONLY checks the repo number LIMIT or whether owner user is real. If new checks are added, those functions should be revised. +// TODO: the callers can only return ErrReachLimitOfRepo, need to fine tune to support other error types in the future. func (u *User) CanCreateRepoIn(owner *User) bool { + if u.ID <= 0 || owner.ID <= 0 { + return false // fake user like Ghost or Actions user + } if u.IsAdmin { return true } @@ -980,7 +985,7 @@ func GetInactiveUsers(ctx context.Context, olderThan time.Duration) ([]*User, er // UserPath returns the path absolute path of user repositories. func UserPath(userName string) string { //revive:disable-line:exported - return filepath.Join(setting.RepoRootPath, strings.ToLower(userName)) + return filepath.Join(setting.RepoRootPath, filepath.Clean(strings.ToLower(userName))) } // GetUserByID returns the user object by given ID if exists. @@ -1444,3 +1449,15 @@ func DisabledFeaturesWithLoginType(user *User) *container.Set[string] { } return &setting.Admin.UserDisabledFeatures } + +// GetUserOrOrgIDByName returns the id for a user or an org by name +func GetUserOrOrgIDByName(ctx context.Context, name string) (int64, error) { + var id int64 + has, err := db.GetEngine(ctx).Table("user").Where("name = ?", name).Cols("id").Get(&id) + if err != nil { + return 0, err + } else if !has { + return 0, fmt.Errorf("user or org with name %s: %w", name, util.ErrNotExist) + } + return id, nil +} diff --git a/models/user/user_system.go b/models/user/user_system.go index e07274d291..11008c77d4 100644 --- a/models/user/user_system.go +++ b/models/user/user_system.go @@ -48,17 +48,16 @@ func IsGiteaActionsUserName(name string) bool { // NewActionsUser creates and returns a fake user for running the actions. func NewActionsUser() *User { return &User{ - ID: ActionsUserID, - Name: ActionsUserName, - LowerName: ActionsUserName, - IsActive: true, - FullName: "Gitea Actions", - Email: ActionsUserEmail, - KeepEmailPrivate: true, - LoginName: ActionsUserName, - Type: UserTypeBot, - AllowCreateOrganization: true, - Visibility: structs.VisibleTypePublic, + ID: ActionsUserID, + Name: ActionsUserName, + LowerName: ActionsUserName, + IsActive: true, + FullName: "Gitea Actions", + Email: ActionsUserEmail, + KeepEmailPrivate: true, + LoginName: ActionsUserName, + Type: UserTypeBot, + Visibility: structs.VisibleTypePublic, } } diff --git a/models/user/user_test.go b/models/user/user_test.go index 4201ec4816..923f2cd40e 100644 --- a/models/user/user_test.go +++ b/models/user/user_test.go @@ -126,7 +126,7 @@ func TestSearchUsers(t *testing.T) { // test orgs testOrgSuccess := func(opts user_model.SearchUserOptions, expectedOrgIDs []int64) { - opts.Type = user_model.UserTypeOrganization + opts.Types = []user_model.UserType{user_model.UserTypeOrganization} testSuccess(opts, expectedOrgIDs) } @@ -150,7 +150,7 @@ func TestSearchUsers(t *testing.T) { // test users testUserSuccess := func(opts user_model.SearchUserOptions, expectedUserIDs []int64) { - opts.Type = user_model.UserTypeIndividual + opts.Types = []user_model.UserType{user_model.UserTypeIndividual} testSuccess(opts, expectedUserIDs) } @@ -648,33 +648,36 @@ func TestGetInactiveUsers(t *testing.T) { func TestCanCreateRepo(t *testing.T) { defer test.MockVariableValue(&setting.Repository.MaxCreationLimit)() const noLimit = -1 - doerNormal := &user_model.User{} - doerAdmin := &user_model.User{IsAdmin: true} + doerActions := user_model.NewActionsUser() + doerNormal := &user_model.User{ID: 2} + doerAdmin := &user_model.User{ID: 1, IsAdmin: true} t.Run("NoGlobalLimit", func(t *testing.T) { setting.Repository.MaxCreationLimit = noLimit - assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: noLimit})) - assert.False(t, doerNormal.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: 0})) - assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: 100})) + assert.False(t, doerNormal.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: 0})) + assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: 100})) + assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: noLimit})) - assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: noLimit})) - assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: 0})) - assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: 100})) + assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: 0})) + assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: 100})) + assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: noLimit})) + assert.False(t, doerActions.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: noLimit})) + assert.False(t, doerAdmin.CanCreateRepoIn(doerActions)) }) t.Run("GlobalLimit50", func(t *testing.T) { setting.Repository.MaxCreationLimit = 50 - assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: noLimit})) - assert.False(t, doerNormal.CanCreateRepoIn(&user_model.User{NumRepos: 60, MaxRepoCreation: noLimit})) // limited by global limit - assert.False(t, doerNormal.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: 0})) - assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: 100})) - assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{NumRepos: 60, MaxRepoCreation: 100})) + assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: noLimit})) + assert.False(t, doerNormal.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 60, MaxRepoCreation: noLimit})) // limited by global limit + assert.False(t, doerNormal.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: 0})) + assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: 100})) + assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 60, MaxRepoCreation: 100})) - assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: noLimit})) - assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{NumRepos: 60, MaxRepoCreation: noLimit})) - assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: 0})) - assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: 100})) - assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{NumRepos: 60, MaxRepoCreation: 100})) + assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: noLimit})) + assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 60, MaxRepoCreation: noLimit})) + assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: 0})) + assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: 100})) + assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 60, MaxRepoCreation: 100})) }) } diff --git a/modules/auth/password/hash/argon2.go b/modules/auth/password/hash/argon2.go index 0cd6472fa1..f4a7497df6 100644 --- a/modules/auth/password/hash/argon2.go +++ b/modules/auth/password/hash/argon2.go @@ -61,17 +61,11 @@ func NewArgon2Hasher(config string) *Argon2Hasher { return nil } - parsed, err := parseUIntParam(vals[0], "time", "argon2", config, nil) - hasher.time = uint32(parsed) - - parsed, err = parseUIntParam(vals[1], "memory", "argon2", config, err) - hasher.memory = uint32(parsed) - - parsed, err = parseUIntParam(vals[2], "threads", "argon2", config, err) - hasher.threads = uint8(parsed) - - parsed, err = parseUIntParam(vals[3], "keyLen", "argon2", config, err) - hasher.keyLen = uint32(parsed) + var err error + hasher.time, err = parseUintParam[uint32](vals[0], "time", "argon2", config, nil) + hasher.memory, err = parseUintParam[uint32](vals[1], "memory", "argon2", config, err) + hasher.threads, err = parseUintParam[uint8](vals[2], "threads", "argon2", config, err) + hasher.keyLen, err = parseUintParam[uint32](vals[3], "keyLen", "argon2", config, err) if err != nil { return nil } diff --git a/modules/auth/password/hash/common.go b/modules/auth/password/hash/common.go index d5e2c34314..1fafc289ed 100644 --- a/modules/auth/password/hash/common.go +++ b/modules/auth/password/hash/common.go @@ -7,6 +7,7 @@ import ( "strconv" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/util" ) func parseIntParam(value, param, algorithmName, config string, previousErr error) (int, error) { @@ -18,11 +19,12 @@ func parseIntParam(value, param, algorithmName, config string, previousErr error return parsed, previousErr // <- Keep the previous error as this function should still return an error once everything has been checked if any call failed } -func parseUIntParam(value, param, algorithmName, config string, previousErr error) (uint64, error) { //nolint:unparam // algorithmName is always argon2 - parsed, err := strconv.ParseUint(value, 10, 64) +func parseUintParam[T uint32 | uint8](value, param, algorithmName, config string, previousErr error) (ret T, _ error) { + _, isUint32 := any(ret).(uint32) + parsed, err := strconv.ParseUint(value, 10, util.Iif(isUint32, 32, 8)) if err != nil { log.Error("invalid integer for %s representation in %s hash spec %s", param, algorithmName, config) return 0, err } - return parsed, previousErr // <- Keep the previous error as this function should still return an error once everything has been checked if any call failed + return T(parsed), previousErr // <- Keep the previous error as this function should still return an error once everything has been checked if any call failed } diff --git a/modules/auth/password/pwn/pwn.go b/modules/auth/password/pwn/pwn.go index 99a6ca6cea..d5ea96c4af 100644 --- a/modules/auth/password/pwn/pwn.go +++ b/modules/auth/password/pwn/pwn.go @@ -72,7 +72,7 @@ func newRequest(ctx context.Context, method, url string, body io.ReadCloser) (*h // Adding padding will make requests more secure, however is also slower // because artificial responses will be added to the response // For more information, see https://www.troyhunt.com/enhancing-pwned-passwords-privacy-with-padding/ -func (c *Client) CheckPassword(pw string, padding bool) (int, error) { +func (c *Client) CheckPassword(pw string, padding bool) (int64, error) { if pw == "" { return -1, ErrEmptyPassword } @@ -111,7 +111,7 @@ func (c *Client) CheckPassword(pw string, padding bool) (int, error) { if err != nil { return -1, err } - return int(count), nil + return count, nil } } return 0, nil diff --git a/modules/auth/password/pwn/pwn_test.go b/modules/auth/password/pwn/pwn_test.go index ae03fabc57..4b760fdf32 100644 --- a/modules/auth/password/pwn/pwn_test.go +++ b/modules/auth/password/pwn/pwn_test.go @@ -37,25 +37,25 @@ func TestPassword(t *testing.T) { count, err := client.CheckPassword("", false) assert.ErrorIs(t, err, ErrEmptyPassword, "blank input should return ErrEmptyPassword") - assert.Equal(t, -1, count) + assert.EqualValues(t, -1, count) count, err = client.CheckPassword("pwned", false) assert.NoError(t, err) - assert.Equal(t, 1, count) + assert.EqualValues(t, 1, count) count, err = client.CheckPassword("notpwned", false) assert.NoError(t, err) - assert.Equal(t, 0, count) + assert.EqualValues(t, 0, count) count, err = client.CheckPassword("paddedpwned", true) assert.NoError(t, err) - assert.Equal(t, 1, count) + assert.EqualValues(t, 1, count) count, err = client.CheckPassword("paddednotpwned", true) assert.NoError(t, err) - assert.Equal(t, 0, count) + assert.EqualValues(t, 0, count) count, err = client.CheckPassword("paddednotpwnedzero", true) assert.NoError(t, err) - assert.Equal(t, 0, count) + assert.EqualValues(t, 0, count) } diff --git a/modules/emoji/emoji.go b/modules/emoji/emoji.go index 3d4ef8599b..891a0b9ab3 100644 --- a/modules/emoji/emoji.go +++ b/modules/emoji/emoji.go @@ -8,7 +8,9 @@ import ( "io" "sort" "strings" - "sync" + "sync/atomic" + + "code.gitea.io/gitea/modules/setting" ) // Gemoji is a set of emoji data. @@ -23,74 +25,78 @@ type Emoji struct { SkinTones bool } -var ( - // codeMap provides a map of the emoji unicode code to its emoji data. - codeMap map[string]int +type globalVarsStruct struct { + codeMap map[string]int // emoji unicode code to its emoji data. + aliasMap map[string]int // the alias to its emoji data. + emptyReplacer *strings.Replacer // string replacer for emoji codes, used for finding emoji positions. + codeReplacer *strings.Replacer // string replacer for emoji codes. + aliasReplacer *strings.Replacer // string replacer for emoji aliases. +} - // aliasMap provides a map of the alias to its emoji data. - aliasMap map[string]int +var globalVarsStore atomic.Pointer[globalVarsStruct] - // emptyReplacer is the string replacer for emoji codes. - emptyReplacer *strings.Replacer +func globalVars() *globalVarsStruct { + vars := globalVarsStore.Load() + if vars != nil { + return vars + } + // although there can be concurrent calls, the result should be the same, and there is no performance problem + vars = &globalVarsStruct{} + vars.codeMap = make(map[string]int, len(GemojiData)) + vars.aliasMap = make(map[string]int, len(GemojiData)) - // codeReplacer is the string replacer for emoji codes. - codeReplacer *strings.Replacer + // process emoji codes and aliases + codePairs := make([]string, 0) + emptyPairs := make([]string, 0) + aliasPairs := make([]string, 0) - // aliasReplacer is the string replacer for emoji aliases. - aliasReplacer *strings.Replacer + // sort from largest to small so we match combined emoji first + sort.Slice(GemojiData, func(i, j int) bool { + return len(GemojiData[i].Emoji) > len(GemojiData[j].Emoji) + }) - once sync.Once -) - -func loadMap() { - once.Do(func() { - // initialize - codeMap = make(map[string]int, len(GemojiData)) - aliasMap = make(map[string]int, len(GemojiData)) - - // process emoji codes and aliases - codePairs := make([]string, 0) - emptyPairs := make([]string, 0) - aliasPairs := make([]string, 0) - - // sort from largest to small so we match combined emoji first - sort.Slice(GemojiData, func(i, j int) bool { - return len(GemojiData[i].Emoji) > len(GemojiData[j].Emoji) - }) - - for i, e := range GemojiData { - if e.Emoji == "" || len(e.Aliases) == 0 { - continue - } - - // setup codes - codeMap[e.Emoji] = i - codePairs = append(codePairs, e.Emoji, ":"+e.Aliases[0]+":") - emptyPairs = append(emptyPairs, e.Emoji, e.Emoji) - - // setup aliases - for _, a := range e.Aliases { - if a == "" { - continue - } - - aliasMap[a] = i - aliasPairs = append(aliasPairs, ":"+a+":", e.Emoji) - } + for idx, emoji := range GemojiData { + if emoji.Emoji == "" || len(emoji.Aliases) == 0 { + continue } - // create replacers - emptyReplacer = strings.NewReplacer(emptyPairs...) - codeReplacer = strings.NewReplacer(codePairs...) - aliasReplacer = strings.NewReplacer(aliasPairs...) - }) + // process aliases + firstAlias := "" + for _, alias := range emoji.Aliases { + if alias == "" { + continue + } + enabled := len(setting.UI.EnabledEmojisSet) == 0 || setting.UI.EnabledEmojisSet.Contains(alias) + if !enabled { + continue + } + if firstAlias == "" { + firstAlias = alias + } + vars.aliasMap[alias] = idx + aliasPairs = append(aliasPairs, ":"+alias+":", emoji.Emoji) + } + + // process emoji code + if firstAlias != "" { + vars.codeMap[emoji.Emoji] = idx + codePairs = append(codePairs, emoji.Emoji, ":"+emoji.Aliases[0]+":") + emptyPairs = append(emptyPairs, emoji.Emoji, emoji.Emoji) + } + } + + // create replacers + vars.emptyReplacer = strings.NewReplacer(emptyPairs...) + vars.codeReplacer = strings.NewReplacer(codePairs...) + vars.aliasReplacer = strings.NewReplacer(aliasPairs...) + globalVarsStore.Store(vars) + return vars } // FromCode retrieves the emoji data based on the provided unicode code (ie, // "\u2618" will return the Gemoji data for "shamrock"). func FromCode(code string) *Emoji { - loadMap() - i, ok := codeMap[code] + i, ok := globalVars().codeMap[code] if !ok { return nil } @@ -102,12 +108,11 @@ func FromCode(code string) *Emoji { // "alias" or ":alias:" (ie, "shamrock" or ":shamrock:" will return the Gemoji // data for "shamrock"). func FromAlias(alias string) *Emoji { - loadMap() if strings.HasPrefix(alias, ":") && strings.HasSuffix(alias, ":") { alias = alias[1 : len(alias)-1] } - i, ok := aliasMap[alias] + i, ok := globalVars().aliasMap[alias] if !ok { return nil } @@ -119,15 +124,13 @@ func FromAlias(alias string) *Emoji { // alias (in the form of ":alias:") (ie, "\u2618" will be converted to // ":shamrock:"). func ReplaceCodes(s string) string { - loadMap() - return codeReplacer.Replace(s) + return globalVars().codeReplacer.Replace(s) } // ReplaceAliases replaces all aliases of the form ":alias:" with its // corresponding unicode value. func ReplaceAliases(s string) string { - loadMap() - return aliasReplacer.Replace(s) + return globalVars().aliasReplacer.Replace(s) } type rememberSecondWriteWriter struct { @@ -163,7 +166,6 @@ func (n *rememberSecondWriteWriter) WriteString(s string) (int, error) { // FindEmojiSubmatchIndex returns index pair of longest emoji in a string func FindEmojiSubmatchIndex(s string) []int { - loadMap() secondWriteWriter := rememberSecondWriteWriter{} // A faster and clean implementation would copy the trie tree formation in strings.NewReplacer but @@ -175,7 +177,7 @@ func FindEmojiSubmatchIndex(s string) []int { // Therefore we can simply take the index of the second write as our first emoji // // FIXME: just copy the trie implementation from strings.NewReplacer - _, _ = emptyReplacer.WriteString(&secondWriteWriter, s) + _, _ = globalVars().emptyReplacer.WriteString(&secondWriteWriter, s) // if we wrote less than twice then we never "replaced" if secondWriteWriter.writecount < 2 { diff --git a/modules/emoji/emoji_test.go b/modules/emoji/emoji_test.go index fbf80fe41a..607299cdc1 100644 --- a/modules/emoji/emoji_test.go +++ b/modules/emoji/emoji_test.go @@ -7,14 +7,13 @@ package emoji import ( "testing" + "code.gitea.io/gitea/modules/container" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" + "github.com/stretchr/testify/assert" ) -func TestDumpInfo(t *testing.T) { - t.Logf("codes: %d", len(codeMap)) - t.Logf("aliases: %d", len(aliasMap)) -} - func TestLookup(t *testing.T) { a := FromCode("\U0001f37a") b := FromCode("🍺") @@ -24,7 +23,6 @@ func TestLookup(t *testing.T) { 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:") @@ -32,7 +30,20 @@ func TestLookup(t *testing.T) { assert.Equal(t, m, n) assert.Equal(t, m, o) - assert.Equal(t, n, o) + + defer test.MockVariableValue(&setting.UI.EnabledEmojisSet, container.SetOf("thumbsup"))() + defer globalVarsStore.Store(nil) + globalVarsStore.Store(nil) + a = FromCode("\U0001f37a") + c = FromAlias(":beer:") + m = FromCode("\U0001f44d") + n = FromAlias(":thumbsup:") + o = FromAlias("+1") + assert.Nil(t, a) + assert.Nil(t, c) + assert.NotNil(t, m) + assert.NotNil(t, n) + assert.Nil(t, o) } func TestReplacers(t *testing.T) { diff --git a/modules/git/attribute/batch.go b/modules/git/attribute/batch.go index 9f805d55c5..27befdfa25 100644 --- a/modules/git/attribute/batch.go +++ b/modules/git/attribute/batch.go @@ -77,13 +77,12 @@ func NewBatchChecker(repo *git.Repository, treeish string, attributes []string) _ = lw.Close() }() stdErr := new(bytes.Buffer) - err := cmd.Run(ctx, &gitcmd.RunOpts{ - Env: envs, - Dir: repo.Path, - Stdin: stdinReader, - Stdout: lw, - Stderr: stdErr, - }) + err := cmd.WithEnv(envs). + WithDir(repo.Path). + WithStdin(stdinReader). + WithStdout(lw). + WithStderr(stdErr). + Run(ctx) if err != nil && !git.IsErrCanceledOrKilled(err) { log.Error("Attribute checker for commit %s exits with error: %v", treeish, err) diff --git a/modules/git/attribute/checker.go b/modules/git/attribute/checker.go index 4b313adf37..49c0eb90ef 100644 --- a/modules/git/attribute/checker.go +++ b/modules/git/attribute/checker.go @@ -71,12 +71,11 @@ func CheckAttributes(ctx context.Context, gitRepo *git.Repository, treeish strin stdOut := new(bytes.Buffer) stdErr := new(bytes.Buffer) - if err := cmd.Run(ctx, &gitcmd.RunOpts{ - Env: append(os.Environ(), envs...), - Dir: gitRepo.Path, - Stdout: stdOut, - Stderr: stdErr, - }); err != nil { + if err := cmd.WithEnv(append(os.Environ(), envs...)). + WithDir(gitRepo.Path). + WithStdout(stdOut). + WithStderr(stdErr). + Run(ctx); err != nil { return nil, fmt.Errorf("failed to run check-attr: %w\n%s\n%s", err, stdOut.String(), stdErr.String()) } diff --git a/modules/git/batch_reader.go b/modules/git/batch_reader.go index f09f4144c8..b5cec130d5 100644 --- a/modules/git/batch_reader.go +++ b/modules/git/batch_reader.go @@ -31,10 +31,9 @@ type WriteCloserError interface { func ensureValidGitRepository(ctx context.Context, repoPath string) error { stderr := strings.Builder{} err := gitcmd.NewCommand("rev-parse"). - Run(ctx, &gitcmd.RunOpts{ - Dir: repoPath, - Stderr: &stderr, - }) + WithDir(repoPath). + WithStderr(&stderr). + Run(ctx) if err != nil { return gitcmd.ConcatenateError(err, (&stderr).String()) } @@ -63,14 +62,12 @@ func catFileBatchCheck(ctx context.Context, repoPath string) (WriteCloserError, go func() { stderr := strings.Builder{} err := gitcmd.NewCommand("cat-file", "--batch-check"). - Run(ctx, &gitcmd.RunOpts{ - Dir: repoPath, - Stdin: batchStdinReader, - Stdout: batchStdoutWriter, - Stderr: &stderr, - - UseContextTimeout: true, - }) + WithDir(repoPath). + WithStdin(batchStdinReader). + WithStdout(batchStdoutWriter). + WithStderr(&stderr). + WithUseContextTimeout(true). + Run(ctx) if err != nil { _ = batchStdoutWriter.CloseWithError(gitcmd.ConcatenateError(err, (&stderr).String())) _ = batchStdinReader.CloseWithError(gitcmd.ConcatenateError(err, (&stderr).String())) @@ -111,14 +108,12 @@ func catFileBatch(ctx context.Context, repoPath string) (WriteCloserError, *bufi go func() { stderr := strings.Builder{} err := gitcmd.NewCommand("cat-file", "--batch"). - Run(ctx, &gitcmd.RunOpts{ - Dir: repoPath, - Stdin: batchStdinReader, - Stdout: batchStdoutWriter, - Stderr: &stderr, - - UseContextTimeout: true, - }) + WithDir(repoPath). + WithStdin(batchStdinReader). + WithStdout(batchStdoutWriter). + WithStderr(&stderr). + WithUseContextTimeout(true). + Run(ctx) if err != nil { _ = batchStdoutWriter.CloseWithError(gitcmd.ConcatenateError(err, (&stderr).String())) _ = batchStdinReader.CloseWithError(gitcmd.ConcatenateError(err, (&stderr).String())) diff --git a/modules/git/blame.go b/modules/git/blame.go index 50cadc41c2..601be96f05 100644 --- a/modules/git/blame.go +++ b/modules/git/blame.go @@ -166,12 +166,11 @@ func CreateBlameReader(ctx context.Context, objectFormat ObjectFormat, repoPath go func() { stderr := bytes.Buffer{} // TODO: it doesn't work for directories (the directories shouldn't be "blamed"), and the "err" should be returned by "Read" but not by "Close" - err := cmd.Run(ctx, &gitcmd.RunOpts{ - UseContextTimeout: true, - Dir: repoPath, - Stdout: stdout, - Stderr: &stderr, - }) + err := cmd.WithDir(repoPath). + WithUseContextTimeout(true). + WithStdout(stdout). + WithStderr(&stderr). + Run(ctx) done <- err _ = stdout.Close() if err != nil { diff --git a/modules/git/commit.go b/modules/git/commit.go index a0c5955ae8..260b81b590 100644 --- a/modules/git/commit.go +++ b/modules/git/commit.go @@ -93,7 +93,7 @@ func AddChanges(ctx context.Context, repoPath string, all bool, files ...string) cmd.AddArguments("--all") } cmd.AddDashesAndList(files...) - _, _, err := cmd.RunStdString(ctx, &gitcmd.RunOpts{Dir: repoPath}) + _, _, err := cmd.WithDir(repoPath).RunStdString(ctx) return err } @@ -122,7 +122,7 @@ func CommitChanges(ctx context.Context, repoPath string, opts CommitChangesOptio } cmd.AddOptionFormat("--message=%s", opts.Message) - _, _, err := cmd.RunStdString(ctx, &gitcmd.RunOpts{Dir: repoPath}) + _, _, err := cmd.WithDir(repoPath).RunStdString(ctx) // No stderr but exit status 1 means nothing to commit. if err != nil && err.Error() == "exit status 1" { return nil @@ -141,7 +141,7 @@ func AllCommitsCount(ctx context.Context, repoPath string, hidePRRefs bool, file cmd.AddDashesAndList(files...) } - stdout, _, err := cmd.RunStdString(ctx, &gitcmd.RunOpts{Dir: repoPath}) + stdout, _, err := cmd.WithDir(repoPath).RunStdString(ctx) if err != nil { return 0, err } @@ -173,7 +173,7 @@ func CommitsCount(ctx context.Context, opts CommitsCountOptions) (int64, error) cmd.AddDashesAndList(opts.RelPath...) } - stdout, _, err := cmd.RunStdString(ctx, &gitcmd.RunOpts{Dir: opts.RepoPath}) + stdout, _, err := cmd.WithDir(opts.RepoPath).RunStdString(ctx) if err != nil { return 0, err } @@ -208,7 +208,10 @@ func (c *Commit) HasPreviousCommit(objectID ObjectID) (bool, error) { return false, nil } - _, _, err := gitcmd.NewCommand("merge-base", "--is-ancestor").AddDynamicArguments(that, this).RunStdString(c.repo.Ctx, &gitcmd.RunOpts{Dir: c.repo.Path}) + _, _, err := gitcmd.NewCommand("merge-base", "--is-ancestor"). + AddDynamicArguments(that, this). + WithDir(c.repo.Path). + RunStdString(c.repo.Ctx) if err == nil { return true, nil } @@ -354,7 +357,7 @@ func (c *Commit) GetBranchName() (string, error) { cmd.AddArguments("--exclude", "refs/tags/*") } cmd.AddArguments("--name-only", "--no-undefined").AddDynamicArguments(c.ID.String()) - data, _, err := cmd.RunStdString(c.repo.Ctx, &gitcmd.RunOpts{Dir: c.repo.Path}) + data, _, err := cmd.WithDir(c.repo.Path).RunStdString(c.repo.Ctx) if err != nil { // handle special case where git can not describe commit if strings.Contains(err.Error(), "cannot describe") { @@ -432,11 +435,12 @@ func GetCommitFileStatus(ctx context.Context, repoPath, commitID string) (*Commi }() stderr := new(bytes.Buffer) - err := gitcmd.NewCommand("log", "--name-status", "-m", "--pretty=format:", "--first-parent", "--no-renames", "-z", "-1").AddDynamicArguments(commitID).Run(ctx, &gitcmd.RunOpts{ - Dir: repoPath, - Stdout: w, - Stderr: stderr, - }) + err := gitcmd.NewCommand("log", "--name-status", "-m", "--pretty=format:", "--first-parent", "--no-renames", "-z", "-1"). + AddDynamicArguments(commitID). + WithDir(repoPath). + WithStdout(w). + WithStderr(stderr). + Run(ctx) w.Close() // Close writer to exit parsing goroutine if err != nil { return nil, gitcmd.ConcatenateError(err, stderr.String()) @@ -448,7 +452,10 @@ func GetCommitFileStatus(ctx context.Context, repoPath, commitID string) (*Commi // GetFullCommitID returns full length (40) of commit ID by given short SHA in a repository. func GetFullCommitID(ctx context.Context, repoPath, shortID string) (string, error) { - commitID, _, err := gitcmd.NewCommand("rev-parse").AddDynamicArguments(shortID).RunStdString(ctx, &gitcmd.RunOpts{Dir: repoPath}) + commitID, _, err := gitcmd.NewCommand("rev-parse"). + AddDynamicArguments(shortID). + WithDir(repoPath). + RunStdString(ctx) if err != nil { if strings.Contains(err.Error(), "exit status 128") { return "", ErrNotExist{shortID, ""} diff --git a/modules/git/config.go b/modules/git/config.go index 2eafe971b3..79aa0535e4 100644 --- a/modules/git/config.go +++ b/modules/git/config.go @@ -118,7 +118,9 @@ func syncGitConfig(ctx context.Context) (err error) { } func configSet(ctx context.Context, key, value string) error { - stdout, _, err := gitcmd.NewCommand("config", "--global", "--get").AddDynamicArguments(key).RunStdString(ctx, nil) + stdout, _, err := gitcmd.NewCommand("config", "--global", "--get"). + AddDynamicArguments(key). + RunStdString(ctx) if err != nil && !gitcmd.IsErrorExitCode(err, 1) { return fmt.Errorf("failed to get git config %s, err: %w", key, err) } @@ -128,8 +130,9 @@ func configSet(ctx context.Context, key, value string) error { return nil } - _, _, err = gitcmd.NewCommand("config", "--global").AddDynamicArguments(key, value).RunStdString(ctx, nil) - if err != nil { + if _, _, err = gitcmd.NewCommand("config", "--global"). + AddDynamicArguments(key, value). + RunStdString(ctx); err != nil { return fmt.Errorf("failed to set git global config %s, err: %w", key, err) } @@ -137,14 +140,14 @@ func configSet(ctx context.Context, key, value string) error { } func configSetNonExist(ctx context.Context, key, value string) error { - _, _, err := gitcmd.NewCommand("config", "--global", "--get").AddDynamicArguments(key).RunStdString(ctx, nil) + _, _, err := gitcmd.NewCommand("config", "--global", "--get").AddDynamicArguments(key).RunStdString(ctx) if err == nil { // already exist return nil } if gitcmd.IsErrorExitCode(err, 1) { // not exist, set new config - _, _, err = gitcmd.NewCommand("config", "--global").AddDynamicArguments(key, value).RunStdString(ctx, nil) + _, _, err = gitcmd.NewCommand("config", "--global").AddDynamicArguments(key, value).RunStdString(ctx) if err != nil { return fmt.Errorf("failed to set git global config %s, err: %w", key, err) } @@ -155,14 +158,14 @@ func configSetNonExist(ctx context.Context, key, value string) error { } func configAddNonExist(ctx context.Context, key, value string) error { - _, _, err := gitcmd.NewCommand("config", "--global", "--get").AddDynamicArguments(key, regexp.QuoteMeta(value)).RunStdString(ctx, nil) + _, _, err := gitcmd.NewCommand("config", "--global", "--get").AddDynamicArguments(key, regexp.QuoteMeta(value)).RunStdString(ctx) if err == nil { // already exist return nil } if gitcmd.IsErrorExitCode(err, 1) { // not exist, add new config - _, _, err = gitcmd.NewCommand("config", "--global", "--add").AddDynamicArguments(key, value).RunStdString(ctx, nil) + _, _, err = gitcmd.NewCommand("config", "--global", "--add").AddDynamicArguments(key, value).RunStdString(ctx) if err != nil { return fmt.Errorf("failed to add git global config %s, err: %w", key, err) } @@ -172,10 +175,10 @@ func configAddNonExist(ctx context.Context, key, value string) error { } func configUnsetAll(ctx context.Context, key, value string) error { - _, _, err := gitcmd.NewCommand("config", "--global", "--get").AddDynamicArguments(key).RunStdString(ctx, nil) + _, _, err := gitcmd.NewCommand("config", "--global", "--get").AddDynamicArguments(key).RunStdString(ctx) if err == nil { // exist, need to remove - _, _, err = gitcmd.NewCommand("config", "--global", "--unset-all").AddDynamicArguments(key, regexp.QuoteMeta(value)).RunStdString(ctx, nil) + _, _, err = gitcmd.NewCommand("config", "--global", "--unset-all").AddDynamicArguments(key, regexp.QuoteMeta(value)).RunStdString(ctx) if err != nil { return fmt.Errorf("failed to unset git global config %s, err: %w", key, err) } diff --git a/modules/git/diff.go b/modules/git/diff.go index d185cc9277..437b26eb05 100644 --- a/modules/git/diff.go +++ b/modules/git/diff.go @@ -35,12 +35,12 @@ func GetRawDiff(repo *Repository, commitID string, diffType RawDiffType, writer // GetReverseRawDiff dumps the reverse diff results of repository in given commit ID to io.Writer. func GetReverseRawDiff(ctx context.Context, repoPath, commitID string, writer io.Writer) error { stderr := new(bytes.Buffer) - cmd := gitcmd.NewCommand("show", "--pretty=format:revert %H%n", "-R").AddDynamicArguments(commitID) - if err := cmd.Run(ctx, &gitcmd.RunOpts{ - Dir: repoPath, - Stdout: writer, - Stderr: stderr, - }); err != nil { + if err := gitcmd.NewCommand("show", "--pretty=format:revert %H%n", "-R"). + AddDynamicArguments(commitID). + WithDir(repoPath). + WithStdout(writer). + WithStderr(stderr). + Run(ctx); err != nil { return fmt.Errorf("Run: %w - %s", err, stderr) } return nil @@ -90,11 +90,10 @@ func GetRepoRawDiffForFile(repo *Repository, startCommit, endCommit string, diff } stderr := new(bytes.Buffer) - if err = cmd.Run(repo.Ctx, &gitcmd.RunOpts{ - Dir: repo.Path, - Stdout: writer, - Stderr: stderr, - }); err != nil { + if err = cmd.WithDir(repo.Path). + WithStdout(writer). + WithStderr(stderr). + Run(repo.Ctx); err != nil { return fmt.Errorf("Run: %w - %s", err, stderr) } return nil @@ -314,29 +313,28 @@ func GetAffectedFiles(repo *Repository, branchName, oldCommitID, newCommitID str // Run `git diff --name-only` to get the names of the changed files err = gitcmd.NewCommand("diff", "--name-only").AddDynamicArguments(oldCommitID, newCommitID). - Run(repo.Ctx, &gitcmd.RunOpts{ - Env: env, - Dir: repo.Path, - Stdout: stdoutWriter, - PipelineFunc: func(ctx context.Context, cancel context.CancelFunc) error { - // Close the writer end of the pipe to begin processing - _ = stdoutWriter.Close() - defer func() { - // Close the reader on return to terminate the git command if necessary - _ = stdoutReader.Close() - }() - // Now scan the output from the command - scanner := bufio.NewScanner(stdoutReader) - for scanner.Scan() { - path := strings.TrimSpace(scanner.Text()) - if len(path) == 0 { - continue - } - affectedFiles = append(affectedFiles, path) + WithEnv(env). + WithDir(repo.Path). + WithStdout(stdoutWriter). + WithPipelineFunc(func(ctx context.Context, cancel context.CancelFunc) error { + // Close the writer end of the pipe to begin processing + _ = stdoutWriter.Close() + defer func() { + // Close the reader on return to terminate the git command if necessary + _ = stdoutReader.Close() + }() + // Now scan the output from the command + scanner := bufio.NewScanner(stdoutReader) + for scanner.Scan() { + path := strings.TrimSpace(scanner.Text()) + if len(path) == 0 { + continue } - return scanner.Err() - }, - }) + affectedFiles = append(affectedFiles, path) + } + return scanner.Err() + }). + Run(repo.Ctx) if err != nil { log.Error("Unable to get affected files for commits from %s to %s in %s: %v", oldCommitID, newCommitID, repo.Path, err) } diff --git a/modules/git/error.go b/modules/git/error.go index 7d131345d0..d4b5412da9 100644 --- a/modules/git/error.go +++ b/modules/git/error.go @@ -98,28 +98,31 @@ func (err *ErrPushRejected) Unwrap() error { // GenerateMessage generates the remote message from the stderr func (err *ErrPushRejected) GenerateMessage() { - messageBuilder := &strings.Builder{} - i := strings.Index(err.StdErr, "remote: ") - if i < 0 { - err.Message = "" + // The stderr is like this: + // + // > remote: error: push is rejected ..... + // > To /work/gitea/tests/integration/gitea-integration-sqlite/gitea-repositories/user2/repo1.git + // > ! [remote rejected] 44e67c77559211d21b630b902cdcc6ab9d4a4f51 -> develop (pre-receive hook declined) + // > error: failed to push some refs to '/work/gitea/tests/integration/gitea-integration-sqlite/gitea-repositories/user2/repo1.git' + // + // The local message contains sensitive information, so we only need the remote message + const prefixRemote = "remote: " + const prefixError = "error: " + pos := strings.Index(err.StdErr, prefixRemote) + if pos < 0 { + err.Message = "push is rejected" return } - for { - if len(err.StdErr) <= i+8 { - break - } - if err.StdErr[i:i+8] != "remote: " { - break - } - i += 8 - nl := strings.IndexByte(err.StdErr[i:], '\n') - if nl >= 0 { - messageBuilder.WriteString(err.StdErr[i : i+nl+1]) - i = i + nl + 1 - } else { - messageBuilder.WriteString(err.StdErr[i:]) - i = len(err.StdErr) + + messageBuilder := &strings.Builder{} + lines := strings.SplitSeq(err.StdErr, "\n") + for line := range lines { + line, ok := strings.CutPrefix(line, prefixRemote) + if !ok { + continue } + line = strings.TrimPrefix(line, prefixError) + messageBuilder.WriteString(strings.TrimSpace(line) + "\n") } err.Message = strings.TrimSpace(messageBuilder.String()) } diff --git a/modules/git/git.go b/modules/git/git.go index 161fa42196..6d2c643b33 100644 --- a/modules/git/git.go +++ b/modules/git/git.go @@ -57,7 +57,7 @@ func DefaultFeatures() *Features { } func loadGitVersionFeatures() (*Features, error) { - stdout, _, runErr := gitcmd.NewCommand("version").RunStdString(context.Background(), nil) + stdout, _, runErr := gitcmd.NewCommand("version").RunStdString(context.Background()) if runErr != nil { return nil, runErr } diff --git a/modules/git/gitcmd/command.go b/modules/git/gitcmd/command.go index ed2f6fb647..ff2827bd6c 100644 --- a/modules/git/gitcmd/command.go +++ b/modules/git/gitcmd/command.go @@ -46,6 +46,7 @@ type Command struct { brokenArgs []string cmd *exec.Cmd // for debug purpose only configArgs []string + opts runOpts } func logArgSanitize(arg string) string { @@ -194,8 +195,8 @@ func ToTrustedCmdArgs(args []string) TrustedCmdArgs { return ret } -// RunOpts represents parameters to run the command. If UseContextTimeout is specified, then Timeout is ignored. -type RunOpts struct { +// runOpts represents parameters to run the command. If UseContextTimeout is specified, then Timeout is ignored. +type runOpts struct { Env []string Timeout time.Duration UseContextTimeout bool @@ -221,6 +222,8 @@ type RunOpts struct { Stdin io.Reader PipelineFunc func(context.Context, context.CancelFunc) error + + callerInfo string } func commonBaseEnvs() []string { @@ -263,44 +266,99 @@ func CommonCmdServEnvs() []string { var ErrBrokenCommand = errors.New("git command is broken") -// Run runs the command with the RunOpts -func (c *Command) Run(ctx context.Context, opts *RunOpts) error { - return c.run(ctx, 1, opts) +func (c *Command) WithDir(dir string) *Command { + c.opts.Dir = dir + return c } -func (c *Command) run(ctx context.Context, skip int, opts *RunOpts) error { +func (c *Command) WithEnv(env []string) *Command { + c.opts.Env = env + return c +} + +func (c *Command) WithTimeout(timeout time.Duration) *Command { + c.opts.Timeout = timeout + return c +} + +func (c *Command) WithStdout(stdout io.Writer) *Command { + c.opts.Stdout = stdout + return c +} + +func (c *Command) WithStderr(stderr io.Writer) *Command { + c.opts.Stderr = stderr + return c +} + +func (c *Command) WithStdin(stdin io.Reader) *Command { + c.opts.Stdin = stdin + return c +} + +func (c *Command) WithPipelineFunc(f func(context.Context, context.CancelFunc) error) *Command { + c.opts.PipelineFunc = f + return c +} + +func (c *Command) WithUseContextTimeout(useContextTimeout bool) *Command { + c.opts.UseContextTimeout = useContextTimeout + return c +} + +// WithParentCallerInfo can be used to set the caller info (usually function name) of the parent function of the caller. +// For most cases, "Run" family functions can get its caller info automatically +// But if you need to call "Run" family functions in a wrapper function: "FeatureFunc -> GeneralWrapperFunc -> RunXxx", +// then you can to call this function in GeneralWrapperFunc to set the caller info of FeatureFunc. +// The caller info can only be set once. +func (c *Command) WithParentCallerInfo(optInfo ...string) *Command { + if c.opts.callerInfo != "" { + return c + } + if len(optInfo) > 0 { + c.opts.callerInfo = optInfo[0] + return c + } + skip := 1 /*parent "wrap/run" functions*/ + 1 /*this function*/ + callerFuncName := util.CallerFuncName(skip) + callerInfo := callerFuncName + if pos := strings.LastIndex(callerInfo, "/"); pos >= 0 { + callerInfo = callerInfo[pos+1:] + } + c.opts.callerInfo = callerInfo + return c +} + +// Run runs the command +func (c *Command) Run(ctx context.Context) error { if len(c.brokenArgs) != 0 { log.Error("git command is broken: %s, broken args: %s", c.LogString(), strings.Join(c.brokenArgs, " ")) return ErrBrokenCommand } - if opts == nil { - opts = &RunOpts{} - } // We must not change the provided options - timeout := opts.Timeout + timeout := c.opts.Timeout if timeout <= 0 { timeout = defaultCommandExecutionTimeout } cmdLogString := c.LogString() - callerInfo := util.CallerFuncName(1 /* util */ + 1 /* this */ + skip /* parent */) - if pos := strings.LastIndex(callerInfo, "/"); pos >= 0 { - callerInfo = callerInfo[pos+1:] + if c.opts.callerInfo == "" { + c.WithParentCallerInfo() } // 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), cmdLogString) + desc := fmt.Sprintf("git.Run(by:%s, repo:%s): %s", c.opts.callerInfo, logArgSanitize(c.opts.Dir), cmdLogString) log.Debug("git.Command: %s", desc) _, span := gtprof.GetTracer().Start(ctx, gtprof.TraceSpanGitRun) defer span.End() - span.SetAttributeString(gtprof.TraceAttrFuncCaller, callerInfo) + span.SetAttributeString(gtprof.TraceAttrFuncCaller, c.opts.callerInfo) span.SetAttributeString(gtprof.TraceAttrGitCommand, cmdLogString) var cancel context.CancelFunc var finished context.CancelFunc - if opts.UseContextTimeout { + if c.opts.UseContextTimeout { ctx, cancel, finished = process.GetManager().AddContext(ctx, desc) } else { ctx, cancel, finished = process.GetManager().AddContextTimeout(ctx, timeout, desc) @@ -311,24 +369,24 @@ func (c *Command) run(ctx context.Context, skip int, opts *RunOpts) error { cmd := exec.CommandContext(ctx, c.prog, append(c.configArgs, c.args...)...) c.cmd = cmd // for debug purpose only - if opts.Env == nil { + if c.opts.Env == nil { cmd.Env = os.Environ() } else { - cmd.Env = opts.Env + cmd.Env = c.opts.Env } process.SetSysProcAttribute(cmd) cmd.Env = append(cmd.Env, CommonGitCmdEnvs()...) - cmd.Dir = opts.Dir - cmd.Stdout = opts.Stdout - cmd.Stderr = opts.Stderr - cmd.Stdin = opts.Stdin + cmd.Dir = c.opts.Dir + cmd.Stdout = c.opts.Stdout + cmd.Stderr = c.opts.Stderr + cmd.Stdin = c.opts.Stdin if err := cmd.Start(); err != nil { return err } - if opts.PipelineFunc != nil { - err := opts.PipelineFunc(ctx, cancel) + if c.opts.PipelineFunc != nil { + err := c.opts.PipelineFunc(ctx, cancel) if err != nil { cancel() _ = cmd.Wait() @@ -374,7 +432,8 @@ type runStdError struct { } func (r *runStdError) Error() string { - // the stderr must be in the returned error text, some code only checks `strings.Contains(err.Error(), "git error")` + // FIXME: GIT-CMD-STDERR: it is a bad design, the stderr should not be put in the error message + // But a lof of code only checks `strings.Contains(err.Error(), "git error")` if r.errMsg == "" { r.errMsg = ConcatenateError(r.err, r.stderr).Error() } @@ -397,51 +456,33 @@ func IsErrorExitCode(err error, code int) bool { return false } -// RunStdString runs the command with options and returns stdout/stderr as string. and store stderr to returned error (err combined with stderr). -func (c *Command) RunStdString(ctx context.Context, opts *RunOpts) (stdout, stderr string, runErr RunStdError) { - stdoutBytes, stderrBytes, err := c.runStdBytes(ctx, opts) - stdout = util.UnsafeBytesToString(stdoutBytes) - stderr = util.UnsafeBytesToString(stderrBytes) - if err != nil { - return stdout, stderr, &runStdError{err: err, stderr: stderr} - } - // even if there is no err, there could still be some stderr output, so we just return stdout/stderr as they are - return stdout, stderr, nil +// RunStdString runs the command and returns stdout/stderr as string. and store stderr to returned error (err combined with stderr). +func (c *Command) RunStdString(ctx context.Context) (stdout, stderr string, runErr RunStdError) { + stdoutBytes, stderrBytes, runErr := c.WithParentCallerInfo().runStdBytes(ctx) + return util.UnsafeBytesToString(stdoutBytes), util.UnsafeBytesToString(stderrBytes), runErr } -// RunStdBytes runs the command with options and returns stdout/stderr as bytes. and store stderr to returned error (err combined with stderr). -func (c *Command) RunStdBytes(ctx context.Context, opts *RunOpts) (stdout, stderr []byte, runErr RunStdError) { - return c.runStdBytes(ctx, opts) +// RunStdBytes runs the command and returns stdout/stderr as bytes. and store stderr to returned error (err combined with stderr). +func (c *Command) RunStdBytes(ctx context.Context) (stdout, stderr []byte, runErr RunStdError) { + return c.WithParentCallerInfo().runStdBytes(ctx) } -func (c *Command) runStdBytes(ctx context.Context, opts *RunOpts) (stdout, stderr []byte, runErr RunStdError) { - if opts == nil { - opts = &RunOpts{} - } - if opts.Stdout != nil || opts.Stderr != nil { +func (c *Command) runStdBytes(ctx context.Context) ( /*stdout*/ []byte /*stderr*/, []byte /*runErr*/, RunStdError) { + if c.opts.Stdout != nil || c.opts.Stderr != nil { // we must panic here, otherwise there would be bugs if developers set Stdin/Stderr by mistake, and it would be very difficult to debug panic("stdout and stderr field must be nil when using RunStdBytes") } stdoutBuf := &bytes.Buffer{} stderrBuf := &bytes.Buffer{} - - // We must not change the provided options as it could break future calls - therefore make a copy. - newOpts := &RunOpts{ - Env: opts.Env, - Timeout: opts.Timeout, - UseContextTimeout: opts.UseContextTimeout, - Dir: opts.Dir, - Stdout: stdoutBuf, - Stderr: stderrBuf, - Stdin: opts.Stdin, - PipelineFunc: opts.PipelineFunc, - } - - err := c.run(ctx, 2, newOpts) - stderr = stderrBuf.Bytes() + err := c.WithParentCallerInfo(). + WithStdout(stdoutBuf). + WithStderr(stderrBuf). + Run(ctx) if err != nil { - return nil, stderr, &runStdError{err: err, stderr: util.UnsafeBytesToString(stderr)} + // FIXME: GIT-CMD-STDERR: it is a bad design, the stderr should not be put in the error message + // But a lot of code depends on it, so we have to keep this behavior + return nil, stderrBuf.Bytes(), &runStdError{err: err, stderr: util.UnsafeBytesToString(stderrBuf.Bytes())} } // even if there is no err, there could still be some stderr output - return stdoutBuf.Bytes(), stderr, nil + return stdoutBuf.Bytes(), stderrBuf.Bytes(), nil } diff --git a/modules/git/gitcmd/command_race_test.go b/modules/git/gitcmd/command_race_test.go index aee2272808..c2f0b124a2 100644 --- a/modules/git/gitcmd/command_race_test.go +++ b/modules/git/gitcmd/command_race_test.go @@ -17,7 +17,7 @@ func TestRunWithContextNoTimeout(t *testing.T) { // 'git --version' does not block so it must be finished before the timeout triggered. cmd := NewCommand("--version") for i := 0; i < maxLoops; i++ { - if err := cmd.Run(t.Context(), &RunOpts{}); err != nil { + if err := cmd.Run(t.Context()); err != nil { t.Fatal(err) } } @@ -29,7 +29,7 @@ func TestRunWithContextTimeout(t *testing.T) { // 'git hash-object --stdin' blocks on stdin so we can have the timeout triggered. cmd := NewCommand("hash-object", "--stdin") for i := 0; i < maxLoops; i++ { - if err := cmd.Run(t.Context(), &RunOpts{Timeout: 1 * time.Millisecond}); err != nil { + if err := cmd.WithTimeout(1 * time.Millisecond).Run(t.Context()); err != nil { if err != context.DeadlineExceeded { t.Fatalf("Testing %d/%d: %v", i, maxLoops, err) } diff --git a/modules/git/gitcmd/command_test.go b/modules/git/gitcmd/command_test.go index 544a97f64c..1ba8b2e3e4 100644 --- a/modules/git/gitcmd/command_test.go +++ b/modules/git/gitcmd/command_test.go @@ -23,38 +23,60 @@ func TestMain(m *testing.M) { defer cleanup() setting.Git.HomePath = gitHomePath + os.Exit(m.Run()) } func TestRunWithContextStd(t *testing.T) { - cmd := NewCommand("--version") - stdout, stderr, err := cmd.RunStdString(t.Context(), &RunOpts{}) - assert.NoError(t, err) - assert.Empty(t, stderr) - assert.Contains(t, stdout, "git version") - - cmd = NewCommand("--no-such-arg") - stdout, stderr, err = cmd.RunStdString(t.Context(), &RunOpts{}) - if assert.Error(t, err) { - assert.Equal(t, stderr, err.Stderr()) - assert.Contains(t, err.Stderr(), "unknown option:") - assert.Contains(t, err.Error(), "exit status 129 - unknown option:") - assert.Empty(t, stdout) + { + cmd := NewCommand("--version") + stdout, stderr, err := cmd.RunStdString(t.Context()) + assert.NoError(t, err) + assert.Empty(t, stderr) + assert.Contains(t, stdout, "git version") } - cmd = NewCommand() - cmd.AddDynamicArguments("-test") - assert.ErrorIs(t, cmd.Run(t.Context(), &RunOpts{}), ErrBrokenCommand) + { + cmd := NewCommand("ls-tree", "no-such") + stdout, stderr, err := cmd.RunStdString(t.Context()) + if assert.Error(t, err) { + assert.Equal(t, stderr, err.Stderr()) + assert.Equal(t, "fatal: Not a valid object name no-such\n", err.Stderr()) + // FIXME: GIT-CMD-STDERR: it is a bad design, the stderr should not be put in the error message + assert.Equal(t, "exit status 128 - fatal: Not a valid object name no-such\n", err.Error()) + assert.Empty(t, stdout) + } + } - cmd = NewCommand() - cmd.AddDynamicArguments("--test") - assert.ErrorIs(t, cmd.Run(t.Context(), &RunOpts{}), ErrBrokenCommand) + { + cmd := NewCommand("ls-tree", "no-such") + stdout, stderr, err := cmd.RunStdBytes(t.Context()) + if assert.Error(t, err) { + assert.Equal(t, string(stderr), err.Stderr()) + assert.Equal(t, "fatal: Not a valid object name no-such\n", err.Stderr()) + // FIXME: GIT-CMD-STDERR: it is a bad design, the stderr should not be put in the error message + assert.Equal(t, "exit status 128 - fatal: Not a valid object name no-such\n", err.Error()) + assert.Empty(t, stdout) + } + } - subCmd := "version" - cmd = NewCommand().AddDynamicArguments(subCmd) // for test purpose only, the sub-command should never be dynamic for production - stdout, stderr, err = cmd.RunStdString(t.Context(), &RunOpts{}) - assert.NoError(t, err) - assert.Empty(t, stderr) - assert.Contains(t, stdout, "git version") + { + cmd := NewCommand() + cmd.AddDynamicArguments("-test") + assert.ErrorIs(t, cmd.Run(t.Context()), ErrBrokenCommand) + + cmd = NewCommand() + cmd.AddDynamicArguments("--test") + assert.ErrorIs(t, cmd.Run(t.Context()), ErrBrokenCommand) + } + + { + subCmd := "version" + cmd := NewCommand().AddDynamicArguments(subCmd) // for test purpose only, the sub-command should never be dynamic for production + stdout, stderr, err := cmd.RunStdString(t.Context()) + assert.NoError(t, err) + assert.Empty(t, stderr) + assert.Contains(t, stdout, "git version") + } } func TestGitArgument(t *testing.T) { diff --git a/modules/git/grep.go b/modules/git/grep.go index f5f6f12041..ed69a788a4 100644 --- a/modules/git/grep.go +++ b/modules/git/grep.go @@ -84,11 +84,10 @@ func GrepSearch(ctx context.Context, repo *Repository, search string, opts GrepO cmd.AddDashesAndList(opts.PathspecList...) opts.MaxResultLimit = util.IfZero(opts.MaxResultLimit, 50) stderr := bytes.Buffer{} - err = cmd.Run(ctx, &gitcmd.RunOpts{ - Dir: repo.Path, - Stdout: stdoutWriter, - Stderr: &stderr, - PipelineFunc: func(ctx context.Context, cancel context.CancelFunc) error { + err = cmd.WithDir(repo.Path). + WithStdout(stdoutWriter). + WithStderr(&stderr). + WithPipelineFunc(func(ctx context.Context, cancel context.CancelFunc) error { _ = stdoutWriter.Close() defer stdoutReader.Close() @@ -133,8 +132,8 @@ func GrepSearch(ctx context.Context, repo *Repository, search string, opts GrepO } } return nil - }, - }) + }). + Run(ctx) // git grep exits by cancel (killed), usually it is caused by the limit of results if gitcmd.IsErrorExitCode(err, -1) && stderr.Len() == 0 { return results, nil diff --git a/modules/git/hook.go b/modules/git/hook.go index 548a59971d..0e19387d97 100644 --- a/modules/git/hook.go +++ b/modules/git/hook.go @@ -45,32 +45,18 @@ func GetHook(repoPath, name string) (*Hook, error) { } h := &Hook{ name: name, - path: filepath.Join(repoPath, "hooks", name+".d", name), + path: filepath.Join(repoPath, filepath.Join("hooks", name+".d", name)), } - isFile, err := util.IsFile(h.path) - if err != nil { - return nil, err - } - if isFile { - data, err := os.ReadFile(h.path) - if err != nil { - return nil, err - } + if data, err := os.ReadFile(h.path); err == nil { h.IsActive = true h.Content = string(data) return h, nil + } else if !os.IsNotExist(err) { + return nil, err } samplePath := filepath.Join(repoPath, "hooks", name+".sample") - isFile, err = util.IsFile(samplePath) - if err != nil { - return nil, err - } - if isFile { - data, err := os.ReadFile(samplePath) - if err != nil { - return nil, err - } + if data, err := os.ReadFile(samplePath); err == nil { h.Sample = string(data) } return h, nil diff --git a/modules/git/key.go b/modules/git/key.go index 8c14742f34..39e79ddbe0 100644 --- a/modules/git/key.go +++ b/modules/git/key.go @@ -3,7 +3,13 @@ package git -import "code.gitea.io/gitea/modules/setting" +import ( + "context" + "strings" + + "code.gitea.io/gitea/modules/git/gitcmd" + "code.gitea.io/gitea/modules/setting" +) // Based on https://git-scm.com/docs/git-config#Documentation/git-config.txt-gpgformat const ( @@ -24,3 +30,48 @@ func (s *SigningKey) String() string { setting.PanicInDevOrTesting("don't call SigningKey.String() - it exposes the KeyID which might be a local file path") return "SigningKey:" + s.Format } + +// GetSigningKey returns the KeyID and git Signature for the repo +func GetSigningKey(ctx context.Context, repoPath string) (*SigningKey, *Signature) { + if setting.Repository.Signing.SigningKey == "none" { + return nil, nil + } + + if setting.Repository.Signing.SigningKey == "default" || setting.Repository.Signing.SigningKey == "" { + // Can ignore the error here as it means that commit.gpgsign is not set + value, _, _ := gitcmd.NewCommand("config", "--get", "commit.gpgsign").WithDir(repoPath).RunStdString(ctx) + sign, valid := ParseBool(strings.TrimSpace(value)) + if !sign || !valid { + return nil, nil + } + + format, _, _ := gitcmd.NewCommand("config", "--default", SigningKeyFormatOpenPGP, "--get", "gpg.format").WithDir(repoPath).RunStdString(ctx) + signingKey, _, _ := gitcmd.NewCommand("config", "--get", "user.signingkey").WithDir(repoPath).RunStdString(ctx) + signingName, _, _ := gitcmd.NewCommand("config", "--get", "user.name").WithDir(repoPath).RunStdString(ctx) + signingEmail, _, _ := gitcmd.NewCommand("config", "--get", "user.email").WithDir(repoPath).RunStdString(ctx) + + if strings.TrimSpace(signingKey) == "" { + return nil, nil + } + + return &SigningKey{ + KeyID: strings.TrimSpace(signingKey), + Format: strings.TrimSpace(format), + }, &Signature{ + Name: strings.TrimSpace(signingName), + Email: strings.TrimSpace(signingEmail), + } + } + + if setting.Repository.Signing.SigningKey == "" { + return nil, nil + } + + return &SigningKey{ + KeyID: setting.Repository.Signing.SigningKey, + Format: setting.Repository.Signing.SigningFormat, + }, &Signature{ + Name: setting.Repository.Signing.SigningName, + Email: setting.Repository.Signing.SigningEmail, + } +} diff --git a/modules/git/log_name_status.go b/modules/git/log_name_status.go index 7a5192f58b..72e513000b 100644 --- a/modules/git/log_name_status.go +++ b/modules/git/log_name_status.go @@ -65,11 +65,10 @@ func LogNameStatusRepo(ctx context.Context, repository, head, treepath string, p go func() { stderr := strings.Builder{} - err := cmd.Run(ctx, &gitcmd.RunOpts{ - Dir: repository, - Stdout: stdoutWriter, - Stderr: &stderr, - }) + err := cmd.WithDir(repository). + WithStdout(stdoutWriter). + WithStderr(&stderr). + Run(ctx) if err != nil { _ = stdoutWriter.CloseWithError(gitcmd.ConcatenateError(err, (&stderr).String())) return diff --git a/modules/git/notes_test.go b/modules/git/notes_test.go index 7db2dbc0b9..5abb68b102 100644 --- a/modules/git/notes_test.go +++ b/modules/git/notes_test.go @@ -47,5 +47,5 @@ func TestGetNonExistentNotes(t *testing.T) { note := Note{} err = GetNote(t.Context(), bareRepo1, "non_existent_sha", ¬e) assert.Error(t, err) - assert.IsType(t, ErrNotExist{}, err) + assert.ErrorAs(t, err, &ErrNotExist{}) } diff --git a/modules/git/pipeline/catfile.go b/modules/git/pipeline/catfile.go index ced8532e6d..a4d1ff64cf 100644 --- a/modules/git/pipeline/catfile.go +++ b/modules/git/pipeline/catfile.go @@ -26,12 +26,11 @@ func CatFileBatchCheck(ctx context.Context, shasToCheckReader *io.PipeReader, ca stderr := new(bytes.Buffer) var errbuf strings.Builder cmd := gitcmd.NewCommand("cat-file", "--batch-check") - if err := cmd.Run(ctx, &gitcmd.RunOpts{ - Dir: tmpBasePath, - Stdin: shasToCheckReader, - Stdout: catFileCheckWriter, - Stderr: stderr, - }); err != nil { + if err := cmd.WithDir(tmpBasePath). + WithStdin(shasToCheckReader). + WithStdout(catFileCheckWriter). + WithStderr(stderr). + Run(ctx); err != nil { _ = catFileCheckWriter.CloseWithError(fmt.Errorf("git cat-file --batch-check [%s]: %w - %s", tmpBasePath, err, errbuf.String())) } } @@ -44,11 +43,10 @@ func CatFileBatchCheckAllObjects(ctx context.Context, catFileCheckWriter *io.Pip stderr := new(bytes.Buffer) var errbuf strings.Builder cmd := gitcmd.NewCommand("cat-file", "--batch-check", "--batch-all-objects") - if err := cmd.Run(ctx, &gitcmd.RunOpts{ - Dir: tmpBasePath, - Stdout: catFileCheckWriter, - Stderr: stderr, - }); err != nil { + if err := cmd.WithDir(tmpBasePath). + WithStdout(catFileCheckWriter). + WithStderr(stderr). + Run(ctx); err != nil { log.Error("git cat-file --batch-check --batch-all-object [%s]: %v - %s", tmpBasePath, err, errbuf.String()) err = fmt.Errorf("git cat-file --batch-check --batch-all-object [%s]: %w - %s", tmpBasePath, err, errbuf.String()) _ = catFileCheckWriter.CloseWithError(err) @@ -64,12 +62,12 @@ func CatFileBatch(ctx context.Context, shasToBatchReader *io.PipeReader, catFile stderr := new(bytes.Buffer) var errbuf strings.Builder - if err := gitcmd.NewCommand("cat-file", "--batch").Run(ctx, &gitcmd.RunOpts{ - Dir: tmpBasePath, - Stdout: catFileBatchWriter, - Stdin: shasToBatchReader, - Stderr: stderr, - }); err != nil { + if err := gitcmd.NewCommand("cat-file", "--batch"). + WithDir(tmpBasePath). + WithStdin(shasToBatchReader). + WithStdout(catFileBatchWriter). + WithStderr(stderr). + Run(ctx); err != nil { _ = shasToBatchReader.CloseWithError(fmt.Errorf("git rev-list [%s]: %w - %s", tmpBasePath, err, errbuf.String())) } } diff --git a/modules/git/pipeline/lfs_nogogit.go b/modules/git/pipeline/lfs_nogogit.go index d2f147854d..4881a2be64 100644 --- a/modules/git/pipeline/lfs_nogogit.go +++ b/modules/git/pipeline/lfs_nogogit.go @@ -33,11 +33,11 @@ func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, err go func() { stderr := strings.Builder{} - err := gitcmd.NewCommand("rev-list", "--all").Run(repo.Ctx, &gitcmd.RunOpts{ - Dir: repo.Path, - Stdout: revListWriter, - Stderr: &stderr, - }) + err := gitcmd.NewCommand("rev-list", "--all"). + WithDir(repo.Path). + WithStdout(revListWriter). + WithStderr(&stderr). + Run(repo.Ctx) if err != nil { _ = revListWriter.CloseWithError(gitcmd.ConcatenateError(err, (&stderr).String())) } else { diff --git a/modules/git/pipeline/namerev.go b/modules/git/pipeline/namerev.go index 0081f7a26d..782b5f0531 100644 --- a/modules/git/pipeline/namerev.go +++ b/modules/git/pipeline/namerev.go @@ -22,12 +22,12 @@ func NameRevStdin(ctx context.Context, shasToNameReader *io.PipeReader, nameRevS stderr := new(bytes.Buffer) var errbuf strings.Builder - if err := gitcmd.NewCommand("name-rev", "--stdin", "--name-only", "--always").Run(ctx, &gitcmd.RunOpts{ - Dir: tmpBasePath, - Stdout: nameRevStdinWriter, - Stdin: shasToNameReader, - Stderr: stderr, - }); err != nil { + if err := gitcmd.NewCommand("name-rev", "--stdin", "--name-only", "--always"). + WithDir(tmpBasePath). + WithStdin(shasToNameReader). + WithStdout(nameRevStdinWriter). + WithStderr(stderr). + Run(ctx); err != nil { _ = shasToNameReader.CloseWithError(fmt.Errorf("git name-rev [%s]: %w - %s", tmpBasePath, err, errbuf.String())) } } diff --git a/modules/git/pipeline/revlist.go b/modules/git/pipeline/revlist.go index 9d4ff75434..755b165a65 100644 --- a/modules/git/pipeline/revlist.go +++ b/modules/git/pipeline/revlist.go @@ -24,11 +24,10 @@ func RevListAllObjects(ctx context.Context, revListWriter *io.PipeWriter, wg *sy stderr := new(bytes.Buffer) var errbuf strings.Builder cmd := gitcmd.NewCommand("rev-list", "--objects", "--all") - if err := cmd.Run(ctx, &gitcmd.RunOpts{ - Dir: basePath, - Stdout: revListWriter, - Stderr: stderr, - }); err != nil { + if err := cmd.WithDir(basePath). + WithStdout(revListWriter). + WithStderr(stderr). + Run(ctx); err != nil { log.Error("git rev-list --objects --all [%s]: %v - %s", basePath, err, errbuf.String()) err = fmt.Errorf("git rev-list --objects --all [%s]: %w - %s", basePath, err, errbuf.String()) _ = revListWriter.CloseWithError(err) @@ -46,11 +45,10 @@ func RevListObjects(ctx context.Context, revListWriter *io.PipeWriter, wg *sync. if baseSHA != "" { cmd = cmd.AddArguments("--not").AddDynamicArguments(baseSHA) } - if err := cmd.Run(ctx, &gitcmd.RunOpts{ - Dir: tmpBasePath, - Stdout: revListWriter, - Stderr: stderr, - }); err != nil { + if err := cmd.WithDir(tmpBasePath). + WithStdout(revListWriter). + WithStderr(stderr). + Run(ctx); err != nil { log.Error("git rev-list [%s]: %v - %s", tmpBasePath, err, errbuf.String()) errChan <- fmt.Errorf("git rev-list [%s]: %w - %s", tmpBasePath, err, errbuf.String()) } diff --git a/modules/git/remote.go b/modules/git/remote.go index 9f12142f91..1999ad4b94 100644 --- a/modules/git/remote.go +++ b/modules/git/remote.go @@ -22,7 +22,7 @@ func GetRemoteAddress(ctx context.Context, repoPath, remoteName string) (string, cmd = gitcmd.NewCommand("config", "--get").AddDynamicArguments("remote." + remoteName + ".url") } - result, _, err := cmd.RunStdString(ctx, &gitcmd.RunOpts{Dir: repoPath}) + result, _, err := cmd.WithDir(repoPath).RunStdString(ctx) if err != nil { return "", err } diff --git a/modules/git/repo.go b/modules/git/repo.go index 9f8b6225c8..29e70d94c8 100644 --- a/modules/git/repo.go +++ b/modules/git/repo.go @@ -12,14 +12,12 @@ import ( "net/url" "os" "path" - "path/filepath" "strconv" "strings" "time" "code.gitea.io/gitea/modules/git/gitcmd" "code.gitea.io/gitea/modules/proxy" - "code.gitea.io/gitea/modules/setting" ) // GPGSettings represents the default GPG settings for this repository @@ -42,8 +40,8 @@ func (repo *Repository) GetAllCommitsCount() (int64, error) { func (repo *Repository) ShowPrettyFormatLogToList(ctx context.Context, revisionRange string) ([]*Commit, error) { // avoid: ambiguous argument 'refs/a...refs/b': unknown revision or path not in the working tree. Use '--': 'git [...] -- [...]' logs, _, err := gitcmd.NewCommand("log").AddArguments(prettyLogFormat). - AddDynamicArguments(revisionRange).AddArguments("--"). - RunStdBytes(ctx, &gitcmd.RunOpts{Dir: repo.Path}) + AddDynamicArguments(revisionRange).AddArguments("--").WithDir(repo.Path). + RunStdBytes(ctx) if err != nil { return nil, err } @@ -71,7 +69,7 @@ func (repo *Repository) parsePrettyFormatLogToList(logs []byte) ([]*Commit, erro // IsRepoURLAccessible checks if given repository URL is accessible. func IsRepoURLAccessible(ctx context.Context, url string) bool { - _, _, err := gitcmd.NewCommand("ls-remote", "-q", "-h").AddDynamicArguments(url, "HEAD").RunStdString(ctx, nil) + _, _, err := gitcmd.NewCommand("ls-remote", "-q", "-h").AddDynamicArguments(url, "HEAD").RunStdString(ctx) return err == nil } @@ -94,19 +92,20 @@ func InitRepository(ctx context.Context, repoPath string, bare bool, objectForma if bare { cmd.AddArguments("--bare") } - _, _, err = cmd.RunStdString(ctx, &gitcmd.RunOpts{Dir: repoPath}) + _, _, err = cmd.WithDir(repoPath).RunStdString(ctx) return err } // IsEmpty Check if repository is empty. func (repo *Repository) IsEmpty() (bool, error) { var errbuf, output strings.Builder - if err := gitcmd.NewCommand().AddOptionFormat("--git-dir=%s", repo.Path).AddArguments("rev-list", "-n", "1", "--all"). - Run(repo.Ctx, &gitcmd.RunOpts{ - Dir: repo.Path, - Stdout: &output, - Stderr: &errbuf, - }); err != nil { + if err := gitcmd.NewCommand(). + AddOptionFormat("--git-dir=%s", repo.Path). + AddArguments("rev-list", "-n", "1", "--all"). + WithDir(repo.Path). + WithStdout(&output). + WithStderr(&errbuf). + Run(repo.Ctx); err != nil { if (err.Error() == "exit status 1" && strings.TrimSpace(errbuf.String()) == "") || err.Error() == "exit status 129" { // git 2.11 exits with 129 if the repo is empty return true, nil @@ -179,12 +178,12 @@ func Clone(ctx context.Context, from, to string, opts CloneRepoOptions) error { } stderr := new(bytes.Buffer) - if err = cmd.Run(ctx, &gitcmd.RunOpts{ - Timeout: opts.Timeout, - Env: envs, - Stdout: io.Discard, - Stderr: stderr, - }); err != nil { + if err = cmd. + WithTimeout(opts.Timeout). + WithEnv(envs). + WithStdout(io.Discard). + WithStderr(stderr). + Run(ctx); err != nil { return gitcmd.ConcatenateError(err, stderr.String()) } return nil @@ -215,7 +214,7 @@ func Push(ctx context.Context, repoPath string, opts PushOptions) error { } cmd.AddDashesAndList(remoteBranchArgs...) - stdout, stderr, err := cmd.RunStdString(ctx, &gitcmd.RunOpts{Env: opts.Env, Timeout: opts.Timeout, Dir: repoPath}) + stdout, stderr, err := cmd.WithEnv(opts.Env).WithTimeout(opts.Timeout).WithDir(repoPath).RunStdString(ctx) if err != nil { if strings.Contains(stderr, "non-fast-forward") { return &ErrPushOutOfDate{StdOut: stdout, StdErr: stderr, Err: err} @@ -235,50 +234,10 @@ func Push(ctx context.Context, repoPath string, opts PushOptions) error { // GetLatestCommitTime returns time for latest commit in repository (across all branches) func GetLatestCommitTime(ctx context.Context, repoPath string) (time.Time, error) { cmd := gitcmd.NewCommand("for-each-ref", "--sort=-committerdate", BranchPrefix, "--count", "1", "--format=%(committerdate)") - stdout, _, err := cmd.RunStdString(ctx, &gitcmd.RunOpts{Dir: repoPath}) + stdout, _, err := cmd.WithDir(repoPath).RunStdString(ctx) if err != nil { return time.Time{}, err } commitTime := strings.TrimSpace(stdout) return time.Parse("Mon Jan _2 15:04:05 2006 -0700", commitTime) } - -// CreateBundle create bundle content to the target path -func (repo *Repository) CreateBundle(ctx context.Context, commit string, out io.Writer) error { - tmp, cleanup, err := setting.AppDataTempDir("git-repo-content").MkdirTempRandom("gitea-bundle") - if err != nil { - return err - } - defer cleanup() - - env := append(os.Environ(), "GIT_OBJECT_DIRECTORY="+filepath.Join(repo.Path, "objects")) - _, _, err = gitcmd.NewCommand("init", "--bare").RunStdString(ctx, &gitcmd.RunOpts{Dir: tmp, Env: env}) - if err != nil { - return err - } - - _, _, err = gitcmd.NewCommand("reset", "--soft").AddDynamicArguments(commit).RunStdString(ctx, &gitcmd.RunOpts{Dir: tmp, Env: env}) - if err != nil { - return err - } - - _, _, err = gitcmd.NewCommand("branch", "-m", "bundle").RunStdString(ctx, &gitcmd.RunOpts{Dir: tmp, Env: env}) - if err != nil { - return err - } - - tmpFile := filepath.Join(tmp, "bundle") - _, _, err = gitcmd.NewCommand("bundle", "create").AddDynamicArguments(tmpFile, "bundle", "HEAD").RunStdString(ctx, &gitcmd.RunOpts{Dir: tmp, Env: env}) - if err != nil { - return err - } - - fi, err := os.Open(tmpFile) - if err != nil { - return err - } - defer fi.Close() - - _, err = io.Copy(out, fi) - return err -} diff --git a/modules/git/repo_archive.go b/modules/git/repo_archive.go index e12300345f..8a9eec9e6a 100644 --- a/modules/git/repo_archive.go +++ b/modules/git/repo_archive.go @@ -63,11 +63,10 @@ func (repo *Repository) CreateArchive(ctx context.Context, format ArchiveType, t cmd.AddDynamicArguments(commitID) var stderr strings.Builder - err := cmd.Run(ctx, &gitcmd.RunOpts{ - Dir: repo.Path, - Stdout: target, - Stderr: &stderr, - }) + err := cmd.WithDir(repo.Path). + WithStdout(target). + WithStderr(&stderr). + Run(ctx) if err != nil { return gitcmd.ConcatenateError(err, stderr.String()) } diff --git a/modules/git/repo_branch.go b/modules/git/repo_branch.go index ef0f9a1e13..1eebc72158 100644 --- a/modules/git/repo_branch.go +++ b/modules/git/repo_branch.go @@ -17,8 +17,8 @@ func (repo *Repository) AddRemote(name, url string, fetch bool) error { if fetch { cmd.AddArguments("-f") } - cmd.AddDynamicArguments(name, url) - - _, _, err := cmd.RunStdString(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) + _, _, err := cmd.AddDynamicArguments(name, url). + WithDir(repo.Path). + RunStdString(repo.Ctx) return err } diff --git a/modules/git/repo_branch_nogogit.go b/modules/git/repo_branch_nogogit.go index 255c2974e9..f1b26b06ab 100644 --- a/modules/git/repo_branch_nogogit.go +++ b/modules/git/repo_branch_nogogit.go @@ -110,11 +110,11 @@ func WalkShowRef(ctx context.Context, repoPath string, extraArgs gitcmd.TrustedC stderrBuilder := &strings.Builder{} args := gitcmd.TrustedCmdArgs{"for-each-ref", "--format=%(objectname) %(refname)"} args = append(args, extraArgs...) - err := gitcmd.NewCommand(args...).Run(ctx, &gitcmd.RunOpts{ - Dir: repoPath, - Stdout: stdoutWriter, - Stderr: stderrBuilder, - }) + err := gitcmd.NewCommand(args...). + WithDir(repoPath). + WithStdout(stdoutWriter). + WithStderr(stderrBuilder). + Run(ctx) if err != nil { if stderrBuilder.Len() == 0 { _ = stdoutWriter.Close() diff --git a/modules/git/repo_commit.go b/modules/git/repo_commit.go index 6e5911f1dd..5f4487ce7e 100644 --- a/modules/git/repo_commit.go +++ b/modules/git/repo_commit.go @@ -60,7 +60,11 @@ func (repo *Repository) getCommitByPathWithID(id ObjectID, relpath string) (*Com relpath = `\` + relpath } - stdout, _, runErr := gitcmd.NewCommand("log", "-1", prettyLogFormat).AddDynamicArguments(id.String()).AddDashesAndList(relpath).RunStdString(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) + stdout, _, runErr := gitcmd.NewCommand("log", "-1", prettyLogFormat). + AddDynamicArguments(id.String()). + AddDashesAndList(relpath). + WithDir(repo.Path). + RunStdString(repo.Ctx) if runErr != nil { return nil, runErr } @@ -75,7 +79,10 @@ func (repo *Repository) getCommitByPathWithID(id ObjectID, relpath string) (*Com // GetCommitByPath returns the last commit of relative path. func (repo *Repository) GetCommitByPath(relpath string) (*Commit, error) { - stdout, _, runErr := gitcmd.NewCommand("log", "-1", prettyLogFormat).AddDashesAndList(relpath).RunStdBytes(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) + stdout, _, runErr := gitcmd.NewCommand("log", "-1", prettyLogFormat). + AddDashesAndList(relpath). + WithDir(repo.Path). + RunStdBytes(repo.Ctx) if runErr != nil { return nil, runErr } @@ -108,7 +115,7 @@ func (repo *Repository) commitsByRangeWithTime(id ObjectID, page, pageSize int, cmd.AddOptionFormat("--until=%s", until) } - stdout, _, err := cmd.RunStdBytes(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) + stdout, _, err := cmd.WithDir(repo.Path).RunStdBytes(repo.Ctx) if err != nil { return nil, err } @@ -162,7 +169,7 @@ func (repo *Repository) searchCommits(id ObjectID, opts SearchCommitsOptions) ([ // search for commits matching given constraints and keywords in commit msg addCommonSearchArgs(cmd) - stdout, _, err := cmd.RunStdBytes(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) + stdout, _, err := cmd.WithDir(repo.Path).RunStdBytes(repo.Ctx) if err != nil { return nil, err } @@ -183,7 +190,7 @@ func (repo *Repository) searchCommits(id ObjectID, opts SearchCommitsOptions) ([ hashCmd.AddDynamicArguments(v) // search with given constraints for commit matching sha hash of v - hashMatching, _, err := hashCmd.RunStdBytes(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) + hashMatching, _, err := hashCmd.WithDir(repo.Path).RunStdBytes(repo.Ctx) if err != nil || bytes.Contains(stdout, hashMatching) { continue } @@ -198,7 +205,11 @@ func (repo *Repository) searchCommits(id ObjectID, opts SearchCommitsOptions) ([ // FileChangedBetweenCommits Returns true if the file changed between commit IDs id1 and id2 // You must ensure that id1 and id2 are valid commit ids. func (repo *Repository) FileChangedBetweenCommits(filename, id1, id2 string) (bool, error) { - stdout, _, err := gitcmd.NewCommand("diff", "--name-only", "-z").AddDynamicArguments(id1, id2).AddDashesAndList(filename).RunStdBytes(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) + stdout, _, err := gitcmd.NewCommand("diff", "--name-only", "-z"). + AddDynamicArguments(id1, id2). + AddDashesAndList(filename). + WithDir(repo.Path). + RunStdBytes(repo.Ctx) if err != nil { return false, err } @@ -249,11 +260,10 @@ func (repo *Repository) CommitsByFileAndRange(opts CommitsByFileAndRangeOptions) } gitCmd.AddDashesAndList(opts.File) - err := gitCmd.Run(repo.Ctx, &gitcmd.RunOpts{ - Dir: repo.Path, - Stdout: stdoutWriter, - Stderr: &stderr, - }) + err := gitCmd.WithDir(repo.Path). + WithStdout(stdoutWriter). + WithStderr(&stderr). + Run(repo.Ctx) if err != nil { _ = stdoutWriter.CloseWithError(gitcmd.ConcatenateError(err, (&stderr).String())) } else { @@ -291,11 +301,17 @@ func (repo *Repository) CommitsByFileAndRange(opts CommitsByFileAndRangeOptions) // FilesCountBetween return the number of files changed between two commits func (repo *Repository) FilesCountBetween(startCommitID, endCommitID string) (int, error) { - stdout, _, err := gitcmd.NewCommand("diff", "--name-only").AddDynamicArguments(startCommitID+"..."+endCommitID).RunStdString(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) + stdout, _, err := gitcmd.NewCommand("diff", "--name-only"). + AddDynamicArguments(startCommitID + "..." + endCommitID). + WithDir(repo.Path). + RunStdString(repo.Ctx) if err != nil && strings.Contains(err.Error(), "no merge base") { // git >= 2.28 now returns an error if startCommitID and endCommitID have become unrelated. // previously it would return the results of git diff --name-only startCommitID endCommitID so let's try that... - stdout, _, err = gitcmd.NewCommand("diff", "--name-only").AddDynamicArguments(startCommitID, endCommitID).RunStdString(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) + stdout, _, err = gitcmd.NewCommand("diff", "--name-only"). + AddDynamicArguments(startCommitID, endCommitID). + WithDir(repo.Path). + RunStdString(repo.Ctx) } if err != nil { return 0, err @@ -309,13 +325,22 @@ func (repo *Repository) CommitsBetween(last, before *Commit) ([]*Commit, error) var stdout []byte var err error if before == nil { - stdout, _, err = gitcmd.NewCommand("rev-list").AddDynamicArguments(last.ID.String()).RunStdBytes(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) + stdout, _, err = gitcmd.NewCommand("rev-list"). + AddDynamicArguments(last.ID.String()). + WithDir(repo.Path). + RunStdBytes(repo.Ctx) } else { - stdout, _, err = gitcmd.NewCommand("rev-list").AddDynamicArguments(before.ID.String()+".."+last.ID.String()).RunStdBytes(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) + stdout, _, err = gitcmd.NewCommand("rev-list"). + AddDynamicArguments(before.ID.String() + ".." + last.ID.String()). + WithDir(repo.Path). + RunStdBytes(repo.Ctx) if err != nil && strings.Contains(err.Error(), "no merge base") { // future versions of git >= 2.28 are likely to return an error if before and last have become unrelated. // previously it would return the results of git rev-list before last so let's try that... - stdout, _, err = gitcmd.NewCommand("rev-list").AddDynamicArguments(before.ID.String(), last.ID.String()).RunStdBytes(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) + stdout, _, err = gitcmd.NewCommand("rev-list"). + AddDynamicArguments(before.ID.String(), last.ID.String()). + WithDir(repo.Path). + RunStdBytes(repo.Ctx) } } if err != nil { @@ -332,19 +357,25 @@ func (repo *Repository) CommitsBetweenLimit(last, before *Commit, limit, skip in stdout, _, err = gitcmd.NewCommand("rev-list"). AddOptionValues("--max-count", strconv.Itoa(limit)). AddOptionValues("--skip", strconv.Itoa(skip)). - AddDynamicArguments(last.ID.String()).RunStdBytes(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) + AddDynamicArguments(last.ID.String()). + WithDir(repo.Path). + RunStdBytes(repo.Ctx) } else { stdout, _, err = gitcmd.NewCommand("rev-list"). AddOptionValues("--max-count", strconv.Itoa(limit)). AddOptionValues("--skip", strconv.Itoa(skip)). - AddDynamicArguments(before.ID.String()+".."+last.ID.String()).RunStdBytes(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) + AddDynamicArguments(before.ID.String() + ".." + last.ID.String()). + WithDir(repo.Path). + RunStdBytes(repo.Ctx) if err != nil && strings.Contains(err.Error(), "no merge base") { // future versions of git >= 2.28 are likely to return an error if before and last have become unrelated. // previously it would return the results of git rev-list --max-count n before last so let's try that... stdout, _, err = gitcmd.NewCommand("rev-list"). AddOptionValues("--max-count", strconv.Itoa(limit)). AddOptionValues("--skip", strconv.Itoa(skip)). - AddDynamicArguments(before.ID.String(), last.ID.String()).RunStdBytes(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) + AddDynamicArguments(before.ID.String(), last.ID.String()). + WithDir(repo.Path). + RunStdBytes(repo.Ctx) } } if err != nil { @@ -359,13 +390,25 @@ func (repo *Repository) CommitsBetweenNotBase(last, before *Commit, baseBranch s var stdout []byte var err error if before == nil { - stdout, _, err = gitcmd.NewCommand("rev-list").AddDynamicArguments(last.ID.String()).AddOptionValues("--not", baseBranch).RunStdBytes(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) + stdout, _, err = gitcmd.NewCommand("rev-list"). + AddDynamicArguments(last.ID.String()). + AddOptionValues("--not", baseBranch). + WithDir(repo.Path). + RunStdBytes(repo.Ctx) } else { - stdout, _, err = gitcmd.NewCommand("rev-list").AddDynamicArguments(before.ID.String()+".."+last.ID.String()).AddOptionValues("--not", baseBranch).RunStdBytes(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) + stdout, _, err = gitcmd.NewCommand("rev-list"). + AddDynamicArguments(before.ID.String()+".."+last.ID.String()). + AddOptionValues("--not", baseBranch). + WithDir(repo.Path). + RunStdBytes(repo.Ctx) if err != nil && strings.Contains(err.Error(), "no merge base") { // future versions of git >= 2.28 are likely to return an error if before and last have become unrelated. // previously it would return the results of git rev-list before last so let's try that... - stdout, _, err = gitcmd.NewCommand("rev-list").AddDynamicArguments(before.ID.String(), last.ID.String()).AddOptionValues("--not", baseBranch).RunStdBytes(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) + stdout, _, err = gitcmd.NewCommand("rev-list"). + AddDynamicArguments(before.ID.String(), last.ID.String()). + AddOptionValues("--not", baseBranch). + WithDir(repo.Path). + RunStdBytes(repo.Ctx) } } if err != nil { @@ -417,7 +460,7 @@ func (repo *Repository) commitsBefore(id ObjectID, limit int) ([]*Commit, error) } cmd.AddDynamicArguments(id.String()) - stdout, _, runErr := cmd.RunStdBytes(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) + stdout, _, runErr := cmd.WithDir(repo.Path).RunStdBytes(repo.Ctx) if runErr != nil { return nil, runErr } @@ -457,10 +500,9 @@ func (repo *Repository) getBranches(env []string, commitID string, limit int) ([ stdout, _, err := gitcmd.NewCommand("for-each-ref", "--format=%(refname:strip=2)"). AddOptionFormat("--count=%d", limit). AddOptionValues("--contains", commitID, BranchPrefix). - RunStdString(repo.Ctx, &gitcmd.RunOpts{ - Dir: repo.Path, - Env: env, - }) + WithDir(repo.Path). + WithEnv(env). + RunStdString(repo.Ctx) if err != nil { return nil, err } @@ -469,10 +511,11 @@ func (repo *Repository) getBranches(env []string, commitID string, limit int) ([ return branches, nil } - stdout, _, err := gitcmd.NewCommand("branch").AddOptionValues("--contains", commitID).RunStdString(repo.Ctx, &gitcmd.RunOpts{ - Dir: repo.Path, - Env: env, - }) + stdout, _, err := gitcmd.NewCommand("branch"). + AddOptionValues("--contains", commitID). + WithDir(repo.Path). + WithEnv(env). + RunStdString(repo.Ctx) if err != nil { return nil, err } @@ -511,7 +554,10 @@ func (repo *Repository) GetCommitsFromIDs(commitIDs []string) []*Commit { // IsCommitInBranch check if the commit is on the branch func (repo *Repository) IsCommitInBranch(commitID, branch string) (r bool, err error) { - stdout, _, err := gitcmd.NewCommand("branch", "--contains").AddDynamicArguments(commitID, branch).RunStdString(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) + stdout, _, err := gitcmd.NewCommand("branch", "--contains"). + AddDynamicArguments(commitID, branch). + WithDir(repo.Path). + RunStdString(repo.Ctx) if err != nil { return false, err } @@ -540,10 +586,9 @@ func (repo *Repository) GetCommitBranchStart(env []string, branch, endCommitID s cmd := gitcmd.NewCommand("log", prettyLogFormat) cmd.AddDynamicArguments(endCommitID) - stdout, _, runErr := cmd.RunStdBytes(repo.Ctx, &gitcmd.RunOpts{ - Dir: repo.Path, - Env: env, - }) + stdout, _, runErr := cmd.WithDir(repo.Path). + WithEnv(env). + RunStdBytes(repo.Ctx) if runErr != nil { return "", runErr } diff --git a/modules/git/repo_commit_gogit.go b/modules/git/repo_commit_gogit.go index fc653714ad..896d656039 100644 --- a/modules/git/repo_commit_gogit.go +++ b/modules/git/repo_commit_gogit.go @@ -51,7 +51,10 @@ func (repo *Repository) ConvertToGitID(commitID string) (ObjectID, error) { } } - actualCommitID, _, err := gitcmd.NewCommand("rev-parse", "--verify").AddDynamicArguments(commitID).RunStdString(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) + actualCommitID, _, err := gitcmd.NewCommand("rev-parse", "--verify"). + AddDynamicArguments(commitID). + WithDir(repo.Path). + RunStdString(repo.Ctx) actualCommitID = strings.TrimSpace(actualCommitID) if err != nil { if strings.Contains(err.Error(), "unknown revision or path") || diff --git a/modules/git/repo_commit_nogogit.go b/modules/git/repo_commit_nogogit.go index d2c66a541b..3f27833fa6 100644 --- a/modules/git/repo_commit_nogogit.go +++ b/modules/git/repo_commit_nogogit.go @@ -17,7 +17,10 @@ import ( // ResolveReference resolves a name to a reference func (repo *Repository) ResolveReference(name string) (string, error) { - stdout, _, err := gitcmd.NewCommand("show-ref", "--hash").AddDynamicArguments(name).RunStdString(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) + stdout, _, err := gitcmd.NewCommand("show-ref", "--hash"). + AddDynamicArguments(name). + WithDir(repo.Path). + RunStdString(repo.Ctx) if err != nil { if strings.Contains(err.Error(), "not a valid ref") { return "", ErrNotExist{name, ""} @@ -57,7 +60,10 @@ func (repo *Repository) IsCommitExist(name string) bool { log.Error("IsCommitExist: %v", err) return false } - _, _, err := gitcmd.NewCommand("cat-file", "-e").AddDynamicArguments(name).RunStdString(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) + _, _, err := gitcmd.NewCommand("cat-file", "-e"). + AddDynamicArguments(name). + WithDir(repo.Path). + RunStdString(repo.Ctx) return err == nil } diff --git a/modules/git/repo_commitgraph.go b/modules/git/repo_commitgraph.go index 331c799b33..3dac74304c 100644 --- a/modules/git/repo_commitgraph.go +++ b/modules/git/repo_commitgraph.go @@ -14,7 +14,7 @@ import ( // this requires git v2.18 to be installed func WriteCommitGraph(ctx context.Context, repoPath string) error { if DefaultFeatures().CheckVersionAtLeast("2.18") { - if _, _, err := gitcmd.NewCommand("commit-graph", "write").RunStdString(ctx, &gitcmd.RunOpts{Dir: repoPath}); err != nil { + if _, _, err := gitcmd.NewCommand("commit-graph", "write").WithDir(repoPath).RunStdString(ctx); err != nil { return fmt.Errorf("unable to write commit-graph for '%s' : %w", repoPath, err) } } diff --git a/modules/git/repo_compare.go b/modules/git/repo_compare.go index 69835521ec..f60696a763 100644 --- a/modules/git/repo_compare.go +++ b/modules/git/repo_compare.go @@ -27,13 +27,20 @@ func (repo *Repository) GetMergeBase(tmpRemote, base, head string) (string, stri if tmpRemote != "origin" { tmpBaseName := RemotePrefix + tmpRemote + "/tmp_" + base // Fetch commit into a temporary branch in order to be able to handle commits and tags - _, _, err := gitcmd.NewCommand("fetch", "--no-tags").AddDynamicArguments(tmpRemote).AddDashesAndList(base+":"+tmpBaseName).RunStdString(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) + _, _, err := gitcmd.NewCommand("fetch", "--no-tags"). + AddDynamicArguments(tmpRemote). + AddDashesAndList(base + ":" + tmpBaseName). + WithDir(repo.Path). + RunStdString(repo.Ctx) if err == nil { base = tmpBaseName } } - stdout, _, err := gitcmd.NewCommand("merge-base").AddDashesAndList(base, head).RunStdString(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) + stdout, _, err := gitcmd.NewCommand("merge-base"). + AddDashesAndList(base, head). + WithDir(repo.Path). + RunStdString(repo.Ctx) return strings.TrimSpace(stdout), base, err } @@ -61,22 +68,25 @@ func (repo *Repository) GetDiffNumChangedFiles(base, head string, directComparis } // avoid: ambiguous argument 'refs/a...refs/b': unknown revision or path not in the working tree. Use '--': 'git [...] -- [...]' - if err := gitcmd.NewCommand("diff", "-z", "--name-only").AddDynamicArguments(base+separator+head).AddArguments("--"). - Run(repo.Ctx, &gitcmd.RunOpts{ - Dir: repo.Path, - Stdout: w, - Stderr: stderr, - }); err != nil { + if err := gitcmd.NewCommand("diff", "-z", "--name-only"). + AddDynamicArguments(base + separator + head). + AddArguments("--"). + WithDir(repo.Path). + WithStdout(w). + WithStderr(stderr). + Run(repo.Ctx); err != nil { if strings.Contains(stderr.String(), "no merge base") { // git >= 2.28 now returns an error if base and head have become unrelated. // previously it would return the results of git diff -z --name-only base head so let's try that... w = &lineCountWriter{} stderr.Reset() - if err = gitcmd.NewCommand("diff", "-z", "--name-only").AddDynamicArguments(base, head).AddArguments("--").Run(repo.Ctx, &gitcmd.RunOpts{ - Dir: repo.Path, - Stdout: w, - Stderr: stderr, - }); err == nil { + if err = gitcmd.NewCommand("diff", "-z", "--name-only"). + AddDynamicArguments(base, head). + AddArguments("--"). + WithDir(repo.Path). + WithStdout(w). + WithStderr(stderr). + Run(repo.Ctx); err == nil { return w.numLines, nil } } @@ -91,30 +101,29 @@ var patchCommits = regexp.MustCompile(`^From\s(\w+)\s`) func (repo *Repository) GetDiff(compareArg string, w io.Writer) error { stderr := new(bytes.Buffer) return gitcmd.NewCommand("diff", "-p").AddDynamicArguments(compareArg). - Run(repo.Ctx, &gitcmd.RunOpts{ - Dir: repo.Path, - Stdout: w, - Stderr: stderr, - }) + WithDir(repo.Path). + WithStdout(w). + WithStderr(stderr). + Run(repo.Ctx) } // GetDiffBinary generates and returns patch data between given revisions, including binary diffs. func (repo *Repository) GetDiffBinary(compareArg string, w io.Writer) error { - return gitcmd.NewCommand("diff", "-p", "--binary", "--histogram").AddDynamicArguments(compareArg).Run(repo.Ctx, &gitcmd.RunOpts{ - Dir: repo.Path, - Stdout: w, - }) + return gitcmd.NewCommand("diff", "-p", "--binary", "--histogram"). + AddDynamicArguments(compareArg). + WithDir(repo.Path). + WithStdout(w). + Run(repo.Ctx) } // GetPatch generates and returns format-patch data between given revisions, able to be used with `git apply` func (repo *Repository) GetPatch(compareArg string, w io.Writer) error { stderr := new(bytes.Buffer) return gitcmd.NewCommand("format-patch", "--binary", "--stdout").AddDynamicArguments(compareArg). - Run(repo.Ctx, &gitcmd.RunOpts{ - Dir: repo.Path, - Stdout: w, - Stderr: stderr, - }) + WithDir(repo.Path). + WithStdout(w). + WithStderr(stderr). + Run(repo.Ctx) } // GetFilesChangedBetween returns a list of all files that have been changed between the given commits @@ -131,7 +140,7 @@ func (repo *Repository) GetFilesChangedBetween(base, head string) ([]string, err } else { cmd.AddDynamicArguments(base, head) } - stdout, _, err := cmd.RunStdString(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) + stdout, _, err := cmd.WithDir(repo.Path).RunStdString(repo.Ctx) if err != nil { return nil, err } diff --git a/modules/git/repo_compare_test.go b/modules/git/repo_compare_test.go index 47fd2ca102..bf16b7cfce 100644 --- a/modules/git/repo_compare_test.go +++ b/modules/git/repo_compare_test.go @@ -103,7 +103,8 @@ func TestReadWritePullHead(t *testing.T) { newCommit := "feaf4ba6bc635fec442f46ddd4512416ec43c2c2" _, _, err = gitcmd.NewCommand("update-ref"). AddDynamicArguments(PullPrefix+"1/head", newCommit). - RunStdString(t.Context(), &gitcmd.RunOpts{Dir: repo.Path}) + WithDir(repo.Path). + RunStdString(t.Context()) if err != nil { assert.NoError(t, err) return @@ -121,8 +122,9 @@ func TestReadWritePullHead(t *testing.T) { // Remove file after the test _, _, err = gitcmd.NewCommand("update-ref", "--no-deref", "-d"). - AddDynamicArguments(PullPrefix+"1/head"). - RunStdString(t.Context(), &gitcmd.RunOpts{Dir: repo.Path}) + AddDynamicArguments(PullPrefix + "1/head"). + WithDir(repo.Path). + RunStdString(t.Context()) assert.NoError(t, err) } diff --git a/modules/git/repo_gpg.go b/modules/git/repo_gpg.go index a999d2dbc6..eb1e71e30a 100644 --- a/modules/git/repo_gpg.go +++ b/modules/git/repo_gpg.go @@ -43,7 +43,7 @@ func (repo *Repository) GetDefaultPublicGPGKey(forceUpdate bool) (*GPGSettings, Sign: true, } - value, _, _ := gitcmd.NewCommand("config", "--get", "commit.gpgsign").RunStdString(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) + value, _, _ := gitcmd.NewCommand("config", "--get", "commit.gpgsign").WithDir(repo.Path).RunStdString(repo.Ctx) sign, valid := ParseBool(strings.TrimSpace(value)) if !sign || !valid { gpgSettings.Sign = false @@ -51,16 +51,16 @@ func (repo *Repository) GetDefaultPublicGPGKey(forceUpdate bool) (*GPGSettings, return gpgSettings, nil } - signingKey, _, _ := gitcmd.NewCommand("config", "--get", "user.signingkey").RunStdString(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) + signingKey, _, _ := gitcmd.NewCommand("config", "--get", "user.signingkey").WithDir(repo.Path).RunStdString(repo.Ctx) gpgSettings.KeyID = strings.TrimSpace(signingKey) - format, _, _ := gitcmd.NewCommand("config", "--default", SigningKeyFormatOpenPGP, "--get", "gpg.format").RunStdString(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) + format, _, _ := gitcmd.NewCommand("config", "--default", SigningKeyFormatOpenPGP, "--get", "gpg.format").WithDir(repo.Path).RunStdString(repo.Ctx) gpgSettings.Format = strings.TrimSpace(format) - defaultEmail, _, _ := gitcmd.NewCommand("config", "--get", "user.email").RunStdString(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) + defaultEmail, _, _ := gitcmd.NewCommand("config", "--get", "user.email").WithDir(repo.Path).RunStdString(repo.Ctx) gpgSettings.Email = strings.TrimSpace(defaultEmail) - defaultName, _, _ := gitcmd.NewCommand("config", "--get", "user.name").RunStdString(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) + defaultName, _, _ := gitcmd.NewCommand("config", "--get", "user.name").WithDir(repo.Path).RunStdString(repo.Ctx) gpgSettings.Name = strings.TrimSpace(defaultName) if err := gpgSettings.LoadPublicKeyContent(); err != nil { diff --git a/modules/git/repo_index.go b/modules/git/repo_index.go index e7b3792d95..4068f86bb2 100644 --- a/modules/git/repo_index.go +++ b/modules/git/repo_index.go @@ -22,7 +22,7 @@ func (repo *Repository) ReadTreeToIndex(treeish string, indexFilename ...string) } if len(treeish) != objectFormat.FullLength() { - res, _, err := gitcmd.NewCommand("rev-parse", "--verify").AddDynamicArguments(treeish).RunStdString(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) + res, _, err := gitcmd.NewCommand("rev-parse", "--verify").AddDynamicArguments(treeish).WithDir(repo.Path).RunStdString(repo.Ctx) if err != nil { return err } @@ -42,7 +42,7 @@ func (repo *Repository) readTreeToIndex(id ObjectID, indexFilename ...string) er if len(indexFilename) > 0 { env = append(os.Environ(), "GIT_INDEX_FILE="+indexFilename[0]) } - _, _, err := gitcmd.NewCommand("read-tree").AddDynamicArguments(id.String()).RunStdString(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path, Env: env}) + _, _, err := gitcmd.NewCommand("read-tree").AddDynamicArguments(id.String()).WithDir(repo.Path).WithEnv(env).RunStdString(repo.Ctx) if err != nil { return err } @@ -75,14 +75,14 @@ func (repo *Repository) ReadTreeToTemporaryIndex(treeish string) (tmpIndexFilena // EmptyIndex empties the index func (repo *Repository) EmptyIndex() error { - _, _, err := gitcmd.NewCommand("read-tree", "--empty").RunStdString(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) + _, _, err := gitcmd.NewCommand("read-tree", "--empty").WithDir(repo.Path).RunStdString(repo.Ctx) return err } // LsFiles checks if the given filenames are in the index func (repo *Repository) LsFiles(filenames ...string) ([]string, error) { cmd := gitcmd.NewCommand("ls-files", "-z").AddDashesAndList(filenames...) - res, _, err := cmd.RunStdBytes(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) + res, _, err := cmd.WithDir(repo.Path).RunStdBytes(repo.Ctx) if err != nil { return nil, err } @@ -110,12 +110,12 @@ func (repo *Repository) RemoveFilesFromIndex(filenames ...string) error { buffer.WriteString("0 blob " + objectFormat.EmptyObjectID().String() + "\t" + file + "\000") } } - return cmd.Run(repo.Ctx, &gitcmd.RunOpts{ - Dir: repo.Path, - Stdin: bytes.NewReader(buffer.Bytes()), - Stdout: stdout, - Stderr: stderr, - }) + return cmd. + WithDir(repo.Path). + WithStdin(bytes.NewReader(buffer.Bytes())). + WithStdout(stdout). + WithStderr(stderr). + Run(repo.Ctx) } type IndexObjectInfo struct { @@ -134,12 +134,12 @@ func (repo *Repository) AddObjectsToIndex(objects ...IndexObjectInfo) error { // using format: mode SP type SP sha1 TAB path buffer.WriteString(object.Mode + " blob " + object.Object.String() + "\t" + object.Filename + "\000") } - return cmd.Run(repo.Ctx, &gitcmd.RunOpts{ - Dir: repo.Path, - Stdin: bytes.NewReader(buffer.Bytes()), - Stdout: stdout, - Stderr: stderr, - }) + return cmd. + WithDir(repo.Path). + WithStdin(bytes.NewReader(buffer.Bytes())). + WithStdout(stdout). + WithStderr(stderr). + Run(repo.Ctx) } // AddObjectToIndex adds the provided object hash to the index at the provided filename @@ -149,7 +149,7 @@ func (repo *Repository) AddObjectToIndex(mode string, object ObjectID, filename // WriteTree writes the current index as a tree to the object db and returns its hash func (repo *Repository) WriteTree() (*Tree, error) { - stdout, _, runErr := gitcmd.NewCommand("write-tree").RunStdString(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) + stdout, _, runErr := gitcmd.NewCommand("write-tree").WithDir(repo.Path).RunStdString(repo.Ctx) if runErr != nil { return nil, runErr } diff --git a/modules/git/repo_object.go b/modules/git/repo_object.go index e8f6510c23..2a39a3c4d8 100644 --- a/modules/git/repo_object.go +++ b/modules/git/repo_object.go @@ -76,12 +76,12 @@ func (repo *Repository) hashObject(reader io.Reader, save bool) (string, error) } stdout := new(bytes.Buffer) stderr := new(bytes.Buffer) - err := cmd.Run(repo.Ctx, &gitcmd.RunOpts{ - Dir: repo.Path, - Stdin: reader, - Stdout: stdout, - Stderr: stderr, - }) + err := cmd. + WithDir(repo.Path). + WithStdin(reader). + WithStdout(stdout). + WithStderr(stderr). + Run(repo.Ctx) if err != nil { return "", err } diff --git a/modules/git/repo_ref.go b/modules/git/repo_ref.go index 577e17c45d..8859a93a57 100644 --- a/modules/git/repo_ref.go +++ b/modules/git/repo_ref.go @@ -28,7 +28,8 @@ func (repo *Repository) ListOccurrences(ctx context.Context, refType, commitSHA default: return nil, util.NewInvalidArgumentErrorf(`can only use "branch" or "tag" for refType, but got %q`, refType) } - stdout, _, err := cmd.AddArguments("--no-color", "--sort=-creatordate", "--contains").AddDynamicArguments(commitSHA).RunStdString(ctx, &gitcmd.RunOpts{Dir: repo.Path}) + stdout, _, err := cmd.AddArguments("--no-color", "--sort=-creatordate", "--contains"). + AddDynamicArguments(commitSHA).WithDir(repo.Path).RunStdString(ctx) if err != nil { return nil, err } diff --git a/modules/git/repo_ref_nogogit.go b/modules/git/repo_ref_nogogit.go index 784efecc65..09bb0df7b8 100644 --- a/modules/git/repo_ref_nogogit.go +++ b/modules/git/repo_ref_nogogit.go @@ -23,11 +23,11 @@ func (repo *Repository) GetRefsFiltered(pattern string) ([]*Reference, error) { go func() { stderrBuilder := &strings.Builder{} - err := gitcmd.NewCommand("for-each-ref").Run(repo.Ctx, &gitcmd.RunOpts{ - Dir: repo.Path, - Stdout: stdoutWriter, - Stderr: stderrBuilder, - }) + err := gitcmd.NewCommand("for-each-ref"). + WithDir(repo.Path). + WithStdout(stdoutWriter). + WithStderr(stderrBuilder). + Run(repo.Ctx) if err != nil { _ = stdoutWriter.CloseWithError(gitcmd.ConcatenateError(err, stderrBuilder.String())) } else { diff --git a/modules/git/repo_stats.go b/modules/git/repo_stats.go index 22082325ef..cfb35288fe 100644 --- a/modules/git/repo_stats.go +++ b/modules/git/repo_stats.go @@ -43,7 +43,8 @@ func (repo *Repository) GetCodeActivityStats(fromTime time.Time, branch string) stdout, _, runErr := gitcmd.NewCommand("rev-list", "--count", "--no-merges", "--branches=*", "--date=iso"). AddOptionFormat("--since=%s", since). - RunStdString(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) + WithDir(repo.Path). + RunStdString(repo.Ctx) if runErr != nil { return nil, runErr } @@ -72,12 +73,11 @@ func (repo *Repository) GetCodeActivityStats(fromTime time.Time, branch string) } stderr := new(strings.Builder) - err = gitCmd.Run(repo.Ctx, &gitcmd.RunOpts{ - Env: []string{}, - Dir: repo.Path, - Stdout: stdoutWriter, - Stderr: stderr, - PipelineFunc: func(ctx context.Context, cancel context.CancelFunc) error { + err = gitCmd. + WithDir(repo.Path). + WithStdout(stdoutWriter). + WithStderr(stderr). + WithPipelineFunc(func(ctx context.Context, cancel context.CancelFunc) error { _ = stdoutWriter.Close() scanner := bufio.NewScanner(stdoutReader) scanner.Split(bufio.ScanLines) @@ -145,8 +145,8 @@ func (repo *Repository) GetCodeActivityStats(fromTime time.Time, branch string) stats.Authors = a _ = stdoutReader.Close() return nil - }, - }) + }). + Run(repo.Ctx) if err != nil { return nil, fmt.Errorf("Failed to get GetCodeActivityStats for repository.\nError: %w\nStderr: %s", err, stderr) } diff --git a/modules/git/repo_tag.go b/modules/git/repo_tag.go index 0cb0932459..4ad0c6e5ab 100644 --- a/modules/git/repo_tag.go +++ b/modules/git/repo_tag.go @@ -19,13 +19,17 @@ const TagPrefix = "refs/tags/" // CreateTag create one tag in the repository func (repo *Repository) CreateTag(name, revision string) error { - _, _, err := gitcmd.NewCommand("tag").AddDashesAndList(name, revision).RunStdString(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) + _, _, err := gitcmd.NewCommand("tag").AddDashesAndList(name, revision).WithDir(repo.Path).RunStdString(repo.Ctx) return err } // CreateAnnotatedTag create one annotated tag in the repository func (repo *Repository) CreateAnnotatedTag(name, message, revision string) error { - _, _, err := gitcmd.NewCommand("tag", "-a", "-m").AddDynamicArguments(message).AddDashesAndList(name, revision).RunStdString(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) + _, _, err := gitcmd.NewCommand("tag", "-a", "-m"). + AddDynamicArguments(message). + AddDashesAndList(name, revision). + WithDir(repo.Path). + RunStdString(repo.Ctx) return err } @@ -35,7 +39,7 @@ func (repo *Repository) GetTagNameBySHA(sha string) (string, error) { return "", fmt.Errorf("SHA is too short: %s", sha) } - stdout, _, err := gitcmd.NewCommand("show-ref", "--tags", "-d").RunStdString(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) + stdout, _, err := gitcmd.NewCommand("show-ref", "--tags", "-d").WithDir(repo.Path).RunStdString(repo.Ctx) if err != nil { return "", err } @@ -58,7 +62,7 @@ func (repo *Repository) GetTagNameBySHA(sha string) (string, error) { // GetTagID returns the object ID for a tag (annotated tags have both an object SHA AND a commit SHA) func (repo *Repository) GetTagID(name string) (string, error) { - stdout, _, err := gitcmd.NewCommand("show-ref", "--tags").AddDashesAndList(name).RunStdString(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) + stdout, _, err := gitcmd.NewCommand("show-ref", "--tags").AddDashesAndList(name).WithDir(repo.Path).RunStdString(repo.Ctx) if err != nil { return "", err } @@ -115,12 +119,15 @@ func (repo *Repository) GetTagInfos(page, pageSize int) ([]*Tag, int, error) { defer stdoutReader.Close() defer stdoutWriter.Close() stderr := strings.Builder{} - rc := &gitcmd.RunOpts{Dir: repo.Path, Stdout: stdoutWriter, Stderr: &stderr} go func() { err := gitcmd.NewCommand("for-each-ref"). AddOptionFormat("--format=%s", forEachRefFmt.Flag()). - AddArguments("--sort", "-*creatordate", "refs/tags").Run(repo.Ctx, rc) + AddArguments("--sort", "-*creatordate", "refs/tags"). + WithDir(repo.Path). + WithStdout(stdoutWriter). + WithStderr(&stderr). + Run(repo.Ctx) if err != nil { _ = stdoutWriter.CloseWithError(gitcmd.ConcatenateError(err, stderr.String())) } else { diff --git a/modules/git/repo_tree.go b/modules/git/repo_tree.go index 1d8c940951..964342ba00 100644 --- a/modules/git/repo_tree.go +++ b/modules/git/repo_tree.go @@ -60,13 +60,12 @@ func (repo *Repository) CommitTree(author, committer *Signature, tree *Tree, opt stdout := new(bytes.Buffer) stderr := new(bytes.Buffer) - err := cmd.Run(repo.Ctx, &gitcmd.RunOpts{ - Env: env, - Dir: repo.Path, - Stdin: messageBytes, - Stdout: stdout, - Stderr: stderr, - }) + err := cmd.WithEnv(env). + WithDir(repo.Path). + WithStdin(messageBytes). + WithStdout(stdout). + WithStderr(stderr). + Run(repo.Ctx) if err != nil { return nil, gitcmd.ConcatenateError(err, stderr.String()) } diff --git a/modules/git/repo_tree_gogit.go b/modules/git/repo_tree_gogit.go index 40524d0c34..e15663a32a 100644 --- a/modules/git/repo_tree_gogit.go +++ b/modules/git/repo_tree_gogit.go @@ -38,7 +38,10 @@ func (repo *Repository) GetTree(idStr string) (*Tree, error) { } if len(idStr) != objectFormat.FullLength() { - res, _, err := gitcmd.NewCommand("rev-parse", "--verify").AddDynamicArguments(idStr).RunStdString(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) + res, _, err := gitcmd.NewCommand("rev-parse", "--verify"). + AddDynamicArguments(idStr). + WithDir(repo.Path). + RunStdString(repo.Ctx) if err != nil { return nil, err } diff --git a/modules/git/submodule.go b/modules/git/submodule.go index 58824adc82..45059eae77 100644 --- a/modules/git/submodule.go +++ b/modules/git/submodule.go @@ -25,10 +25,11 @@ func GetTemplateSubmoduleCommits(ctx context.Context, repoPath string) (submodul if err != nil { return nil, err } - opts := &gitcmd.RunOpts{ - Dir: repoPath, - Stdout: stdoutWriter, - PipelineFunc: func(ctx context.Context, cancel context.CancelFunc) error { + + err = gitcmd.NewCommand("ls-tree", "-r", "--", "HEAD"). + WithDir(repoPath). + WithStdout(stdoutWriter). + WithPipelineFunc(func(ctx context.Context, cancel context.CancelFunc) error { _ = stdoutWriter.Close() defer stdoutReader.Close() @@ -44,9 +45,8 @@ func GetTemplateSubmoduleCommits(ctx context.Context, repoPath string) (submodul } } return scanner.Err() - }, - } - err = gitcmd.NewCommand("ls-tree", "-r", "--", "HEAD").Run(ctx, opts) + }). + Run(ctx) if err != nil { return nil, fmt.Errorf("GetTemplateSubmoduleCommits: error running git ls-tree: %v", err) } @@ -58,7 +58,7 @@ func GetTemplateSubmoduleCommits(ctx context.Context, repoPath string) (submodul func AddTemplateSubmoduleIndexes(ctx context.Context, repoPath string, submodules []TemplateSubmoduleCommit) error { for _, submodule := range submodules { cmd := gitcmd.NewCommand("update-index", "--add", "--cacheinfo", "160000").AddDynamicArguments(submodule.Commit, submodule.Path) - if stdout, _, err := cmd.RunStdString(ctx, &gitcmd.RunOpts{Dir: repoPath}); err != nil { + if stdout, _, err := cmd.WithDir(repoPath).RunStdString(ctx); err != nil { log.Error("Unable to add %s as submodule to repo %s: stdout %s\nError: %v", submodule.Path, repoPath, stdout, err) return err } diff --git a/modules/git/submodule_test.go b/modules/git/submodule_test.go index d2df8b2a91..22bd5bf71e 100644 --- a/modules/git/submodule_test.go +++ b/modules/git/submodule_test.go @@ -32,14 +32,14 @@ func TestAddTemplateSubmoduleIndexes(t *testing.T) { ctx := t.Context() tmpDir := t.TempDir() var err error - _, _, err = gitcmd.NewCommand("init").RunStdString(ctx, &gitcmd.RunOpts{Dir: tmpDir}) + _, _, err = gitcmd.NewCommand("init").WithDir(tmpDir).RunStdString(ctx) 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 = gitcmd.NewCommand("add", "--all").RunStdString(ctx, &gitcmd.RunOpts{Dir: tmpDir}) + _, _, err = gitcmd.NewCommand("add", "--all").WithDir(tmpDir).RunStdString(ctx) require.NoError(t, err) - _, _, err = gitcmd.NewCommand("-c", "user.name=a", "-c", "user.email=b", "commit", "-m=test").RunStdString(ctx, &gitcmd.RunOpts{Dir: tmpDir}) + _, _, err = gitcmd.NewCommand("-c", "user.name=a", "-c", "user.email=b", "commit", "-m=test").WithDir(tmpDir).RunStdString(ctx) require.NoError(t, err) submodules, err := GetTemplateSubmoduleCommits(t.Context(), tmpDir) require.NoError(t, err) diff --git a/modules/git/tree.go b/modules/git/tree.go index a8c4929c7c..9c73aec735 100644 --- a/modules/git/tree.go +++ b/modules/git/tree.go @@ -53,7 +53,7 @@ func (repo *Repository) LsTree(ref string, filenames ...string) ([]string, error cmd := gitcmd.NewCommand("ls-tree", "-z", "--name-only"). AddDashesAndList(append([]string{ref}, filenames...)...) - res, _, err := cmd.RunStdBytes(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) + res, _, err := cmd.WithDir(repo.Path).RunStdBytes(repo.Ctx) if err != nil { return nil, err } @@ -69,7 +69,8 @@ func (repo *Repository) LsTree(ref string, filenames ...string) ([]string, error func (repo *Repository) GetTreePathLatestCommit(refName, treePath string) (*Commit, error) { stdout, _, err := gitcmd.NewCommand("rev-list", "-1"). AddDynamicArguments(refName).AddDashesAndList(treePath). - RunStdString(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) + WithDir(repo.Path). + RunStdString(repo.Ctx) if err != nil { return nil, err } diff --git a/modules/git/tree_nogogit.go b/modules/git/tree_nogogit.go index 045d78c42c..956a5938f0 100644 --- a/modules/git/tree_nogogit.go +++ b/modules/git/tree_nogogit.go @@ -72,7 +72,7 @@ func (t *Tree) ListEntries() (Entries, error) { } } - stdout, _, runErr := gitcmd.NewCommand("ls-tree", "-l").AddDynamicArguments(t.ID.String()).RunStdBytes(t.repo.Ctx, &gitcmd.RunOpts{Dir: t.repo.Path}) + stdout, _, runErr := gitcmd.NewCommand("ls-tree", "-l").AddDynamicArguments(t.ID.String()).WithDir(t.repo.Path).RunStdBytes(t.repo.Ctx) if runErr != nil { if strings.Contains(runErr.Error(), "fatal: Not a valid object name") || strings.Contains(runErr.Error(), "fatal: not a tree object") { return nil, ErrNotExist{ @@ -101,7 +101,8 @@ func (t *Tree) listEntriesRecursive(extraArgs gitcmd.TrustedCmdArgs) (Entries, e stdout, _, runErr := gitcmd.NewCommand("ls-tree", "-t", "-r"). AddArguments(extraArgs...). AddDynamicArguments(t.ID.String()). - RunStdBytes(t.repo.Ctx, &gitcmd.RunOpts{Dir: t.repo.Path}) + WithDir(t.repo.Path). + RunStdBytes(t.repo.Ctx) if runErr != nil { return nil, runErr } diff --git a/modules/git/url/url_test.go b/modules/git/url/url_test.go index 6655c20be3..76aa74a128 100644 --- a/modules/git/url/url_test.go +++ b/modules/git/url/url_test.go @@ -34,12 +34,12 @@ func TestParseGitURLs(t *testing.T) { }, }, { - kase: "git@[fe80:14fc:cec5:c174:d88%2510]:go-gitea/gitea.git", + kase: "git@[fe80::14fc:cec5:c174:d88%2510]:go-gitea/gitea.git", expected: &GitURL{ URL: &url.URL{ Scheme: "ssh", User: url.User("git"), - Host: "[fe80:14fc:cec5:c174:d88%10]", + Host: "[fe80::14fc:cec5:c174:d88%10]", Path: "go-gitea/gitea.git", }, extraMark: 1, @@ -137,11 +137,11 @@ func TestParseGitURLs(t *testing.T) { }, }, { - kase: "https://[fe80:14fc:cec5:c174:d88%2510]:20/go-gitea/gitea.git", + kase: "https://[fe80::14fc:cec5:c174:d88%2510]:20/go-gitea/gitea.git", expected: &GitURL{ URL: &url.URL{ Scheme: "https", - Host: "[fe80:14fc:cec5:c174:d88%10]:20", + Host: "[fe80::14fc:cec5:c174:d88%10]:20", Path: "/go-gitea/gitea.git", }, extraMark: 0, diff --git a/modules/git/utils.go b/modules/git/utils.go index b5f188904a..e7d30ce9ee 100644 --- a/modules/git/utils.go +++ b/modules/git/utils.go @@ -6,7 +6,6 @@ package git import ( "crypto/sha1" "encoding/hex" - "io" "strconv" "strings" "sync" @@ -68,32 +67,6 @@ func ParseBool(value string) (result, valid bool) { return intValue != 0, true } -// LimitedReaderCloser is a limited reader closer -type LimitedReaderCloser struct { - R io.Reader - C io.Closer - N int64 -} - -// Read implements io.Reader -func (l *LimitedReaderCloser) Read(p []byte) (n int, err error) { - if l.N <= 0 { - _ = l.C.Close() - return 0, io.EOF - } - if int64(len(p)) > l.N { - p = p[0:l.N] - } - n, err = l.R.Read(p) - l.N -= int64(n) - return n, err -} - -// Close implements io.Closer -func (l *LimitedReaderCloser) Close() error { - return l.C.Close() -} - func HashFilePathForWebUI(s string) string { h := sha1.New() _, _ = h.Write([]byte(s)) diff --git a/modules/gitrepo/archive.go b/modules/gitrepo/archive.go new file mode 100644 index 0000000000..b78922e126 --- /dev/null +++ b/modules/gitrepo/archive.go @@ -0,0 +1,76 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package gitrepo + +import ( + "context" + "fmt" + "io" + "os" + "path/filepath" + "strings" + + "code.gitea.io/gitea/modules/git/gitcmd" + "code.gitea.io/gitea/modules/setting" +) + +// CreateArchive create archive content to the target path +func CreateArchive(ctx context.Context, repo Repository, format string, target io.Writer, usePrefix bool, commitID string) error { + if format == "unknown" { + return fmt.Errorf("unknown format: %v", format) + } + + cmd := gitcmd.NewCommand("archive") + if usePrefix { + cmd.AddOptionFormat("--prefix=%s", filepath.Base(strings.TrimSuffix(repo.RelativePath(), ".git"))+"/") + } + cmd.AddOptionFormat("--format=%s", format) + cmd.AddDynamicArguments(commitID) + + var stderr strings.Builder + if err := RunCmd(ctx, repo, cmd.WithStdout(target).WithStderr(&stderr)); err != nil { + return gitcmd.ConcatenateError(err, stderr.String()) + } + return nil +} + +// CreateBundle create bundle content to the target path +func CreateBundle(ctx context.Context, repo Repository, commit string, out io.Writer) error { + tmp, cleanup, err := setting.AppDataTempDir("git-repo-content").MkdirTempRandom("gitea-bundle") + if err != nil { + return err + } + defer cleanup() + + env := append(os.Environ(), "GIT_OBJECT_DIRECTORY="+filepath.Join(repoPath(repo), "objects")) + _, _, err = gitcmd.NewCommand("init", "--bare").WithDir(tmp).WithEnv(env).RunStdString(ctx) + if err != nil { + return err + } + + _, _, err = gitcmd.NewCommand("reset", "--soft").AddDynamicArguments(commit).WithDir(tmp).WithEnv(env).RunStdString(ctx) + if err != nil { + return err + } + + _, _, err = gitcmd.NewCommand("branch", "-m", "bundle").WithDir(tmp).WithEnv(env).RunStdString(ctx) + if err != nil { + return err + } + + tmpFile := filepath.Join(tmp, "bundle") + _, _, err = gitcmd.NewCommand("bundle", "create").AddDynamicArguments(tmpFile, "bundle", "HEAD").WithDir(tmp).WithEnv(env).RunStdString(ctx) + if err != nil { + return err + } + + fi, err := os.Open(tmpFile) + if err != nil { + return err + } + defer fi.Close() + + _, err = io.Copy(out, fi) + return err +} diff --git a/modules/gitrepo/blame.go b/modules/gitrepo/blame.go index 02ada58130..3ce808d9b3 100644 --- a/modules/gitrepo/blame.go +++ b/modules/gitrepo/blame.go @@ -10,7 +10,7 @@ import ( ) func LineBlame(ctx context.Context, repo Repository, revision, file string, line uint) (string, error) { - return runCmdString(ctx, repo, + return RunCmdString(ctx, repo, gitcmd.NewCommand("blame"). AddOptionFormat("-L %d,%d", line, line). AddOptionValues("-p", revision). diff --git a/modules/gitrepo/branch.go b/modules/gitrepo/branch.go index b857b2ad47..e05d75caf8 100644 --- a/modules/gitrepo/branch.go +++ b/modules/gitrepo/branch.go @@ -36,14 +36,14 @@ func GetBranchCommitID(ctx context.Context, repo Repository, branch string) (str // SetDefaultBranch sets default branch of repository. func SetDefaultBranch(ctx context.Context, repo Repository, name string) error { - _, err := runCmdString(ctx, repo, gitcmd.NewCommand("symbolic-ref", "HEAD"). + _, err := RunCmdString(ctx, repo, gitcmd.NewCommand("symbolic-ref", "HEAD"). AddDynamicArguments(git.BranchPrefix+name)) return err } // GetDefaultBranch gets default branch of repository. func GetDefaultBranch(ctx context.Context, repo Repository) (string, error) { - stdout, err := runCmdString(ctx, repo, gitcmd.NewCommand("symbolic-ref", "HEAD")) + stdout, err := RunCmdString(ctx, repo, gitcmd.NewCommand("symbolic-ref", "HEAD")) if err != nil { return "", err } @@ -56,7 +56,7 @@ func GetDefaultBranch(ctx context.Context, repo Repository) (string, error) { // IsReferenceExist returns true if given reference exists in the repository. func IsReferenceExist(ctx context.Context, repo Repository, name string) bool { - _, err := runCmdString(ctx, repo, gitcmd.NewCommand("show-ref", "--verify").AddDashesAndList(name)) + _, err := RunCmdString(ctx, repo, gitcmd.NewCommand("show-ref", "--verify").AddDashesAndList(name)) return err == nil } @@ -76,7 +76,7 @@ func DeleteBranch(ctx context.Context, repo Repository, name string, force bool) } cmd.AddDashesAndList(name) - _, err := runCmdString(ctx, repo, cmd) + _, err := RunCmdString(ctx, repo, cmd) return err } @@ -85,12 +85,12 @@ func CreateBranch(ctx context.Context, repo Repository, branch, oldbranchOrCommi cmd := gitcmd.NewCommand("branch") cmd.AddDashesAndList(branch, oldbranchOrCommit) - _, err := runCmdString(ctx, repo, cmd) + _, err := RunCmdString(ctx, repo, cmd) return err } // RenameBranch rename a branch func RenameBranch(ctx context.Context, repo Repository, from, to string) error { - _, err := runCmdString(ctx, repo, gitcmd.NewCommand("branch", "-m").AddDynamicArguments(from, to)) + _, err := RunCmdString(ctx, repo, gitcmd.NewCommand("branch", "-m").AddDynamicArguments(from, to)) return err } diff --git a/modules/gitrepo/clone.go b/modules/gitrepo/clone.go new file mode 100644 index 0000000000..8c437f657c --- /dev/null +++ b/modules/gitrepo/clone.go @@ -0,0 +1,20 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package gitrepo + +import ( + "context" + + "code.gitea.io/gitea/modules/git" +) + +// CloneExternalRepo clones an external repository to the managed repository. +func CloneExternalRepo(ctx context.Context, fromRemoteURL string, toRepo Repository, opts git.CloneRepoOptions) error { + return git.Clone(ctx, fromRemoteURL, repoPath(toRepo), opts) +} + +// CloneRepoToLocal clones a managed repository to a local path. +func CloneRepoToLocal(ctx context.Context, fromRepo Repository, toLocalPath string, opts git.CloneRepoOptions) error { + return git.Clone(ctx, repoPath(fromRepo), toLocalPath, opts) +} diff --git a/modules/gitrepo/command.go b/modules/gitrepo/command.go index 58dee2aef0..d4cb6093fc 100644 --- a/modules/gitrepo/command.go +++ b/modules/gitrepo/command.go @@ -9,7 +9,15 @@ import ( "code.gitea.io/gitea/modules/git/gitcmd" ) -func runCmdString(ctx context.Context, repo Repository, cmd *gitcmd.Command) (string, error) { - res, _, err := cmd.RunStdString(ctx, &gitcmd.RunOpts{Dir: repoPath(repo)}) +func RunCmd(ctx context.Context, repo Repository, cmd *gitcmd.Command) error { + return cmd.WithDir(repoPath(repo)).WithParentCallerInfo().Run(ctx) +} + +func RunCmdString(ctx context.Context, repo Repository, cmd *gitcmd.Command) (string, error) { + res, _, err := cmd.WithDir(repoPath(repo)).WithParentCallerInfo().RunStdString(ctx) return res, err } + +func RunCmdBytes(ctx context.Context, repo Repository, cmd *gitcmd.Command) ([]byte, []byte, error) { + return cmd.WithDir(repoPath(repo)).WithParentCallerInfo().RunStdBytes(ctx) +} diff --git a/modules/gitrepo/commitgraph.go b/modules/gitrepo/commitgraph.go new file mode 100644 index 0000000000..7310e167f6 --- /dev/null +++ b/modules/gitrepo/commitgraph.go @@ -0,0 +1,14 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package gitrepo + +import ( + "context" + + "code.gitea.io/gitea/modules/git" +) + +func WriteCommitGraph(ctx context.Context, repo Repository) error { + return git.WriteCommitGraph(ctx, repoPath(repo)) +} diff --git a/modules/gitrepo/compare.go b/modules/gitrepo/compare.go index 1c8f5421fa..b8e4c30d6c 100644 --- a/modules/gitrepo/compare.go +++ b/modules/gitrepo/compare.go @@ -22,7 +22,7 @@ type DivergeObject struct { func GetDivergingCommits(ctx context.Context, repo Repository, baseBranch, targetBranch string) (*DivergeObject, error) { cmd := gitcmd.NewCommand("rev-list", "--count", "--left-right"). AddDynamicArguments(baseBranch + "..." + targetBranch).AddArguments("--") - stdout, _, err1 := cmd.RunStdString(ctx, &gitcmd.RunOpts{Dir: repoPath(repo)}) + stdout, err1 := RunCmdString(ctx, repo, cmd) if err1 != nil { return nil, err1 } diff --git a/modules/gitrepo/config.go b/modules/gitrepo/config.go index 5dfdb02b94..bc1746fc3f 100644 --- a/modules/gitrepo/config.go +++ b/modules/gitrepo/config.go @@ -12,7 +12,7 @@ import ( ) func GitConfigGet(ctx context.Context, repo Repository, key string) (string, error) { - result, err := runCmdString(ctx, repo, gitcmd.NewCommand("config", "--get"). + result, err := RunCmdString(ctx, repo, gitcmd.NewCommand("config", "--get"). AddDynamicArguments(key)) if err != nil { return "", err @@ -27,7 +27,7 @@ func getRepoConfigLockKey(repoStoragePath string) string { // GitConfigAdd add a git configuration key to a specific value for the given repository. func GitConfigAdd(ctx context.Context, repo Repository, key, value string) error { return globallock.LockAndDo(ctx, getRepoConfigLockKey(repo.RelativePath()), func(ctx context.Context) error { - _, err := runCmdString(ctx, repo, gitcmd.NewCommand("config", "--add"). + _, err := RunCmdString(ctx, repo, gitcmd.NewCommand("config", "--add"). AddDynamicArguments(key, value)) return err }) @@ -38,7 +38,7 @@ func GitConfigAdd(ctx context.Context, repo Repository, key, value string) error // If the key exists, it will be updated to the new value. func GitConfigSet(ctx context.Context, repo Repository, key, value string) error { return globallock.LockAndDo(ctx, getRepoConfigLockKey(repo.RelativePath()), func(ctx context.Context) error { - _, err := runCmdString(ctx, repo, gitcmd.NewCommand("config"). + _, err := RunCmdString(ctx, repo, gitcmd.NewCommand("config"). AddDynamicArguments(key, value)) return err }) diff --git a/modules/gitrepo/diff.go b/modules/gitrepo/diff.go index 31a7c153b7..c98c3ffcfe 100644 --- a/modules/gitrepo/diff.go +++ b/modules/gitrepo/diff.go @@ -20,7 +20,7 @@ func GetDiffShortStatByCmdArgs(ctx context.Context, repo Repository, trustedArgs // we get: // " 9902 files changed, 2034198 insertions(+), 298800 deletions(-)\n" cmd := gitcmd.NewCommand("diff", "--shortstat").AddArguments(trustedArgs...).AddDynamicArguments(dynamicArgs...) - stdout, err := runCmdString(ctx, repo, cmd) + stdout, err := RunCmdString(ctx, repo, cmd) if err != nil { return 0, 0, 0, err } diff --git a/modules/gitrepo/fsck.go b/modules/gitrepo/fsck.go index ffccff28a9..f74ca3b46a 100644 --- a/modules/gitrepo/fsck.go +++ b/modules/gitrepo/fsck.go @@ -12,5 +12,5 @@ import ( // Fsck verifies the connectivity and validity of the objects in the database func Fsck(ctx context.Context, repo Repository, timeout time.Duration, args gitcmd.TrustedCmdArgs) error { - return gitcmd.NewCommand("fsck").AddArguments(args...).Run(ctx, &gitcmd.RunOpts{Timeout: timeout, Dir: repoPath(repo)}) + return RunCmd(ctx, repo, gitcmd.NewCommand("fsck").AddArguments(args...).WithTimeout(timeout)) } diff --git a/modules/gitrepo/gitrepo.go b/modules/gitrepo/gitrepo.go index 59d2323599..4dd03c18fe 100644 --- a/modules/gitrepo/gitrepo.go +++ b/modules/gitrepo/gitrepo.go @@ -7,9 +7,12 @@ import ( "context" "fmt" "io" + "io/fs" + "os" "path/filepath" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/git/gitcmd" "code.gitea.io/gitea/modules/reqctx" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" @@ -86,3 +89,12 @@ func RenameRepository(ctx context.Context, repo, newRepo Repository) error { func InitRepository(ctx context.Context, repo Repository, objectFormatName string) error { return git.InitRepository(ctx, repoPath(repo), true, objectFormatName) } + +func UpdateServerInfo(ctx context.Context, repo Repository) error { + _, _, err := RunCmdBytes(ctx, repo, gitcmd.NewCommand("update-server-info")) + return err +} + +func GetRepoFS(repo Repository) fs.FS { + return os.DirFS(repoPath(repo)) +} diff --git a/modules/gitrepo/push.go b/modules/gitrepo/push.go new file mode 100644 index 0000000000..18808cac24 --- /dev/null +++ b/modules/gitrepo/push.go @@ -0,0 +1,14 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package gitrepo + +import ( + "context" + + "code.gitea.io/gitea/modules/git" +) + +func Push(ctx context.Context, repo Repository, opts git.PushOptions) error { + return git.Push(ctx, repoPath(repo), opts) +} diff --git a/modules/gitrepo/ref.go b/modules/gitrepo/ref.go index babef8b65f..5212528326 100644 --- a/modules/gitrepo/ref.go +++ b/modules/gitrepo/ref.go @@ -10,12 +10,10 @@ import ( ) func UpdateRef(ctx context.Context, repo Repository, refName, newCommitID string) error { - _, _, err := gitcmd.NewCommand("update-ref").AddDynamicArguments(refName, newCommitID).RunStdString(ctx, &gitcmd.RunOpts{Dir: repoPath(repo)}) - return err + return RunCmd(ctx, repo, gitcmd.NewCommand("update-ref").AddDynamicArguments(refName, newCommitID)) } func RemoveRef(ctx context.Context, repo Repository, refName string) error { - _, _, err := gitcmd.NewCommand("update-ref", "--no-deref", "-d"). - AddDynamicArguments(refName).RunStdString(ctx, &gitcmd.RunOpts{Dir: repoPath(repo)}) - return err + return RunCmd(ctx, repo, gitcmd.NewCommand("update-ref", "--no-deref", "-d"). + AddDynamicArguments(refName)) } diff --git a/modules/gitrepo/remote.go b/modules/gitrepo/remote.go index f56f6d4702..ce43988461 100644 --- a/modules/gitrepo/remote.go +++ b/modules/gitrepo/remote.go @@ -36,7 +36,7 @@ func GitRemoteAdd(ctx context.Context, repo Repository, remoteName, remoteURL st return errors.New("unknown remote option: " + string(options[0])) } } - _, err := runCmdString(ctx, repo, cmd.AddDynamicArguments(remoteName, remoteURL)) + _, err := RunCmdString(ctx, repo, cmd.AddDynamicArguments(remoteName, remoteURL)) return err }) } @@ -44,7 +44,7 @@ func GitRemoteAdd(ctx context.Context, repo Repository, remoteName, remoteURL st func GitRemoteRemove(ctx context.Context, repo Repository, remoteName string) error { return globallock.LockAndDo(ctx, getRepoConfigLockKey(repo.RelativePath()), func(ctx context.Context) error { cmd := gitcmd.NewCommand("remote", "rm").AddDynamicArguments(remoteName) - _, err := runCmdString(ctx, repo, cmd) + _, err := RunCmdString(ctx, repo, cmd) return err }) } @@ -63,22 +63,18 @@ func GitRemoteGetURL(ctx context.Context, repo Repository, remoteName string) (* // GitRemotePrune prunes the remote branches that no longer exist in the remote repository. func GitRemotePrune(ctx context.Context, repo Repository, remoteName string, timeout time.Duration, stdout, stderr io.Writer) error { - return gitcmd.NewCommand("remote", "prune").AddDynamicArguments(remoteName). - Run(ctx, &gitcmd.RunOpts{ - Timeout: timeout, - Dir: repoPath(repo), - Stdout: stdout, - Stderr: stderr, - }) + return RunCmd(ctx, repo, gitcmd.NewCommand("remote", "prune"). + AddDynamicArguments(remoteName). + WithTimeout(timeout). + WithStdout(stdout). + WithStderr(stderr)) } // GitRemoteUpdatePrune updates the remote branches and prunes the ones that no longer exist in the remote repository. func GitRemoteUpdatePrune(ctx context.Context, repo Repository, remoteName string, timeout time.Duration, stdout, stderr io.Writer) error { - return gitcmd.NewCommand("remote", "update", "--prune").AddDynamicArguments(remoteName). - Run(ctx, &gitcmd.RunOpts{ - Timeout: timeout, - Dir: repoPath(repo), - Stdout: stdout, - Stderr: stderr, - }) + return RunCmd(ctx, repo, gitcmd.NewCommand("remote", "update", "--prune"). + AddDynamicArguments(remoteName). + WithTimeout(timeout). + WithStdout(stdout). + WithStderr(stderr)) } diff --git a/modules/gitrepo/signing.go b/modules/gitrepo/signing.go new file mode 100644 index 0000000000..c50978d15a --- /dev/null +++ b/modules/gitrepo/signing.go @@ -0,0 +1,14 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package gitrepo + +import ( + "context" + + "code.gitea.io/gitea/modules/git" +) + +func GetSigningKey(ctx context.Context, repo Repository) (*git.SigningKey, *git.Signature) { + return git.GetSigningKey(ctx, repoPath(repo)) +} diff --git a/modules/gitrepo/size.go b/modules/gitrepo/size.go new file mode 100644 index 0000000000..7524bb2542 --- /dev/null +++ b/modules/gitrepo/size.go @@ -0,0 +1,37 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package gitrepo + +import ( + "os" + "path/filepath" +) + +const notRegularFileMode = os.ModeSymlink | os.ModeNamedPipe | os.ModeSocket | os.ModeDevice | os.ModeCharDevice | os.ModeIrregular + +// CalcRepositorySize returns the disk consumption for a given path +func CalcRepositorySize(repo Repository) (int64, error) { + var size int64 + err := filepath.WalkDir(repoPath(repo), func(_ string, entry os.DirEntry, err error) error { + if os.IsNotExist(err) { // ignore the error because some files (like temp/lock file) may be deleted during traversing. + return nil + } else if err != nil { + return err + } + if entry.IsDir() { + return nil + } + info, err := entry.Info() + if os.IsNotExist(err) { // ignore the error as above + return nil + } else if err != nil { + return err + } + if (info.Mode() & notRegularFileMode) == 0 { + size += info.Size() + } + return nil + }) + return size, err +} diff --git a/modules/graceful/server.go b/modules/graceful/server.go index 2525a83e77..b440f68ab5 100644 --- a/modules/graceful/server.go +++ b/modules/graceful/server.go @@ -11,7 +11,6 @@ import ( "os" "strings" "sync" - "sync/atomic" "syscall" "time" @@ -30,12 +29,15 @@ type ServeFunction = func(net.Listener) error // Server represents our graceful server type Server struct { - network string - address string - listener net.Listener - wg sync.WaitGroup - state state - lock *sync.RWMutex + network string + address string + listener net.Listener + + lock sync.RWMutex + state state + connCounter int64 + connEmptyCond *sync.Cond + BeforeBegin func(network, address string) OnShutdown func() PerWriteTimeout time.Duration @@ -50,14 +52,13 @@ func NewServer(network, address, name string) *Server { log.Info("Starting new %s server: %s:%s on PID: %d", name, network, address, os.Getpid()) } srv := &Server{ - wg: sync.WaitGroup{}, state: stateInit, - lock: &sync.RWMutex{}, network: network, address: address, PerWriteTimeout: setting.PerWriteTimeout, PerWritePerKbTimeout: setting.PerWritePerKbTimeout, } + srv.connEmptyCond = sync.NewCond(&srv.lock) srv.BeforeBegin = func(network, addr string) { log.Debug("Starting server on %s:%s (PID: %d)", network, addr, syscall.Getpid()) @@ -154,7 +155,7 @@ func (srv *Server) Serve(serve ServeFunction) error { GetManager().RegisterServer() err := serve(srv.listener) log.Debug("Waiting for connections to finish... (PID: %d)", syscall.Getpid()) - srv.wg.Wait() + srv.waitForActiveConnections() srv.setState(stateTerminate) GetManager().ServerDone() // use of closed means that the listeners are closed - i.e. we should be shutting down - return nil @@ -178,16 +179,62 @@ func (srv *Server) setState(st state) { srv.state = st } +func (srv *Server) waitForActiveConnections() { + srv.lock.Lock() + for srv.connCounter > 0 { + srv.connEmptyCond.Wait() + } + srv.lock.Unlock() +} + +func (srv *Server) wrapConnection(c net.Conn) (net.Conn, error) { + srv.lock.Lock() + defer srv.lock.Unlock() + + if srv.state != stateRunning { + _ = c.Close() + return nil, syscall.EINVAL // same as AcceptTCP + } + + srv.connCounter++ + return &wrappedConn{Conn: c, server: srv}, nil +} + +func (srv *Server) removeConnection(_ *wrappedConn) { + srv.lock.Lock() + defer srv.lock.Unlock() + + srv.connCounter-- + if srv.connCounter <= 0 { + srv.connEmptyCond.Broadcast() + } +} + +// closeAllConnections forcefully closes all active connections +func (srv *Server) closeAllConnections() { + srv.lock.Lock() + if srv.connCounter > 0 { + log.Warn("After graceful shutdown period, %d connections are still active. Forcefully close.", srv.connCounter) + srv.connCounter = 0 // OS will close all the connections after the process exits, so we just assume there is no active connection now + } + srv.lock.Unlock() + srv.connEmptyCond.Broadcast() +} + type filer interface { File() (*os.File, error) } type wrappedListener struct { net.Listener - stopped bool - server *Server + server *Server } +var ( + _ net.Listener = (*wrappedListener)(nil) + _ filer = (*wrappedListener)(nil) +) + func newWrappedListener(l net.Listener, srv *Server) *wrappedListener { return &wrappedListener{ Listener: l, @@ -195,46 +242,24 @@ func newWrappedListener(l net.Listener, srv *Server) *wrappedListener { } } -func (wl *wrappedListener) Accept() (net.Conn, error) { - var c net.Conn - // Set keepalive on TCPListeners connections. +func (wl *wrappedListener) Accept() (c net.Conn, err error) { if tcl, ok := wl.Listener.(*net.TCPListener); ok { + // Set keepalive on TCPListeners connections if possible, see http.tcpKeepAliveListener tc, err := tcl.AcceptTCP() if err != nil { return nil, err } - _ = tc.SetKeepAlive(true) // see http.tcpKeepAliveListener - _ = tc.SetKeepAlivePeriod(3 * time.Minute) // see http.tcpKeepAliveListener + _ = tc.SetKeepAlive(true) + _ = tc.SetKeepAlivePeriod(3 * time.Minute) c = tc } else { - var err error c, err = wl.Listener.Accept() if err != nil { return nil, err } } - closed := int32(0) - - c = &wrappedConn{ - Conn: c, - server: wl.server, - closed: &closed, - perWriteTimeout: wl.server.PerWriteTimeout, - perWritePerKbTimeout: wl.server.PerWritePerKbTimeout, - } - - wl.server.wg.Add(1) - return c, nil -} - -func (wl *wrappedListener) Close() error { - if wl.stopped { - return syscall.EINVAL - } - - wl.stopped = true - return wl.Listener.Close() + return wl.server.wrapConnection(c) } func (wl *wrappedListener) File() (*os.File, error) { @@ -244,17 +269,14 @@ func (wl *wrappedListener) File() (*os.File, error) { type wrappedConn struct { net.Conn - server *Server - closed *int32 - deadline time.Time - perWriteTimeout time.Duration - perWritePerKbTimeout time.Duration + server *Server + deadline time.Time } func (w *wrappedConn) Write(p []byte) (n int, err error) { - if w.perWriteTimeout > 0 { - minTimeout := time.Duration(len(p)/1024) * w.perWritePerKbTimeout - minDeadline := time.Now().Add(minTimeout).Add(w.perWriteTimeout) + if w.server.PerWriteTimeout > 0 { + minTimeout := time.Duration(len(p)/1024) * w.server.PerWritePerKbTimeout + minDeadline := time.Now().Add(minTimeout).Add(w.server.PerWriteTimeout) w.deadline = w.deadline.Add(minTimeout) if minDeadline.After(w.deadline) { @@ -266,19 +288,6 @@ func (w *wrappedConn) Write(p []byte) (n int, err error) { } func (w *wrappedConn) Close() error { - if atomic.CompareAndSwapInt32(w.closed, 0, 1) { - defer func() { - if err := recover(); err != nil { - select { - case <-GetManager().IsHammer(): - // Likely deadlocked request released at hammertime - log.Warn("Panic during connection close! %v. Likely there has been a deadlocked request which has been released by forced shutdown.", err) - default: - log.Error("Panic during connection close! %v", err) - } - } - }() - w.server.wg.Done() - } + w.server.removeConnection(w) return w.Conn.Close() } diff --git a/modules/graceful/server_hooks.go b/modules/graceful/server_hooks.go index 9b67589571..b800c32503 100644 --- a/modules/graceful/server_hooks.go +++ b/modules/graceful/server_hooks.go @@ -5,7 +5,6 @@ package graceful import ( "os" - "runtime" "code.gitea.io/gitea/modules/log" ) @@ -48,26 +47,8 @@ func (srv *Server) doShutdown() { } func (srv *Server) doHammer() { - defer func() { - // We call srv.wg.Done() until it panics. - // This happens if we call Done() when the WaitGroup counter is already at 0 - // So if it panics -> we're done, Serve() will return and the - // parent will goroutine will exit. - if r := recover(); r != nil { - log.Error("WaitGroup at 0: Error: %v", r) - } - }() if srv.getState() != stateShuttingDown { return } - log.Warn("Forcefully shutting down parent") - for { - if srv.getState() == stateTerminate { - break - } - srv.wg.Done() - - // Give other goroutines a chance to finish before we forcibly stop them. - runtime.Gosched() - } + srv.closeAllConnections() } diff --git a/modules/hcaptcha/hcaptcha_test.go b/modules/hcaptcha/hcaptcha_test.go index 55e01ec535..5906faf17c 100644 --- a/modules/hcaptcha/hcaptcha_test.go +++ b/modules/hcaptcha/hcaptcha_test.go @@ -4,7 +4,10 @@ package hcaptcha import ( + "errors" + "io" "net/http" + "net/url" "os" "strings" "testing" @@ -21,6 +24,33 @@ func TestMain(m *testing.M) { os.Exit(m.Run()) } +type mockTransport struct{} + +func (mockTransport) RoundTrip(req *http.Request) (*http.Response, error) { + if req.URL.String() != verifyURL { + return nil, errors.New("unsupported url") + } + + body, err := io.ReadAll(req.Body) + if err != nil { + return nil, err + } + + bodyValues, err := url.ParseQuery(string(body)) + if err != nil { + return nil, err + } + + var responseText string + if bodyValues.Get("response") == dummyToken { + responseText = `{"success":true,"credit":false,"hostname":"dummy-key-pass","challenge_ts":"2025-10-08T16:02:56.136Z"}` + } else { + responseText = `{"success":false,"error-codes":["invalid-input-response"]}` + } + + return &http.Response{Request: req, Body: io.NopCloser(strings.NewReader(responseText))}, nil +} + func TestCaptcha(t *testing.T) { tt := []struct { Name string @@ -54,7 +84,8 @@ func TestCaptcha(t *testing.T) { for _, tc := range tt { t.Run(tc.Name, func(t *testing.T) { client, err := New(tc.Secret, WithHTTP(&http.Client{ - Timeout: time.Second * 5, + Timeout: time.Second * 5, + Transport: mockTransport{}, })) if err != nil { // The only error that can be returned from creating a client diff --git a/modules/httplib/request.go b/modules/httplib/request.go index 49ea6f4b73..8542a57d36 100644 --- a/modules/httplib/request.go +++ b/modules/httplib/request.go @@ -7,54 +7,53 @@ package httplib import ( "bytes" "context" - "crypto/tls" - "errors" "fmt" "io" "net" "net/http" "net/url" "strings" + "sync" "time" ) -var defaultSetting = Settings{"GiteaServer", 60 * time.Second, 60 * time.Second, nil, nil} - -// newRequest returns *Request with specific method -func newRequest(url, method string) *Request { - var resp http.Response - req := http.Request{ - Method: method, - Header: make(http.Header), - Proto: "HTTP/1.1", - ProtoMajor: 1, - ProtoMinor: 1, +var defaultTransport = sync.OnceValue(func() http.RoundTripper { + return &http.Transport{ + Proxy: http.ProxyFromEnvironment, + DialContext: DialContextWithTimeout(10 * time.Second), // it is good enough in modern days + } +}) + +func DialContextWithTimeout(timeout time.Duration) func(ctx context.Context, network, address string) (net.Conn, error) { + return func(ctx context.Context, network, address string) (net.Conn, error) { + return (&net.Dialer{Timeout: timeout}).DialContext(ctx, network, address) } - return &Request{url, &req, map[string]string{}, defaultSetting, &resp, nil} } -// NewRequest returns *Request with specific method func NewRequest(url, method string) *Request { - return newRequest(url, method) + return &Request{ + url: url, + req: &http.Request{ + Method: method, + Header: make(http.Header), + Proto: "HTTP/1.1", // FIXME: from legacy httplib, it shouldn't be hardcoded + ProtoMajor: 1, + ProtoMinor: 1, + }, + params: map[string]string{}, + + // ATTENTION: from legacy httplib, callers must pay more attention to it, it will cause annoying bugs when the response takes a long time + readWriteTimeout: 60 * time.Second, + } } -// Settings is the default settings for http client -type Settings struct { - UserAgent string - ConnectTimeout time.Duration - ReadWriteTimeout time.Duration - TLSClientConfig *tls.Config - Transport http.RoundTripper -} - -// Request provides more useful methods for requesting one url than http.Request. type Request struct { - url string - req *http.Request - params map[string]string - setting Settings - resp *http.Response - body []byte + url string + req *http.Request + params map[string]string + + readWriteTimeout time.Duration + transport http.RoundTripper } // SetContext sets the request's Context @@ -63,36 +62,24 @@ func (r *Request) SetContext(ctx context.Context) *Request { return r } -// SetTimeout sets connect time out and read-write time out for BeegoRequest. -func (r *Request) SetTimeout(connectTimeout, readWriteTimeout time.Duration) *Request { - r.setting.ConnectTimeout = connectTimeout - r.setting.ReadWriteTimeout = readWriteTimeout +// SetTransport sets the request transport, if not set, will use httplib's default transport with environment proxy support +// ATTENTION: the http.Transport has a connection pool, so it should be reused as much as possible, do not create a lot of transports +func (r *Request) SetTransport(transport http.RoundTripper) *Request { + r.transport = transport return r } func (r *Request) SetReadWriteTimeout(readWriteTimeout time.Duration) *Request { - r.setting.ReadWriteTimeout = readWriteTimeout + r.readWriteTimeout = readWriteTimeout return r } -// SetTLSClientConfig sets tls connection configurations if visiting https url. -func (r *Request) SetTLSClientConfig(config *tls.Config) *Request { - r.setting.TLSClientConfig = config - return r -} - -// Header add header item string in request. +// Header set header item string in request. func (r *Request) Header(key, value string) *Request { r.req.Header.Set(key, value) return r } -// SetTransport sets transport to -func (r *Request) SetTransport(transport http.RoundTripper) *Request { - r.setting.Transport = transport - return r -} - // Param adds query param in to request. // params build query string as ?key1=value1&key2=value2... func (r *Request) Param(key, value string) *Request { @@ -125,11 +112,9 @@ func (r *Request) Body(data any) *Request { return r } -func (r *Request) getResponse() (*http.Response, error) { - if r.resp.StatusCode != 0 { - return r.resp, nil - } - +// Response executes request client and returns the response. +// Caller MUST close the response body if no error occurs. +func (r *Request) Response() (*http.Response, error) { var paramBody string if len(r.params) > 0 { var buf bytes.Buffer @@ -160,59 +145,19 @@ func (r *Request) getResponse() (*http.Response, error) { return nil, err } - trans := r.setting.Transport - if trans == nil { - // create default transport - trans = &http.Transport{ - TLSClientConfig: r.setting.TLSClientConfig, - Proxy: http.ProxyFromEnvironment, - DialContext: TimeoutDialer(r.setting.ConnectTimeout), - } - } else if t, ok := trans.(*http.Transport); ok { - if t.TLSClientConfig == nil { - t.TLSClientConfig = r.setting.TLSClientConfig - } - if t.DialContext == nil { - t.DialContext = TimeoutDialer(r.setting.ConnectTimeout) - } - } - client := &http.Client{ - Transport: trans, - Timeout: r.setting.ReadWriteTimeout, + Transport: r.transport, + Timeout: r.readWriteTimeout, + } + if client.Transport == nil { + client.Transport = defaultTransport() } - if len(r.setting.UserAgent) > 0 && len(r.req.Header.Get("User-Agent")) == 0 { - r.req.Header.Set("User-Agent", r.setting.UserAgent) + if r.req.Header.Get("User-Agent") == "" { + r.req.Header.Set("User-Agent", "GiteaHttpLib") } - resp, err := client.Do(r.req) - if err != nil { - return nil, err - } - r.resp = resp - return resp, nil -} - -// Response executes request client gets response manually. -// Caller MUST close the response body if no error occurs -func (r *Request) Response() (*http.Response, error) { - if r == nil { - return nil, errors.New("invalid request") - } - return r.getResponse() -} - -// TimeoutDialer returns functions of connection dialer with timeout settings for http.Transport Dial field. -func TimeoutDialer(cTimeout time.Duration) func(ctx context.Context, net, addr string) (c net.Conn, err error) { - return func(ctx context.Context, netw, addr string) (net.Conn, error) { - d := net.Dialer{Timeout: cTimeout} - conn, err := d.DialContext(ctx, netw, addr) - if err != nil { - return nil, err - } - return conn, nil - } + return client.Do(r.req) } func (r *Request) GoString() string { diff --git a/modules/httplib/serve.go b/modules/httplib/serve.go index 7c1edf432d..b4c5e7fe1e 100644 --- a/modules/httplib/serve.go +++ b/modules/httplib/serve.go @@ -126,6 +126,7 @@ func setServeHeadersByFile(r *http.Request, w http.ResponseWriter, mineBuf []byt // no sandbox attribute for pdf as it breaks rendering in at least safari. this // should generally be safe as scripts inside PDF can not escape the PDF document // see https://bugs.chromium.org/p/chromium/issues/detail?id=413851 for more discussion + // HINT: PDF-RENDER-SANDBOX: PDF won't render in sandboxed context w.Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'") } diff --git a/modules/indexer/code/bleve/bleve.go b/modules/indexer/code/bleve/bleve.go index c233f491e3..0e2d0f879a 100644 --- a/modules/indexer/code/bleve/bleve.go +++ b/modules/indexer/code/bleve/bleve.go @@ -17,6 +17,7 @@ import ( "code.gitea.io/gitea/modules/charset" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git/gitcmd" + "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/indexer" path_filter "code.gitea.io/gitea/modules/indexer/code/bleve/token/path" "code.gitea.io/gitea/modules/indexer/code/internal" @@ -163,7 +164,7 @@ func (b *Indexer) addUpdate(ctx context.Context, batchWriter git.WriteCloserErro var err error if !update.Sized { var stdout string - stdout, _, err = gitcmd.NewCommand("cat-file", "-s").AddDynamicArguments(update.BlobSha).RunStdString(ctx, &gitcmd.RunOpts{Dir: repo.RepoPath()}) + stdout, err = gitrepo.RunCmdString(ctx, repo, gitcmd.NewCommand("cat-file", "-s").AddDynamicArguments(update.BlobSha)) if err != nil { return err } diff --git a/modules/indexer/code/elasticsearch/elasticsearch.go b/modules/indexer/code/elasticsearch/elasticsearch.go index b08d837a2a..012c57da29 100644 --- a/modules/indexer/code/elasticsearch/elasticsearch.go +++ b/modules/indexer/code/elasticsearch/elasticsearch.go @@ -16,6 +16,7 @@ import ( "code.gitea.io/gitea/modules/charset" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git/gitcmd" + "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/indexer" "code.gitea.io/gitea/modules/indexer/code/internal" indexer_internal "code.gitea.io/gitea/modules/indexer/internal" @@ -148,7 +149,7 @@ func (b *Indexer) addUpdate(ctx context.Context, batchWriter git.WriteCloserErro var err error if !update.Sized { var stdout string - stdout, _, err = gitcmd.NewCommand("cat-file", "-s").AddDynamicArguments(update.BlobSha).RunStdString(ctx, &gitcmd.RunOpts{Dir: repo.RepoPath()}) + stdout, err = gitrepo.RunCmdString(ctx, repo, gitcmd.NewCommand("cat-file", "-s").AddDynamicArguments(update.BlobSha)) if err != nil { return nil, err } diff --git a/modules/indexer/code/git.go b/modules/indexer/code/git.go index f1513d66b0..ca9c6a2974 100644 --- a/modules/indexer/code/git.go +++ b/modules/indexer/code/git.go @@ -11,13 +11,14 @@ import ( repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git/gitcmd" + "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/indexer/code/internal" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" ) func getDefaultBranchSha(ctx context.Context, repo *repo_model.Repository) (string, error) { - stdout, _, err := gitcmd.NewCommand("show-ref", "-s").AddDynamicArguments(git.BranchPrefix+repo.DefaultBranch).RunStdString(ctx, &gitcmd.RunOpts{Dir: repo.RepoPath()}) + stdout, err := gitrepo.RunCmdString(ctx, repo, gitcmd.NewCommand("show-ref", "-s").AddDynamicArguments(git.BranchPrefix+repo.DefaultBranch)) if err != nil { return "", err } @@ -34,7 +35,7 @@ func getRepoChanges(ctx context.Context, repo *repo_model.Repository, revision s needGenesis := len(status.CommitSha) == 0 if !needGenesis { hasAncestorCmd := gitcmd.NewCommand("merge-base").AddDynamicArguments(status.CommitSha, revision) - stdout, _, _ := hasAncestorCmd.RunStdString(ctx, &gitcmd.RunOpts{Dir: repo.RepoPath()}) + stdout, _ := gitrepo.RunCmdString(ctx, repo, hasAncestorCmd) needGenesis = len(stdout) == 0 } @@ -87,7 +88,7 @@ func parseGitLsTreeOutput(stdout []byte) ([]internal.FileUpdate, error) { // genesisChanges get changes to add repo to the indexer for the first time func genesisChanges(ctx context.Context, repo *repo_model.Repository, revision string) (*internal.RepoChanges, error) { var changes internal.RepoChanges - stdout, _, runErr := gitcmd.NewCommand("ls-tree", "--full-tree", "-l", "-r").AddDynamicArguments(revision).RunStdBytes(ctx, &gitcmd.RunOpts{Dir: repo.RepoPath()}) + stdout, _, runErr := gitrepo.RunCmdBytes(ctx, repo, gitcmd.NewCommand("ls-tree", "--full-tree", "-l", "-r").AddDynamicArguments(revision)) if runErr != nil { return nil, runErr } @@ -100,7 +101,7 @@ func genesisChanges(ctx context.Context, repo *repo_model.Repository, revision s // nonGenesisChanges get changes since the previous indexer update func nonGenesisChanges(ctx context.Context, repo *repo_model.Repository, revision string) (*internal.RepoChanges, error) { diffCmd := gitcmd.NewCommand("diff", "--name-status").AddDynamicArguments(repo.CodeIndexerStatus.CommitSha, revision) - stdout, _, runErr := diffCmd.RunStdString(ctx, &gitcmd.RunOpts{Dir: repo.RepoPath()}) + stdout, runErr := gitrepo.RunCmdString(ctx, repo, diffCmd) if runErr != nil { // previous commit sha may have been removed by a force push, so // try rebuilding from scratch @@ -118,7 +119,7 @@ func nonGenesisChanges(ctx context.Context, repo *repo_model.Repository, revisio updateChanges := func() error { cmd := gitcmd.NewCommand("ls-tree", "--full-tree", "-l").AddDynamicArguments(revision). AddDashesAndList(updatedFilenames...) - lsTreeStdout, _, err := cmd.RunStdBytes(ctx, &gitcmd.RunOpts{Dir: repo.RepoPath()}) + lsTreeStdout, _, err := gitrepo.RunCmdBytes(ctx, repo, cmd) if err != nil { return err } diff --git a/modules/indexer/code/indexer.go b/modules/indexer/code/indexer.go index 6035ddfe95..98df6944a6 100644 --- a/modules/indexer/code/indexer.go +++ b/modules/indexer/code/indexer.go @@ -22,6 +22,7 @@ import ( "code.gitea.io/gitea/modules/process" "code.gitea.io/gitea/modules/queue" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" ) var ( @@ -166,12 +167,12 @@ func Init() { log.Fatal("PID: %d Unable to initialize the bleve Repository Indexer at path: %s Error: %v", os.Getpid(), setting.Indexer.RepoPath, err) } case "elasticsearch": - log.Info("PID: %d Initializing Repository Indexer at: %s", os.Getpid(), setting.Indexer.RepoConnStr) + log.Info("PID: %d Initializing Repository Indexer at: %s", os.Getpid(), util.SanitizeCredentialURLs(setting.Indexer.RepoConnStr)) defer func() { if err := recover(); err != nil { log.Error("PANIC whilst initializing repository indexer: %v\nStacktrace: %s", err, log.Stack(2)) log.Error("The indexer files are likely corrupted and may need to be deleted") - log.Error("You can completely remove the \"%s\" index to make Gitea recreate the indexes", setting.Indexer.RepoConnStr) + log.Error("You can completely remove the \"%s\" index to make Gitea recreate the indexes", util.SanitizeCredentialURLs(setting.Indexer.RepoConnStr)) } }() @@ -181,7 +182,7 @@ func Init() { cancel() (*globalIndexer.Load()).Close() close(waitChannel) - log.Fatal("PID: %d Unable to initialize the elasticsearch Repository Indexer connstr: %s Error: %v", os.Getpid(), setting.Indexer.RepoConnStr, err) + log.Fatal("PID: %d Unable to initialize the elasticsearch Repository Indexer connstr: %s Error: %v", os.Getpid(), util.SanitizeCredentialURLs(setting.Indexer.RepoConnStr), err) } default: diff --git a/modules/indexer/issues/indexer.go b/modules/indexer/issues/indexer.go index bbc78aecbe..52b25c1794 100644 --- a/modules/indexer/issues/indexer.go +++ b/modules/indexer/issues/indexer.go @@ -25,6 +25,7 @@ import ( "code.gitea.io/gitea/modules/process" "code.gitea.io/gitea/modules/queue" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" ) // IndexerMetadata is used to send data to the queue, so it contains only the ids. @@ -100,7 +101,7 @@ func InitIssueIndexer(syncReindex bool) { issueIndexer = elasticsearch.NewIndexer(setting.Indexer.IssueConnStr, setting.Indexer.IssueIndexerName) existed, err = issueIndexer.Init(ctx) if err != nil { - log.Fatal("Unable to issueIndexer.Init with connection %s Error: %v", setting.Indexer.IssueConnStr, err) + log.Fatal("Unable to issueIndexer.Init with connection %s Error: %v", util.SanitizeCredentialURLs(setting.Indexer.IssueConnStr), err) } case "db": issueIndexer = db.GetIndexer() @@ -108,7 +109,7 @@ func InitIssueIndexer(syncReindex bool) { issueIndexer = meilisearch.NewIndexer(setting.Indexer.IssueConnStr, setting.Indexer.IssueConnAuth, setting.Indexer.IssueIndexerName) existed, err = issueIndexer.Init(ctx) if err != nil { - log.Fatal("Unable to issueIndexer.Init with connection %s Error: %v", setting.Indexer.IssueConnStr, err) + log.Fatal("Unable to issueIndexer.Init with connection %s Error: %v", util.SanitizeCredentialURLs(setting.Indexer.IssueConnStr), err) } default: log.Fatal("Unknown issue indexer type: %s", setting.Indexer.IssueType) diff --git a/modules/lfstransfer/backend/backend.go b/modules/lfstransfer/backend/backend.go index dd4108ea56..f4e6157091 100644 --- a/modules/lfstransfer/backend/backend.go +++ b/modules/lfstransfer/backend/backend.go @@ -157,7 +157,7 @@ func (g *GiteaBackend) Batch(_ string, pointers []transfer.BatchItem, args trans } // 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) { +func (g *GiteaBackend) Download(oid string, args transfer.Args) (_ io.ReadCloser, _ int64, retErr error) { idMapStr, exists := args[argID] if !exists { return nil, 0, ErrMissingID @@ -188,7 +188,15 @@ func (g *GiteaBackend) Download(oid string, args transfer.Args) (io.ReadCloser, if err != nil { 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 + // We must return the ReaderCloser but not "ReadAll", to avoid OOM. + // "transfer.Backend" will check io.Closer interface and close the Body reader. + // So only close the Body when error occurs + defer func() { + if retErr != nil { + _ = resp.Body.Close() + } + }() + if resp.StatusCode != http.StatusOK { return nil, 0, statusCodeToErr(resp.StatusCode) } @@ -197,7 +205,6 @@ func (g *GiteaBackend) Download(oid string, args transfer.Args) (io.ReadCloser, if err != nil { return nil, 0, fmt.Errorf("failed to parse content length: %w", err) } - // transfer.Backend will check io.Closer interface and close this Body reader return resp.Body, respSize, nil } diff --git a/modules/log/logger_global.go b/modules/log/logger_global.go index 07c25cd62f..2bc8c4f449 100644 --- a/modules/log/logger_global.go +++ b/modules/log/logger_global.go @@ -18,6 +18,7 @@ func GetLevel() Level { } func Log(skip int, level Level, format string, v ...any) { + // codeql[disable-next-line=go/clear-text-logging] GetLogger(DEFAULT).Log(skip+1, &Event{Level: level}, format, v...) } diff --git a/modules/log/misc.go b/modules/log/misc.go index c9d230e4ac..a58b3757da 100644 --- a/modules/log/misc.go +++ b/modules/log/misc.go @@ -20,6 +20,7 @@ func BaseLoggerToGeneralLogger(b BaseLogger) Logger { var _ Logger = (*baseToLogger)(nil) func (s *baseToLogger) Log(skip int, event *Event, format string, v ...any) { + // codeql[disable-next-line=go/clear-text-logging] s.base.Log(skip+1, event, format, v...) } diff --git a/modules/markup/external/external.go b/modules/markup/external/external.go index 39861ade12..3cbe14b86a 100644 --- a/modules/markup/external/external.go +++ b/modules/markup/external/external.go @@ -15,6 +15,8 @@ import ( "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/process" "code.gitea.io/gitea/modules/setting" + + "github.com/kballard/go-shellquote" ) // RegisterRenderers registers all supported third part renderers according settings @@ -56,14 +58,11 @@ func (p *Renderer) SanitizerRules() []setting.MarkupSanitizerRule { return p.MarkupSanitizerRules } -// SanitizerDisabled disabled sanitize if return true -func (p *Renderer) SanitizerDisabled() bool { - return p.RenderContentMode == setting.RenderContentModeNoSanitizer || p.RenderContentMode == setting.RenderContentModeIframe -} - -// DisplayInIFrame represents whether render the content with an iframe -func (p *Renderer) DisplayInIFrame() bool { - return p.RenderContentMode == setting.RenderContentModeIframe +func (p *Renderer) GetExternalRendererOptions() (ret markup.ExternalRendererOptions) { + ret.SanitizerDisabled = p.RenderContentMode == setting.RenderContentModeNoSanitizer || p.RenderContentMode == setting.RenderContentModeIframe + ret.DisplayInIframe = p.RenderContentMode == setting.RenderContentModeIframe + ret.ContentSandbox = p.RenderContentSandbox + return ret } func envMark(envName string) string { @@ -81,7 +80,10 @@ func (p *Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io. envMark("GITEA_PREFIX_SRC"), baseLinkSrc, envMark("GITEA_PREFIX_RAW"), baseLinkRaw, ).Replace(p.Command) - commands := strings.Fields(command) + commands, err := shellquote.Split(command) + if err != nil || len(commands) == 0 { + return fmt.Errorf("%s invalid command %q: %w", p.Name(), p.Command, err) + } args := commands[1:] if p.IsInputFile { diff --git a/modules/markup/html_emoji.go b/modules/markup/html_emoji.go index c638065425..91ba26c676 100644 --- a/modules/markup/html_emoji.go +++ b/modules/markup/html_emoji.go @@ -5,6 +5,7 @@ package markup import ( "strings" + "unicode" "code.gitea.io/gitea/modules/emoji" "code.gitea.io/gitea/modules/setting" @@ -66,26 +67,31 @@ func emojiShortCodeProcessor(ctx *RenderContext, node *html.Node) { } m[0] += start m[1] += start - start = m[1] alias := node.Data[m[0]:m[1]] - alias = strings.ReplaceAll(alias, ":", "") - converted := emoji.FromAlias(alias) - if converted == nil { - // check if this is a custom reaction - if _, exist := setting.UI.CustomEmojisMap[alias]; exist { - replaceContent(node, m[0], m[1], createCustomEmoji(ctx, alias)) - node = node.NextSibling.NextSibling - start = 0 - continue - } + + var nextChar byte + if m[1] < len(node.Data) { + nextChar = node.Data[m[1]] + } + if nextChar == ':' || unicode.IsLetter(rune(nextChar)) || unicode.IsDigit(rune(nextChar)) { continue } - replaceContent(node, m[0], m[1], createEmoji(ctx, converted.Emoji, converted.Description)) - node = node.NextSibling.NextSibling - start = 0 + alias = strings.Trim(alias, ":") + converted := emoji.FromAlias(alias) + if converted != nil { + // standard emoji + replaceContent(node, m[0], m[1], createEmoji(ctx, converted.Emoji, converted.Description)) + node = node.NextSibling.NextSibling + start = 0 // restart searching start since node has changed + } else if _, exist := setting.UI.CustomEmojisMap[alias]; exist { + // custom reaction + replaceContent(node, m[0], m[1], createCustomEmoji(ctx, alias)) + node = node.NextSibling.NextSibling + start = 0 // restart searching start since node has changed + } } } diff --git a/modules/markup/html_test.go b/modules/markup/html_test.go index 5fdbf43f7c..08b050baae 100644 --- a/modules/markup/html_test.go +++ b/modules/markup/html_test.go @@ -357,12 +357,9 @@ func TestRender_emoji(t *testing.T) { `

😎🤪🔐🤑

`) // should match nothing - test( - "2001:0db8:85a3:0000:0000:8a2e:0370:7334", - `

2001:0db8:85a3:0000:0000:8a2e:0370:7334

`) - test( - ":not exist:", - `

:not exist:

`) + test(":100:200", `

:100:200

`) + test("std::thread::something", `

std::thread::something

`) + test(":not exist:", `

:not exist:

`) } func TestRender_ShortLinks(t *testing.T) { diff --git a/modules/markup/internal/finalprocessor.go b/modules/markup/internal/finalprocessor.go index 14d46a161f..4442afa0c9 100644 --- a/modules/markup/internal/finalprocessor.go +++ b/modules/markup/internal/finalprocessor.go @@ -5,11 +5,13 @@ package internal import ( "bytes" + "html/template" "io" ) type finalProcessor struct { renderInternal *RenderInternal + extraHeadHTML template.HTML output io.Writer buf bytes.Buffer @@ -25,6 +27,32 @@ func (p *finalProcessor) Close() error { // because "postProcess" already does so. In the future we could optimize the code to process data on the fly. buf := p.buf.Bytes() buf = bytes.ReplaceAll(buf, []byte(` data-attr-class="`+p.renderInternal.secureIDPrefix), []byte(` class="`)) - _, err := p.output.Write(buf) + + tmp := bytes.TrimSpace(buf) + isLikelyHTML := len(tmp) != 0 && tmp[0] == '<' && tmp[len(tmp)-1] == '>' && bytes.Index(tmp, []byte(` 0 + if !isLikelyHTML { + // not HTML, write back directly + _, err := p.output.Write(buf) + return err + } + + // add our extra head HTML into output + headBytes := []byte("") + posHead := bytes.Index(buf, headBytes) + var part1, part2 []byte + if posHead >= 0 { + part1, part2 = buf[:posHead+len(headBytes)], buf[posHead+len(headBytes):] + } else { + part1, part2 = nil, buf + } + if len(part1) > 0 { + if _, err := p.output.Write(part1); err != nil { + return err + } + } + if _, err := io.WriteString(p.output, string(p.extraHeadHTML)); err != nil { + return err + } + _, err := p.output.Write(part2) return err } diff --git a/modules/markup/internal/internal_test.go b/modules/markup/internal/internal_test.go index 590bcbb67f..a216d75203 100644 --- a/modules/markup/internal/internal_test.go +++ b/modules/markup/internal/internal_test.go @@ -12,7 +12,7 @@ import ( "github.com/stretchr/testify/assert" ) -func TestRenderInternal(t *testing.T) { +func TestRenderInternalAttrs(t *testing.T) { cases := []struct { input, protected, recovered string }{ @@ -30,7 +30,7 @@ func TestRenderInternal(t *testing.T) { for _, c := range cases { var r RenderInternal out := &bytes.Buffer{} - in := r.init("sec", out) + in := r.init("sec", out, "") protected := r.ProtectSafeAttrs(template.HTML(c.input)) assert.EqualValues(t, c.protected, protected) _, _ = io.WriteString(in, string(protected)) @@ -41,7 +41,7 @@ func TestRenderInternal(t *testing.T) { var r1, r2 RenderInternal protected := r1.ProtectSafeAttrs(`
`) assert.EqualValues(t, `
`, protected, "non-initialized RenderInternal should not protect any attributes") - _ = r1.init("sec", nil) + _ = r1.init("sec", nil, "") protected = r1.ProtectSafeAttrs(`
`) assert.EqualValues(t, `
`, protected) assert.Equal(t, "data-attr-class", r1.SafeAttr("class")) @@ -54,8 +54,37 @@ func TestRenderInternal(t *testing.T) { assert.Empty(t, recovered) out2 := &bytes.Buffer{} - in2 := r2.init("sec-other", out2) + in2 := r2.init("sec-other", out2, "") _, _ = io.WriteString(in2, string(protected)) _ = in2.Close() assert.Equal(t, `
`, out2.String(), "different secureID should not recover the value") } + +func TestRenderInternalExtraHead(t *testing.T) { + t.Run("HeadExists", func(t *testing.T) { + out := &bytes.Buffer{} + var r RenderInternal + in := r.init("sec", out, ``) + _, _ = io.WriteString(in, `any`) + _ = in.Close() + assert.Equal(t, `any`, out.String()) + }) + + t.Run("HeadNotExists", func(t *testing.T) { + out := &bytes.Buffer{} + var r RenderInternal + in := r.init("sec", out, ``) + _, _ = io.WriteString(in, `
`) + _ = in.Close() + assert.Equal(t, `
`, out.String()) + }) + + t.Run("NotHTML", func(t *testing.T) { + out := &bytes.Buffer{} + var r RenderInternal + in := r.init("sec", out, ``) + _, _ = io.WriteString(in, ``) + _ = in.Close() + assert.Equal(t, ``, out.String()) + }) +} diff --git a/modules/markup/internal/renderinternal.go b/modules/markup/internal/renderinternal.go index 7a3e37b120..9fd9a1c0e8 100644 --- a/modules/markup/internal/renderinternal.go +++ b/modules/markup/internal/renderinternal.go @@ -29,19 +29,19 @@ type RenderInternal struct { secureIDPrefix string } -func (r *RenderInternal) Init(output io.Writer) io.WriteCloser { +func (r *RenderInternal) Init(output io.Writer, extraHeadHTML template.HTML) io.WriteCloser { buf := make([]byte, 12) _, err := rand.Read(buf) if err != nil { panic("unable to generate secure id") } - return r.init(base64.URLEncoding.EncodeToString(buf), output) + return r.init(base64.URLEncoding.EncodeToString(buf), output, extraHeadHTML) } -func (r *RenderInternal) init(secID string, output io.Writer) io.WriteCloser { +func (r *RenderInternal) init(secID string, output io.Writer, extraHeadHTML template.HTML) io.WriteCloser { r.secureID = secID r.secureIDPrefix = r.secureID + ":" - return &finalProcessor{renderInternal: r, output: output} + return &finalProcessor{renderInternal: r, output: output, extraHeadHTML: extraHeadHTML} } func (r *RenderInternal) RecoverProtectedValue(v string) (string, bool) { diff --git a/modules/markup/render.go b/modules/markup/render.go index 79f1f473c2..c645749065 100644 --- a/modules/markup/render.go +++ b/modules/markup/render.go @@ -6,12 +6,14 @@ package markup import ( "context" "fmt" + "html/template" "io" "net/url" "strconv" "strings" "time" + "code.gitea.io/gitea/modules/htmlutil" "code.gitea.io/gitea/modules/markup/internal" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" @@ -120,31 +122,38 @@ func (ctx *RenderContext) WithHelper(helper RenderHelper) *RenderContext { return ctx } -// Render renders markup file to HTML with all specific handling stuff. -func Render(ctx *RenderContext, input io.Reader, output io.Writer) error { +// FindRendererByContext finds renderer by RenderContext +// TODO: it should be merged with other similar functions like GetRendererByFileName, DetectMarkupTypeByFileName, etc +func FindRendererByContext(ctx *RenderContext) (Renderer, error) { if ctx.RenderOptions.MarkupType == "" && ctx.RenderOptions.RelativePath != "" { ctx.RenderOptions.MarkupType = DetectMarkupTypeByFileName(ctx.RenderOptions.RelativePath) if ctx.RenderOptions.MarkupType == "" { - return util.NewInvalidArgumentErrorf("unsupported file to render: %q", ctx.RenderOptions.RelativePath) + return nil, util.NewInvalidArgumentErrorf("unsupported file to render: %q", ctx.RenderOptions.RelativePath) } } renderer := renderers[ctx.RenderOptions.MarkupType] if renderer == nil { - return util.NewInvalidArgumentErrorf("unsupported markup type: %q", ctx.RenderOptions.MarkupType) + return nil, util.NewNotExistErrorf("unsupported markup type: %q", ctx.RenderOptions.MarkupType) } - if ctx.RenderOptions.RelativePath != "" { - if externalRender, ok := renderer.(ExternalRenderer); ok && externalRender.DisplayInIFrame() { - if !ctx.RenderOptions.InStandalonePage { - // for an external "DisplayInIFrame" render, it could only output its content in a standalone page - // otherwise, a `, - setting.AppSubURL, +func renderIFrame(ctx *RenderContext, sandbox string, output io.Writer) error { + src := fmt.Sprintf("%s/%s/%s/render/%s/%s", setting.AppSubURL, url.PathEscape(ctx.RenderOptions.Metas["user"]), url.PathEscape(ctx.RenderOptions.Metas["repo"]), - ctx.RenderOptions.Metas["RefTypeNameSubURL"], - url.PathEscape(ctx.RenderOptions.RelativePath), - )) + util.PathEscapeSegments(ctx.RenderOptions.Metas["RefTypeNameSubURL"]), + util.PathEscapeSegments(ctx.RenderOptions.RelativePath), + ) + + var sandboxAttrValue template.HTML + if sandbox != "" { + sandboxAttrValue = htmlutil.HTMLFormat(`sandbox="%s"`, sandbox) + } + iframe := htmlutil.HTMLFormat(``, src, sandboxAttrValue) + _, err := io.WriteString(output, string(iframe)) return err } @@ -185,13 +190,34 @@ func pipes() (io.ReadCloser, io.WriteCloser, func()) { } } -func render(ctx *RenderContext, renderer Renderer, input io.Reader, output io.Writer) error { +func getExternalRendererOptions(renderer Renderer) (ret ExternalRendererOptions, _ bool) { + if externalRender, ok := renderer.(ExternalRenderer); ok { + return externalRender.GetExternalRendererOptions(), true + } + return ret, false +} + +func RenderWithRenderer(ctx *RenderContext, renderer Renderer, input io.Reader, output io.Writer) error { + var extraHeadHTML template.HTML + if extOpts, ok := getExternalRendererOptions(renderer); ok && extOpts.DisplayInIframe { + if !ctx.RenderOptions.InStandalonePage { + // for an external "DisplayInIFrame" render, it could only output its content in a standalone page + // otherwise, a