diff options
259 files changed, 4212 insertions, 2536 deletions
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 9f9f6f27d1..ab30e1789d 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,6 +1,6 @@ { "name": "Gitea DevContainer", - "image": "mcr.microsoft.com/devcontainers/go:1.23-bookworm", + "image": "mcr.microsoft.com/devcontainers/go:1.24-bookworm", "features": { // installs nodejs into container "ghcr.io/devcontainers/features/node:1": { diff --git a/.editorconfig b/.editorconfig index e23e4cd649..13aa8d50f0 100644 --- a/.editorconfig +++ b/.editorconfig @@ -12,6 +12,9 @@ insert_final_newline = true [*.{go,tmpl,html}] indent_style = tab +[go.*] +indent_style = tab + [templates/custom/*.tmpl] insert_final_newline = false diff --git a/.github/workflows/pull-db-tests.yml b/.github/workflows/pull-db-tests.yml index 6e879053d3..a3fd8ca621 100644 --- a/.github/workflows/pull-db-tests.yml +++ b/.github/workflows/pull-db-tests.yml @@ -98,7 +98,7 @@ jobs: ports: - "9200:9200" meilisearch: - image: getmeili/meilisearch:v1.2.0 + image: getmeili/meilisearch:v1 env: MEILI_ENV: development # disable auth ports: diff --git a/.golangci.yml b/.golangci.yml index cf7a6f1a1f..0f194097ed 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,7 +1,9 @@ +version: "2" +output: + sort-order: + - file linters: - enable-all: false - disable-all: true - fast: false + default: none enable: - bidichk - depguard @@ -9,141 +11,163 @@ linters: - errcheck - forbidigo - gocritic - - gofmt - - gofumpt - - gosimple - govet - ineffassign - nakedret - nolintlint - revive - staticcheck - - stylecheck - testifylint - - typecheck - unconvert - - unused - unparam + - unused - usetesting - wastedassign - -run: - timeout: 10m - -output: - sort-results: true - sort-order: [file] - show-stats: true - -linters-settings: - testifylint: - disable: - - go-require - - require-error - stylecheck: - checks: ["all", "-ST1005", "-ST1003"] - nakedret: - max-func-lines: 0 - gocritic: - disabled-checks: - - ifElseChain - - singleCaseSwitch # Every time this occurred in the code, there was no other way. - revive: - severity: error - rules: - - name: atomic - - name: bare-return - - name: blank-imports - - name: constant-logical-expr - - name: context-as-argument - - name: context-keys-type - - name: dot-imports - - name: duplicated-imports - - name: empty-lines - - name: error-naming - - name: error-return - - name: error-strings - - name: errorf - - name: exported - - name: identical-branches - - name: if-return - - name: increment-decrement - - name: indent-error-flow - - name: modifies-value-receiver - - name: package-comments - - name: range - - name: receiver-naming - - name: redefines-builtin-id - - name: string-of-int - - name: superfluous-else - - name: time-naming - - name: unconditional-recursion - - name: unexported-return - - name: unreachable-code - - name: var-declaration - - name: var-naming - gofumpt: - extra-rules: true - depguard: + settings: + depguard: + rules: + main: + deny: + - pkg: encoding/json + desc: use gitea's modules/json instead of encoding/json + - pkg: github.com/unknwon/com + desc: use gitea's util and replacements + - pkg: io/ioutil + desc: use os or io instead + - pkg: golang.org/x/exp + desc: it's experimental and unreliable + - pkg: code.gitea.io/gitea/modules/git/internal + desc: do not use the internal package, use AddXxx function instead + - pkg: gopkg.in/ini.v1 + desc: do not use the ini package, use gitea's config system instead + - pkg: gitea.com/go-chi/cache + desc: do not use the go-chi cache package, use gitea's cache system + gocritic: + disabled-checks: + - ifElseChain + - singleCaseSwitch # Every time this occurred in the code, there was no other way. + revive: + severity: error + rules: + - name: atomic + - name: bare-return + - name: blank-imports + - name: constant-logical-expr + - name: context-as-argument + - name: context-keys-type + - name: dot-imports + - name: duplicated-imports + - name: empty-lines + - name: error-naming + - name: error-return + - name: error-strings + - name: errorf + - name: exported + - name: identical-branches + - name: if-return + - name: increment-decrement + - name: indent-error-flow + - name: modifies-value-receiver + - name: package-comments + - name: range + - name: receiver-naming + - name: redefines-builtin-id + - name: string-of-int + - name: superfluous-else + - name: time-naming + - name: unconditional-recursion + - name: unexported-return + - name: unreachable-code + - name: var-declaration + - name: var-naming + staticcheck: + checks: + - all + - -ST1003 + - -ST1005 + - -QF1001 + - -QF1006 + - -QF1008 + testifylint: + disable: + - go-require + - require-error + - equal-values + - empty + - formatter + - len + usetesting: + os-temp-dir: true + exclusions: + generated: lax + presets: + - comments + - common-false-positives + - legacy + - std-error-handling rules: - main: - deny: - - pkg: encoding/json - desc: use gitea's modules/json instead of encoding/json - - pkg: github.com/unknwon/com - desc: use gitea's util and replacements - - pkg: io/ioutil - desc: use os or io instead - - pkg: golang.org/x/exp - desc: it's experimental and unreliable - - pkg: code.gitea.io/gitea/modules/git/internal - desc: do not use the internal package, use AddXxx function instead - - pkg: gopkg.in/ini.v1 - desc: do not use the ini package, use gitea's config system instead - - pkg: gitea.com/go-chi/cache - desc: do not use the go-chi cache package, use gitea's cache system - usetesting: - os-temp-dir: true - + - linters: + - dupl + - errcheck + - gocyclo + - gosec + - staticcheck + - unparam + path: _test\.go + - linters: + - dupl + - errcheck + - gocyclo + - gosec + path: models/migrations/v + - linters: + - forbidigo + path: cmd + - linters: + - dupl + text: (?i)webhook + - linters: + - gocritic + text: (?i)`ID' should not be capitalized + - linters: + - deadcode + - unused + text: (?i)swagger + - linters: + - staticcheck + text: (?i)argument x is overwritten before first use + - linters: + - gocritic + text: '(?i)commentFormatting: put a space between `//` and comment text' + - linters: + - gocritic + text: '(?i)exitAfterDefer:' + paths: + - node_modules + - public + - web_src + - third_party$ + - builtin$ + - examples$ issues: max-issues-per-linter: 0 max-same-issues: 0 - exclude-dirs: [node_modules, public, web_src] - exclude-case-sensitive: true - exclude-rules: - - path: _test\.go - linters: - - gocyclo - - errcheck - - dupl - - gosec - - unparam - - staticcheck - - path: models/migrations/v - linters: - - gocyclo - - errcheck - - dupl - - gosec - - path: cmd - linters: - - forbidigo - - text: "webhook" - linters: - - dupl - - text: "`ID' should not be capitalized" - linters: - - gocritic - - text: "swagger" - linters: - - unused - - deadcode - - text: "argument x is overwritten before first use" - linters: - - staticcheck - - text: "commentFormatting: put a space between `//` and comment text" - linters: - - gocritic - - text: "exitAfterDefer:" - linters: - - gocritic +formatters: + enable: + - gofmt + - gofumpt + settings: + gofumpt: + extra-rules: true + exclusions: + generated: lax + paths: + - node_modules + - public + - web_src + - third_party$ + - builtin$ + - examples$ + +run: + timeout: 10m diff --git a/CHANGELOG.md b/CHANGELOG.md index 7541bccb2a..ca2e67929c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,28 @@ This changelog goes through the changes that have been made in each release without substantial changes to our git log; to see the highlights of what has been added to each release, please refer to the [blog](https://blog.gitea.com). +## [1.23.6](https://github.com/go-gitea/gitea/releases/tag/v1.23.6) - 2025-03-24 + +* SECURITY + * Fix LFS URL (#33840) (#33843) + * Update jwt and redis packages (#33984) (#33987) + * Update golang crypto and net (#33989) +* BUGFIXES + * Drop timeout for requests made to the internal hook api (#33947) (#33970) + * Fix maven panic when no package exists (#33888) (#33889) + * Fix markdown render (#33870) (#33875) + * Fix auto concurrency cancellation skips commit status updates (#33764) (#33849) + * Fix oauth2 auth (#33961) (#33962) + * Fix incorrect 1.23 translations (#33932) + * Try to figure out attribute checker problem (#33901) (#33902) + * Ignore trivial errors when updating push data (#33864) (#33887) + * Fix some UI problems for 1.23 (#33856) + * Removing unwanted ui container (#33833) (#33835) + * Support disable passkey auth (#33348) (#33819) + * Do not call "git diff" when listing PRs (#33817) + * Try to fix ACME (3rd) (#33807) (#33808) + * Fix incorrect code search indexer options (#33992) #33999 + ## [1.23.5](https://github.com/go-gitea/gitea/releases/tag/v1.23.5) - 2025-03-04 * SECURITY @@ -28,7 +28,7 @@ XGO_VERSION := go-1.24.x AIR_PACKAGE ?= github.com/air-verse/air@v1 EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/v3/cmd/editorconfig-checker@v3.2.1 GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.7.0 -GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/cmd/golangci-lint@v1.64.7 +GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.0.2 GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.12 MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.6.0 SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.31.0 @@ -410,12 +410,12 @@ watch-backend: go-check ## watch backend files and continuously rebuild test: test-frontend test-backend ## test everything .PHONY: test-backend -test-backend: ## test frontend files +test-backend: ## test backend files @echo "Running go test with $(GOTESTFLAGS) -tags '$(TEST_TAGS)'..." @$(GO) test $(GOTESTFLAGS) -tags='$(TEST_TAGS)' $(GO_TEST_PACKAGES) .PHONY: test-frontend -test-frontend: node_modules ## test backend files +test-frontend: node_modules ## test frontend files npx vitest .PHONY: test-check @@ -737,7 +737,7 @@ generate-go: $(TAGS_PREREQ) .PHONY: security-check security-check: - go run $(GOVULNCHECK_PACKAGE) ./... + go run $(GOVULNCHECK_PACKAGE) -show color ./... $(EXECUTABLE): $(GO_SOURCES) $(TAGS_PREREQ) CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) build $(GOFLAGS) $(EXTRA_GOFLAGS) -tags '$(TAGS)' -ldflags '-s -w $(LDFLAGS)' -o $@ diff --git a/assets/go-licenses.json b/assets/go-licenses.json index b98b5d6471..0b6a7fd99a 100644 --- a/assets/go-licenses.json +++ b/assets/go-licenses.json @@ -620,6 +620,11 @@ "licenseText": "\n 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/google/flatbuffers/go", + "path": "github.com/google/flatbuffers/go/LICENSE", + "licenseText": "\n 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/google/go-github/v61/github", "path": "github.com/google/go-github/v61/github/LICENSE", "licenseText": "Copyright (c) 2013 The go-github AUTHORS. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" @@ -832,7 +837,7 @@ { "name": "github.com/meilisearch/meilisearch-go", "path": "github.com/meilisearch/meilisearch-go/LICENSE", - "licenseText": "MIT License\n\nCopyright (c) 2020-2024 Meili SAS\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" + "licenseText": "MIT License\n\nCopyright (c) 2020-2025 Meili SAS\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n" }, { "name": "github.com/mholt/acmez/v3", diff --git a/cmd/admin_auth_ldap.go b/cmd/admin_auth_ldap.go index aff2a12855..274ec181d1 100644 --- a/cmd/admin_auth_ldap.go +++ b/cmd/admin_auth_ldap.go @@ -127,6 +127,34 @@ var ( &cli.UintFlag{ Name: "page-size", Usage: "Search page size.", + }, + &cli.BoolFlag{ + Name: "enable-groups", + Usage: "Enable LDAP groups", + }, + &cli.StringFlag{ + Name: "group-search-base-dn", + Usage: "The LDAP base DN at which group accounts will be searched for", + }, + &cli.StringFlag{ + Name: "group-member-attribute", + Usage: "Group attribute containing list of users", + }, + &cli.StringFlag{ + Name: "group-user-attribute", + Usage: "User attribute listed in group", + }, + &cli.StringFlag{ + Name: "group-filter", + Usage: "Verify group membership in LDAP", + }, + &cli.StringFlag{ + Name: "group-team-map", + Usage: "Map LDAP groups to Organization teams", + }, + &cli.BoolFlag{ + Name: "group-team-map-removal", + Usage: "Remove users from synchronized teams if user does not belong to corresponding LDAP group", }) ldapSimpleAuthCLIFlags = append(commonLdapCLIFlags, @@ -273,6 +301,27 @@ func parseLdapConfig(c *cli.Context, config *ldap.Source) error { if c.IsSet("skip-local-2fa") { config.SkipLocalTwoFA = c.Bool("skip-local-2fa") } + if c.IsSet("enable-groups") { + config.GroupsEnabled = c.Bool("enable-groups") + } + if c.IsSet("group-search-base-dn") { + config.GroupDN = c.String("group-search-base-dn") + } + if c.IsSet("group-member-attribute") { + config.GroupMemberUID = c.String("group-member-attribute") + } + if c.IsSet("group-user-attribute") { + config.UserUID = c.String("group-user-attribute") + } + if c.IsSet("group-filter") { + config.GroupFilter = c.String("group-filter") + } + if c.IsSet("group-team-map") { + config.GroupTeamMap = c.String("group-team-map") + } + if c.IsSet("group-team-map-removal") { + config.GroupTeamMapRemoval = c.Bool("group-team-map-removal") + } return nil } diff --git a/cmd/admin_auth_ldap_test.go b/cmd/admin_auth_ldap_test.go index 7791f3a9cc..bab42226ae 100644 --- a/cmd/admin_auth_ldap_test.go +++ b/cmd/admin_auth_ldap_test.go @@ -51,6 +51,13 @@ func TestAddLdapBindDn(t *testing.T) { "--attributes-in-bind", "--synchronize-users", "--page-size", "99", + "--enable-groups", + "--group-search-base-dn", "ou=group,dc=full-domain-bind,dc=org", + "--group-member-attribute", "memberUid", + "--group-user-attribute", "uid", + "--group-filter", "(|(cn=gitea_users)(cn=admins))", + "--group-team-map", `{"cn=my-group,cn=groups,dc=example,dc=org": {"MyGiteaOrganization": ["MyGiteaTeam1", "MyGiteaTeam2"]}}`, + "--group-team-map-removal", }, source: &auth.Source{ Type: auth.LDAP, @@ -78,6 +85,13 @@ func TestAddLdapBindDn(t *testing.T) { AdminFilter: "(memberOf=cn=admin-group,ou=example,dc=full-domain-bind,dc=org)", RestrictedFilter: "(memberOf=cn=restricted-group,ou=example,dc=full-domain-bind,dc=org)", Enabled: true, + GroupsEnabled: true, + GroupDN: "ou=group,dc=full-domain-bind,dc=org", + GroupMemberUID: "memberUid", + UserUID: "uid", + GroupFilter: "(|(cn=gitea_users)(cn=admins))", + GroupTeamMap: `{"cn=my-group,cn=groups,dc=example,dc=org": {"MyGiteaOrganization": ["MyGiteaTeam1", "MyGiteaTeam2"]}}`, + GroupTeamMapRemoval: true, }, }, }, @@ -510,6 +524,13 @@ func TestUpdateLdapBindDn(t *testing.T) { "--bind-password", "secret-bind-full", "--synchronize-users", "--page-size", "99", + "--enable-groups", + "--group-search-base-dn", "ou=group,dc=full-domain-bind,dc=org", + "--group-member-attribute", "memberUid", + "--group-user-attribute", "uid", + "--group-filter", "(|(cn=gitea_users)(cn=admins))", + "--group-team-map", `{"cn=my-group,cn=groups,dc=example,dc=org": {"MyGiteaOrganization": ["MyGiteaTeam1", "MyGiteaTeam2"]}}`, + "--group-team-map-removal", }, id: 23, existingAuthSource: &auth.Source{ @@ -545,6 +566,13 @@ func TestUpdateLdapBindDn(t *testing.T) { AdminFilter: "(memberOf=cn=admin-group,ou=example,dc=full-domain-bind,dc=org)", RestrictedFilter: "(memberOf=cn=restricted-group,ou=example,dc=full-domain-bind,dc=org)", Enabled: true, + GroupsEnabled: true, + GroupDN: "ou=group,dc=full-domain-bind,dc=org", + GroupMemberUID: "memberUid", + UserUID: "uid", + GroupFilter: "(|(cn=gitea_users)(cn=admins))", + GroupTeamMap: `{"cn=my-group,cn=groups,dc=example,dc=org": {"MyGiteaOrganization": ["MyGiteaTeam1", "MyGiteaTeam2"]}}`, + GroupTeamMapRemoval: true, }, }, }, diff --git a/cmd/doctor.go b/cmd/doctor.go index 52699cc4dd..4a12b957f5 100644 --- a/cmd/doctor.go +++ b/cmd/doctor.go @@ -144,11 +144,12 @@ func setupDoctorDefaultLogger(ctx *cli.Context, colorize bool) { setupConsoleLogger(log.FATAL, log.CanColorStderr, os.Stderr) logFile := ctx.String("log-file") - if logFile == "" { + switch logFile { + case "": return // if no doctor log-file is set, do not show any log from default logger - } else if logFile == "-" { + case "-": setupConsoleLogger(log.TRACE, colorize, os.Stdout) - } else { + default: logFile, _ = filepath.Abs(logFile) writeMode := log.WriterMode{Level: log.TRACE, WriterOption: log.WriterFileOption{FileName: logFile}} writer, err := log.NewEventWriter("console-to-file", "file", writeMode) diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 0fc49accef..05b7494f96 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -780,6 +780,9 @@ LEVEL = Info ;ALLOW_ONLY_EXTERNAL_REGISTRATION = false ;; ;; User must sign in to view anything. +;; It could be set to "expensive" to block anonymous users accessing some pages which consume a lot of resources, +;; for example: block anonymous AI crawlers from accessing repo code pages. +;; The "expensive" mode is experimental and subject to change. ;REQUIRE_SIGNIN_VIEW = false ;; ;; Mail notification @@ -21,19 +21,19 @@ require ( gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4 github.com/42wim/httpsig v1.2.2 github.com/42wim/sshsig v0.0.0-20240818000253-e3a6333df815 - github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.1 github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.0 github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 github.com/ProtonMail/go-crypto v1.1.6 github.com/PuerkitoBio/goquery v1.10.2 github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.7.3 github.com/alecthomas/chroma/v2 v2.15.0 - github.com/aws/aws-sdk-go-v2/credentials v1.17.60 - github.com/aws/aws-sdk-go-v2/service/codecommit v1.27.16 + github.com/aws/aws-sdk-go-v2/credentials v1.17.62 + github.com/aws/aws-sdk-go-v2/service/codecommit v1.28.1 github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb github.com/blevesearch/bleve/v2 v2.4.2 - github.com/buildkite/terminal-to-html/v3 v3.16.6 - github.com/caddyserver/certmagic v0.21.7 + github.com/buildkite/terminal-to-html/v3 v3.16.8 + github.com/caddyserver/certmagic v0.22.0 github.com/charmbracelet/git-lfs-transfer v0.2.0 github.com/chi-middleware/proxy v1.1.1 github.com/dimiro1/reply v0.0.0-20200315094148-d0136a4c9e21 @@ -41,7 +41,7 @@ require ( github.com/djherbis/nio/v3 v3.0.1 github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 github.com/dustin/go-humanize v1.0.1 - github.com/editorconfig/editorconfig-core-go/v2 v2.6.2 + github.com/editorconfig/editorconfig-core-go/v2 v2.6.3 github.com/emersion/go-imap v1.2.1 github.com/emirpasic/gods v1.18.1 github.com/ethantkoenig/rupture v1.0.1 @@ -55,19 +55,19 @@ 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.13.2 + github.com/go-git/go-git/v5 v5.14.0 github.com/go-ldap/ldap/v3 v3.4.10 github.com/go-redsync/redsync/v4 v4.13.0 - github.com/go-sql-driver/mysql v1.9.0 + github.com/go-sql-driver/mysql v1.9.1 github.com/go-swagger/go-swagger v0.31.0 - github.com/go-webauthn/webauthn v0.11.2 + github.com/go-webauthn/webauthn v0.12.2 github.com/gobwas/glob v0.2.3 github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f github.com/gogs/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85 - github.com/golang-jwt/jwt/v5 v5.2.1 + github.com/golang-jwt/jwt/v5 v5.2.2 github.com/google/go-github/v61 v61.0.0 github.com/google/licenseclassifier/v2 v2.0.0 - github.com/google/pprof v0.0.0-20250208200701-d0013a598941 + github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e github.com/google/uuid v1.6.0 github.com/gorilla/feeds v1.2.0 github.com/gorilla/sessions v1.4.0 @@ -79,27 +79,27 @@ require ( github.com/json-iterator/go v1.1.12 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 github.com/klauspost/compress v1.18.0 - github.com/klauspost/cpuid/v2 v2.2.9 + github.com/klauspost/cpuid/v2 v2.2.10 github.com/lib/pq v1.10.9 github.com/markbates/goth v1.80.0 github.com/mattn/go-isatty v0.0.20 github.com/mattn/go-sqlite3 v1.14.24 - github.com/meilisearch/meilisearch-go v0.29.1-0.20241106140435-0bf60fad690a + github.com/meilisearch/meilisearch-go v0.31.0 github.com/mholt/archiver/v3 v3.5.1 github.com/microcosm-cc/bluemonday v1.0.27 github.com/microsoft/go-mssqldb v1.8.0 - github.com/minio/minio-go/v7 v7.0.87 + github.com/minio/minio-go/v7 v7.0.88 github.com/msteinert/pam v1.2.0 github.com/nektos/act v0.2.63 github.com/niklasfasching/go-org v1.7.0 github.com/olivere/elastic/v7 v7.0.32 github.com/opencontainers/go-digest v1.0.0 - github.com/opencontainers/image-spec v1.1.0 + github.com/opencontainers/image-spec v1.1.1 github.com/pkg/errors v0.9.1 github.com/pquerna/otp v1.4.0 - github.com/prometheus/client_golang v1.21.0 + github.com/prometheus/client_golang v1.21.1 github.com/quasoft/websspi v1.1.2 - github.com/redis/go-redis/v9 v9.7.0 + github.com/redis/go-redis/v9 v9.7.3 github.com/robfig/cron/v3 v3.0.1 github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 github.com/sassoftware/go-rpmutils v0.4.0 @@ -109,23 +109,23 @@ require ( github.com/syndtr/goleveldb v1.0.0 github.com/tstranex/u2f v1.0.0 github.com/ulikunitz/xz v0.5.12 - github.com/urfave/cli/v2 v2.27.5 + github.com/urfave/cli/v2 v2.27.6 github.com/wneessen/go-mail v0.6.2 github.com/xeipuuv/gojsonschema v1.2.0 github.com/yohcop/openid-go v1.0.1 github.com/yuin/goldmark v1.7.8 github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc github.com/yuin/goldmark-meta v1.1.0 - gitlab.com/gitlab-org/api/client-go v0.123.0 - golang.org/x/crypto v0.35.0 - golang.org/x/image v0.24.0 - golang.org/x/net v0.36.0 - golang.org/x/oauth2 v0.27.0 - golang.org/x/sync v0.11.0 - golang.org/x/sys v0.30.0 - golang.org/x/text v0.22.0 - golang.org/x/tools v0.30.0 - google.golang.org/grpc v1.70.0 + gitlab.com/gitlab-org/api/client-go v0.126.0 + golang.org/x/crypto v0.36.0 + golang.org/x/image v0.25.0 + golang.org/x/net v0.37.0 + golang.org/x/oauth2 v0.28.0 + golang.org/x/sync v0.12.0 + golang.org/x/sys v0.31.0 + golang.org/x/text v0.23.0 + golang.org/x/tools v0.31.0 + google.golang.org/grpc v1.71.0 google.golang.org/protobuf v1.36.5 gopkg.in/ini.v1 v1.67.0 gopkg.in/yaml.v3 v3.0.1 @@ -151,13 +151,13 @@ require ( github.com/andybalholm/cascadia v1.3.3 // indirect github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect - github.com/aws/aws-sdk-go-v2 v1.36.2 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.33 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.33 // indirect + github.com/aws/aws-sdk-go-v2 v1.36.3 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 // indirect github.com/aws/smithy-go v1.22.3 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/beorn7/perks v1.0.1 // indirect - github.com/bits-and-blooms/bitset v1.20.0 // indirect + github.com/bits-and-blooms/bitset v1.22.0 // indirect github.com/blevesearch/bleve_index_api v1.1.12 // indirect github.com/blevesearch/geo v0.1.20 // indirect github.com/blevesearch/go-faiss v1.0.20 // indirect @@ -183,7 +183,7 @@ require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cloudflare/circl v1.6.0 // indirect github.com/couchbase/go-couchbase v0.1.1 // indirect - github.com/couchbase/gomemcached v0.3.2 // indirect + github.com/couchbase/gomemcached v0.3.3 // indirect github.com/couchbase/goutils v0.1.2 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect github.com/cyphar/filepath-securejoin v0.4.1 // indirect @@ -203,26 +203,28 @@ require ( github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-ini/ini v1.67.0 // indirect github.com/go-openapi/analysis v0.23.0 // indirect - github.com/go-openapi/errors v0.22.0 // indirect - github.com/go-openapi/inflect v0.21.0 // indirect - github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/errors v0.22.1 // indirect + github.com/go-openapi/inflect v0.21.2 // indirect + github.com/go-openapi/jsonpointer v0.21.1 // indirect github.com/go-openapi/jsonreference v0.21.0 // indirect github.com/go-openapi/loads v0.22.0 // indirect github.com/go-openapi/runtime v0.28.0 // indirect github.com/go-openapi/spec v0.21.0 // indirect github.com/go-openapi/strfmt v0.23.0 // indirect - github.com/go-openapi/swag v0.23.0 // indirect + github.com/go-openapi/swag v0.23.1 // indirect github.com/go-openapi/validate v0.24.0 // indirect - github.com/go-webauthn/x v0.1.16 // indirect + github.com/go-viper/mapstructure/v2 v2.2.1 // indirect + github.com/go-webauthn/x v0.1.19 // indirect github.com/goccy/go-json v0.10.5 // indirect - github.com/golang-jwt/jwt/v4 v4.5.1 // indirect + github.com/golang-jwt/jwt/v4 v4.5.2 // indirect github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect github.com/golang-sql/sqlexp v0.1.0 // indirect - github.com/golang/geo v0.0.0-20230421003525-6adc56603217 // indirect + github.com/golang/geo v0.0.0-20250321002858-2bb09a976f49 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/golang/protobuf v1.5.4 // indirect - github.com/golang/snappy v0.0.4 // indirect + github.com/golang/snappy v1.0.0 // indirect github.com/google/btree v1.1.3 // indirect + github.com/google/flatbuffers v25.2.10+incompatible // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/go-tpm v0.9.3 // indirect github.com/gorilla/css v1.0.1 // indirect @@ -233,7 +235,6 @@ require ( github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-retryablehttp v0.7.7 // indirect - github.com/hashicorp/hcl v1.0.0 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/jessevdk/go-flags v1.6.1 // indirect github.com/josharian/intern v1.0.0 // indirect @@ -242,14 +243,13 @@ require ( github.com/kr/pretty v0.3.1 // indirect github.com/kr/text v0.2.0 // indirect github.com/libdns/libdns v0.2.3 // indirect - github.com/magiconair/properties v1.8.9 // indirect github.com/mailru/easyjson v0.9.0 // indirect github.com/markbates/going v1.0.3 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mattn/go-shellwords v1.0.12 // indirect - github.com/mholt/acmez/v3 v3.0.1 // indirect - github.com/miekg/dns v1.1.63 // indirect + github.com/mholt/acmez/v3 v3.1.0 // indirect + github.com/miekg/dns v1.1.64 // indirect github.com/minio/crc64nvme v1.0.1 // indirect github.com/minio/md5-simd v1.1.2 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect @@ -269,24 +269,23 @@ require ( github.com/pjbgf/sha1cd v0.3.2 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.62.0 // indirect - github.com/prometheus/procfs v0.15.1 // indirect + github.com/prometheus/common v0.63.0 // indirect + github.com/prometheus/procfs v0.16.0 // indirect github.com/rhysd/actionlint v1.7.7 // indirect github.com/rivo/uniseg v0.4.7 // indirect - github.com/rogpeppe/go-internal v1.13.2-0.20241226121412-a5dc8ff20d0a // indirect + github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/rs/xid v1.6.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect - github.com/sagikazarmark/locafero v0.7.0 // indirect - github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/sagikazarmark/locafero v0.8.0 // indirect github.com/shopspring/decimal v1.4.0 // indirect github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/skeema/knownhosts v1.3.1 // indirect github.com/sourcegraph/conc v0.3.0 // indirect - github.com/spf13/afero v1.12.0 // indirect + github.com/spf13/afero v1.14.0 // indirect github.com/spf13/cast v1.7.1 // indirect github.com/spf13/pflag v1.0.6 // indirect - github.com/spf13/viper v1.19.0 // indirect + github.com/spf13/viper v1.20.0 // indirect github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/toqueteos/webbrowser v1.2.0 // indirect @@ -301,15 +300,15 @@ require ( github.com/zeebo/assert v1.3.0 // indirect github.com/zeebo/blake3 v0.2.4 // indirect go.etcd.io/bbolt v1.4.0 // indirect - go.mongodb.org/mongo-driver v1.17.2 // indirect + go.mongodb.org/mongo-driver v1.17.3 // indirect go.uber.org/atomic v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect go.uber.org/zap/exp v0.3.0 // indirect - golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa // indirect - golang.org/x/mod v0.23.0 // indirect - golang.org/x/time v0.10.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250219182151-9fdb1cabc7b2 // indirect + golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect + golang.org/x/mod v0.24.0 // indirect + golang.org/x/time v0.11.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) @@ -40,8 +40,8 @@ github.com/42wim/sshsig v0.0.0-20240818000253-e3a6333df815 h1:5EoemV++kUK2Sw98yW github.com/42wim/sshsig v0.0.0-20240818000253-e3a6333df815/go.mod h1:zjsWZdDLrcDojDIfpQg7A6J4YZLT0cbwuAD26AppDBo= github.com/6543/go-version v1.3.1 h1:HvOp+Telns7HWJ2Xo/05YXQSB2bE0WmVgbHqwMPZT4U= github.com/6543/go-version v1.3.1/go.mod h1:oqFAHCwtLVUTLdhQmVZWYvaHXTdsbB4SY85at64SQEo= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 h1:g0EZJwz7xkXQiZAI5xi9f3WWFYBlX1CPTrR+NDToRkQ= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0/go.mod h1:XCW7KnZet0Opnr7HccfUw1PLc4CjHqpcaxW8DHklNkQ= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.1 h1:DSDNVxqkoXJiko6x8a90zidoYqnYYa6c1MTzDKzKkTo= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.1/go.mod h1:zGqV2R4Cr/k8Uye5w+dgQ06WJtEcbQG/8J7BB6hnCr4= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0 h1:B/dfvscEQtew9dVuoxqxrUKKv8Ih2f55PydknDamU+g= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0/go.mod h1:fiPSssYvltE08HJchL04dOy+RD4hgrjph0cwGGMntdI= github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY= @@ -105,16 +105,16 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPd github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= -github.com/aws/aws-sdk-go-v2 v1.36.2 h1:Ub6I4lq/71+tPb/atswvToaLGVMxKZvjYDVOWEExOcU= -github.com/aws/aws-sdk-go-v2 v1.36.2/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg= -github.com/aws/aws-sdk-go-v2/credentials v1.17.60 h1:1dq+ELaT5ogfmqtV1eocq8SpOK1NRsuUfmhQtD/XAh4= -github.com/aws/aws-sdk-go-v2/credentials v1.17.60/go.mod h1:HDes+fn/xo9VeszXqjBVkxOo/aUy8Mc6QqKvZk32GlE= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.33 h1:knLyPMw3r3JsU8MFHWctE4/e2qWbPaxDYLlohPvnY8c= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.33/go.mod h1:EBp2HQ3f+XCB+5J+IoEbGhoV7CpJbnrsd4asNXmTL0A= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.33 h1:K0+Ne08zqti8J9jwENxZ5NoUyBnaFDTu3apwQJWrwwA= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.33/go.mod h1:K97stwwzaWzmqxO8yLGHhClbVW1tC6VT1pDLk1pGrq4= -github.com/aws/aws-sdk-go-v2/service/codecommit v1.27.16 h1:DodiGgET+sAVT1Wsx9lHDpEPxzoY7Y6wCpYh6LsGTWQ= -github.com/aws/aws-sdk-go-v2/service/codecommit v1.27.16/go.mod h1:FyuFDtt95CXzQCClFbkb9rqKDX8+IRvSzmjFSkQOX4g= +github.com/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38yqWM= +github.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg= +github.com/aws/aws-sdk-go-v2/credentials v1.17.62 h1:fvtQY3zFzYJ9CfixuAQ96IxDrBajbBWGqjNTCa79ocU= +github.com/aws/aws-sdk-go-v2/credentials v1.17.62/go.mod h1:ElETBxIQqcxej++Cs8GyPBbgMys5DgQPTwo7cUPDKt8= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 h1:ZK5jHhnrioRkUNOc+hOgQKlUL5JeC3S6JgLxtQ+Rm0Q= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34/go.mod h1:p4VfIceZokChbA9FzMbRGz5OV+lekcVtHlPKEO0gSZY= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 h1:SZwFm17ZUNNg5Np0ioo/gq8Mn6u9w19Mri8DnJ15Jf0= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34/go.mod h1:dFZsC0BLo346mvKQLWmoJxT+Sjp+qcVR1tRVHQGOH9Q= +github.com/aws/aws-sdk-go-v2/service/codecommit v1.28.1 h1:NNOxK3fdcLeE+meE7Ry4TwEzBL2Yh4HVnC63Nlj/NYg= +github.com/aws/aws-sdk-go-v2/service/codecommit v1.28.1/go.mod h1:JsdLne5QNlqJdCQFm2DbHLNmNfEWSU7HnTuvi8SIl+E= github.com/aws/smithy-go v1.22.3 h1:Z//5NuZCSW6R4PhQ93hShNbyBbn8BWCmCVCt+Q8Io5k= github.com/aws/smithy-go v1.22.3/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= @@ -124,8 +124,8 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r github.com/bits-and-blooms/bitset v1.1.10/go.mod h1:w0XsmFg8qg6cmpTtJ0z3pKgjTDBMMnI/+I2syrE6XBE= github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA= github.com/bits-and-blooms/bitset v1.12.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= -github.com/bits-and-blooms/bitset v1.20.0 h1:2F+rfL86jE2d/bmw7OhqUg2Sj/1rURkBn3MdfoPyRVU= -github.com/bits-and-blooms/bitset v1.20.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/bits-and-blooms/bitset v1.22.0 h1:Tquv9S8+SGaS3EhyA+up3FXzmkhxPGjQQCkcs2uw7w4= +github.com/bits-and-blooms/bitset v1.22.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb h1:m935MPodAbYS46DG4pJSv7WO+VECIWUQ7OJYSoTrMh4= github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb/go.mod h1:PkYb9DJNAwrSvRx5DYA+gUcOIgTGVMNkfSCbZM8cWpI= github.com/blevesearch/bleve/v2 v2.0.5/go.mod h1:ZjWibgnbRX33c+vBRgla9QhPb4QOjD6fdVJ+R1Bk8LM= @@ -188,10 +188,10 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= -github.com/buildkite/terminal-to-html/v3 v3.16.6 h1:QKHWPjAnKQnV1hVG/Nb2TwYDr4pliSbvCQDENk8EaJo= -github.com/buildkite/terminal-to-html/v3 v3.16.6/go.mod h1:PgzeBymbRFC8I2m46Sci3S18AbwonEgpaz3TGhD7EPs= -github.com/caddyserver/certmagic v0.21.7 h1:66KJioPFJwttL43KYSWk7ErSmE6LfaJgCQuhm8Sg6fg= -github.com/caddyserver/certmagic v0.21.7/go.mod h1:LCPG3WLxcnjVKl/xpjzM0gqh0knrKKKiO5WVttX2eEI= +github.com/buildkite/terminal-to-html/v3 v3.16.8 h1:QN/daUob6cmK8GcdKnwn9+YTlPr1vNj+oeAIiJK6fPc= +github.com/buildkite/terminal-to-html/v3 v3.16.8/go.mod h1:+k1KVKROZocrTLsEQ9PEf9A+8+X8uaVV5iO1ZIOwKYM= +github.com/caddyserver/certmagic v0.22.0 h1:hi2skv2jouUw9uQUEyYSTTmqPZPHgf61dOANSIVCLOw= +github.com/caddyserver/certmagic v0.22.0/go.mod h1:Vc0msarAPhOagbDc/SU6M2zbzdwVuZ0lkTh2EqtH4vs= github.com/caddyserver/zerossl v0.1.3 h1:onS+pxp3M8HnHpN5MMbOMyNjmTheJyWRaZYwn+YTAyA= github.com/caddyserver/zerossl v0.1.3/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4= github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a h1:MISbI8sU/PSK/ztvmWKFcI7UGb5/HQT7B+i3a2myKgI= @@ -214,8 +214,8 @@ github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3Ee github.com/couchbase/ghistogram v0.1.0/go.mod h1:s1Jhy76zqfEecpNWJfWUiKZookAFaiGOEoyzgHt9i7k= github.com/couchbase/go-couchbase v0.1.1 h1:ClFXELcKj/ojyoTYbsY34QUrrYCBi/1G749sXSCkdhk= github.com/couchbase/go-couchbase v0.1.1/go.mod h1:+/bddYDxXsf9qt0xpDUtRR47A2GjaXmGGAqQ/k3GJ8A= -github.com/couchbase/gomemcached v0.3.2 h1:08rxiOoNcv0x5LTxgcYhnx1aPvV7iEtfeyUgqsJyPk0= -github.com/couchbase/gomemcached v0.3.2/go.mod h1:mxliKQxOv84gQ0bJWbI+w9Wxdpt9HjDvgW9MjCym5Vo= +github.com/couchbase/gomemcached v0.3.3 h1:D7qqXLO8wNa4pn5oE65lT3pA3IeStn4joT7/JgGXzKc= +github.com/couchbase/gomemcached v0.3.3/go.mod h1:pISAjweI42vljCumsJIo7CVhqIMIIP9g3Wfhl1JJw68= github.com/couchbase/goutils v0.1.2 h1:gWr8B6XNWPIhfalHNog3qQKfGiYyh4K4VhO3P2o9BCs= github.com/couchbase/goutils v0.1.2/go.mod h1:h89Ek/tiOxxqjz30nPPlwZdQbdB8BwgnuBxeoUe/ViE= github.com/couchbase/moss v0.1.0/go.mod h1:9MaHIaRuy9pvLPUJxB8sh8OrLfyDczECVL37grCIubs= @@ -251,11 +251,11 @@ github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdf github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/dvyukov/go-fuzz v0.0.0-20210429054444-fca39067bc72/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw= -github.com/editorconfig/editorconfig-core-go/v2 v2.6.2 h1:dKG8sc7n321deIVRcQtwlMNoBEra7j0qQ8RwxO8RN0w= -github.com/editorconfig/editorconfig-core-go/v2 v2.6.2/go.mod h1:7dvD3GCm7eBw53xZ/lsiq72LqobdMg3ITbMBxnmJmqY= +github.com/editorconfig/editorconfig-core-go/v2 v2.6.3 h1:XVUp6qW3BIkmM3/1EkrHpa6bL56APOynfXcZEmIgOhs= +github.com/editorconfig/editorconfig-core-go/v2 v2.6.3/go.mod h1:ThHVc+hqbUsmE1wmK/MASpQEhCleWu1JDJDNhUOMy0c= github.com/elazarl/go-bindata-assetfs v1.0.1/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4= -github.com/elazarl/goproxy v1.4.0 h1:4GyuSbFa+s26+3rmYNSuUVsx+HgPrV1bk1jXI0l9wjM= -github.com/elazarl/goproxy v1.4.0/go.mod h1:X/5W/t+gzDyLfHW4DrMdpjqYjpXsURlBt9lpBDxZZZQ= +github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= +github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= github.com/emersion/go-imap v1.2.1 h1:+s9ZjMEjOB8NzZMVTM3cCenz2JrQIGGo5j1df19WjTA= github.com/emersion/go-imap v1.2.1/go.mod h1:Qlx1FSx2FTxjnjWpIlVNEuX+ylerZQNFE5NsmKFSejY= github.com/emersion/go-message v0.15.0/go.mod h1:wQUEfE+38+7EW8p8aZ96ptg6bAb1iwdgej19uXASlE4= @@ -316,20 +316,20 @@ 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.13.2 h1:7O7xvsK7K+rZPKW6AQR1YyNhfywkv7B8/FsP3ki6Zv0= -github.com/go-git/go-git/v5 v5.13.2/go.mod h1:hWdW5P4YZRjmpGHwRH2v3zkWcNl6HeXaXQEMGb3NJ9A= +github.com/go-git/go-git/v5 v5.14.0 h1:/MD3lCrGjCen5WfEAzKg00MJJffKhC8gzS80ycmCi60= +github.com/go-git/go-git/v5 v5.14.0/go.mod h1:Z5Xhoia5PcWA3NF8vRLURn9E5FRhSl7dGj9ItW3Wk5k= github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/go-ldap/ldap/v3 v3.4.10 h1:ot/iwPOhfpNVgB1o+AVXljizWZ9JTp7YF5oeyONmcJU= github.com/go-ldap/ldap/v3 v3.4.10/go.mod h1:JXh4Uxgi40P6E9rdsYqpUtbW46D9UTjJ9QSwGRznplY= github.com/go-openapi/analysis v0.23.0 h1:aGday7OWupfMs+LbmLZG4k0MYXIANxcuBTYUC03zFCU= github.com/go-openapi/analysis v0.23.0/go.mod h1:9mz9ZWaSlV8TvjQHLl2mUW2PbZtemkE8yA5v22ohupo= -github.com/go-openapi/errors v0.22.0 h1:c4xY/OLxUBSTiepAg3j/MHuAv5mJhnf53LLMWFB+u/w= -github.com/go-openapi/errors v0.22.0/go.mod h1:J3DmZScxCDufmIMsdOuDHxJbdOGC0xtUynjIx092vXE= -github.com/go-openapi/inflect v0.21.0 h1:FoBjBTQEcbg2cJUWX6uwL9OyIW8eqc9k4KhN4lfbeYk= -github.com/go-openapi/inflect v0.21.0/go.mod h1:INezMuUu7SJQc2AyR3WO0DqqYUJSj8Kb4hBd7WtjlAw= -github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= -github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/errors v0.22.1 h1:kslMRRnK7NCb/CvR1q1VWuEQCEIsBGn5GgKD9e+HYhU= +github.com/go-openapi/errors v0.22.1/go.mod h1:+n/5UdIqdVnLIJ6Q9Se8HNGUXYaY6CN8ImWzfi/Gzp0= +github.com/go-openapi/inflect v0.21.2 h1:0gClGlGcxifcJR56zwvhaOulnNgnhc4qTAkob5ObnSM= +github.com/go-openapi/inflect v0.21.2/go.mod h1:INezMuUu7SJQc2AyR3WO0DqqYUJSj8Kb4hBd7WtjlAw= +github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic= +github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk= github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= github.com/go-openapi/loads v0.22.0 h1:ECPGd4jX1U6NApCGG1We+uEozOAvXvJSF4nnwHZ8Aco= @@ -340,8 +340,8 @@ github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9Z github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk= github.com/go-openapi/strfmt v0.23.0 h1:nlUS6BCqcnAk0pyhi9Y+kdDVZdZMHfEKQiS4HaMgO/c= github.com/go-openapi/strfmt v0.23.0/go.mod h1:NrtIpfKtWIygRkKVsxh7XQMDQW5HKQl6S5ik2elW+K4= -github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= -github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= +github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= github.com/go-openapi/validate v0.24.0 h1:LdfDKwNbpB6Vn40xhTdNZAnfLECL81w+VX3BumrGD58= github.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ= github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg= @@ -352,17 +352,19 @@ github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= github.com/go-redsync/redsync/v4 v4.13.0 h1:49X6GJfnbLGaIpBBREM/zA4uIMDXKAh1NDkvQ1EkZKA= github.com/go-redsync/redsync/v4 v4.13.0/go.mod h1:HMW4Q224GZQz6x1Xc7040Yfgacukdzu7ifTDAKiyErQ= -github.com/go-sql-driver/mysql v1.9.0 h1:Y0zIbQXhQKmQgTp44Y1dp3wTXcn804QoTptLZT1vtvo= -github.com/go-sql-driver/mysql v1.9.0/go.mod h1:pDetrLJeA3oMujJuvXc8RJoasr589B6A9fwzD3QMrqw= +github.com/go-sql-driver/mysql v1.9.1 h1:FrjNGn/BsJQjVRuSa8CBrM5BWA9BWoXXat3KrtSb/iI= +github.com/go-sql-driver/mysql v1.9.1/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= github.com/go-swagger/go-swagger v0.31.0 h1:H8eOYQnY2u7vNKWDNykv2xJP3pBhRG/R+SOCAmKrLlc= github.com/go-swagger/go-swagger v0.31.0/go.mod h1:WSigRRWEig8zV6t6Sm8Y+EmUjlzA/HoaZJ5edupq7po= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg= github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= -github.com/go-webauthn/webauthn v0.11.2 h1:Fgx0/wlmkClTKlnOsdOQ+K5HcHDsDcYIvtYmfhEOSUc= -github.com/go-webauthn/webauthn v0.11.2/go.mod h1:aOtudaF94pM71g3jRwTYYwQTG1KyTILTcZqN1srkmD0= -github.com/go-webauthn/x v0.1.16 h1:EaVXZntpyHviN9ykjdRBQIw9B0Ed3LO5FW7mDiMQEa8= -github.com/go-webauthn/x v0.1.16/go.mod h1:jhYjfwe/AVYaUs2mUXArj7vvZj+SpooQPyyQGNab+Us= +github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= +github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/go-webauthn/webauthn v0.12.2 h1:yLaNPgBUEXDQtWnOjhsGhMMCEWbXwjg/aNkC8riJQI8= +github.com/go-webauthn/webauthn v0.12.2/go.mod h1:Q8SZPPj4sZ469fNTcQXxRpzJOdb30jQrn/36FX8jilA= +github.com/go-webauthn/x v0.1.19 h1:IUfdHiBRoTdujpBA/14qbrMXQ3LGzYe/PRGWdZcmudg= +github.com/go-webauthn/x v0.1.19/go.mod h1:C5arLuTQ3pVHKPw89v7CDGnqAZSZJj+4Jnr40dsn7tk= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= @@ -374,16 +376,17 @@ github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f h1:3BSP1Tbs2djlpprl7w github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14= github.com/gogs/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85 h1:UjoPNDAQ5JPCjlxoJd6K8ALZqSDDhk2ymieAZOVaDg0= github.com/gogs/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85/go.mod h1:fR6z1Ie6rtF7kl/vBYMfgD5/G5B1blui7z426/sj2DU= -github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo= github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= -github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= -github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= +github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= +github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= -github.com/golang/geo v0.0.0-20230421003525-6adc56603217 h1:HKlyj6in2JV6wVkmQ4XmG/EIm+SCYlPZ+V4GWit7Z+I= -github.com/golang/geo v0.0.0-20230421003525-6adc56603217/go.mod h1:8wI0hitZ3a1IxZfeH3/5I97CI8i5cLGsYe7xNhQGs9U= +github.com/golang/geo v0.0.0-20250321002858-2bb09a976f49 h1:JJ32MDIC/hIQ/Fq8Ej9Hgh1s3D82FYNMt54WYViOI7Y= +github.com/golang/geo v0.0.0-20250321002858-2bb09a976f49/go.mod h1:J+F9/3Ofc8ysEOY2/cNjxTMl2eB1gvPIywEHUplPgDA= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -399,19 +402,23 @@ github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6 github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= -github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= +github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/gomodule/redigo v1.8.9 h1:Sl3u+2BI/kk+VEatbj0scLdrFhjPmbxOc1myhDP41ws= github.com/gomodule/redigo v1.8.9/go.mod h1:7ArFNvsTjH8GMMzB4uy1snslv2BwmginuMs06a1uzZE= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/flatbuffers v24.3.25+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q= +github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-github/v61 v61.0.0 h1:VwQCBwhyE9JclCI+22/7mLB1PuU9eowCXKY5pNlu1go= github.com/google/go-github/v61 v61.0.0/go.mod h1:0WR+KmsWX75G2EbpyGsGmradjo3IiciuI4BmdVCobQY= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= @@ -424,8 +431,8 @@ github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/ github.com/google/licenseclassifier/v2 v2.0.0 h1:1Y57HHILNf4m0ABuMVb6xk4vAJYEUO0gDxNpog0pyeA= github.com/google/licenseclassifier/v2 v2.0.0/go.mod h1:cOjbdH0kyC9R22sdQbYsFkto4NGCAc+ZSwbeThazEtM= github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= -github.com/google/pprof v0.0.0-20250208200701-d0013a598941 h1:43XjGa6toxLpeksjcxs1jIoIyr+vUfOqY2c6HB4bpoc= -github.com/google/pprof v0.0.0-20250208200701-d0013a598941/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -467,7 +474,6 @@ github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/C github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= -github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= @@ -513,8 +519,8 @@ github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zt github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= -github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= +github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= +github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= @@ -538,8 +544,6 @@ github.com/libdns/libdns v0.2.3/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfs github.com/lunny/vfsgen v0.0.0-20220105142115-2c99e1ffdfa0 h1:F/3FfGmKdiKFa8kL3YrpZ7pe9H4l4AzA1pbaOUnRvPI= github.com/lunny/vfsgen v0.0.0-20220105142115-2c99e1ffdfa0/go.mod h1:JEfTc3+2DF9Z4PXhLLvXL42zexJyh8rIq3OzUj/0rAk= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a8RkpQM= -github.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= @@ -558,22 +562,22 @@ github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebG github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= -github.com/meilisearch/meilisearch-go v0.29.1-0.20241106140435-0bf60fad690a h1:F0y+3QtCG00mr4KueQWuHv1tlIQeNXhH+XAKYLhb3X4= -github.com/meilisearch/meilisearch-go v0.29.1-0.20241106140435-0bf60fad690a/go.mod h1:NYOgjEGt/+oExD+NixreBMqxtIB0kCndXOOgpGhoqEs= -github.com/mholt/acmez/v3 v3.0.1 h1:4PcjKjaySlgXK857aTfDuRbmnM5gb3Ruz3tvoSJAUp8= -github.com/mholt/acmez/v3 v3.0.1/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ= +github.com/meilisearch/meilisearch-go v0.31.0 h1:yZRhY1qJqdH8h6GFZALGtkDLyj8f9v5aJpsNMyrUmnY= +github.com/meilisearch/meilisearch-go v0.31.0/go.mod h1:aNtyuwurDg/ggxQIcKqWH6G9g2ptc8GyY7PLY4zMn/g= +github.com/mholt/acmez/v3 v3.1.0 h1:RlOx2SSZ8dIAM5GfkMe8TdaxjjkiHTGorlMUt8GeMzg= +github.com/mholt/acmez/v3 v3.1.0/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ= 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.8.0 h1:7cyZ/AT7ycDsEoWPIXibd+aVKFtteUNhDGf3aobP+tw= github.com/microsoft/go-mssqldb v1.8.0/go.mod h1:6znkekS3T2vp0waiMhen4GPU1BiAsrP+iXHcE7a7rFo= -github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY= -github.com/miekg/dns v1.1.63/go.mod h1:6NGHfjhpmr5lt3XPLuyfDJi5AXbNIPM9PY6H6sF1Nfs= +github.com/miekg/dns v1.1.64 h1:wuZgD9wwCE6XMT05UU/mlSko71eRSXEAm2EbjQXLKnQ= +github.com/miekg/dns v1.1.64/go.mod h1:Dzw9769uoKVaLuODMDZz9M6ynFU6Em65csPuoi8G0ck= github.com/minio/crc64nvme v1.0.1 h1:DHQPrYPdqK7jQG/Ls5CTBZWeex/2FMS3G5XGkycuFrY= github.com/minio/crc64nvme v1.0.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg= 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.87 h1:nkr9x0u53PespfxfUqxP3UYWiE2a41gaofgNnC4Y8WQ= -github.com/minio/minio-go/v7 v7.0.87/go.mod h1:33+O8h0tO7pCeCWwBVa07RhVVfB/3vS4kEX7rwYKmIg= +github.com/minio/minio-go/v7 v7.0.88 h1:v8MoIJjwYxOkehp+eiLIuvXk87P2raUtoU5klrAAshs= +github.com/minio/minio-go/v7 v7.0.88/go.mod h1:33+O8h0tO7pCeCWwBVa07RhVVfB/3vS4kEX7rwYKmIg= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= @@ -622,8 +626,8 @@ github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= -github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= @@ -644,19 +648,19 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg= github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= -github.com/prometheus/client_golang v1.21.0 h1:DIsaGmiaBkSangBgMtWdNfxbMNdku5IK6iNhrEqWvdA= -github.com/prometheus/client_golang v1.21.0/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg= +github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk= +github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= -github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= -github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= -github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= -github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k= +github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18= +github.com/prometheus/procfs v0.16.0 h1:xh6oHhKwnOJKMYiYBDWmkHqQPyiY40sny36Cmx2bbsM= +github.com/prometheus/procfs v0.16.0/go.mod h1:8veyXUu3nGP7oaCxhX6yeaM5u4stL2FeMXnCqhDthZg= github.com/quasoft/websspi v1.1.2 h1:/mA4w0LxWlE3novvsoEL6BBA1WnjJATbjkh1kFrTidw= github.com/quasoft/websspi v1.1.2/go.mod h1:HmVdl939dQ0WIXZhyik+ARdI03M6bQzaSEKcgpFmewk= github.com/rcrowley/go-metrics v0.0.0-20190826022208-cac0b30c2563/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= -github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E= -github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw= +github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM= +github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA= github.com/redis/rueidis v1.0.19 h1:s65oWtotzlIFN8eMPhyYwxlwLR1lUdhza2KtWprKYSo= github.com/redis/rueidis v1.0.19/go.mod h1:8B+r5wdnjwK3lTFml5VtxjzGOQAC+5UmujoD12pDrEo= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk= @@ -672,17 +676,15 @@ github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzG github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= -github.com/rogpeppe/go-internal v1.13.2-0.20241226121412-a5dc8ff20d0a h1:w3tdWGKbLGBPtR/8/oO74W6hmz0qE5q0z9aqSAewaaM= -github.com/rogpeppe/go-internal v1.13.2-0.20241226121412-a5dc8ff20d0a/go.mod h1:S8kfXMp+yh77OxPD4fdM6YUknrZpQxLhvxzS4gDHENY= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= -github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= -github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= -github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/sagikazarmark/locafero v0.8.0 h1:mXaMVw7IqxNBxfv3LdWt9MDmcWDQ1fagDH918lOdVaQ= +github.com/sagikazarmark/locafero v0.8.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk= github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4= github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY= github.com/sassoftware/go-rpmutils v0.4.0 h1:ojND82NYBxgwrV+mX1CWsd5QJvvEZTKddtCdFLPWhpg= @@ -709,8 +711,8 @@ github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337/go.mod h1:s github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= -github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs= -github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4= +github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA= +github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= @@ -720,8 +722,8 @@ github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnIn github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= -github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= -github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= +github.com/spf13/viper v1.20.0 h1:zrxIyR3RQIOsarIrgL8+sAvALXul9jeEPa06Y0Ph6vY= +github.com/spf13/viper v1.20.0/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo= github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM= github.com/stephens2424/writerset v1.0.2/go.mod h1:aS2JhsMn6eA7e82oNmW4rfsgAOp9COBTTl8mzkwADnc= @@ -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/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= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -738,6 +741,7 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stvp/tempredis v0.0.0-20181119212430-b82af8480203 h1:QVqDTf3h2WHt08YuiTGPZLls0Wq99X9bWd0Q5ZSBesM= @@ -758,8 +762,8 @@ github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc= github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/unknwon/com v1.0.1 h1:3d1LTxD+Lnf3soQiD4Cp/0BRB+Rsa/+RTvz8GMMzIXs= github.com/unknwon/com v1.0.1/go.mod h1:tOOxU81rwgoCLoOVVPHb6T/wt8HZygqH5id+GNnlCXM= -github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= -github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= +github.com/urfave/cli/v2 v2.27.6 h1:VdRdS98FNhKZ8/Az8B7MTyGQmpIr36O1EHybx/LaZ4g= +github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= 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= @@ -801,13 +805,13 @@ github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI= github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE= github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo= github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4= -gitlab.com/gitlab-org/api/client-go v0.123.0 h1:W3LZ5QNyiSCJA0Zchkwz8nQIUzOuDoSWMZtRDT5DjPI= -gitlab.com/gitlab-org/api/client-go v0.123.0/go.mod h1:Jh0qjLILEdbO6z/OY94RD+3NDQRUKiuFSFYozN6cpKM= +gitlab.com/gitlab-org/api/client-go v0.126.0 h1:VV5TdkF6pMbEdFGvbR2CwEgJwg6qdg1u3bj5eD2tiWk= +gitlab.com/gitlab-org/api/client-go v0.126.0/go.mod h1:bYC6fPORKSmtuPRyD9Z2rtbAjE7UeNatu2VWHRf4/LE= go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= go.etcd.io/bbolt v1.4.0 h1:TU77id3TnN/zKr7CO/uk+fBCwF2jGcMuw2B/FMAzYIk= go.etcd.io/bbolt v1.4.0/go.mod h1:AsD+OCi/qPN1giOX1aiLAha3o1U8rAz65bvN4j0sRuk= -go.mongodb.org/mongo-driver v1.17.2 h1:gvZyk8352qSfzyZ2UMWcpDpMSGEr1eqE4T793SqyhzM= -go.mongodb.org/mongo-driver v1.17.2/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= +go.mongodb.org/mongo-driver v1.17.3 h1:TQyXhnsWfWtgAhMtOgtYHMTkZIfBTpMTsMnd9ZBeHxQ= +go.mongodb.org/mongo-driver v1.17.3/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= @@ -831,13 +835,14 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= 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.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= -golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= -golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= -golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa h1:t2QcU6V556bFjYgu4L6C+6VrCPyJZ+eyRsABUPs1mz4= -golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa/go.mod h1:BHOTPb3L19zxehTsLoJXVaTktb06DFgmdW6Wb9s8jqk= -golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ= -golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= +golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= +golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ= +golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -847,8 +852,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.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM= -golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= +golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -867,10 +872,10 @@ 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.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA= -golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I= -golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= -golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= +golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc= +golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -882,8 +887,9 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= +golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181221143128-b4a75ba826a6/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -914,8 +920,10 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 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= @@ -925,8 +933,10 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= 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.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= +golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= +golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= +golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -937,10 +947,11 @@ 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.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= -golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4= -golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= @@ -951,16 +962,16 @@ 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.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY= -golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= +golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= +golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= 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= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250219182151-9fdb1cabc7b2 h1:DMTIbak9GhdaSxEjvVzAeNZvyc03I61duqNbnm3SU0M= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250219182151-9fdb1cabc7b2/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I= -google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= -google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 h1:iK2jbkWL86DXjEx0qiHcRE9dE4/Ahua5k6V8OWFb//c= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I= +google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= +google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= diff --git a/models/actions/run_job.go b/models/actions/run_job.go index de4b6aab66..d0dfd10db6 100644 --- a/models/actions/run_job.go +++ b/models/actions/run_job.go @@ -10,6 +10,7 @@ import ( "time" "code.gitea.io/gitea/models/db" + repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" @@ -19,11 +20,12 @@ import ( // ActionRunJob represents a job of a run type ActionRunJob struct { ID int64 - RunID int64 `xorm:"index"` - Run *ActionRun `xorm:"-"` - RepoID int64 `xorm:"index"` - OwnerID int64 `xorm:"index"` - CommitSHA string `xorm:"index"` + RunID int64 `xorm:"index"` + Run *ActionRun `xorm:"-"` + RepoID int64 `xorm:"index"` + Repo *repo_model.Repository `xorm:"-"` + OwnerID int64 `xorm:"index"` + CommitSHA string `xorm:"index"` IsForkPullRequest bool Name string `xorm:"VARCHAR(255)"` Attempt int64 @@ -58,6 +60,17 @@ func (job *ActionRunJob) LoadRun(ctx context.Context) error { return nil } +func (job *ActionRunJob) LoadRepo(ctx context.Context) error { + if job.Repo == nil { + repo, err := repo_model.GetRepositoryByID(ctx, job.RepoID) + if err != nil { + return err + } + job.Repo = repo + } + return nil +} + // LoadAttributes load Run if not loaded func (job *ActionRunJob) LoadAttributes(ctx context.Context) error { if job == nil { @@ -83,7 +96,7 @@ func GetRunJobByID(ctx context.Context, id int64) (*ActionRunJob, error) { return &job, nil } -func GetRunJobsByRunID(ctx context.Context, runID int64) ([]*ActionRunJob, error) { +func GetRunJobsByRunID(ctx context.Context, runID int64) (ActionJobList, error) { var jobs []*ActionRunJob if err := db.GetEngine(ctx).Where("run_id=?", runID).OrderBy("id").Find(&jobs); err != nil { return nil, err diff --git a/models/actions/run_job_list.go b/models/actions/run_job_list.go index 6c5d3b3252..1d50c9c8dd 100644 --- a/models/actions/run_job_list.go +++ b/models/actions/run_job_list.go @@ -7,6 +7,7 @@ import ( "context" "code.gitea.io/gitea/models/db" + repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/timeutil" @@ -21,7 +22,33 @@ func (jobs ActionJobList) GetRunIDs() []int64 { }) } +func (jobs ActionJobList) LoadRepos(ctx context.Context) error { + repoIDs := container.FilterSlice(jobs, func(j *ActionRunJob) (int64, bool) { + return j.RepoID, j.RepoID != 0 && j.Repo == nil + }) + if len(repoIDs) == 0 { + return nil + } + + repos := make(map[int64]*repo_model.Repository, len(repoIDs)) + if err := db.GetEngine(ctx).In("id", repoIDs).Find(&repos); err != nil { + return err + } + for _, j := range jobs { + if j.RepoID > 0 && j.Repo == nil { + j.Repo = repos[j.RepoID] + } + } + return nil +} + func (jobs ActionJobList) LoadRuns(ctx context.Context, withRepo bool) error { + if withRepo { + if err := jobs.LoadRepos(ctx); err != nil { + return err + } + } + runIDs := jobs.GetRunIDs() runs := make(map[int64]*ActionRun, len(runIDs)) if err := db.GetEngine(ctx).In("id", runIDs).Find(&runs); err != nil { @@ -30,15 +57,9 @@ func (jobs ActionJobList) LoadRuns(ctx context.Context, withRepo bool) error { for _, j := range jobs { if j.RunID > 0 && j.Run == nil { j.Run = runs[j.RunID] + j.Run.Repo = j.Repo } } - if withRepo { - var runsList RunList = make([]*ActionRun, 0, len(runs)) - for _, r := range runs { - runsList = append(runsList, r) - } - return runsList.LoadRepos(ctx) - } return nil } diff --git a/models/actions/runner.go b/models/actions/runner.go index 9ddf346aa6..0411a48393 100644 --- a/models/actions/runner.go +++ b/models/actions/runner.go @@ -86,9 +86,10 @@ func (r *ActionRunner) BelongsToOwnerType() types.OwnerType { return types.OwnerTypeRepository } if r.OwnerID != 0 { - if r.Owner.Type == user_model.UserTypeOrganization { + switch r.Owner.Type { + case user_model.UserTypeOrganization: return types.OwnerTypeOrganization - } else if r.Owner.Type == user_model.UserTypeIndividual { + case user_model.UserTypeIndividual: return types.OwnerTypeIndividual } } diff --git a/models/activities/action.go b/models/activities/action.go index c16c49c0ac..c89ba3e14e 100644 --- a/models/activities/action.go +++ b/models/activities/action.go @@ -172,7 +172,10 @@ func (a *Action) TableIndices() []*schemas.Index { cuIndex := schemas.NewIndex("c_u", schemas.IndexType) cuIndex.AddColumn("user_id", "is_deleted") - indices := []*schemas.Index{actUserIndex, repoIndex, cudIndex, cuIndex} + actUserUserIndex := schemas.NewIndex("au_c_u", schemas.IndexType) + actUserUserIndex.AddColumn("act_user_id", "created_unix", "user_id") + + indices := []*schemas.Index{actUserIndex, repoIndex, cudIndex, cuIndex, actUserUserIndex} return indices } @@ -442,6 +445,7 @@ type GetFeedsOptions struct { OnlyPerformedBy bool // only actions performed by requested user IncludeDeleted bool // include deleted actions Date string // the day we want activity for: YYYY-MM-DD + DontCount bool // do counting in GetFeeds } // ActivityReadable return whether doer can read activities of user diff --git a/models/activities/action_list.go b/models/activities/action_list.go index f7ea48f03e..6789ebcb99 100644 --- a/models/activities/action_list.go +++ b/models/activities/action_list.go @@ -243,7 +243,11 @@ func GetFeeds(ctx context.Context, opts GetFeedsOptions) (ActionList, int64, err sess := db.GetEngine(ctx).Where(cond) sess = db.SetSessionPagination(sess, &opts) - count, err = sess.Desc("`action`.created_unix").FindAndCount(&actions) + if opts.DontCount { + err = sess.Desc("`action`.created_unix").Find(&actions) + } else { + count, err = sess.Desc("`action`.created_unix").FindAndCount(&actions) + } if err != nil { return nil, 0, fmt.Errorf("FindAndCount: %w", err) } @@ -257,11 +261,13 @@ func GetFeeds(ctx context.Context, opts GetFeedsOptions) (ActionList, int64, err return nil, 0, fmt.Errorf("Find(actionsIDs): %w", err) } - count, err = db.GetEngine(ctx).Where(cond). - Table("action"). - Cols("`action`.id").Count() - if err != nil { - return nil, 0, fmt.Errorf("Count: %w", err) + if !opts.DontCount { + count, err = db.GetEngine(ctx).Where(cond). + Table("action"). + Cols("`action`.id").Count() + if err != nil { + return nil, 0, fmt.Errorf("Count: %w", err) + } } if err := db.GetEngine(ctx).In("`action`.id", actionIDs).Desc("`action`.created_unix").Find(&actions); err != nil { @@ -275,3 +281,9 @@ func GetFeeds(ctx context.Context, opts GetFeedsOptions) (ActionList, int64, err return actions, count, nil } + +func CountUserFeeds(ctx context.Context, userID int64) (int64, error) { + return db.GetEngine(ctx).Where("user_id = ?", userID). + And("is_deleted = ?", false). + Count(&Action{}) +} diff --git a/models/db/engine_init.go b/models/db/engine_init.go index 7a071fa29b..bb02aff274 100644 --- a/models/db/engine_init.go +++ b/models/db/engine_init.go @@ -42,9 +42,10 @@ func newXORMEngine() (*xorm.Engine, error) { if err != nil { return nil, err } - if setting.Database.Type == "mysql" { + switch setting.Database.Type { + case "mysql": engine.Dialect().SetParams(map[string]string{"rowFormat": "DYNAMIC"}) - } else if setting.Database.Type == "mssql" { + case "mssql": engine.Dialect().SetParams(map[string]string{"DEFAULT_VARCHAR": "nvarchar"}) } engine.SetSchema(setting.Database.Schema) diff --git a/models/db/search.go b/models/db/search.go index e0a1b6bde9..44d54f21fc 100644 --- a/models/db/search.go +++ b/models/db/search.go @@ -29,7 +29,3 @@ const ( // NoConditionID means a condition to filter the records which don't match any id. // eg: "milestone_id=-1" means "find the items without any milestone. const NoConditionID int64 = -1 - -// NonExistingID means a condition to match no result (eg: a non-existing user) -// It doesn't use -1 or -2 because they are used as builtin users. -const NonExistingID int64 = -1000000 diff --git a/models/issues/issue_search.go b/models/issues/issue_search.go index 694b918755..737b69f154 100644 --- a/models/issues/issue_search.go +++ b/models/issues/issue_search.go @@ -27,8 +27,8 @@ type IssuesOptions struct { //nolint RepoIDs []int64 // overwrites RepoCond if the length is not 0 AllPublic bool // include also all public repositories RepoCond builder.Cond - AssigneeID optional.Option[int64] - PosterID optional.Option[int64] + AssigneeID string // "(none)" or "(any)" or a user ID + PosterID string // "(none)" or "(any)" or a user ID MentionedID int64 ReviewRequestedID int64 ReviewedID int64 @@ -356,26 +356,25 @@ func issuePullAccessibleRepoCond(repoIDstr string, userID int64, owner *user_mod return cond } -func applyAssigneeCondition(sess *xorm.Session, assigneeID optional.Option[int64]) { +func applyAssigneeCondition(sess *xorm.Session, assigneeID string) { // old logic: 0 is also treated as "not filtering assignee", because the "assignee" was read as FormInt64 - if !assigneeID.Has() || assigneeID.Value() == 0 { - return - } - if assigneeID.Value() == db.NoConditionID { + if assigneeID == "(none)" { sess.Where("issue.id NOT IN (SELECT issue_id FROM issue_assignees)") - } else { + } else if assigneeID == "(any)" { + sess.Where("issue.id IN (SELECT issue_id FROM issue_assignees)") + } else if assigneeIDInt64, _ := strconv.ParseInt(assigneeID, 10, 64); assigneeIDInt64 > 0 { sess.Join("INNER", "issue_assignees", "issue.id = issue_assignees.issue_id"). - And("issue_assignees.assignee_id = ?", assigneeID.Value()) + And("issue_assignees.assignee_id = ?", assigneeIDInt64) } } -func applyPosterCondition(sess *xorm.Session, posterID optional.Option[int64]) { - if !posterID.Has() { - return - } - // poster doesn't need to support db.NoConditionID(-1), so just use the value as-is - if posterID.Has() { - sess.And("issue.poster_id=?", posterID.Value()) +func applyPosterCondition(sess *xorm.Session, posterID string) { + // Actually every issue has a poster. + // The "(none)" is for internal usage only: when doer tries to search non-existing user as poster, use "(none)" to return empty result. + if posterID == "(none)" { + sess.And("issue.poster_id=0") + } else if posterIDInt64, _ := strconv.ParseInt(posterID, 10, 64); posterIDInt64 > 0 { + sess.And("issue.poster_id=?", posterIDInt64) } } diff --git a/models/issues/issue_test.go b/models/issues/issue_test.go index 3f76a81bb6..c32aa26b2b 100644 --- a/models/issues/issue_test.go +++ b/models/issues/issue_test.go @@ -15,7 +15,6 @@ 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/optional" "code.gitea.io/gitea/modules/setting" "github.com/stretchr/testify/assert" @@ -155,7 +154,7 @@ func TestIssues(t *testing.T) { }{ { issues_model.IssuesOptions{ - AssigneeID: optional.Some(int64(1)), + AssigneeID: "1", SortType: "oldest", }, []int64{1, 6}, diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index 297c50a267..6e631f98c7 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -377,6 +377,8 @@ func prepareMigrationTasks() []*migration { newMigration(314, "Update OwnerID as zero for repository level action tables", v1_24.UpdateOwnerIDOfRepoLevelActionsTables), newMigration(315, "Add Ephemeral to ActionRunner", v1_24.AddEphemeralToActionRunner), newMigration(316, "Add description for secrets and variables", v1_24.AddDescriptionForSecretsAndVariables), + newMigration(317, "Add new index for action for heatmap", v1_24.AddNewIndexForUserDashboard), + newMigration(318, "Add anonymous_access_mode for repo_unit", v1_24.AddRepoUnitAnonymousAccessMode), } return preparedMigrations } diff --git a/models/migrations/v1_24/v317.go b/models/migrations/v1_24/v317.go new file mode 100644 index 0000000000..3da5a4a078 --- /dev/null +++ b/models/migrations/v1_24/v317.go @@ -0,0 +1,56 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_24 //nolint + +import ( + "code.gitea.io/gitea/modules/timeutil" + + "xorm.io/xorm" + "xorm.io/xorm/schemas" +) + +type improveActionTableIndicesAction struct { + ID int64 `xorm:"pk autoincr"` + UserID int64 `xorm:"INDEX"` // Receiver user id. + OpType int + ActUserID int64 // Action user id. + RepoID int64 + CommentID int64 `xorm:"INDEX"` + IsDeleted bool `xorm:"NOT NULL DEFAULT false"` + RefName string + IsPrivate bool `xorm:"NOT NULL DEFAULT false"` + Content string `xorm:"TEXT"` + CreatedUnix timeutil.TimeStamp `xorm:"created"` +} + +// TableName sets the name of this table +func (*improveActionTableIndicesAction) TableName() string { + return "action" +} + +// TableIndices implements xorm's TableIndices interface +func (a *improveActionTableIndicesAction) TableIndices() []*schemas.Index { + repoIndex := schemas.NewIndex("r_u_d", schemas.IndexType) + repoIndex.AddColumn("repo_id", "user_id", "is_deleted") + + actUserIndex := schemas.NewIndex("au_r_c_u_d", schemas.IndexType) + actUserIndex.AddColumn("act_user_id", "repo_id", "created_unix", "user_id", "is_deleted") + + cudIndex := schemas.NewIndex("c_u_d", schemas.IndexType) + cudIndex.AddColumn("created_unix", "user_id", "is_deleted") + + cuIndex := schemas.NewIndex("c_u", schemas.IndexType) + cuIndex.AddColumn("user_id", "is_deleted") + + actUserUserIndex := schemas.NewIndex("au_c_u", schemas.IndexType) + actUserUserIndex.AddColumn("act_user_id", "created_unix", "user_id") + + indices := []*schemas.Index{actUserIndex, repoIndex, cudIndex, cuIndex, actUserUserIndex} + + return indices +} + +func AddNewIndexForUserDashboard(x *xorm.Engine) error { + return x.Sync(new(improveActionTableIndicesAction)) +} diff --git a/models/migrations/v1_24/v318.go b/models/migrations/v1_24/v318.go new file mode 100644 index 0000000000..83fb0061d3 --- /dev/null +++ b/models/migrations/v1_24/v318.go @@ -0,0 +1,17 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_24 //nolint + +import ( + "code.gitea.io/gitea/models/perm" + + "xorm.io/xorm" +) + +func AddRepoUnitAnonymousAccessMode(x *xorm.Engine) error { + type RepoUnit struct { //revive:disable-line:exported + AnonymousAccessMode perm.AccessMode `xorm:"NOT NULL DEFAULT 0"` + } + return x.Sync(&RepoUnit{}) +} diff --git a/models/perm/access/repo_permission.go b/models/perm/access/repo_permission.go index 5e7ecb31ea..f42c96bbe2 100644 --- a/models/perm/access/repo_permission.go +++ b/models/perm/access/repo_permission.go @@ -15,6 +15,7 @@ import ( "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" ) @@ -25,7 +26,8 @@ type Permission struct { units []*repo_model.RepoUnit unitsMode map[unit.Type]perm_model.AccessMode - everyoneAccessMode map[unit.Type]perm_model.AccessMode + everyoneAccessMode map[unit.Type]perm_model.AccessMode // the unit's minimal access mode for every signed-in user + anonymousAccessMode map[unit.Type]perm_model.AccessMode // the unit's minimal access mode for anonymous (non-signed-in) user } // IsOwner returns true if current user is the owner of repository. @@ -39,7 +41,7 @@ func (p *Permission) IsAdmin() bool { } // HasAnyUnitAccess returns true if the user might have at least one access mode to any unit of this repository. -// It doesn't count the "everyone access mode". +// It doesn't count the "public(anonymous/everyone) access mode". func (p *Permission) HasAnyUnitAccess() bool { for _, v := range p.unitsMode { if v >= perm_model.AccessModeRead { @@ -49,13 +51,22 @@ func (p *Permission) HasAnyUnitAccess() bool { return p.AccessMode >= perm_model.AccessModeRead } -func (p *Permission) HasAnyUnitAccessOrEveryoneAccess() bool { +func (p *Permission) HasAnyUnitPublicAccess() bool { + for _, v := range p.anonymousAccessMode { + if v >= perm_model.AccessModeRead { + return true + } + } for _, v := range p.everyoneAccessMode { if v >= perm_model.AccessModeRead { return true } } - return p.HasAnyUnitAccess() + return false +} + +func (p *Permission) HasAnyUnitAccessOrPublicAccess() bool { + return p.HasAnyUnitPublicAccess() || p.HasAnyUnitAccess() } // HasUnits returns true if the permission contains attached units @@ -73,14 +84,16 @@ func (p *Permission) GetFirstUnitRepoID() int64 { } // UnitAccessMode returns current user access mode to the specify unit of the repository -// It also considers "everyone access mode" +// It also considers "public (anonymous/everyone) access mode" func (p *Permission) UnitAccessMode(unitType unit.Type) perm_model.AccessMode { // if the units map contains the access mode, use it, but admin/owner mode could override it if m, ok := p.unitsMode[unitType]; ok { return util.Iif(p.AccessMode >= perm_model.AccessModeAdmin, p.AccessMode, m) } // if the units map does not contain the access mode, return the default access mode if the unit exists - unitDefaultAccessMode := max(p.AccessMode, p.everyoneAccessMode[unitType]) + unitDefaultAccessMode := p.AccessMode + unitDefaultAccessMode = max(unitDefaultAccessMode, p.anonymousAccessMode[unitType]) + unitDefaultAccessMode = max(unitDefaultAccessMode, p.everyoneAccessMode[unitType]) hasUnit := slices.ContainsFunc(p.units, func(u *repo_model.RepoUnit) bool { return u.Type == unitType }) return util.Iif(hasUnit, unitDefaultAccessMode, perm_model.AccessModeNone) } @@ -171,27 +184,41 @@ func (p *Permission) LogString() string { format += "\n\tunitsMode[%-v]: %-v" args = append(args, key.LogString(), value.LogString()) } + format += "\n\tanonymousAccessMode: %-v" + args = append(args, p.anonymousAccessMode) format += "\n\teveryoneAccessMode: %-v" args = append(args, p.everyoneAccessMode) format += "\n\t]>" return fmt.Sprintf(format, args...) } +func applyPublicAccessPermission(unitType unit.Type, accessMode perm_model.AccessMode, modeMap *map[unit.Type]perm_model.AccessMode) { + if setting.Repository.ForcePrivate { + return + } + if accessMode >= perm_model.AccessModeRead && accessMode > (*modeMap)[unitType] { + if *modeMap == nil { + *modeMap = make(map[unit.Type]perm_model.AccessMode) + } + (*modeMap)[unitType] = accessMode + } +} + func finalProcessRepoUnitPermission(user *user_model.User, perm *Permission) { + // apply public (anonymous) access permissions + for _, u := range perm.units { + applyPublicAccessPermission(u.Type, u.AnonymousAccessMode, &perm.anonymousAccessMode) + } + if user == nil || user.ID <= 0 { // for anonymous access, it could be: // AccessMode is None or Read, units has repo units, unitModes is nil return } - // apply everyone access permissions + // apply public (everyone) access permissions for _, u := range perm.units { - if u.EveryoneAccessMode >= perm_model.AccessModeRead && u.EveryoneAccessMode > perm.everyoneAccessMode[u.Type] { - if perm.everyoneAccessMode == nil { - perm.everyoneAccessMode = make(map[unit.Type]perm_model.AccessMode) - } - perm.everyoneAccessMode[u.Type] = u.EveryoneAccessMode - } + applyPublicAccessPermission(u.Type, u.EveryoneAccessMode, &perm.everyoneAccessMode) } if perm.unitsMode == nil { @@ -209,6 +236,11 @@ func finalProcessRepoUnitPermission(user *user_model.User, perm *Permission) { break } } + for t := range perm.anonymousAccessMode { + if shouldKeep = shouldKeep || u.Type == t; shouldKeep { + break + } + } for t := range perm.everyoneAccessMode { if shouldKeep = shouldKeep || u.Type == t; shouldKeep { break diff --git a/models/perm/access/repo_permission_test.go b/models/perm/access/repo_permission_test.go index 9862da0673..024f4400b3 100644 --- a/models/perm/access/repo_permission_test.go +++ b/models/perm/access/repo_permission_test.go @@ -22,14 +22,21 @@ func TestHasAnyUnitAccess(t *testing.T) { units: []*repo_model.RepoUnit{{Type: unit.TypeWiki}}, } assert.False(t, perm.HasAnyUnitAccess()) - assert.False(t, perm.HasAnyUnitAccessOrEveryoneAccess()) + assert.False(t, perm.HasAnyUnitAccessOrPublicAccess()) perm = Permission{ units: []*repo_model.RepoUnit{{Type: unit.TypeWiki}}, everyoneAccessMode: map[unit.Type]perm_model.AccessMode{unit.TypeIssues: perm_model.AccessModeRead}, } assert.False(t, perm.HasAnyUnitAccess()) - assert.True(t, perm.HasAnyUnitAccessOrEveryoneAccess()) + assert.True(t, perm.HasAnyUnitAccessOrPublicAccess()) + + perm = Permission{ + units: []*repo_model.RepoUnit{{Type: unit.TypeWiki}}, + anonymousAccessMode: map[unit.Type]perm_model.AccessMode{unit.TypeIssues: perm_model.AccessModeRead}, + } + assert.False(t, perm.HasAnyUnitAccess()) + assert.True(t, perm.HasAnyUnitAccessOrPublicAccess()) perm = Permission{ AccessMode: perm_model.AccessModeRead, @@ -43,7 +50,7 @@ func TestHasAnyUnitAccess(t *testing.T) { assert.True(t, perm.HasAnyUnitAccess()) } -func TestApplyEveryoneRepoPermission(t *testing.T) { +func TestApplyPublicAccessRepoPermission(t *testing.T) { perm := Permission{ AccessMode: perm_model.AccessModeNone, units: []*repo_model.RepoUnit{ @@ -56,6 +63,15 @@ func TestApplyEveryoneRepoPermission(t *testing.T) { perm = Permission{ AccessMode: perm_model.AccessModeNone, units: []*repo_model.RepoUnit{ + {Type: unit.TypeWiki, AnonymousAccessMode: perm_model.AccessModeRead}, + }, + } + finalProcessRepoUnitPermission(nil, &perm) + assert.True(t, perm.CanRead(unit.TypeWiki)) + + perm = Permission{ + AccessMode: perm_model.AccessModeNone, + units: []*repo_model.RepoUnit{ {Type: unit.TypeWiki, EveryoneAccessMode: perm_model.AccessModeRead}, }, } diff --git a/models/repo/repo.go b/models/repo/repo.go index 13473699f3..515c57916c 100644 --- a/models/repo/repo.go +++ b/models/repo/repo.go @@ -215,12 +215,24 @@ func init() { db.RegisterModel(new(Repository)) } -func (repo *Repository) GetName() string { - return repo.Name +func RelativePath(ownerName, repoName string) string { + return strings.ToLower(ownerName) + "/" + strings.ToLower(repoName) + ".git" } -func (repo *Repository) GetOwnerName() string { - return repo.OwnerName +// RelativePath should be an unix style path like username/reponame.git +func (repo *Repository) RelativePath() string { + return RelativePath(repo.OwnerName, repo.Name) +} + +type StorageRepo string + +// RelativePath should be an unix style path like username/reponame.git +func (sr StorageRepo) RelativePath() string { + return string(sr) +} + +func (repo *Repository) WikiStorageRepo() StorageRepo { + return StorageRepo(strings.ToLower(repo.OwnerName) + "/" + strings.ToLower(repo.Name) + ".wiki.git") } // SanitizedOriginalURL returns a sanitized OriginalURL @@ -413,32 +425,33 @@ func (repo *Repository) MustGetUnit(ctx context.Context, tp unit.Type) *RepoUnit return ru } - if tp == unit.TypeExternalWiki { + switch tp { + case unit.TypeExternalWiki: return &RepoUnit{ Type: tp, Config: new(ExternalWikiConfig), } - } else if tp == unit.TypeExternalTracker { + case unit.TypeExternalTracker: return &RepoUnit{ Type: tp, Config: new(ExternalTrackerConfig), } - } else if tp == unit.TypePullRequests { + case unit.TypePullRequests: return &RepoUnit{ Type: tp, Config: new(PullRequestsConfig), } - } else if tp == unit.TypeIssues { + case unit.TypeIssues: return &RepoUnit{ Type: tp, Config: new(IssuesConfig), } - } else if tp == unit.TypeActions { + case unit.TypeActions: return &RepoUnit{ Type: tp, Config: new(ActionsConfig), } - } else if tp == unit.TypeProjects { + case unit.TypeProjects: cfg := new(ProjectsConfig) cfg.ProjectsMode = ProjectsModeNone return &RepoUnit{ diff --git a/models/repo/repo_unit.go b/models/repo/repo_unit.go index cb52c2c9e2..e83a3dc8c2 100644 --- a/models/repo/repo_unit.go +++ b/models/repo/repo_unit.go @@ -42,12 +42,13 @@ func (err ErrUnitTypeNotExist) Unwrap() error { // RepoUnit describes all units of a repository type RepoUnit struct { //revive:disable-line:exported - ID int64 - RepoID int64 `xorm:"INDEX(s)"` - Type unit.Type `xorm:"INDEX(s)"` - Config convert.Conversion `xorm:"TEXT"` - CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED"` - EveryoneAccessMode perm.AccessMode `xorm:"NOT NULL DEFAULT 0"` + ID int64 + RepoID int64 `xorm:"INDEX(s)"` + Type unit.Type `xorm:"INDEX(s)"` + Config convert.Conversion `xorm:"TEXT"` + CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED"` + AnonymousAccessMode perm.AccessMode `xorm:"NOT NULL DEFAULT 0"` + EveryoneAccessMode perm.AccessMode `xorm:"NOT NULL DEFAULT 0"` } func init() { @@ -341,3 +342,9 @@ func UpdateRepoUnit(ctx context.Context, unit *RepoUnit) error { _, err := db.GetEngine(ctx).ID(unit.ID).Update(unit) return err } + +func UpdateRepoUnitPublicAccess(ctx context.Context, unit *RepoUnit) error { + _, err := db.GetEngine(ctx).Where("repo_id=? AND `type`=?", unit.RepoID, unit.Type). + Cols("anonymous_access_mode", "everyone_access_mode").Update(unit) + return err +} diff --git a/models/repo/upload.go b/models/repo/upload.go index 18834f6b83..cae11df96a 100644 --- a/models/repo/upload.go +++ b/models/repo/upload.go @@ -10,7 +10,7 @@ import ( "io" "mime/multipart" "os" - "path" + "path/filepath" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/modules/log" @@ -53,7 +53,7 @@ func init() { // UploadLocalPath returns where uploads is stored in local file system based on given UUID. func UploadLocalPath(uuid string) string { - return path.Join(setting.Repository.Upload.TempPath, uuid[0:1], uuid[1:2], uuid) + return filepath.Join(setting.Repository.Upload.TempPath, uuid[0:1], uuid[1:2], uuid) } // LocalPath returns where uploads are temporarily stored in local file system. @@ -69,7 +69,7 @@ func NewUpload(ctx context.Context, name string, buf []byte, file multipart.File } localPath := upload.LocalPath() - if err = os.MkdirAll(path.Dir(localPath), os.ModePerm); err != nil { + if err = os.MkdirAll(filepath.Dir(localPath), os.ModePerm); err != nil { return nil, fmt.Errorf("MkdirAll: %w", err) } diff --git a/models/unittest/fscopy.go b/models/unittest/fscopy.go index b7ba6b7ef5..98b01815bd 100644 --- a/models/unittest/fscopy.go +++ b/models/unittest/fscopy.go @@ -28,7 +28,7 @@ func SyncFile(srcPath, destPath string) error { } if src.Size() == dest.Size() && - src.ModTime() == dest.ModTime() && + src.ModTime().Equal(dest.ModTime()) && src.Mode() == dest.Mode() { return nil } diff --git a/models/user/search.go b/models/user/search.go index 85915f4020..f4436be09a 100644 --- a/models/user/search.go +++ b/models/user/search.go @@ -45,13 +45,14 @@ func (opts *SearchUserOptions) toSearchQueryBase(ctx context.Context) *xorm.Sess var cond builder.Cond cond = builder.Eq{"type": opts.Type} if opts.IncludeReserved { - if opts.Type == UserTypeIndividual { + switch opts.Type { + case UserTypeIndividual: cond = cond.Or(builder.Eq{"type": UserTypeUserReserved}).Or( builder.Eq{"type": UserTypeBot}, ).Or( builder.Eq{"type": UserTypeRemoteUser}, ) - } else if opts.Type == UserTypeOrganization { + case UserTypeOrganization: cond = cond.Or(builder.Eq{"type": UserTypeOrganizationReserved}) } } diff --git a/models/user/user_system.go b/models/user/user_system.go index 6fbfd9e69e..e07274d291 100644 --- a/models/user/user_system.go +++ b/models/user/user_system.go @@ -10,8 +10,8 @@ import ( ) const ( - GhostUserID = -1 - GhostUserName = "Ghost" + GhostUserID int64 = -1 + GhostUserName = "Ghost" ) // NewGhostUser creates and returns a fake user for someone has deleted their account. @@ -36,9 +36,9 @@ func (u *User) IsGhost() bool { } const ( - ActionsUserID = -2 - ActionsUserName = "gitea-actions" - ActionsUserEmail = "teabot@gitea.io" + ActionsUserID int64 = -2 + ActionsUserName = "gitea-actions" + ActionsUserEmail = "teabot@gitea.io" ) func IsGiteaActionsUserName(name string) bool { diff --git a/models/webhook/hooktask.go b/models/webhook/hooktask.go index ff3fdbadb2..96ec11e43f 100644 --- a/models/webhook/hooktask.go +++ b/models/webhook/hooktask.go @@ -198,7 +198,8 @@ func MarkTaskDelivered(ctx context.Context, task *HookTask) (bool, error) { func CleanupHookTaskTable(ctx context.Context, cleanupType HookTaskCleanupType, olderThan time.Duration, numberToKeep int) error { log.Trace("Doing: CleanupHookTaskTable") - if cleanupType == OlderThan { + switch cleanupType { + case OlderThan: deleteOlderThan := time.Now().Add(-olderThan).UnixNano() deletes, err := db.GetEngine(ctx). Where("is_delivered = ? and delivered < ?", true, deleteOlderThan). @@ -207,7 +208,7 @@ func CleanupHookTaskTable(ctx context.Context, cleanupType HookTaskCleanupType, return err } log.Trace("Deleted %d rows from hook_task", deletes) - } else if cleanupType == PerWebhook { + case PerWebhook: hookIDs := make([]int64, 0, 10) err := db.GetEngine(ctx). Table("webhook"). diff --git a/modules/badge/badge.go b/modules/badge/badge.go index b30d0b4729..fdf9866f60 100644 --- a/modules/badge/badge.go +++ b/modules/badge/badge.go @@ -4,6 +4,9 @@ package badge import ( + "strings" + "unicode" + actions_model "code.gitea.io/gitea/models/actions" ) @@ -11,54 +14,35 @@ import ( // We use 10x scale to calculate more precisely // Then scale down to normal size in tmpl file -type Label struct { - text string - width int -} - -func (l Label) Text() string { - return l.text -} - -func (l Label) Width() int { - return l.width -} - -func (l Label) TextLength() int { - return int(float64(l.width-defaultOffset) * 9.5) -} - -func (l Label) X() int { - return l.width*5 + 10 -} - -type Message struct { +type Text struct { text string width int x int } -func (m Message) Text() string { - return m.text +func (t Text) Text() string { + return t.text } -func (m Message) Width() int { - return m.width +func (t Text) Width() int { + return t.width } -func (m Message) X() int { - return m.x +func (t Text) X() int { + return t.x } -func (m Message) TextLength() int { - return int(float64(m.width-defaultOffset) * 9.5) +func (t Text) TextLength() int { + return int(float64(t.width-defaultOffset) * 10) } type Badge struct { - Color string - FontSize int - Label Label - Message Message + IDPrefix string + FontFamily string + Color string + FontSize int + Label Text + Message Text } func (b Badge) Width() int { @@ -66,10 +50,10 @@ func (b Badge) Width() int { } const ( - defaultOffset = 9 - defaultFontSize = 11 - DefaultColor = "#9f9f9f" // Grey - defaultFontWidth = 7 // approximate speculation + defaultOffset = 10 + defaultFontSize = 11 + DefaultColor = "#9f9f9f" // Grey + DefaultFontFamily = "DejaVu Sans,Verdana,Geneva,sans-serif" ) var StatusColorMap = map[actions_model.Status]string{ @@ -85,20 +69,43 @@ var StatusColorMap = map[actions_model.Status]string{ // GenerateBadge generates badge with given template func GenerateBadge(label, message, color string) Badge { - lw := defaultFontWidth*len(label) + defaultOffset - mw := defaultFontWidth*len(message) + defaultOffset - x := lw*10 + mw*5 - 10 + lw := calculateTextWidth(label) + defaultOffset + mw := calculateTextWidth(message) + defaultOffset + + lx := lw * 5 + mx := lw*10 + mw*5 - 10 return Badge{ - Label: Label{ + FontFamily: DefaultFontFamily, + Label: Text{ text: label, width: lw, + x: lx, }, - Message: Message{ + Message: Text{ text: message, width: mw, - x: x, + x: mx, }, FontSize: defaultFontSize * 10, Color: color, } } + +func calculateTextWidth(text string) int { + width := 0 + widthData := DejaVuGlyphWidthData() + for _, char := range strings.TrimSpace(text) { + charWidth, ok := widthData[char] + if !ok { + // use the width of 'm' in case of missing glyph width data for a printable character + if unicode.IsPrint(char) { + charWidth = widthData['m'] + } else { + charWidth = 0 + } + } + width += int(charWidth) + } + + return width +} diff --git a/modules/badge/badge_glyph_width.go b/modules/badge/badge_glyph_width.go new file mode 100644 index 0000000000..e8e43ec9cb --- /dev/null +++ b/modules/badge/badge_glyph_width.go @@ -0,0 +1,208 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package badge + +import "sync" + +// DejaVuGlyphWidthData is generated by `sfnt.Face.GlyphAdvance(nil, <rune>, 11, font.HintingNone)` with DejaVu Sans +// v2.37 (https://github.com/dejavu-fonts/dejavu-fonts/releases/download/version_2_37/dejavu-sans-ttf-2.37.zip). +// +// Fonts defined in "DefaultFontFamily" all have similar widths (including "DejaVu Sans"), +// and these widths are fixed and don't seem to change. +// +// A devtest page "/devtest/badge-actions-svg" could be used to check the rendered images. + +var DejaVuGlyphWidthData = sync.OnceValue(func() map[rune]uint8 { + return map[rune]uint8{ + 32: 3, + 33: 4, + 34: 5, + 35: 9, + 36: 7, + 37: 10, + 38: 9, + 39: 3, + 40: 4, + 41: 4, + 42: 6, + 43: 9, + 44: 3, + 45: 4, + 46: 3, + 47: 4, + 48: 7, + 49: 7, + 50: 7, + 51: 7, + 52: 7, + 53: 7, + 54: 7, + 55: 7, + 56: 7, + 57: 7, + 58: 4, + 59: 4, + 60: 9, + 61: 9, + 62: 9, + 63: 6, + 64: 11, + 65: 8, + 66: 8, + 67: 8, + 68: 8, + 69: 7, + 70: 6, + 71: 9, + 72: 8, + 73: 3, + 74: 3, + 75: 7, + 76: 6, + 77: 9, + 78: 8, + 79: 9, + 80: 7, + 81: 9, + 82: 8, + 83: 7, + 84: 7, + 85: 8, + 86: 8, + 87: 11, + 88: 8, + 89: 7, + 90: 8, + 91: 4, + 92: 4, + 93: 4, + 94: 9, + 95: 6, + 96: 6, + 97: 7, + 98: 7, + 99: 6, + 100: 7, + 101: 7, + 102: 4, + 103: 7, + 104: 7, + 105: 3, + 106: 3, + 107: 6, + 108: 3, + 109: 11, + 110: 7, + 111: 7, + 112: 7, + 113: 7, + 114: 5, + 115: 6, + 116: 4, + 117: 7, + 118: 7, + 119: 9, + 120: 7, + 121: 7, + 122: 6, + 123: 7, + 124: 4, + 125: 7, + 126: 9, + 161: 4, + 162: 7, + 163: 7, + 164: 7, + 165: 7, + 166: 4, + 167: 6, + 168: 6, + 169: 11, + 170: 5, + 171: 7, + 172: 9, + 174: 11, + 175: 6, + 176: 6, + 177: 9, + 178: 4, + 179: 4, + 180: 6, + 181: 7, + 182: 7, + 183: 3, + 184: 6, + 185: 4, + 186: 5, + 187: 7, + 188: 11, + 189: 11, + 190: 11, + 191: 6, + 192: 8, + 193: 8, + 194: 8, + 195: 8, + 196: 8, + 197: 8, + 198: 11, + 199: 8, + 200: 7, + 201: 7, + 202: 7, + 203: 7, + 204: 3, + 205: 3, + 206: 3, + 207: 3, + 208: 9, + 209: 8, + 210: 9, + 211: 9, + 212: 9, + 213: 9, + 214: 9, + 215: 9, + 216: 9, + 217: 8, + 218: 8, + 219: 8, + 220: 8, + 221: 7, + 222: 7, + 223: 7, + 224: 7, + 225: 7, + 226: 7, + 227: 7, + 228: 7, + 229: 7, + 230: 11, + 231: 6, + 232: 7, + 233: 7, + 234: 7, + 235: 7, + 236: 3, + 237: 3, + 238: 3, + 239: 3, + 240: 7, + 241: 7, + 242: 7, + 243: 7, + 244: 7, + 245: 7, + 246: 7, + 247: 9, + 248: 7, + 249: 7, + 250: 7, + 251: 7, + 252: 7, + 253: 7, + 254: 7, + 255: 7, + } +}) diff --git a/modules/git/batch.go b/modules/git/batch.go index 3ec4f1ddcc..f9e1748b54 100644 --- a/modules/git/batch.go +++ b/modules/git/batch.go @@ -14,25 +14,26 @@ type Batch struct { Writer WriteCloserError } -func (repo *Repository) NewBatch(ctx context.Context) (*Batch, error) { +// NewBatch creates a new batch for the given repository, the Close must be invoked before release the batch +func NewBatch(ctx context.Context, repoPath string) (*Batch, error) { // Now because of some insanity with git cat-file not immediately failing if not run in a valid git directory we need to run git rev-parse first! - if err := ensureValidGitRepository(ctx, repo.Path); err != nil { + if err := ensureValidGitRepository(ctx, repoPath); err != nil { return nil, err } var batch Batch - batch.Writer, batch.Reader, batch.cancel = catFileBatch(ctx, repo.Path) + batch.Writer, batch.Reader, batch.cancel = catFileBatch(ctx, repoPath) return &batch, nil } -func (repo *Repository) NewBatchCheck(ctx context.Context) (*Batch, error) { +func NewBatchCheck(ctx context.Context, repoPath string) (*Batch, error) { // Now because of some insanity with git cat-file not immediately failing if not run in a valid git directory we need to run git rev-parse first! - if err := ensureValidGitRepository(ctx, repo.Path); err != nil { + if err := ensureValidGitRepository(ctx, repoPath); err != nil { return nil, err } var check Batch - check.Writer, check.Reader, check.cancel = catFileBatchCheck(ctx, repo.Path) + check.Writer, check.Reader, check.cancel = catFileBatchCheck(ctx, repoPath) return &check, nil } diff --git a/modules/git/command.go b/modules/git/command.go index d85a91804a..f449f1ff0e 100644 --- a/modules/git/command.go +++ b/modules/git/command.go @@ -350,9 +350,10 @@ func (c *Command) run(ctx context.Context, skip int, opts *RunOpts) error { // We need to check if the context is canceled by the program on Windows. // This is because Windows does not have signal checking when terminating the process. // It always returns exit code 1, unlike Linux, which has many exit codes for signals. + // `err.Error()` returns "exit status 1" when using the `git check-attr` command after the context is canceled. if runtime.GOOS == "windows" && err != nil && - err.Error() == "" && + (err.Error() == "" || err.Error() == "exit status 1") && cmd.ProcessState.ExitCode() == 1 && ctx.Err() == context.Canceled { return ctx.Err() diff --git a/modules/git/grep.go b/modules/git/grep.go index 44ec6ca2be..51ebcb832f 100644 --- a/modules/git/grep.go +++ b/modules/git/grep.go @@ -62,13 +62,14 @@ func GrepSearch(ctx context.Context, repo *Repository, search string, opts GrepO var results []*GrepResult cmd := NewCommand("grep", "--null", "--break", "--heading", "--line-number", "--full-name") cmd.AddOptionValues("--context", fmt.Sprint(opts.ContextLineNumber)) - if opts.GrepMode == GrepModeExact { + switch opts.GrepMode { + case GrepModeExact: cmd.AddArguments("--fixed-strings") cmd.AddOptionValues("-e", strings.TrimLeft(search, "-")) - } else if opts.GrepMode == GrepModeRegexp { + case GrepModeRegexp: cmd.AddArguments("--perl-regexp") cmd.AddOptionValues("-e", strings.TrimLeft(search, "-")) - } else /* words */ { + default: /* words */ words := strings.Fields(search) cmd.AddArguments("--fixed-strings", "--ignore-case") for i, word := range words { diff --git a/modules/git/hook.go b/modules/git/hook.go index 46f93ce13e..a6f6b18855 100644 --- a/modules/git/hook.go +++ b/modules/git/hook.go @@ -7,11 +7,9 @@ package git import ( "errors" "os" - "path" "path/filepath" "strings" - "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/util" ) @@ -51,17 +49,28 @@ func GetHook(repoPath, name string) (*Hook, error) { } h := &Hook{ name: name, - path: path.Join(repoPath, "hooks", name+".d", name), + path: filepath.Join(repoPath, "hooks", name+".d", name), } - samplePath := filepath.Join(repoPath, "hooks", name+".sample") - if isFile(h.path) { + 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 } h.IsActive = true h.Content = string(data) - } else if isFile(samplePath) { + return h, nil + } + + 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 @@ -79,7 +88,11 @@ func (h *Hook) Name() string { // Update updates hook settings. func (h *Hook) Update() error { if len(strings.TrimSpace(h.Content)) == 0 { - if isExist(h.path) { + exist, err := util.IsExist(h.path) + if err != nil { + return err + } + if exist { err := util.Remove(h.path) if err != nil { return err @@ -103,7 +116,10 @@ func (h *Hook) Update() error { // ListHooks returns a list of Git hooks of given repository. func ListHooks(repoPath string) (_ []*Hook, err error) { - if !isDir(path.Join(repoPath, "hooks")) { + exist, err := util.IsDir(filepath.Join(repoPath, "hooks")) + if err != nil { + return nil, err + } else if !exist { return nil, errors.New("hooks path does not exist") } @@ -116,28 +132,3 @@ func ListHooks(repoPath string) (_ []*Hook, err error) { } return hooks, nil } - -const ( - // HookPathUpdate hook update path - HookPathUpdate = "hooks/update" -) - -// SetUpdateHook writes given content to update hook of the repository. -func SetUpdateHook(repoPath, content string) (err error) { - log.Debug("Setting update hook: %s", repoPath) - hookPath := path.Join(repoPath, HookPathUpdate) - isExist, err := util.IsExist(hookPath) - if err != nil { - log.Debug("Unable to check if %s exists. Error: %v", hookPath, err) - return err - } - if isExist { - err = util.Remove(hookPath) - } else { - err = os.MkdirAll(path.Dir(hookPath), os.ModePerm) - } - if err != nil { - return err - } - return os.WriteFile(hookPath, []byte(content), 0o777) -} diff --git a/modules/git/log_name_status.go b/modules/git/log_name_status.go index 0e9e22f1dc..3ee462f68e 100644 --- a/modules/git/log_name_status.go +++ b/modules/git/log_name_status.go @@ -118,11 +118,12 @@ func (g *LogNameStatusRepoParser) Next(treepath string, paths2ids map[string]int g.buffull = false g.next, err = g.rd.ReadSlice('\x00') if err != nil { - if err == bufio.ErrBufferFull { + switch err { + case bufio.ErrBufferFull: g.buffull = true - } else if err == io.EOF { + case io.EOF: return nil, nil - } else { + default: return nil, err } } @@ -132,11 +133,12 @@ func (g *LogNameStatusRepoParser) Next(treepath string, paths2ids map[string]int if bytes.Equal(g.next, []byte("commit\000")) { g.next, err = g.rd.ReadSlice('\x00') if err != nil { - if err == bufio.ErrBufferFull { + switch err { + case bufio.ErrBufferFull: g.buffull = true - } else if err == io.EOF { + case io.EOF: return nil, nil - } else { + default: return nil, err } } @@ -214,11 +216,12 @@ diffloop: } g.next, err = g.rd.ReadSlice('\x00') if err != nil { - if err == bufio.ErrBufferFull { + switch err { + case bufio.ErrBufferFull: g.buffull = true - } else if err == io.EOF { + case io.EOF: return &ret, nil - } else { + default: return nil, err } } diff --git a/modules/git/repo_attribute.go b/modules/git/repo_attribute.go index 89101e5af3..8a5e5fa983 100644 --- a/modules/git/repo_attribute.go +++ b/modules/git/repo_attribute.go @@ -280,7 +280,7 @@ func (wr *nulSeparatedAttributeWriter) Write(p []byte) (n int, err error) { } } wr.tmp = append(wr.tmp, p...) - return len(p), nil + return l, nil } func (wr *nulSeparatedAttributeWriter) ReadAttribute() <-chan attributeTriple { diff --git a/modules/git/repo_base_gogit.go b/modules/git/repo_base_gogit.go index 0ca1ea79c2..293aca159c 100644 --- a/modules/git/repo_base_gogit.go +++ b/modules/git/repo_base_gogit.go @@ -49,7 +49,12 @@ func OpenRepository(ctx context.Context, repoPath string) (*Repository, error) { repoPath, err := filepath.Abs(repoPath) if err != nil { return nil, err - } else if !isDir(repoPath) { + } + exist, err := util.IsDir(repoPath) + if err != nil { + return nil, err + } + if !exist { return nil, util.NewNotExistErrorf("no such file or directory") } diff --git a/modules/git/repo_base_nogogit.go b/modules/git/repo_base_nogogit.go index 477e3b8742..6f9bfd4b43 100644 --- a/modules/git/repo_base_nogogit.go +++ b/modules/git/repo_base_nogogit.go @@ -47,7 +47,12 @@ func OpenRepository(ctx context.Context, repoPath string) (*Repository, error) { repoPath, err := filepath.Abs(repoPath) if err != nil { return nil, err - } else if !isDir(repoPath) { + } + exist, err := util.IsDir(repoPath) + if err != nil { + return nil, err + } + if !exist { return nil, util.NewNotExistErrorf("no such file or directory") } @@ -62,7 +67,7 @@ func OpenRepository(ctx context.Context, repoPath string) (*Repository, error) { func (repo *Repository) CatFileBatch(ctx context.Context) (WriteCloserError, *bufio.Reader, func(), error) { if repo.batch == nil { var err error - repo.batch, err = repo.NewBatch(ctx) + repo.batch, err = NewBatch(ctx, repo.Path) if err != nil { return nil, nil, nil, err } @@ -76,7 +81,7 @@ func (repo *Repository) CatFileBatch(ctx context.Context) (WriteCloserError, *bu } log.Debug("Opening temporary cat file batch for: %s", repo.Path) - tempBatch, err := repo.NewBatch(ctx) + tempBatch, err := NewBatch(ctx, repo.Path) if err != nil { return nil, nil, nil, err } @@ -87,7 +92,7 @@ func (repo *Repository) CatFileBatch(ctx context.Context) (WriteCloserError, *bu func (repo *Repository) CatFileBatchCheck(ctx context.Context) (WriteCloserError, *bufio.Reader, func(), error) { if repo.check == nil { var err error - repo.check, err = repo.NewBatchCheck(ctx) + repo.check, err = NewBatchCheck(ctx, repo.Path) if err != nil { return nil, nil, nil, err } @@ -101,7 +106,7 @@ func (repo *Repository) CatFileBatchCheck(ctx context.Context) (WriteCloserError } log.Debug("Opening temporary cat file batch-check for: %s", repo.Path) - tempBatchCheck, err := repo.NewBatchCheck(ctx) + tempBatchCheck, err := NewBatchCheck(ctx, repo.Path) if err != nil { return nil, nil, nil, err } diff --git a/modules/git/repo_commitgraph_gogit.go b/modules/git/repo_commitgraph_gogit.go index d3182f15c6..c0082b62c8 100644 --- a/modules/git/repo_commitgraph_gogit.go +++ b/modules/git/repo_commitgraph_gogit.go @@ -8,7 +8,7 @@ package git import ( "os" - "path" + "path/filepath" gitealog "code.gitea.io/gitea/modules/log" @@ -18,7 +18,7 @@ import ( // CommitNodeIndex returns the index for walking commit graph func (r *Repository) CommitNodeIndex() (cgobject.CommitNodeIndex, *os.File) { - indexPath := path.Join(r.Path, "objects", "info", "commit-graph") + indexPath := filepath.Join(r.Path, "objects", "info", "commit-graph") file, err := os.Open(indexPath) if err == nil { diff --git a/modules/git/repo_ref.go b/modules/git/repo_ref.go index 739cfb972c..554f9f73e1 100644 --- a/modules/git/repo_ref.go +++ b/modules/git/repo_ref.go @@ -19,11 +19,12 @@ func (repo *Repository) GetRefs() ([]*Reference, error) { // refType should only be a literal "branch" or "tag" and nothing else func (repo *Repository) ListOccurrences(ctx context.Context, refType, commitSHA string) ([]string, error) { cmd := NewCommand() - if refType == "branch" { + switch refType { + case "branch": cmd.AddArguments("branch") - } else if refType == "tag" { + case "tag": cmd.AddArguments("tag") - } else { + 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, &RunOpts{Dir: repo.Path}) diff --git a/modules/git/url/url.go b/modules/git/url/url.go index 1c5e8377a6..aa6fa31c5e 100644 --- a/modules/git/url/url.go +++ b/modules/git/url/url.go @@ -133,12 +133,13 @@ func ParseRepositoryURL(ctx context.Context, repoURL string) (*RepositoryURL, er } } - if parsed.URL.Scheme == "http" || parsed.URL.Scheme == "https" { + switch parsed.URL.Scheme { + case "http", "https": if !httplib.IsCurrentGiteaSiteURL(ctx, repoURL) { return ret, nil } fillPathParts(strings.TrimPrefix(parsed.URL.Path, setting.AppSubURL)) - } else if parsed.URL.Scheme == "ssh" || parsed.URL.Scheme == "git+ssh" { + case "ssh", "git+ssh": domainSSH := setting.SSH.Domain domainCur := httplib.GuessCurrentHostDomain(ctx) urlDomain, _, _ := net.SplitHostPort(parsed.URL.Host) @@ -166,9 +167,10 @@ func MakeRepositoryWebLink(repoURL *RepositoryURL) string { // now, let's guess, for example: // * git@github.com:owner/submodule.git // * https://github.com/example/submodule1.git - if repoURL.GitURL.Scheme == "http" || repoURL.GitURL.Scheme == "https" { + switch repoURL.GitURL.Scheme { + case "http", "https": return strings.TrimSuffix(repoURL.GitURL.String(), ".git") - } else if repoURL.GitURL.Scheme == "ssh" || repoURL.GitURL.Scheme == "git+ssh" { + case "ssh", "git+ssh": hostname, _, _ := net.SplitHostPort(repoURL.GitURL.Host) hostname = util.IfZero(hostname, repoURL.GitURL.Host) urlPath := strings.TrimSuffix(repoURL.GitURL.Path, ".git") diff --git a/modules/git/utils.go b/modules/git/utils.go index 56cba9087a..897306efd0 100644 --- a/modules/git/utils.go +++ b/modules/git/utils.go @@ -8,7 +8,6 @@ import ( "encoding/hex" "fmt" "io" - "os" "strconv" "strings" "sync" @@ -41,33 +40,6 @@ func (oc *ObjectCache[T]) Get(id string) (T, bool) { return obj, has } -// isDir returns true if given path is a directory, -// or returns false when it's a file or does not exist. -func isDir(dir string) bool { - f, e := os.Stat(dir) - if e != nil { - return false - } - return f.IsDir() -} - -// isFile returns true if given path is a file, -// or returns false when it's a directory or does not exist. -func isFile(filePath string) bool { - f, e := os.Stat(filePath) - if e != nil { - return false - } - return !f.IsDir() -} - -// isExist checks whether a file or directory exists. -// It returns false when the file or directory does not exist. -func isExist(path string) bool { - _, err := os.Stat(path) - return err == nil || os.IsExist(err) -} - // ConcatenateError concatenats an error with stderr string func ConcatenateError(err error, stderr string) error { if len(stderr) == 0 { diff --git a/modules/gitrepo/branch.go b/modules/gitrepo/branch.go index 9c4bdc5bdf..25ea5abfca 100644 --- a/modules/gitrepo/branch.go +++ b/modules/gitrepo/branch.go @@ -44,24 +44,12 @@ func GetDefaultBranch(ctx context.Context, repo Repository) (string, error) { return git.GetDefaultBranch(ctx, repoPath(repo)) } -func GetWikiDefaultBranch(ctx context.Context, repo Repository) (string, error) { - return git.GetDefaultBranch(ctx, wikiPath(repo)) -} - // IsReferenceExist returns true if given reference exists in the repository. func IsReferenceExist(ctx context.Context, repo Repository, name string) bool { return git.IsReferenceExist(ctx, repoPath(repo), name) } -func IsWikiReferenceExist(ctx context.Context, repo Repository, name string) bool { - return git.IsReferenceExist(ctx, wikiPath(repo), name) -} - // IsBranchExist returns true if given branch exists in the repository. func IsBranchExist(ctx context.Context, repo Repository, name string) bool { return IsReferenceExist(ctx, repo, git.BranchPrefix+name) } - -func IsWikiBranchExist(ctx context.Context, repo Repository, name string) bool { - return IsWikiReferenceExist(ctx, repo, git.BranchPrefix+name) -} diff --git a/modules/gitrepo/gitrepo.go b/modules/gitrepo/gitrepo.go index 5e2ec9ed1e..5da65e2452 100644 --- a/modules/gitrepo/gitrepo.go +++ b/modules/gitrepo/gitrepo.go @@ -8,7 +8,6 @@ import ( "fmt" "io" "path/filepath" - "strings" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/reqctx" @@ -16,21 +15,15 @@ import ( "code.gitea.io/gitea/modules/util" ) +// Repository represents a git repository which stored in a disk type Repository interface { - GetName() string - GetOwnerName() string -} - -func absPath(owner, name string) string { - return filepath.Join(setting.RepoRootPath, strings.ToLower(owner), strings.ToLower(name)+".git") + RelativePath() string // We don't assume how the directory structure of the repository is, so we only need the relative path } +// RelativePath should be an unix style path like username/reponame.git +// This method should change it according to the current OS. func repoPath(repo Repository) string { - return absPath(repo.GetOwnerName(), repo.GetName()) -} - -func wikiPath(repo Repository) string { - return filepath.Join(setting.RepoRootPath, strings.ToLower(repo.GetOwnerName()), strings.ToLower(repo.GetName())+".wiki.git") + return filepath.Join(setting.RepoRootPath, filepath.FromSlash(repo.RelativePath())) } // OpenRepository opens the repository at the given relative path with the provided context. @@ -38,10 +31,6 @@ func OpenRepository(ctx context.Context, repo Repository) (*git.Repository, erro return git.OpenRepository(ctx, repoPath(repo)) } -func OpenWikiRepository(ctx context.Context, repo Repository) (*git.Repository, error) { - return git.OpenRepository(ctx, wikiPath(repo)) -} - // contextKey is a value for use with context.WithValue. type contextKey struct { repoPath string @@ -86,9 +75,8 @@ func DeleteRepository(ctx context.Context, repo Repository) error { } // RenameRepository renames a repository's name on disk -func RenameRepository(ctx context.Context, repo Repository, newName string) error { - newRepoPath := absPath(repo.GetOwnerName(), newName) - if err := util.Rename(repoPath(repo), newRepoPath); err != nil { +func RenameRepository(ctx context.Context, repo, newRepo Repository) error { + if err := util.Rename(repoPath(repo), repoPath(newRepo)); err != nil { return fmt.Errorf("rename repository directory: %w", err) } return nil diff --git a/modules/gitrepo/hooks.go b/modules/gitrepo/hooks.go index cf44f792c6..d9d4a88ff1 100644 --- a/modules/gitrepo/hooks.go +++ b/modules/gitrepo/hooks.go @@ -106,16 +106,11 @@ done return hookNames, hookTpls, giteaHookTpls } -// CreateDelegateHooksForRepo creates all the hooks scripts for the repo -func CreateDelegateHooksForRepo(_ context.Context, repo Repository) (err error) { +// CreateDelegateHooks creates all the hooks scripts for the repo +func CreateDelegateHooks(_ context.Context, repo Repository) (err error) { return createDelegateHooks(filepath.Join(repoPath(repo), "hooks")) } -// CreateDelegateHooksForWiki creates all the hooks scripts for the wiki repo -func CreateDelegateHooksForWiki(_ context.Context, repo Repository) (err error) { - return createDelegateHooks(filepath.Join(wikiPath(repo), "hooks")) -} - func createDelegateHooks(hookDir string) (err error) { hookNames, hookTpls, giteaHookTpls := getHookTemplates() @@ -178,16 +173,11 @@ func ensureExecutable(filename string) error { return os.Chmod(filename, mode) } -// CheckDelegateHooksForRepo checks the hooks scripts for the repo -func CheckDelegateHooksForRepo(_ context.Context, repo Repository) ([]string, error) { +// CheckDelegateHooks checks the hooks scripts for the repo +func CheckDelegateHooks(_ context.Context, repo Repository) ([]string, error) { return checkDelegateHooks(filepath.Join(repoPath(repo), "hooks")) } -// CheckDelegateHooksForWiki checks the hooks scripts for the repo -func CheckDelegateHooksForWiki(_ context.Context, repo Repository) ([]string, error) { - return checkDelegateHooks(filepath.Join(wikiPath(repo), "hooks")) -} - func checkDelegateHooks(hookDir string) ([]string, error) { hookNames, hookTpls, giteaHookTpls := getHookTemplates() diff --git a/modules/gtprof/trace_builtin.go b/modules/gtprof/trace_builtin.go index 41743a25e4..2590ed3a13 100644 --- a/modules/gtprof/trace_builtin.go +++ b/modules/gtprof/trace_builtin.go @@ -40,7 +40,7 @@ func (t *traceBuiltinSpan) toString(out *strings.Builder, indent int) { if t.ts.endTime.IsZero() { out.WriteString(" duration: (not ended)") } else { - out.WriteString(fmt.Sprintf(" duration=%.4fs", t.ts.endTime.Sub(t.ts.startTime).Seconds())) + fmt.Fprintf(out, " duration=%.4fs", t.ts.endTime.Sub(t.ts.startTime).Seconds()) } for _, a := range t.ts.attributes { out.WriteString(" ") diff --git a/modules/indexer/code/bleve/bleve.go b/modules/indexer/code/bleve/bleve.go index e1a381f992..70f0995a01 100644 --- a/modules/indexer/code/bleve/bleve.go +++ b/modules/indexer/code/bleve/bleve.go @@ -16,7 +16,6 @@ import ( "code.gitea.io/gitea/modules/analyze" "code.gitea.io/gitea/modules/charset" "code.gitea.io/gitea/modules/git" - "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" @@ -30,7 +29,6 @@ import ( "github.com/blevesearch/bleve/v2" analyzer_custom "github.com/blevesearch/bleve/v2/analysis/analyzer/custom" analyzer_keyword "github.com/blevesearch/bleve/v2/analysis/analyzer/keyword" - "github.com/blevesearch/bleve/v2/analysis/token/camelcase" "github.com/blevesearch/bleve/v2/analysis/token/lowercase" "github.com/blevesearch/bleve/v2/analysis/token/unicodenorm" "github.com/blevesearch/bleve/v2/analysis/tokenizer/letter" @@ -72,7 +70,7 @@ const ( filenameIndexerAnalyzer = "filenameIndexerAnalyzer" filenameIndexerTokenizer = "filenameIndexerTokenizer" repoIndexerDocType = "repoIndexerDocType" - repoIndexerLatestVersion = 8 + repoIndexerLatestVersion = 9 ) // generateBleveIndexMapping generates a bleve index mapping for the repo indexer @@ -109,7 +107,7 @@ func generateBleveIndexMapping() (mapping.IndexMapping, error) { "type": analyzer_custom.Name, "char_filters": []string{}, "tokenizer": letter.Name, - "token_filters": []string{unicodeNormalizeName, camelcase.Name, lowercase.Name}, + "token_filters": []string{unicodeNormalizeName, lowercase.Name}, }); err != nil { return nil, err } @@ -191,7 +189,8 @@ func (b *Indexer) addUpdate(ctx context.Context, batchWriter git.WriteCloserErro return err } else if !typesniffer.DetectContentType(fileContents).IsText() { // FIXME: UTF-16 files will probably fail here - return nil + // Even if the file is not recognized as a "text file", we could still put its name into the indexers to make the filename become searchable, while leave the content to empty. + fileContents = nil } if _, err = batchReader.Discard(1); err != nil { @@ -217,12 +216,7 @@ func (b *Indexer) addDelete(filename string, repo *repo_model.Repository, batch func (b *Indexer) Index(ctx context.Context, repo *repo_model.Repository, sha string, changes *internal.RepoChanges) error { batch := inner_bleve.NewFlushingBatch(b.inner.Indexer, maxBatchSize) if len(changes.Updates) > 0 { - r, err := gitrepo.OpenRepository(ctx, repo) - if err != nil { - return err - } - defer r.Close() - gitBatch, err := r.NewBatch(ctx) + gitBatch, err := git.NewBatch(ctx, repo.RepoPath()) if err != nil { return err } diff --git a/modules/indexer/code/elasticsearch/elasticsearch.go b/modules/indexer/code/elasticsearch/elasticsearch.go index be8efad5fd..f925ce396a 100644 --- a/modules/indexer/code/elasticsearch/elasticsearch.go +++ b/modules/indexer/code/elasticsearch/elasticsearch.go @@ -15,7 +15,6 @@ import ( "code.gitea.io/gitea/modules/analyze" "code.gitea.io/gitea/modules/charset" "code.gitea.io/gitea/modules/git" - "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" @@ -209,12 +208,7 @@ func (b *Indexer) addDelete(filename string, repo *repo_model.Repository) elasti func (b *Indexer) Index(ctx context.Context, repo *repo_model.Repository, sha string, changes *internal.RepoChanges) error { reqs := make([]elastic.BulkableRequest, 0) if len(changes.Updates) > 0 { - r, err := gitrepo.OpenRepository(ctx, repo) - if err != nil { - return err - } - defer r.Close() - batch, err := r.NewBatch(ctx) + batch, err := git.NewBatch(ctx, repo.RepoPath()) if err != nil { return err } diff --git a/modules/indexer/code/gitgrep/gitgrep.go b/modules/indexer/code/gitgrep/gitgrep.go index 093c189ba3..6f6e0b47b9 100644 --- a/modules/indexer/code/gitgrep/gitgrep.go +++ b/modules/indexer/code/gitgrep/gitgrep.go @@ -26,9 +26,10 @@ func indexSettingToGitGrepPathspecList() (list []string) { func PerformSearch(ctx context.Context, page int, repoID int64, gitRepo *git.Repository, ref git.RefName, keyword string, searchMode indexer.SearchModeType) (searchResults []*code_indexer.Result, total int, err error) { grepMode := git.GrepModeWords - if searchMode == indexer.SearchModeExact { + switch searchMode { + case indexer.SearchModeExact: grepMode = git.GrepModeExact - } else if searchMode == indexer.SearchModeRegexp { + case indexer.SearchModeRegexp: grepMode = git.GrepModeRegexp } res, err := git.GrepSearch(ctx, gitRepo, keyword, git.GrepOptions{ diff --git a/modules/indexer/issues/bleve/bleve.go b/modules/indexer/issues/bleve/bleve.go index 9534b0b750..39d96cab98 100644 --- a/modules/indexer/issues/bleve/bleve.go +++ b/modules/indexer/issues/bleve/bleve.go @@ -5,11 +5,13 @@ package bleve import ( "context" + "strconv" "code.gitea.io/gitea/modules/indexer" indexer_internal "code.gitea.io/gitea/modules/indexer/internal" inner_bleve "code.gitea.io/gitea/modules/indexer/internal/bleve" "code.gitea.io/gitea/modules/indexer/issues/internal" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/util" "github.com/blevesearch/bleve/v2" @@ -246,12 +248,20 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) ( queries = append(queries, inner_bleve.NumericEqualityQuery(options.ProjectColumnID.Value(), "project_board_id")) } - if options.PosterID.Has() { - queries = append(queries, inner_bleve.NumericEqualityQuery(options.PosterID.Value(), "poster_id")) + if options.PosterID != "" { + // "(none)" becomes 0, it means no poster + posterIDInt64, _ := strconv.ParseInt(options.PosterID, 10, 64) + queries = append(queries, inner_bleve.NumericEqualityQuery(posterIDInt64, "poster_id")) } - if options.AssigneeID.Has() { - queries = append(queries, inner_bleve.NumericEqualityQuery(options.AssigneeID.Value(), "assignee_id")) + if options.AssigneeID != "" { + if options.AssigneeID == "(any)" { + queries = append(queries, inner_bleve.NumericRangeInclusiveQuery(optional.Some[int64](1), optional.None[int64](), "assignee_id")) + } else { + // "(none)" becomes 0, it means no assignee + assigneeIDInt64, _ := strconv.ParseInt(options.AssigneeID, 10, 64) + queries = append(queries, inner_bleve.NumericEqualityQuery(assigneeIDInt64, "assignee_id")) + } } if options.MentionID.Has() { diff --git a/modules/indexer/issues/db/options.go b/modules/indexer/issues/db/options.go index 87ce398a20..3b19d742ba 100644 --- a/modules/indexer/issues/db/options.go +++ b/modules/indexer/issues/db/options.go @@ -54,7 +54,7 @@ func ToDBOptions(ctx context.Context, options *internal.SearchOptions) (*issue_m RepoIDs: options.RepoIDs, AllPublic: options.AllPublic, RepoCond: nil, - AssigneeID: optional.Some(convertID(options.AssigneeID)), + AssigneeID: options.AssigneeID, PosterID: options.PosterID, MentionedID: convertID(options.MentionID), ReviewRequestedID: convertID(options.ReviewRequestedID), diff --git a/modules/indexer/issues/dboptions.go b/modules/indexer/issues/dboptions.go index 4f6ad96d22..4e2dff572a 100644 --- a/modules/indexer/issues/dboptions.go +++ b/modules/indexer/issues/dboptions.go @@ -45,11 +45,7 @@ func ToSearchOptions(keyword string, opts *issues_model.IssuesOptions) *SearchOp searchOpt.ProjectID = optional.Some[int64](0) // Those issues with no project(projectid==0) } - if opts.AssigneeID.Value() == db.NoConditionID { - searchOpt.AssigneeID = optional.Some[int64](0) // FIXME: this is inconsistent from other places, 0 means "no assignee" - } else if opts.AssigneeID.Value() != 0 { - searchOpt.AssigneeID = opts.AssigneeID - } + searchOpt.AssigneeID = opts.AssigneeID // See the comment of issues_model.SearchOptions for the reason why we need to convert convertID := func(id int64) optional.Option[int64] { diff --git a/modules/indexer/issues/elasticsearch/elasticsearch.go b/modules/indexer/issues/elasticsearch/elasticsearch.go index 464c0028f2..e3b1b17059 100644 --- a/modules/indexer/issues/elasticsearch/elasticsearch.go +++ b/modules/indexer/issues/elasticsearch/elasticsearch.go @@ -212,12 +212,22 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) ( query.Must(elastic.NewTermQuery("project_board_id", options.ProjectColumnID.Value())) } - if options.PosterID.Has() { - query.Must(elastic.NewTermQuery("poster_id", options.PosterID.Value())) + if options.PosterID != "" { + // "(none)" becomes 0, it means no poster + posterIDInt64, _ := strconv.ParseInt(options.PosterID, 10, 64) + query.Must(elastic.NewTermQuery("poster_id", posterIDInt64)) } - if options.AssigneeID.Has() { - query.Must(elastic.NewTermQuery("assignee_id", options.AssigneeID.Value())) + if options.AssigneeID != "" { + if options.AssigneeID == "(any)" { + q := elastic.NewRangeQuery("assignee_id") + q.Gte(1) + query.Must(q) + } else { + // "(none)" becomes 0, it means no assignee + assigneeIDInt64, _ := strconv.ParseInt(options.AssigneeID, 10, 64) + query.Must(elastic.NewTermQuery("assignee_id", assigneeIDInt64)) + } } if options.MentionID.Has() { diff --git a/modules/indexer/issues/indexer_test.go b/modules/indexer/issues/indexer_test.go index 14dd6ba101..3e38ac49b7 100644 --- a/modules/indexer/issues/indexer_test.go +++ b/modules/indexer/issues/indexer_test.go @@ -44,6 +44,7 @@ func TestDBSearchIssues(t *testing.T) { t.Run("search issues with order", searchIssueWithOrder) t.Run("search issues in project", searchIssueInProject) t.Run("search issues with paginator", searchIssueWithPaginator) + t.Run("search issues with any assignee", searchIssueWithAnyAssignee) } func searchIssueWithKeyword(t *testing.T) { @@ -176,19 +177,19 @@ func searchIssueByID(t *testing.T) { }{ { opts: SearchOptions{ - PosterID: optional.Some(int64(1)), + PosterID: "1", }, expectedIDs: []int64{11, 6, 3, 2, 1}, }, { opts: SearchOptions{ - AssigneeID: optional.Some(int64(1)), + AssigneeID: "1", }, expectedIDs: []int64{6, 1}, }, { - // NOTE: This tests no assignees filtering and also ToSearchOptions() to ensure it will set AssigneeID to 0 when it is passed as -1. - opts: *ToSearchOptions("", &issues.IssuesOptions{AssigneeID: optional.Some(db.NoConditionID)}), + // NOTE: This tests no assignees filtering and also ToSearchOptions() to ensure it handles the filter correctly + opts: *ToSearchOptions("", &issues.IssuesOptions{AssigneeID: "(none)"}), expectedIDs: []int64{22, 21, 16, 15, 14, 13, 12, 11, 20, 5, 19, 18, 10, 7, 4, 9, 8, 3, 2}, }, { @@ -462,3 +463,25 @@ func searchIssueWithPaginator(t *testing.T) { assert.Equal(t, test.expectedTotal, total) } } + +func searchIssueWithAnyAssignee(t *testing.T) { + tests := []struct { + opts SearchOptions + expectedIDs []int64 + expectedTotal int64 + }{ + { + SearchOptions{ + AssigneeID: "(any)", + }, + []int64{17, 6, 1}, + 3, + }, + } + for _, test := range tests { + issueIDs, total, err := SearchIssues(t.Context(), &test.opts) + require.NoError(t, err) + assert.Equal(t, test.expectedIDs, issueIDs) + assert.Equal(t, test.expectedTotal, total) + } +} diff --git a/modules/indexer/issues/internal/model.go b/modules/indexer/issues/internal/model.go index 976e2d696b..0d4f0f727d 100644 --- a/modules/indexer/issues/internal/model.go +++ b/modules/indexer/issues/internal/model.go @@ -97,9 +97,8 @@ type SearchOptions struct { ProjectID optional.Option[int64] // project the issues belong to ProjectColumnID optional.Option[int64] // project column the issues belong to - PosterID optional.Option[int64] // poster of the issues - - AssigneeID optional.Option[int64] // assignee of the issues, zero means no assignee + PosterID string // poster of the issues, "(none)" or "(any)" or a user ID + AssigneeID string // assignee of the issues, "(none)" or "(any)" or a user ID MentionID optional.Option[int64] // mentioned user of the issues diff --git a/modules/indexer/issues/internal/tests/tests.go b/modules/indexer/issues/internal/tests/tests.go index 0483853dfd..6e92c7885c 100644 --- a/modules/indexer/issues/internal/tests/tests.go +++ b/modules/indexer/issues/internal/tests/tests.go @@ -379,7 +379,7 @@ var cases = []*testIndexerCase{ Paginator: &db.ListOptions{ PageSize: 5, }, - PosterID: optional.Some(int64(1)), + PosterID: "1", }, Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { assert.Len(t, result.Hits, 5) @@ -397,7 +397,7 @@ var cases = []*testIndexerCase{ Paginator: &db.ListOptions{ PageSize: 5, }, - AssigneeID: optional.Some(int64(1)), + AssigneeID: "1", }, Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { assert.Len(t, result.Hits, 5) @@ -415,7 +415,7 @@ var cases = []*testIndexerCase{ Paginator: &db.ListOptions{ PageSize: 5, }, - AssigneeID: optional.Some(int64(0)), + AssigneeID: "(none)", }, Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { assert.Len(t, result.Hits, 5) @@ -647,6 +647,21 @@ var cases = []*testIndexerCase{ } }, }, + { + Name: "SearchAnyAssignee", + SearchOptions: &internal.SearchOptions{ + AssigneeID: "(any)", + }, + Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { + assert.Len(t, result.Hits, 180) + for _, v := range result.Hits { + assert.GreaterOrEqual(t, data[v.ID].AssigneeID, int64(1)) + } + assert.Equal(t, countIndexerData(data, func(v *internal.IndexerData) bool { + return v.AssigneeID >= 1 + }), result.Total) + }, + }, } type testIndexerCase struct { diff --git a/modules/indexer/issues/meilisearch/meilisearch.go b/modules/indexer/issues/meilisearch/meilisearch.go index a1746f5954..759a98473f 100644 --- a/modules/indexer/issues/meilisearch/meilisearch.go +++ b/modules/indexer/issues/meilisearch/meilisearch.go @@ -187,12 +187,20 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) ( query.And(inner_meilisearch.NewFilterEq("project_board_id", options.ProjectColumnID.Value())) } - if options.PosterID.Has() { - query.And(inner_meilisearch.NewFilterEq("poster_id", options.PosterID.Value())) - } - - if options.AssigneeID.Has() { - query.And(inner_meilisearch.NewFilterEq("assignee_id", options.AssigneeID.Value())) + if options.PosterID != "" { + // "(none)" becomes 0, it means no poster + posterIDInt64, _ := strconv.ParseInt(options.PosterID, 10, 64) + query.And(inner_meilisearch.NewFilterEq("poster_id", posterIDInt64)) + } + + if options.AssigneeID != "" { + if options.AssigneeID == "(any)" { + query.And(inner_meilisearch.NewFilterGte("assignee_id", 1)) + } else { + // "(none)" becomes 0, it means no assignee + assigneeIDInt64, _ := strconv.ParseInt(options.AssigneeID, 10, 64) + query.And(inner_meilisearch.NewFilterEq("assignee_id", assigneeIDInt64)) + } } if options.MentionID.Has() { diff --git a/modules/json/json.go b/modules/json/json.go index 34568c75c6..acd4118573 100644 --- a/modules/json/json.go +++ b/modules/json/json.go @@ -145,6 +145,12 @@ func Valid(data []byte) bool { // UnmarshalHandleDoubleEncode - due to a bug in xorm (see https://gitea.com/xorm/xorm/pulls/1957) - it's // possible that a Blob may be double encoded or gain an unwanted prefix of 0xff 0xfe. func UnmarshalHandleDoubleEncode(bs []byte, v any) error { + if len(bs) == 0 { + // json.Unmarshal will report errors if input is empty (nil or zero-length) + // It seems that XORM ignores the nil but still passes zero-length string into this function + // To be consistent, we should treat all empty inputs as success + return nil + } err := json.Unmarshal(bs, v) if err != nil { ok := true diff --git a/modules/json/json_test.go b/modules/json/json_test.go new file mode 100644 index 0000000000..ace7167913 --- /dev/null +++ b/modules/json/json_test.go @@ -0,0 +1,18 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package json + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGiteaDBJSONUnmarshal(t *testing.T) { + var m map[any]any + err := UnmarshalHandleDoubleEncode(nil, &m) + assert.NoError(t, err) + err = UnmarshalHandleDoubleEncode([]byte(""), &m) + assert.NoError(t, err) +} diff --git a/modules/log/event_writer_base.go b/modules/log/event_writer_base.go index c327c48ca2..9189ca4e90 100644 --- a/modules/log/event_writer_base.go +++ b/modules/log/event_writer_base.go @@ -105,7 +105,7 @@ func (b *EventWriterBaseImpl) Run(ctx context.Context) { case io.WriterTo: _, err = msg.WriteTo(b.OutputWriteCloser) default: - _, err = b.OutputWriteCloser.Write([]byte(fmt.Sprint(msg))) + _, err = fmt.Fprint(b.OutputWriteCloser, msg) } if err != nil { FallbackErrorf("unable to write log message of %q (%v): %v", b.Name, err, event.Msg) diff --git a/modules/markup/common/linkify.go b/modules/markup/common/linkify.go index 52888958fa..3eecb97eac 100644 --- a/modules/markup/common/linkify.go +++ b/modules/markup/common/linkify.go @@ -85,9 +85,10 @@ func (s *linkifyParser) Parse(parent ast.Node, block text.Reader, pc parser.Cont } else if lastChar == ')' { closing := 0 for i := m[1] - 1; i >= m[0]; i-- { - if line[i] == ')' { + switch line[i] { + case ')': closing++ - } else if line[i] == '(' { + case '(': closing-- } } diff --git a/modules/markup/markdown/goldmark.go b/modules/markup/markdown/goldmark.go index 69c2a96ff1..3021f4bdde 100644 --- a/modules/markup/markdown/goldmark.go +++ b/modules/markup/markdown/goldmark.go @@ -80,9 +80,10 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa // many places render non-comment contents with no mode=document, then these contents also use comment's hard line break setting // especially in many tests. markdownLineBreakStyle := ctx.RenderOptions.Metas["markdownLineBreakStyle"] - if markdownLineBreakStyle == "comment" { + switch markdownLineBreakStyle { + case "comment": v.SetHardLineBreak(setting.Markdown.EnableHardLineBreakInComments) - } else if markdownLineBreakStyle == "document" { + case "document": v.SetHardLineBreak(setting.Markdown.EnableHardLineBreakInDocuments) } } @@ -155,7 +156,7 @@ func (r *HTMLRenderer) renderDocument(w util.BufWriter, source []byte, node ast. if entering { _, err = w.WriteString("<div") if err == nil { - _, err = w.WriteString(fmt.Sprintf(` lang=%q`, val)) + _, err = fmt.Fprintf(w, ` lang=%q`, val) } if err == nil { _, err = w.WriteRune('>') diff --git a/modules/markup/markdown/math/inline_parser.go b/modules/markup/markdown/math/inline_parser.go index a57abe9f9b..d24fd50955 100644 --- a/modules/markup/markdown/math/inline_parser.go +++ b/modules/markup/markdown/math/inline_parser.go @@ -70,10 +70,11 @@ func (parser *inlineParser) Parse(parent ast.Node, block text.Reader, pc parser. startMarkLen = 1 stopMark = parser.endBytesSingleDollar if len(line) > 1 { - if line[1] == '$' { + switch line[1] { + case '$': startMarkLen = 2 stopMark = parser.endBytesDoubleDollar - } else if line[1] == '`' { + case '`': pos := 1 for ; pos < len(line) && line[pos] == '`'; pos++ { } @@ -121,9 +122,10 @@ func (parser *inlineParser) Parse(parent ast.Node, block text.Reader, pc parser. i++ continue } - if line[i] == '{' { + switch line[i] { + case '{': depth++ - } else if line[i] == '}' { + case '}': depth-- } } diff --git a/modules/markup/mdstripper/mdstripper.go b/modules/markup/mdstripper/mdstripper.go index fe0eabb473..c589926b5e 100644 --- a/modules/markup/mdstripper/mdstripper.go +++ b/modules/markup/mdstripper/mdstripper.go @@ -107,11 +107,12 @@ func (r *stripRenderer) processAutoLink(w io.Writer, link []byte) { } var sep string - if parts[3] == "issues" { + switch parts[3] { + case "issues": sep = "#" - } else if parts[3] == "pulls" { + case "pulls": sep = "!" - } else { + default: // Process out of band r.links = append(r.links, linkStr) return diff --git a/modules/optional/option.go b/modules/optional/option.go index af9e5ac852..ccbad259c2 100644 --- a/modules/optional/option.go +++ b/modules/optional/option.go @@ -3,6 +3,8 @@ package optional +import "strconv" + type Option[T any] []T func None[T any]() Option[T] { @@ -43,3 +45,12 @@ func (o Option[T]) ValueOrDefault(v T) T { } return v } + +// ParseBool get the corresponding optional.Option[bool] of a string using strconv.ParseBool +func ParseBool(s string) Option[bool] { + v, e := strconv.ParseBool(s) + if e != nil { + return None[bool]() + } + return Some(v) +} diff --git a/modules/optional/option_test.go b/modules/optional/option_test.go index 203e9221e3..f600ff5a2c 100644 --- a/modules/optional/option_test.go +++ b/modules/optional/option_test.go @@ -57,3 +57,16 @@ func TestOption(t *testing.T) { assert.True(t, opt3.Has()) assert.Equal(t, int(1), opt3.Value()) } + +func Test_ParseBool(t *testing.T) { + assert.Equal(t, optional.None[bool](), optional.ParseBool("")) + assert.Equal(t, optional.None[bool](), optional.ParseBool("x")) + + assert.Equal(t, optional.Some(false), optional.ParseBool("0")) + assert.Equal(t, optional.Some(false), optional.ParseBool("f")) + assert.Equal(t, optional.Some(false), optional.ParseBool("False")) + + assert.Equal(t, optional.Some(true), optional.ParseBool("1")) + assert.Equal(t, optional.Some(true), optional.ParseBool("t")) + assert.Equal(t, optional.Some(true), optional.ParseBool("True")) +} diff --git a/modules/paginator/paginator.go b/modules/paginator/paginator.go index 8258d194c2..0f64e89d9a 100644 --- a/modules/paginator/paginator.go +++ b/modules/paginator/paginator.go @@ -4,6 +4,8 @@ package paginator +import "code.gitea.io/gitea/modules/util" + /* In template: @@ -32,25 +34,43 @@ Output: // Paginator represents a set of results of pagination calculations. type Paginator struct { - total int // total rows count + total int // total rows count, -1 means unknown + totalPages int // total pages count, -1 means unknown + current int // current page number + curRows int // current page rows count + pagingNum int // how many rows in one page - current int // current page number numPages int // how many pages to show on the UI } // New initialize a new pagination calculation and returns a Paginator as result. func New(total, pagingNum, current, numPages int) *Paginator { - if pagingNum <= 0 { - pagingNum = 1 + pagingNum = max(pagingNum, 1) + totalPages := util.Iif(total == -1, -1, (total+pagingNum-1)/pagingNum) + if total >= 0 { + current = min(current, totalPages) } - if current <= 0 { - current = 1 + current = max(current, 1) + return &Paginator{ + total: total, + totalPages: totalPages, + current: current, + pagingNum: pagingNum, + numPages: numPages, } - p := &Paginator{total, pagingNum, current, numPages} - if p.current > p.TotalPages() { - p.current = p.TotalPages() +} + +func (p *Paginator) SetCurRows(rows int) { + // For "unlimited paging", we need to know the rows of current page to determine if there is a next page. + // There is still an edge case: when curRows==pagingNum, then the "next page" will be an empty page. + // Ideally we should query one more row to determine if there is really a next page, but it's impossible in current framework. + p.curRows = rows + if p.total == -1 && p.current == 1 && !p.HasNext() { + // if there is only one page for the "unlimited paging", set total rows/pages count + // then the tmpl could decide to hide the nav bar. + p.total = rows + p.totalPages = util.Iif(p.total == 0, 0, 1) } - return p } // IsFirst returns true if current page is the first page. @@ -72,7 +92,10 @@ func (p *Paginator) Previous() int { // HasNext returns true if there is a next page relative to current page. func (p *Paginator) HasNext() bool { - return p.total > p.current*p.pagingNum + if p.total == -1 { + return p.curRows >= p.pagingNum + } + return p.current*p.pagingNum < p.total } func (p *Paginator) Next() int { @@ -84,10 +107,7 @@ func (p *Paginator) Next() int { // IsLast returns true if current page is the last page. func (p *Paginator) IsLast() bool { - if p.total == 0 { - return true - } - return p.total > (p.current-1)*p.pagingNum && !p.HasNext() + return !p.HasNext() } // Total returns number of total rows. @@ -97,10 +117,7 @@ func (p *Paginator) Total() int { // TotalPages returns number of total pages. func (p *Paginator) TotalPages() int { - if p.total == 0 { - return 1 - } - return (p.total + p.pagingNum - 1) / p.pagingNum + return p.totalPages } // Current returns current page number. @@ -135,10 +152,10 @@ func getMiddleIdx(numPages int) int { // If value is -1 means "..." that more pages are not showing. func (p *Paginator) Pages() []*Page { if p.numPages == 0 { - return []*Page{} - } else if p.numPages == 1 && p.TotalPages() == 1 { + return nil + } else if p.total == -1 || (p.numPages == 1 && p.TotalPages() == 1) { // Only show current page. - return []*Page{{1, true}} + return []*Page{{p.current, true}} } // Total page number is less or equal. diff --git a/modules/paginator/paginator_test.go b/modules/paginator/paginator_test.go index 8a56ee5121..ed46ecea94 100644 --- a/modules/paginator/paginator_test.go +++ b/modules/paginator/paginator_test.go @@ -76,9 +76,7 @@ func TestPaginator(t *testing.T) { t.Run("Only current page", func(t *testing.T) { p := New(0, 10, 1, 1) pages := p.Pages() - assert.Len(t, pages, 1) - assert.Equal(t, 1, pages[0].Num()) - assert.True(t, pages[0].IsCurrent()) + assert.Empty(t, pages) // no "total", so no pages p = New(1, 10, 1, 1) pages = p.Pages() diff --git a/modules/private/hook.go b/modules/private/hook.go index 87d6549f9c..215996b9b9 100644 --- a/modules/private/hook.go +++ b/modules/private/hook.go @@ -7,9 +7,9 @@ import ( "context" "fmt" "net/url" - "time" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/httplib" "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" ) @@ -82,29 +82,32 @@ type HookProcReceiveRefResult struct { HeadBranch string } +func newInternalRequestAPIForHooks(ctx context.Context, hookName, ownerName, repoName string, opts HookOptions) *httplib.Request { + reqURL := setting.LocalURL + fmt.Sprintf("api/internal/hook/%s/%s/%s", hookName, url.PathEscape(ownerName), url.PathEscape(repoName)) + req := newInternalRequestAPI(ctx, reqURL, "POST", opts) + // This "timeout" applies to http.Client's timeout: A Timeout of zero means no timeout. + // This "timeout" was previously set to `time.Duration(60+len(opts.OldCommitIDs))` seconds, but it caused unnecessary timeout failures. + // It should be good enough to remove the client side timeout, only respect the "ctx" and server side timeout. + req.SetReadWriteTimeout(0) + return req +} + // HookPreReceive check whether the provided commits are allowed func HookPreReceive(ctx context.Context, ownerName, repoName string, opts HookOptions) ResponseExtra { - reqURL := setting.LocalURL + fmt.Sprintf("api/internal/hook/pre-receive/%s/%s", url.PathEscape(ownerName), url.PathEscape(repoName)) - req := newInternalRequestAPI(ctx, reqURL, "POST", opts) - req.SetReadWriteTimeout(time.Duration(60+len(opts.OldCommitIDs)) * time.Second) + req := newInternalRequestAPIForHooks(ctx, "pre-receive", ownerName, repoName, opts) _, extra := requestJSONResp(req, &ResponseText{}) return extra } // HookPostReceive updates services and users func HookPostReceive(ctx context.Context, ownerName, repoName string, opts HookOptions) (*HookPostReceiveResult, ResponseExtra) { - reqURL := setting.LocalURL + fmt.Sprintf("api/internal/hook/post-receive/%s/%s", url.PathEscape(ownerName), url.PathEscape(repoName)) - req := newInternalRequestAPI(ctx, reqURL, "POST", opts) - req.SetReadWriteTimeout(time.Duration(60+len(opts.OldCommitIDs)) * time.Second) + req := newInternalRequestAPIForHooks(ctx, "post-receive", ownerName, repoName, opts) return requestJSONResp(req, &HookPostReceiveResult{}) } // HookProcReceive proc-receive hook func HookProcReceive(ctx context.Context, ownerName, repoName string, opts HookOptions) (*HookProcReceiveResult, ResponseExtra) { - reqURL := setting.LocalURL + fmt.Sprintf("api/internal/hook/proc-receive/%s/%s", url.PathEscape(ownerName), url.PathEscape(repoName)) - - req := newInternalRequestAPI(ctx, reqURL, "POST", opts) - req.SetReadWriteTimeout(time.Duration(60+len(opts.OldCommitIDs)) * time.Second) + req := newInternalRequestAPIForHooks(ctx, "proc-receive", ownerName, repoName, opts) return requestJSONResp(req, &HookProcReceiveResult{}) } diff --git a/modules/references/references.go b/modules/references/references.go index a5b102b7f2..592bd4cbe4 100644 --- a/modules/references/references.go +++ b/modules/references/references.go @@ -462,11 +462,12 @@ func findAllIssueReferencesBytes(content []byte, links []string) []*rawReference continue } var sep string - if parts[3] == "issues" { + switch parts[3] { + case "issues": sep = "#" - } else if parts[3] == "pulls" { + case "pulls": sep = "!" - } else { + default: continue } // Note: closing/reopening keywords not supported with URLs diff --git a/modules/repository/create.go b/modules/repository/create.go index b4f7033bd7..a53632bb57 100644 --- a/modules/repository/create.go +++ b/modules/repository/create.go @@ -7,7 +7,6 @@ import ( "context" "fmt" "os" - "path" "path/filepath" "strings" @@ -72,7 +71,7 @@ func CheckDaemonExportOK(ctx context.Context, repo *repo_model.Repository) error } // Create/Remove git-daemon-export-ok for git-daemon... - daemonExportFile := path.Join(repo.RepoPath(), `git-daemon-export-ok`) + daemonExportFile := filepath.Join(repo.RepoPath(), `git-daemon-export-ok`) isExist, err := util.IsExist(daemonExportFile) if err != nil { diff --git a/modules/repository/init.go b/modules/repository/init.go index 3fc1261baa..ace21254ba 100644 --- a/modules/repository/init.go +++ b/modules/repository/init.go @@ -138,7 +138,7 @@ func CheckInitRepository(ctx context.Context, repo *repo_model.Repository) (err // Init git bare new repository. if err = git.InitRepository(ctx, repo.RepoPath(), true, repo.ObjectFormatName); err != nil { return fmt.Errorf("git.InitRepository: %w", err) - } else if err = gitrepo.CreateDelegateHooksForRepo(ctx, repo); err != nil { + } else if err = gitrepo.CreateDelegateHooks(ctx, repo); err != nil { return fmt.Errorf("createDelegateHooks: %w", err) } return nil diff --git a/modules/repository/temp.go b/modules/repository/temp.go index 04faa9db3d..76b9bda4ad 100644 --- a/modules/repository/temp.go +++ b/modules/repository/temp.go @@ -6,7 +6,6 @@ package repository import ( "fmt" "os" - "path" "path/filepath" "code.gitea.io/gitea/modules/log" @@ -19,7 +18,7 @@ func LocalCopyPath() string { if filepath.IsAbs(setting.Repository.Local.LocalCopyPath) { return setting.Repository.Local.LocalCopyPath } - return path.Join(setting.AppDataPath, setting.Repository.Local.LocalCopyPath) + return filepath.Join(setting.AppDataPath, setting.Repository.Local.LocalCopyPath) } // CreateTemporaryPath creates a temporary path diff --git a/modules/setting/config_provider.go b/modules/setting/config_provider.go index 3138f8a63e..b34751e959 100644 --- a/modules/setting/config_provider.go +++ b/modules/setting/config_provider.go @@ -26,6 +26,7 @@ type ConfigKey interface { In(defaultVal string, candidates []string) string String() string Strings(delim string) []string + Bool() (bool, error) MustString(defaultVal string) string MustBool(defaultVal ...bool) bool diff --git a/modules/setting/log.go b/modules/setting/log.go index 50c5779994..614d9ee75a 100644 --- a/modules/setting/log.go +++ b/modules/setting/log.go @@ -7,7 +7,6 @@ import ( "fmt" golog "log" "os" - "path" "path/filepath" "strings" @@ -41,7 +40,7 @@ func loadLogGlobalFrom(rootCfg ConfigProvider) { Log.BufferLen = sec.Key("BUFFER_LEN").MustInt(10000) Log.Mode = sec.Key("MODE").MustString("console") - Log.RootPath = sec.Key("ROOT_PATH").MustString(path.Join(AppWorkPath, "log")) + Log.RootPath = sec.Key("ROOT_PATH").MustString(filepath.Join(AppWorkPath, "log")) if !filepath.IsAbs(Log.RootPath) { Log.RootPath = filepath.Join(AppWorkPath, Log.RootPath) } diff --git a/modules/setting/repository.go b/modules/setting/repository.go index c5619d0f04..43bfb3256d 100644 --- a/modules/setting/repository.go +++ b/modules/setting/repository.go @@ -5,7 +5,6 @@ package setting import ( "os/exec" - "path" "path/filepath" "strings" @@ -284,7 +283,7 @@ func loadRepositoryFrom(rootCfg ConfigProvider) { Repository.GoGetCloneURLProtocol = sec.Key("GO_GET_CLONE_URL_PROTOCOL").MustString("https") Repository.MaxCreationLimit = sec.Key("MAX_CREATION_LIMIT").MustInt(-1) Repository.DefaultBranch = sec.Key("DEFAULT_BRANCH").MustString(Repository.DefaultBranch) - RepoRootPath = sec.Key("ROOT").MustString(path.Join(AppDataPath, "gitea-repositories")) + RepoRootPath = sec.Key("ROOT").MustString(filepath.Join(AppDataPath, "gitea-repositories")) if !filepath.IsAbs(RepoRootPath) { RepoRootPath = filepath.Join(AppWorkPath, RepoRootPath) } else { @@ -363,7 +362,7 @@ func loadRepositoryFrom(rootCfg ConfigProvider) { } if !filepath.IsAbs(Repository.Upload.TempPath) { - Repository.Upload.TempPath = path.Join(AppWorkPath, Repository.Upload.TempPath) + Repository.Upload.TempPath = filepath.Join(AppWorkPath, Repository.Upload.TempPath) } if err := loadRepoArchiveFrom(rootCfg); err != nil { diff --git a/modules/setting/service.go b/modules/setting/service.go index 8c1843eeb7..d9535efec6 100644 --- a/modules/setting/service.go +++ b/modules/setting/service.go @@ -43,7 +43,8 @@ var Service = struct { ShowRegistrationButton bool EnablePasswordSignInForm bool ShowMilestonesDashboardPage bool - RequireSignInView bool + RequireSignInViewStrict bool + BlockAnonymousAccessExpensive bool EnableNotifyMail bool EnableBasicAuth bool EnablePasskeyAuth bool @@ -159,7 +160,18 @@ func loadServiceFrom(rootCfg ConfigProvider) { Service.EmailDomainBlockList = CompileEmailGlobList(sec, "EMAIL_DOMAIN_BLOCKLIST") Service.ShowRegistrationButton = sec.Key("SHOW_REGISTRATION_BUTTON").MustBool(!(Service.DisableRegistration || Service.AllowOnlyExternalRegistration)) Service.ShowMilestonesDashboardPage = sec.Key("SHOW_MILESTONES_DASHBOARD_PAGE").MustBool(true) - Service.RequireSignInView = sec.Key("REQUIRE_SIGNIN_VIEW").MustBool() + + // boolean values are considered as "strict" + var err error + Service.RequireSignInViewStrict, err = sec.Key("REQUIRE_SIGNIN_VIEW").Bool() + if s := sec.Key("REQUIRE_SIGNIN_VIEW").String(); err != nil && s != "" { + // non-boolean value only supports "expensive" at the moment + Service.BlockAnonymousAccessExpensive = s == "expensive" + if !Service.BlockAnonymousAccessExpensive { + log.Fatal("Invalid config option: REQUIRE_SIGNIN_VIEW = %s", s) + } + } + Service.EnableBasicAuth = sec.Key("ENABLE_BASIC_AUTHENTICATION").MustBool(true) Service.EnablePasswordSignInForm = sec.Key("ENABLE_PASSWORD_SIGNIN_FORM").MustBool(true) Service.EnablePasskeyAuth = sec.Key("ENABLE_PASSKEY_AUTHENTICATION").MustBool(true) diff --git a/modules/setting/service_test.go b/modules/setting/service_test.go index 1647bcec16..73736b793a 100644 --- a/modules/setting/service_test.go +++ b/modules/setting/service_test.go @@ -7,16 +7,14 @@ import ( "testing" "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/test" "github.com/gobwas/glob" "github.com/stretchr/testify/assert" ) func TestLoadServices(t *testing.T) { - oldService := Service - defer func() { - Service = oldService - }() + defer test.MockVariableValue(&Service)() cfg, err := NewConfigProviderFromData(` [service] @@ -48,10 +46,7 @@ EMAIL_DOMAIN_BLOCKLIST = d3, *.b } func TestLoadServiceVisibilityModes(t *testing.T) { - oldService := Service - defer func() { - Service = oldService - }() + defer test.MockVariableValue(&Service)() kases := map[string]func(){ ` @@ -130,3 +125,33 @@ ALLOWED_USER_VISIBILITY_MODES = public, limit, privated }) } } + +func TestLoadServiceRequireSignInView(t *testing.T) { + defer test.MockVariableValue(&Service)() + + cfg, err := NewConfigProviderFromData(` +[service] +`) + assert.NoError(t, err) + loadServiceFrom(cfg) + assert.False(t, Service.RequireSignInViewStrict) + assert.False(t, Service.BlockAnonymousAccessExpensive) + + cfg, err = NewConfigProviderFromData(` +[service] +REQUIRE_SIGNIN_VIEW = true +`) + assert.NoError(t, err) + loadServiceFrom(cfg) + assert.True(t, Service.RequireSignInViewStrict) + assert.False(t, Service.BlockAnonymousAccessExpensive) + + cfg, err = NewConfigProviderFromData(` +[service] +REQUIRE_SIGNIN_VIEW = expensive +`) + assert.NoError(t, err) + loadServiceFrom(cfg) + assert.False(t, Service.RequireSignInViewStrict) + assert.True(t, Service.BlockAnonymousAccessExpensive) +} diff --git a/modules/setting/setting.go b/modules/setting/setting.go index 20da796b58..e14997801f 100644 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -12,8 +12,8 @@ import ( "time" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/user" - "code.gitea.io/gitea/modules/util" ) // settings @@ -159,7 +159,7 @@ func loadRunModeFrom(rootCfg ConfigProvider) { // The following is a purposefully undocumented option. Please do not run Gitea as root. It will only cause future headaches. // Please don't use root as a bandaid to "fix" something that is broken, instead the broken thing should instead be fixed properly. unsafeAllowRunAsRoot := ConfigSectionKeyBool(rootSec, "I_AM_BEING_UNSAFE_RUNNING_AS_ROOT") - unsafeAllowRunAsRoot = unsafeAllowRunAsRoot || util.OptionalBoolParse(os.Getenv("GITEA_I_AM_BEING_UNSAFE_RUNNING_AS_ROOT")).Value() + unsafeAllowRunAsRoot = unsafeAllowRunAsRoot || optional.ParseBool(os.Getenv("GITEA_I_AM_BEING_UNSAFE_RUNNING_AS_ROOT")).Value() RunMode = os.Getenv("GITEA_RUN_MODE") if RunMode == "" { RunMode = rootSec.Key("RUN_MODE").MustString("prod") diff --git a/modules/setting/ssh.go b/modules/setting/ssh.go index ea387e521f..46eb49bfd4 100644 --- a/modules/setting/ssh.go +++ b/modules/setting/ssh.go @@ -5,7 +5,6 @@ package setting import ( "os" - "path" "path/filepath" "strings" "text/template" @@ -111,7 +110,7 @@ func loadSSHFrom(rootCfg ConfigProvider) { } homeDir = strings.ReplaceAll(homeDir, "\\", "/") - SSH.RootPath = path.Join(homeDir, ".ssh") + SSH.RootPath = filepath.Join(homeDir, ".ssh") serverCiphers := sec.Key("SSH_SERVER_CIPHERS").Strings(",") if len(serverCiphers) > 0 { SSH.ServerCiphers = serverCiphers diff --git a/modules/setting/storage.go b/modules/setting/storage.go index d3d1fb9f30..e1d9b1fa7a 100644 --- a/modules/setting/storage.go +++ b/modules/setting/storage.go @@ -210,8 +210,8 @@ func getStorageTargetSection(rootCfg ConfigProvider, name, typ string, sec Confi targetSec, _ := rootCfg.GetSection(storageSectionName + "." + name) if targetSec != nil { targetType := targetSec.Key("STORAGE_TYPE").String() - switch { - case targetType == "": + switch targetType { + case "": if targetSec.Key("PATH").String() == "" { // both storage type and path are empty, use default return getDefaultStorageSection(rootCfg), targetSecIsDefault, nil } diff --git a/modules/storage/minio.go b/modules/storage/minio.go index 6b92be61fb..1c5d25b2d4 100644 --- a/modules/storage/minio.go +++ b/modules/storage/minio.go @@ -86,13 +86,14 @@ func NewMinioStorage(ctx context.Context, cfg *setting.Storage) (ObjectStorage, log.Info("Creating Minio storage at %s:%s with base path %s", config.Endpoint, config.Bucket, config.BasePath) var lookup minio.BucketLookupType - if config.BucketLookUpType == "auto" || config.BucketLookUpType == "" { + switch config.BucketLookUpType { + case "auto", "": lookup = minio.BucketLookupAuto - } else if config.BucketLookUpType == "dns" { + case "dns": lookup = minio.BucketLookupDNS - } else if config.BucketLookUpType == "path" { + case "path": lookup = minio.BucketLookupPath - } else { + default: return nil, fmt.Errorf("invalid minio bucket lookup type: %s", config.BucketLookUpType) } diff --git a/modules/templates/util_avatar.go b/modules/templates/util_avatar.go index f7dd408ee2..470e24fa61 100644 --- a/modules/templates/util_avatar.go +++ b/modules/templates/util_avatar.go @@ -34,7 +34,8 @@ func AvatarHTML(src string, size int, class, name string) template.HTML { name = "avatar" } - return template.HTML(`<img loading="lazy" class="` + class + `" src="` + src + `" title="` + html.EscapeString(name) + `" width="` + sizeStr + `" height="` + sizeStr + `"/>`) + // use empty alt, otherwise if the image fails to load, the width will follow the "alt" text's width + return template.HTML(`<img loading="lazy" alt="" class="` + class + `" src="` + src + `" title="` + html.EscapeString(name) + `" width="` + sizeStr + `" height="` + sizeStr + `"/>`) } // Avatar renders user avatars. args: user, size (int), class (string) diff --git a/modules/templates/util_misc.go b/modules/templates/util_misc.go index 2d42bc76b5..cc5bf67b42 100644 --- a/modules/templates/util_misc.go +++ b/modules/templates/util_misc.go @@ -38,10 +38,11 @@ func sortArrow(normSort, revSort, urlSort string, isDefault bool) template.HTML } else { // if sort arg is in url test if it correlates with column header sort arguments // the direction of the arrow should indicate the "current sort order", up means ASC(normal), down means DESC(rev) - if urlSort == normSort { + switch urlSort { + case normSort: // the table is sorted with this header normal return svg.RenderHTML("octicon-triangle-up", 16) - } else if urlSort == revSort { + case revSort: // the table is sorted with this header reverse return svg.RenderHTML("octicon-triangle-down", 16) } diff --git a/modules/util/path.go b/modules/util/path.go index d9f17bd124..0e56348978 100644 --- a/modules/util/path.go +++ b/modules/util/path.go @@ -36,9 +36,10 @@ func PathJoinRel(elem ...string) string { elems[i] = path.Clean("/" + e) } p := path.Join(elems...) - if p == "" { + switch p { + case "": return "" - } else if p == "/" { + case "/": return "." } return p[1:] diff --git a/modules/util/util.go b/modules/util/util.go index 1fb4cb21cb..72fcddbe13 100644 --- a/modules/util/util.go +++ b/modules/util/util.go @@ -11,21 +11,10 @@ import ( "strconv" "strings" - "code.gitea.io/gitea/modules/optional" - "golang.org/x/text/cases" "golang.org/x/text/language" ) -// OptionalBoolParse get the corresponding optional.Option[bool] of a string using strconv.ParseBool -func OptionalBoolParse(s string) optional.Option[bool] { - v, e := strconv.ParseBool(s) - if e != nil { - return optional.None[bool]() - } - return optional.Some(v) -} - // IsEmptyString checks if the provided string is empty func IsEmptyString(s string) bool { return len(strings.TrimSpace(s)) == 0 diff --git a/modules/util/util_test.go b/modules/util/util_test.go index effbc6da1e..fe4125cdb5 100644 --- a/modules/util/util_test.go +++ b/modules/util/util_test.go @@ -8,8 +8,6 @@ import ( "strings" "testing" - "code.gitea.io/gitea/modules/optional" - "github.com/stretchr/testify/assert" ) @@ -175,19 +173,6 @@ func Test_RandomBytes(t *testing.T) { assert.NotEqual(t, bytes3, bytes4) } -func TestOptionalBoolParse(t *testing.T) { - assert.Equal(t, optional.None[bool](), OptionalBoolParse("")) - assert.Equal(t, optional.None[bool](), OptionalBoolParse("x")) - - assert.Equal(t, optional.Some(false), OptionalBoolParse("0")) - assert.Equal(t, optional.Some(false), OptionalBoolParse("f")) - assert.Equal(t, optional.Some(false), OptionalBoolParse("False")) - - assert.Equal(t, optional.Some(true), OptionalBoolParse("1")) - assert.Equal(t, optional.Some(true), OptionalBoolParse("t")) - assert.Equal(t, optional.Some(true), OptionalBoolParse("True")) -} - // Test case for any function which accepts and returns a single string. type StringTest struct { in, out string diff --git a/options/locale/locale_cs-CZ.ini b/options/locale/locale_cs-CZ.ini index c66258b386..a021837002 100644 --- a/options/locale/locale_cs-CZ.ini +++ b/options/locale/locale_cs-CZ.ini @@ -446,7 +446,6 @@ oauth_signup_submit=DokonÄit úÄet oauth_signin_tab=Propojit s existujÃcÃm úÄtem oauth_signin_title=PÅ™ihlaste se pro ověřenà propojeného úÄtu oauth_signin_submit=Propojit úÄet -oauth.signin.error=DoÅ¡lo k chybÄ› pÅ™i zpracovánà žádosti o autorizaci. Pokud tato chyba pÅ™etrvává, obraÅ¥te se na správce webu. oauth.signin.error.access_denied=Žádost o autorizaci byla zamÃtnuta. oauth.signin.error.temporarily_unavailable=Autorizace se nezdaÅ™ila, protože ověřovacà server je doÄasnÄ› nedostupný. Opakujte akci pozdÄ›ji. oauth_callback_unable_auto_reg=Automatická registrace je povolena, ale OAuth2 poskytovatel %[1]s vrátil chybÄ›jÃcà pole: %[2]s, nelze vytvoÅ™it úÄet automaticky, vytvoÅ™te úÄet nebo se pÅ™ipojte k úÄtu, nebo kontaktujte správce webu. @@ -1530,7 +1529,6 @@ issues.filter_project=Projekt issues.filter_project_all=VÅ¡echny projekty issues.filter_project_none=Žádný projekt issues.filter_assignee=Zpracovatel -issues.filter_assginee_no_select=VÅ¡ichni zpracovatelé issues.filter_assginee_no_assignee=Bez zpracovatele issues.filter_poster=Autor issues.filter_user_placeholder=Hledat uživatele @@ -2159,7 +2157,6 @@ settings.advanced_settings=PokroÄilá nastavenà settings.wiki_desc=Povolit Wiki repozitáře settings.use_internal_wiki=PoužÃvat vestavÄ›nou Wiki settings.default_wiki_branch_name=Výchozà název vÄ›tve Wiki -settings.default_permission_everyone_access=Výchozà pÅ™Ãstupová práva pro vÅ¡echny pÅ™ihlášené uživatele: settings.failed_to_change_default_wiki_branch=ZmÄ›na výchozà vÄ›tve wiki se nezdaÅ™ila. settings.use_external_wiki=PoužÃvat externà Wiki settings.external_wiki_url=URL externà Wiki @@ -3679,6 +3676,7 @@ secrets=Tajné klÃÄe description=Tejné klÃÄe budou pÅ™edány urÄitým akcÃm a nelze je pÅ™eÄÃst jinak. none=ZatÃm zde nejsou žádné tajné klÃÄe. creation=PÅ™idat tajný klÃÄ +creation.description=Popis creation.name_placeholder=nerozliÅ¡ovat velká a malá pÃsmena, pouze alfanumerické znaky nebo podtržÃtka, nemohou zaÄÃnat na GITEA_ nebo GITHUB_ creation.value_placeholder=Vložte jakýkoliv obsah. Mezery na zaÄátku a konci budou vynechány. creation.success=Tajný klÃÄ â€ž%s“ byl pÅ™idán. diff --git a/options/locale/locale_de-DE.ini b/options/locale/locale_de-DE.ini index 513ce39137..0b0544b89c 100644 --- a/options/locale/locale_de-DE.ini +++ b/options/locale/locale_de-DE.ini @@ -452,7 +452,6 @@ oauth_signup_submit=Konto vervollständigen oauth_signin_tab=Mit existierendem Konto verbinden oauth_signin_title=Anmelden um verbundenes Konto zu autorisieren oauth_signin_submit=Konto verbinden -oauth.signin.error=Beim Verarbeiten der Autorisierungsanfrage ist ein Fehler aufgetreten. Wenn dieser Fehler weiterhin besteht, wende dich bitte an deinen Administrator. oauth.signin.error.access_denied=Die Autorisierungsanfrage wurde abgelehnt. oauth.signin.error.temporarily_unavailable=Autorisierung fehlgeschlagen, da der Authentifizierungsserver vorübergehend nicht verfügbar ist. Bitte versuch es später erneut. oauth_callback_unable_auto_reg=Automatische Registrierung ist aktiviert, aber der OAuth2-Provider %[1]s hat fehlende Felder zurückgegeben: %[2]s, kann den Account nicht automatisch erstellen. Bitte erstelle oder verbinde einen Account oder kontaktieren den Administrator. @@ -1531,7 +1530,6 @@ issues.filter_project=Projekt issues.filter_project_all=Alle Projekte issues.filter_project_none=Kein Projekt issues.filter_assignee=Zuständig -issues.filter_assginee_no_select=Alle Zuständigen issues.filter_assginee_no_assignee=Niemand zuständig issues.filter_poster=Autor issues.filter_user_placeholder=Benutzer suchen @@ -3670,6 +3668,7 @@ secrets=Secrets description=Secrets werden an bestimmte Aktionen weitergegeben und können nicht anderweitig ausgelesen werden. none=Noch keine Secrets vorhanden. creation=Secret hinzufügen +creation.description=Beschreibung creation.name_placeholder=Groß-/Kleinschreibung wird ignoriert, nur alphanumerische Zeichen oder Unterstriche, darf nicht mit GITEA_ oder GITHUB_ beginnen creation.value_placeholder=Beliebigen Inhalt eingeben. Leerzeichen am Anfang und Ende werden weggelassen. creation.success=Das Secret "%s" wurde hinzugefügt. diff --git a/options/locale/locale_el-GR.ini b/options/locale/locale_el-GR.ini index df5e5e9553..f430c0506c 100644 --- a/options/locale/locale_el-GR.ini +++ b/options/locale/locale_el-GR.ini @@ -390,7 +390,6 @@ oauth_signup_submit=ΟλοκληÏωμÎνος ΛογαÏιασμός oauth_signin_tab=ΣÏνδεση με υπάÏχων λογαÏιασμό oauth_signin_title=Συνδεθείτε για να εγκÏίνετε τον ΣυνδεδεμÎνο ΛογαÏιασμό oauth_signin_submit=ΣÏνδεση ΛογαÏÎ¹Î±ÏƒÎ¼Î¿Ï -oauth.signin.error=ΠαÏουσιάστηκε σφάλμα κατά την επεξεÏγασία του αιτήματος εξουσιοδότησης. Εάν αυτό το σφάλμα επιμÎνει, παÏακαλοÏμε επικοινωνήστε με το διαχειÏιστή του ιστοτόπου. oauth.signin.error.access_denied=Η αίτηση εξουσιοδότησης αποÏÏίφθηκε. oauth.signin.error.temporarily_unavailable=Η εξουσιοδότηση απÎτυχε επειδή ο διακομιστής ταυτοποίησης δεν είναι διαθÎσιμος Ï€ÏοσωÏινά. ΠαÏακαλώ Ï€Ïοσπαθήστε ξανά αÏγότεÏα. openid_connect_submit=ΣÏνδεση @@ -1378,7 +1377,6 @@ issues.filter_project=ΈÏγο issues.filter_project_all=Όλα τα ÎÏγα issues.filter_project_none=ΧωÏίς ÎÏγα issues.filter_assignee=ΑποδÎκτης -issues.filter_assginee_no_select=Όλοι οι αποδÎκτες issues.filter_assginee_no_assignee=ΚανÎνας ΑποδÎκτης issues.filter_poster=ΣυγγÏαφÎας issues.filter_type=ΤÏπος @@ -3338,6 +3336,7 @@ secrets=Μυστικά description=Τα μυστικά θα πεÏάσουν σε οÏισμÎνες δÏάσεις και δεν μποÏοÏν να αναγνωστοÏν αλλοÏ. none=Δεν υπάÏχουν ακόμα μυστικά. creation=Î Ïοσθήκη ÎœÏ…ÏƒÏ„Î¹ÎºÎ¿Ï +creation.description=ΠεÏιγÏαφή creation.name_placeholder=αλφαÏιθμητικοί χαÏακτήÏες ή κάτω παÏλες μόνο, δεν μποÏοÏν να ξεκινοÏν με GITEA_ ή GITHUB_ creation.value_placeholder=Εισάγετε οποιοδήποτε πεÏιεχόμενο. Τα κενά στην αÏχή παÏαλείπονται. creation.success=Το μυστικό "%s" Ï€ÏοστÎθηκε. diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 876e135b22..8ce469359c 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -457,7 +457,7 @@ oauth_signup_submit = Complete Account oauth_signin_tab = Link to Existing Account oauth_signin_title = Sign In to Authorize Linked Account oauth_signin_submit = Link Account -oauth.signin.error = There was an error processing the authorization request. If this error persists, please contact the site administrator. +oauth.signin.error.general = There was an error processing the authorization request: %s. If this error persists, please contact the site administrator. oauth.signin.error.access_denied = The authorization request was denied. oauth.signin.error.temporarily_unavailable = Authorization failed because the authentication server is temporarily unavailable. Please try again later. oauth_callback_unable_auto_reg = Auto Registration is enabled, but OAuth2 Provider %[1]s returned missing fields: %[2]s, unable to create an account automatically, please create or link to an account, or contact the site administrator. @@ -926,6 +926,9 @@ permission_not_set = Not set permission_no_access = No Access permission_read = Read permission_write = Read and Write +permission_anonymous_read = Anonymous Read +permission_everyone_read = Everyone Read +permission_everyone_write = Everyone Write access_token_desc = Selected token permissions limit authorization only to the corresponding <a %s>API</a> routes. Read the <a %s>documentation</a> for more information. at_least_one_permission = You must select at least one permission to create a token permissions_list = Permissions: @@ -1138,6 +1141,7 @@ transfer.no_permission_to_reject = You do not have permission to reject this tra desc.private = Private desc.public = Public +desc.public_access = Public Access desc.template = Template desc.internal = Internal desc.archived = Archived @@ -1547,8 +1551,8 @@ issues.filter_project = Project issues.filter_project_all = All projects issues.filter_project_none = No project issues.filter_assignee = Assignee -issues.filter_assginee_no_select = All assignees -issues.filter_assginee_no_assignee = No assignee +issues.filter_assginee_no_assignee = Assigned to nobody +issues.filter_assignee_any_assignee = Assigned to anybody issues.filter_poster = Author issues.filter_user_placeholder = Search users issues.filter_user_no_select = All users @@ -2133,6 +2137,12 @@ contributors.contribution_type.deletions = Deletions settings = Settings settings.desc = Settings is where you can manage the settings for the repository settings.options = Repository +settings.public_access = Public Access +settings.public_access_desc = Configure public visitor's access permissions to override the defaults of this repository. +settings.public_access.docs.not_set = Not Set: no extra public access permission. The visitor's permission follows the repository's visibility and member permissions. +settings.public_access.docs.anonymous_read = Anonymous Read: users who are not logged in can access the unit with read permission. +settings.public_access.docs.everyone_read = Everyone Read: all logged-in users can access the unit with read permission. Read permission of issues/pull-requests units also means users can create new issues/pull-requests. +settings.public_access.docs.everyone_write = Everyone Write: all logged-in users have write permission to the unit. Only Wiki unit supports this permission. settings.collaboration = Collaborators settings.collaboration.admin = Administrator settings.collaboration.write = Write @@ -2179,7 +2189,6 @@ settings.advanced_settings = Advanced Settings settings.wiki_desc = Enable Repository Wiki settings.use_internal_wiki = Use Built-In Wiki settings.default_wiki_branch_name = Default Wiki Branch Name -settings.default_permission_everyone_access = Default access permission for all signed-in users: settings.failed_to_change_default_wiki_branch = Failed to change the default wiki branch. settings.use_external_wiki = Use External Wiki settings.external_wiki_url = External Wiki URL diff --git a/options/locale/locale_es-ES.ini b/options/locale/locale_es-ES.ini index f856eaebd6..debf9a6f5d 100644 --- a/options/locale/locale_es-ES.ini +++ b/options/locale/locale_es-ES.ini @@ -387,7 +387,6 @@ oauth_signup_submit=Completar Cuenta oauth_signin_tab=Vincular a una Cuenta Existente oauth_signin_title=RegÃstrese para autorizar cuenta vinculada oauth_signin_submit=Vincular Cuenta -oauth.signin.error=Hubo un error al procesar la solicitud de autorización. Si este error persiste, póngase en contacto con el administrador del sitio. oauth.signin.error.access_denied=La solicitud de autorización fue denegada. oauth.signin.error.temporarily_unavailable=La autorización falló porque el servidor de autenticación no está disponible temporalmente. Inténtalo de nuevo más tarde. openid_connect_submit=Conectar @@ -1368,7 +1367,6 @@ issues.filter_project=Proyecto issues.filter_project_all=Todos los proyectos issues.filter_project_none=Ningún proyecto issues.filter_assignee=Asignada a -issues.filter_assginee_no_select=Todos los asignados issues.filter_assginee_no_assignee=Sin asignado issues.filter_poster=Autor issues.filter_type=Tipo @@ -3318,6 +3316,7 @@ secrets=Secretos description=Los secretos pasarán a ciertas acciones y no se podrán leer de otro modo. none=TodavÃa no hay secretos. creation=Añadir secreto +creation.description=Descripción creation.name_placeholder=sin distinción de mayúsculas, solo carácteres alfanuméricos o guiones bajos, no puede empezar por GITEA_ o GITHUB_ creation.value_placeholder=Introduce cualquier contenido. Se omitirá el espacio en blanco en el inicio y el final. creation.success=El secreto "%s" ha sido añadido. diff --git a/options/locale/locale_fa-IR.ini b/options/locale/locale_fa-IR.ini index c82cfc61bd..5c96cea8f6 100644 --- a/options/locale/locale_fa-IR.ini +++ b/options/locale/locale_fa-IR.ini @@ -1059,7 +1059,6 @@ issues.filter_label_no_select=تمامی برچسب‎ها issues.filter_milestone=نقطه عط٠issues.filter_project_none=هیچ پروژه ثبت نشده issues.filter_assignee=مسئول رسیدگی -issues.filter_assginee_no_select=تمامی مسئولان رسیدگی issues.filter_assginee_no_assignee=بدون مسئول رسیدگی issues.filter_type=نوع issues.filter_type.all_issues=همه مسائل @@ -2513,6 +2512,7 @@ conan.details.repository=مخزن owner.settings.cleanuprules.enabled=Ùعال شده [secrets] +creation.description=Ø´Ø±Ø [actions] diff --git a/options/locale/locale_fi-FI.ini b/options/locale/locale_fi-FI.ini index d78a06ae20..e853273375 100644 --- a/options/locale/locale_fi-FI.ini +++ b/options/locale/locale_fi-FI.ini @@ -1694,6 +1694,7 @@ conan.details.repository=Repo owner.settings.cleanuprules.enabled=Käytössä [secrets] +creation.description=Kuvaus [actions] diff --git a/options/locale/locale_fr-FR.ini b/options/locale/locale_fr-FR.ini index 11a7960bc9..150101f3e2 100644 --- a/options/locale/locale_fr-FR.ini +++ b/options/locale/locale_fr-FR.ini @@ -113,6 +113,7 @@ copy_type_unsupported=Ce type de fichier ne peut pas être copié write=Écrire preview=Aperçu loading=Chargement… +files=Fichiers error=Erreur error404=La page que vous essayez d'atteindre <strong>n'existe pas</strong> ou <strong>vous n'êtes pas autorisé</strong> à la voir. @@ -169,6 +170,10 @@ search=Rechercher… type_tooltip=Type de recherche fuzzy=Approximative fuzzy_tooltip=Inclure également les résultats proches de la recherche +words=Mots +words_tooltip=Inclure uniquement les résultats qui correspondent exactement aux mots recherchés +regexp=Regexp +regexp_tooltip=Inclure uniquement les résultats qui correspondent à l’expression régulière recherchée exact=Exact exact_tooltip=Inclure uniquement les résultats qui correspondent exactement au terme de recherche repo_kind=Chercher des dépôts… @@ -452,7 +457,7 @@ oauth_signup_submit=Finaliser la création du compte oauth_signin_tab=Lier à un compte existant oauth_signin_title=Connectez-vous pour autoriser le compte lié oauth_signin_submit=Lier un compte -oauth.signin.error=Une erreur s'est produite lors du traitement de la demande d'autorisation. Si cette erreur persiste, veuillez contacter l'administrateur du site. +oauth.signin.error.general=Une erreur s’est produite lors du traitement de la demande d’autorisation : %s. Si l’erreur persiste, veuillez contacter l’administrateur du site. oauth.signin.error.access_denied=La demande d'autorisation a été refusée. oauth.signin.error.temporarily_unavailable=L'autorisation a échoué car le serveur d'authentification est temporairement indisponible. Veuillez réessayer plus tard. oauth_callback_unable_auto_reg=L’inscription automatique est activée, mais le fournisseur OAuth2 %[1]s a signalé des champs manquants : %[2]s, impossible de créer un compte automatiquement, veuillez créer ou lier un compte, ou bien contacter l’administrateur du site. @@ -1403,6 +1408,7 @@ commits.signed_by_untrusted_user_unmatched=Signature discordante de l'auteur de commits.gpg_key_id=ID de la clé GPG commits.ssh_key_fingerprint=Empreinte numérique de la clé SSH commits.view_path=Voir à ce point de l'historique +commits.view_file_diff=Voir les modifications du fichier dans cette révision commit.operations=Opérations commit.revert=Rétablir @@ -1540,8 +1546,6 @@ issues.filter_project=Projet issues.filter_project_all=Tous les projets issues.filter_project_none=Aucun projet issues.filter_assignee=Assigné -issues.filter_assginee_no_select=Tous les assignés -issues.filter_assginee_no_assignee=Aucun assigné issues.filter_poster=Auteur issues.filter_user_placeholder=Rechercher des utilisateurs issues.filter_user_no_select=Tous les utilisateurs @@ -2172,7 +2176,6 @@ settings.advanced_settings=Paramètres avancés settings.wiki_desc=Activer le wiki du dépôt settings.use_internal_wiki=Utiliser le wiki interne settings.default_wiki_branch_name=Nom de la branche du Wiki par défaut -settings.default_permission_everyone_access=Autorisation d’accès par défaut pour tous les utilisateurs connectés : settings.failed_to_change_default_wiki_branch=Impossible de modifier la branche du wiki par défaut. settings.use_external_wiki=Utiliser un wiki externe settings.external_wiki_url=URL Wiki externe @@ -3705,8 +3708,10 @@ secrets=Secrets description=Les secrets seront transmis à certaines actions et ne pourront pas être lus autrement. none=Il n'y a pas encore de secrets. creation=Ajouter un secret +creation.description=Description creation.name_placeholder=Caractères alphanumériques ou tirets bas uniquement, insensibles à la casse, ne peut commencer par GITEA_ ou GITHUB_. creation.value_placeholder=Entrez n’importe quoi. Les blancs cernant seront taillés. +creation.description_placeholder=Décrire brièvement votre dépôt (optionnel). creation.success=Le secret "%s" a été ajouté. creation.failed=Impossible d'ajouter le secret. deletion=Supprimer le secret diff --git a/options/locale/locale_ga-IE.ini b/options/locale/locale_ga-IE.ini index 2371e807e2..bb2bf6f15f 100644 --- a/options/locale/locale_ga-IE.ini +++ b/options/locale/locale_ga-IE.ini @@ -113,6 +113,7 @@ copy_type_unsupported=Nà féidir an cineál comhaid seo a chóipeáil write=ScrÃobh preview=Réamhamharc loading=à lódáil... +files=Comhaid error=Earráid error404=NÃl an leathanach atá tú ag iarraidh a bhaint amach <strong>ann</strong> nó <strong>nÃl tú údaraithe</strong> chun é a fheiceáil. @@ -169,6 +170,10 @@ search=Cuardaigh... type_tooltip=Cineál cuardaigh fuzzy=Doiléir fuzzy_tooltip=Cuir san áireamh torthaà a mheaitseálann an téarma cuardaigh go dlúth freisin +words=Focail +words_tooltip=Ná cuir san áireamh ach torthaà a mheaitseálann na focail téarma cuardaigh +regexp=Nathanna Rialta +regexp_tooltip=Ná cuir ach torthaà a mheaitseálann an téarma cuardaigh nathanna rialta san áireamh exact=Beacht exact_tooltip=Nà chuir san áireamh ach torthaà a mheaitseálann leis an téarma repo_kind=Cuardaigh stórtha... @@ -452,7 +457,7 @@ oauth_signup_submit=Cuntas Comhlánaigh oauth_signin_tab=Nasc leis an gCuntas Reatha oauth_signin_title=SÃnigh isteach chun Cuntas Nasctha a Údarú oauth_signin_submit=Cuntas Nasc -oauth.signin.error=Bhà earráid ann ag próiseáil an t-iarratas ar údarú. Má leanann an earráid seo, déan teagmháil le riarthóir an láithreáin. +oauth.signin.error.general=Tharla earráid agus an t-iarratas údaraithe á phróiseáil: %s. Má leanann an earráid seo, téigh i dteagmháil le riarthóir an tsuÃmh. oauth.signin.error.access_denied=DiúltaÃodh an t-iarratas ar údarú. oauth.signin.error.temporarily_unavailable=Theip ar údarú toisc nach bhfuil an fhreastalaà fÃordheimhnithe ar fáil Bain triail as arÃs nÃos déanaÃ. oauth_callback_unable_auto_reg=Tá Clárú UathoibrÃoch cumasaithe, ach sheol Soláthraà OAuth2 %[1]s réimsà in easnamh ar ais: %[2]s, nà raibh sé in ann cuntas a chruthú go huathoibrÃoch, cruthaigh nó nasc le cuntas, nó déan teagmháil le riarthóir an tsuÃmh. @@ -1403,6 +1408,7 @@ commits.signed_by_untrusted_user_unmatched=SÃnithe ag úsáideoir neamhiontaofa commits.gpg_key_id=GPG Eochair ID commits.ssh_key_fingerprint=Méarloirg Eochair SSH commits.view_path=Féach ag an bpointe seo sa stair +commits.view_file_diff=Féach ar athruithe ar an gcomhad seo sa tiomantas seo commit.operations=OibrÃochtaà commit.revert=Téigh ar ais @@ -1540,8 +1546,8 @@ issues.filter_project=Tionscadal issues.filter_project_all=Gach tionscadal issues.filter_project_none=Gan aon tionscadal issues.filter_assignee=Sannaitheoir -issues.filter_assginee_no_select=Gach sannaithe -issues.filter_assginee_no_assignee=Gan sannaitheoir +issues.filter_assginee_no_assignee=Sannta do dhuine ar bith +issues.filter_assignee_any_assignee=Sannta do dhuine ar bith issues.filter_poster=Údar issues.filter_user_placeholder=Cuardaigh úsáideoirà issues.filter_user_no_select=Gach úsáideoir @@ -2172,7 +2178,6 @@ settings.advanced_settings=Ardsocruithe settings.wiki_desc=Cumasaigh Stór Vicà settings.use_internal_wiki=Úsáid Vicà Insuite settings.default_wiki_branch_name=Ainm Brainse Réamhshocraithe Vicà -settings.default_permission_everyone_access=Cead rochtana réamhshocraithe do gach úsáideoir sÃnithe isteach: settings.failed_to_change_default_wiki_branch=Theip ar an brainse réamhshocraithe vicà a athrú. settings.use_external_wiki=Úsáid Vicà Seachtrach settings.external_wiki_url=URL Vicà Seachtrach @@ -3705,8 +3710,10 @@ secrets=Rúin description=Cuirfear rúin ar aghaidh chuig gnÃomhartha áirithe agus nà féidir iad a léamh ar mhalairt. none=NÃl aon rúin ann fós. creation=Cuir Rúnda leis +creation.description=Cur sÃos creation.name_placeholder=carachtair alfanumair nó Ãoslaghda amháin nach féidir a thosú le GITEA_ nó GITHUB_ creation.value_placeholder=Ionchur ábhar ar bith. Fágfar spás bán ag tús agus ag deireadh ar lár. +creation.description_placeholder=Cuir isteach cur sÃos gairid (roghnach). creation.success=Tá an rún "%s" curtha leis. creation.failed=Theip ar an rún a chur leis. deletion=Bain rún diff --git a/options/locale/locale_hu-HU.ini b/options/locale/locale_hu-HU.ini index 08a71c27d2..a57d6960dd 100644 --- a/options/locale/locale_hu-HU.ini +++ b/options/locale/locale_hu-HU.ini @@ -774,8 +774,6 @@ issues.filter_label=CÃmke issues.filter_label_no_select=Minden cÃmke issues.filter_milestone=MérföldkÅ‘ issues.filter_assignee=MegbÃzott -issues.filter_assginee_no_select=Minden megbÃzott -issues.filter_assginee_no_assignee=Nincs megbÃzott issues.filter_type=TÃpus issues.filter_type.all_issues=Minden hibajegy issues.filter_type.assigned_to_you=Hozzám rendelt @@ -1598,6 +1596,7 @@ conan.details.repository=Tároló owner.settings.cleanuprules.enabled=Engedélyezett [secrets] +creation.description=LeÃrás [actions] diff --git a/options/locale/locale_id-ID.ini b/options/locale/locale_id-ID.ini index 2beade34ad..c54bfbb924 100644 --- a/options/locale/locale_id-ID.ini +++ b/options/locale/locale_id-ID.ini @@ -763,7 +763,6 @@ issues.delete_branch_at=`telah dihapus cabang <b>%s</b> %s` issues.filter_label=Label issues.filter_milestone=Tonggak issues.filter_assignee=Menerima -issues.filter_assginee_no_assignee=Tidak ada yang menerima issues.filter_type=Tipe issues.filter_type.all_issues=Semua masalah issues.filter_type.assigned_to_you=Ditugaskan kepada anda @@ -1398,6 +1397,7 @@ conan.details.repository=Repositori owner.settings.cleanuprules.enabled=Aktif [secrets] +creation.description=Deskripsi [actions] diff --git a/options/locale/locale_is-IS.ini b/options/locale/locale_is-IS.ini index 98a8615130..ee5eb4a7dd 100644 --- a/options/locale/locale_is-IS.ini +++ b/options/locale/locale_is-IS.ini @@ -1326,6 +1326,7 @@ npm.details.tag=Merki pypi.requires=Þarfnast Python [secrets] +creation.description=Lýsing [actions] diff --git a/options/locale/locale_it-IT.ini b/options/locale/locale_it-IT.ini index 48a43210bf..3ce5a6770f 100644 --- a/options/locale/locale_it-IT.ini +++ b/options/locale/locale_it-IT.ini @@ -323,7 +323,6 @@ oauth_signup_submit=Completa l'Account oauth_signin_tab=Collegamento ad un Account Esistente oauth_signin_title=Accedi per autorizzare l' Account collegato oauth_signin_submit=Collega Account -oauth.signin.error=Si è verificato un errore nell'elaborazione della richiesta di autorizzazione. Se questo errore persiste, si prega di contattare l'amministratore del sito. oauth.signin.error.access_denied=La richiesta di autorizzazione è stata negata. oauth.signin.error.temporarily_unavailable=Autorizzazione non riuscita perché il server di autenticazione non è temporaneamente disponibile. Riprova più tardi. openid_connect_submit=Connetti @@ -1144,7 +1143,6 @@ issues.filter_milestone=Traguardo issues.filter_project=Progetto issues.filter_project_none=Nessun progetto issues.filter_assignee=Assegnatario -issues.filter_assginee_no_select=Tutte le assegnazioni issues.filter_assginee_no_assignee=Nessun assegnatario issues.filter_poster=Autore issues.filter_type=Tipo @@ -2790,6 +2788,7 @@ settings.delete.error=Impossibile eliminare il pacchetto. owner.settings.cleanuprules.enabled=Attivo [secrets] +creation.description=Descrizione [actions] diff --git a/options/locale/locale_ja-JP.ini b/options/locale/locale_ja-JP.ini index b33f794bdb..7fe0f4332d 100644 --- a/options/locale/locale_ja-JP.ini +++ b/options/locale/locale_ja-JP.ini @@ -113,6 +113,7 @@ copy_type_unsupported=ã“ã®ãƒ•ã‚¡ã‚¤ãƒ«ã‚¿ã‚¤ãƒ—ã¯ã‚³ãƒ”ーã§ãã¾ã›ã‚“ write=書ã込㿠preview=プレビュー loading=èªã¿è¾¼ã¿ä¸â€¦ +files=ファイル error=エラー error404=アクセスã—よã†ã¨ã—ãŸãƒšãƒ¼ã‚¸ã¯<strong>å˜åœ¨ã—ãªã„</strong>ã‹ã€é–²è¦§ãŒ<strong>許å¯ã•ã‚Œã¦ã„ã¾ã›ã‚“</strong>。 @@ -169,6 +170,10 @@ search=検索… type_tooltip=検索タイプ fuzzy=ã‚ã„ã¾ã„ fuzzy_tooltip=検索語ã«ãŠãŠã‚ˆã一致ã™ã‚‹çµæžœã‚‚å«ã‚ã¾ã™ +words=å˜èªž +words_tooltip=検索語ã¨ä¸€è‡´ã™ã‚‹çµæžœã ã‘ã‚’å«ã‚ã¾ã™ +regexp=æ£è¦è¡¨ç¾ +regexp_tooltip=æ£è¦è¡¨ç¾æ¤œç´¢ãƒ‘ターンã¨ä¸€è‡´ã™ã‚‹çµæžœã ã‘ã‚’å«ã‚ã¾ã™ exact=完全一致 exact_tooltip=検索語ã¨å®Œå…¨ã«ä¸€è‡´ã™ã‚‹çµæžœã ã‘ã‚’å«ã‚ã¾ã™ repo_kind=リãƒã‚¸ãƒˆãƒªã‚’検索... @@ -452,7 +457,7 @@ oauth_signup_submit=アカウント登録完了 oauth_signin_tab=æ—¢å˜ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã«ãƒªãƒ³ã‚¯ oauth_signin_title=リンク先アカウントèªå¯ã®ãŸã‚サインイン oauth_signin_submit=アカウントã«ãƒªãƒ³ã‚¯ -oauth.signin.error=èªå¯ãƒªã‚¯ã‚¨ã‚¹ãƒˆã®å‡¦ç†ä¸ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚ã“ã®ã‚¨ãƒ©ãƒ¼ãŒè§£æ±ºã—ãªã„å ´åˆã¯ã€ã‚µã‚¤ãƒˆç®¡ç†è€…ã«å•ã„åˆã‚ã›ã¦ãã ã•ã„。 +oauth.signin.error.general=èªå¯ãƒªã‚¯ã‚¨ã‚¹ãƒˆã®å‡¦ç†ä¸ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸ: %s 。ã“ã®ã‚¨ãƒ©ãƒ¼ãŒè§£æ±ºã—ãªã„å ´åˆã¯ã€ã‚µã‚¤ãƒˆç®¡ç†è€…ã«å•ã„åˆã‚ã›ã¦ãã ã•ã„。 oauth.signin.error.access_denied=èªå¯ãƒªã‚¯ã‚¨ã‚¹ãƒˆãŒæ‹’å¦ã•ã‚Œã¾ã—ãŸã€‚ oauth.signin.error.temporarily_unavailable=èªè¨¼ã‚µãƒ¼ãƒãƒ¼ãŒä¸€æ™‚çš„ã«åˆ©ç”¨ã§ããªã„ãŸã‚ã€èªå¯ã«å¤±æ•—ã—ã¾ã—ãŸã€‚後ã§ã‚‚ã†ä¸€åº¦ã‚„ã‚Šç›´ã—ã¦ãã ã•ã„。 oauth_callback_unable_auto_reg=自動登録ãŒæœ‰åŠ¹ã«ãªã£ã¦ã„ã¾ã™ãŒã€OAuth2プãƒãƒã‚¤ãƒ€ãƒ¼ %[1]s ã®å¿œç”ã¯ãƒ•ã‚£ãƒ¼ãƒ«ãƒ‰ %[2]s ãŒä¸è¶³ã—ã¦ãŠã‚Šã€è‡ªå‹•ã§ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã‚’作æˆã™ã‚‹ã“ã¨ãŒã§ãã¾ã›ã‚“。 アカウントを作æˆã¾ãŸã¯ãƒªãƒ³ã‚¯ã™ã‚‹ã‹ã€ã‚µã‚¤ãƒˆç®¡ç†è€…ã«å•ã„åˆã‚ã›ã¦ãã ã•ã„。 @@ -1403,6 +1408,7 @@ commits.signed_by_untrusted_user_unmatched=コミッターã¨ä¸€è‡´ã—ãªã„ä¿¡é commits.gpg_key_id=GPGã‚ーID commits.ssh_key_fingerprint=SSHéµã®ãƒ•ã‚£ãƒ³ã‚¬ãƒ¼ãƒ—リント commits.view_path=ã“ã®æ™‚点を表示 +commits.view_file_diff=ã“ã®ãƒ•ã‚¡ã‚¤ãƒ«ã®ã€ã“ã®ã‚³ãƒŸãƒƒãƒˆã§ã®å¤‰æ›´å†…容を表示 commit.operations=æ“作 commit.revert=リãƒãƒ¼ãƒˆ @@ -1463,6 +1469,8 @@ issues.filter_milestones=マイルストーンã®çµžã‚Šè¾¼ã¿ issues.filter_projects=プãƒã‚¸ã‚§ã‚¯ãƒˆã®çµžã‚Šè¾¼ã¿ issues.filter_labels=ラベルã®çµžã‚Šè¾¼ã¿ issues.filter_reviewers=レビューアã®çµžã‚Šè¾¼ã¿ +issues.filter_no_results=検索çµæžœãªã— +issues.filter_no_results_placeholder=検索フィルターを変ãˆã¦ã¿ã¦ä¸‹ã•ã„。 issues.new=æ–°ã—ã„イシュー issues.new.title_empty=タイトルã¯ç©ºã«ã§ãã¾ã›ã‚“ issues.new.labels=ラベル @@ -1538,8 +1546,8 @@ issues.filter_project=プãƒã‚¸ã‚§ã‚¯ãƒˆ issues.filter_project_all=ã™ã¹ã¦ã®ãƒ—ãƒã‚¸ã‚§ã‚¯ãƒˆ issues.filter_project_none=プãƒã‚¸ã‚§ã‚¯ãƒˆãªã— issues.filter_assignee=担当者 -issues.filter_assginee_no_select=ã™ã¹ã¦ã®æ‹…当者 issues.filter_assginee_no_assignee=担当者ãªã— +issues.filter_assignee_any_assignee=担当者ã‚ã‚Š issues.filter_poster=作æˆè€… issues.filter_user_placeholder=ユーザーを検索 issues.filter_user_no_select=ã™ã¹ã¦ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ @@ -1935,6 +1943,7 @@ pulls.outdated_with_base_branch=ã“ã®ãƒ–ランãƒã¯ãƒ™ãƒ¼ã‚¹ãƒ–ランãƒã«å¯¾ pulls.close=プルリクエストをクãƒãƒ¼ã‚º pulls.closed_at=`ãŒãƒ—ルリクエストをクãƒãƒ¼ã‚º <a id="%[1]s" href="#%[1]s">%[2]s</a>` pulls.reopened_at=`ãŒãƒ—ルリクエストをå†ã‚ªãƒ¼ãƒ—ン <a id="%[1]s" href="#%[1]s">%[2]s</a>` +pulls.cmd_instruction_hint=コマンドラインã®æ‰‹é †ã‚’表示 pulls.cmd_instruction_checkout_title=ãƒã‚§ãƒƒã‚¯ã‚¢ã‚¦ãƒˆ pulls.cmd_instruction_checkout_desc=プãƒã‚¸ã‚§ã‚¯ãƒˆãƒªãƒã‚¸ãƒˆãƒªã‹ã‚‰æ–°ã—ã„ブランãƒã‚’ãƒã‚§ãƒƒã‚¯ã‚¢ã‚¦ãƒˆã—ã€å¤‰æ›´å†…容をテストã—ã¾ã™ã€‚ pulls.cmd_instruction_merge_title=マージ @@ -2169,7 +2178,6 @@ settings.advanced_settings=æ‹¡å¼µè¨å®š settings.wiki_desc=Wikiを有効ã«ã™ã‚‹ settings.use_internal_wiki=ビルトインã®Wikiを使用ã™ã‚‹ settings.default_wiki_branch_name=デフォルトã®Wikiブランãƒå -settings.default_permission_everyone_access=ã™ã¹ã¦ã®ã‚µã‚¤ãƒ³ã‚¤ãƒ³ãƒ¦ãƒ¼ã‚¶ãƒ¼ã«ãƒ‡ãƒ•ã‚©ãƒ«ãƒˆã§è¨±å¯ã™ã‚‹ã‚¢ã‚¯ã‚»ã‚¹æ¨©é™: settings.failed_to_change_default_wiki_branch=デフォルトã®Wikiブランãƒã‚’変更ã§ãã¾ã›ã‚“ã§ã—ãŸã€‚ settings.use_external_wiki=外部ã®Wikiを使用ã™ã‚‹ settings.external_wiki_url=外部Wikiã®URL @@ -2376,6 +2384,9 @@ settings.event_pull_request_review_request=プルリクエストã®ãƒ¬ãƒ“ューä settings.event_pull_request_review_request_desc=プルリクエストã®ãƒ¬ãƒ“ューãŒä¾é ¼ã•ã‚ŒãŸã¨ãã€ã¾ãŸã¯ä¾é ¼ãŒå‰Šé™¤ã•ã‚ŒãŸã¨ã。 settings.event_pull_request_approvals=プルリクエストã®æ‰¿èª settings.event_pull_request_merge=プルリクエストã®ãƒžãƒ¼ã‚¸ +settings.event_header_workflow=ワークフãƒãƒ¼ã‚¤ãƒ™ãƒ³ãƒˆ +settings.event_workflow_job=ワークフãƒãƒ¼ã‚¸ãƒ§ãƒ– +settings.event_workflow_job_desc=Gitea Actions ã®ãƒ¯ãƒ¼ã‚¯ãƒ•ãƒãƒ¼ã‚¸ãƒ§ãƒ–ãŒã€ã‚ューã«è¿½åŠ ã€å¾…æ©Ÿä¸ã€å®Ÿè¡Œä¸ã€å®Œäº†ã«ãªã£ãŸã¨ã。 settings.event_package=パッケージ settings.event_package_desc=リãƒã‚¸ãƒˆãƒªã«ãƒ‘ッケージãŒä½œæˆã¾ãŸã¯å‰Šé™¤ã•ã‚ŒãŸã¨ã。 settings.branch_filter=ブランムフィルター @@ -3699,8 +3710,10 @@ secrets=シークレット description=シークレットã¯ç‰¹å®šã®Actionsã«æ¸¡ã•ã‚Œã¾ã™ã€‚ ãれ以外ã§èªã¿å‡ºã•ã‚Œã‚‹ã“ã¨ã¯ã‚ã‚Šã¾ã›ã‚“。 none=シークレットã¯ã¾ã ã‚ã‚Šã¾ã›ã‚“。 creation=ã‚·ãƒ¼ã‚¯ãƒ¬ãƒƒãƒˆã‚’è¿½åŠ +creation.description=説明 creation.name_placeholder=大文å—å°æ–‡å—ã®åŒºåˆ¥ãªã—ã€è‹±æ•°å—ã¨ã‚¢ãƒ³ãƒ€ãƒ¼ã‚¹ã‚³ã‚¢ã®ã¿ã€GITEA_ ã‚„ GITHUB_ ã§å§‹ã¾ã‚‹ã‚‚ã®ã¯ä¸å¯ creation.value_placeholder=内容を入力ã—ã¦ãã ã•ã„。å‰å¾Œã®ç©ºç™½ã¯é™¤åŽ»ã•ã‚Œã¾ã™ã€‚ +creation.description_placeholder=ç°¡å˜ãªèª¬æ˜Žã‚’入力ã—ã¦ãã ã•ã„。 (オプション) creation.success=シークレット "%s" ã‚’è¿½åŠ ã—ã¾ã—ãŸã€‚ creation.failed=シークレットã®è¿½åŠ ã«å¤±æ•—ã—ã¾ã—ãŸã€‚ deletion=シークレットã®å‰Šé™¤ diff --git a/options/locale/locale_ko-KR.ini b/options/locale/locale_ko-KR.ini index a570a05274..697371c2c9 100644 --- a/options/locale/locale_ko-KR.ini +++ b/options/locale/locale_ko-KR.ini @@ -707,7 +707,6 @@ issues.filter_label=ë ˆì´ë¸” issues.filter_label_no_select=ëª¨ë“ ë ˆì´ë¸” issues.filter_milestone=마ì¼ìŠ¤í†¤ issues.filter_assignee=ë‹´ë‹¹ìž -issues.filter_assginee_no_select=ëª¨ë“ ë‹´ë‹¹ìž issues.filter_assginee_no_assignee=ë‹´ë‹¹ìž ì—†ìŒ issues.filter_type=ìœ í˜• issues.filter_type.all_issues=ëª¨ë“ ì´ìŠˆ @@ -1547,6 +1546,7 @@ conan.details.repository=ì €ìž¥ì†Œ owner.settings.cleanuprules.enabled=í™œì„±í™”ë¨ [secrets] +creation.description=설명 [actions] diff --git a/options/locale/locale_lv-LV.ini b/options/locale/locale_lv-LV.ini index bc8cab4781..4439537fd7 100644 --- a/options/locale/locale_lv-LV.ini +++ b/options/locale/locale_lv-LV.ini @@ -393,7 +393,6 @@ oauth_signup_submit=Pabeigt reÄ£istrÄciju oauth_signin_tab=SasaistÄ«t ar esoÅ¡u kontu oauth_signin_title=Pieteikties, lai autorizÄ“tu saistÄ«to kontu oauth_signin_submit=SasaistÄ«t kontu -oauth.signin.error=RadÄs kļūda apstrÄdÄjot pieteikÅ¡anÄs pieprasÄ«jumu. Ja Å¡Ä« kļūda atkÄrtojas, sazinieties ar lapas administratoru. oauth.signin.error.access_denied=AutorizÄcijas pieprasÄ«jums tika noraidÄ«ts. oauth.signin.error.temporarily_unavailable=PieteikÅ¡anÄs neizdevÄs, jo autentifikÄcijas serveris ir Ä«slaicÄ«gi nepieejams. MÄ“Ä£iniet autorizÄ“ties vÄ“lÄk. openid_connect_submit=Pievienoties @@ -1384,7 +1383,6 @@ issues.filter_project=Projekts issues.filter_project_all=Visi projekti issues.filter_project_none=Nav projekta issues.filter_assignee=AtbildÄ«gais -issues.filter_assginee_no_select=Visi atbildÄ«gie issues.filter_assginee_no_assignee=Nav atbildÄ«gÄ issues.filter_poster=Autors issues.filter_type=Veids @@ -3341,6 +3339,7 @@ secrets=NoslÄ“pumi description=NoslÄ“pumi tiks padoti atseviÅ¡Ä·Äm darbÄ«bÄm un citÄdi nevar tikt nolasÄ«ti. none=PagaidÄm nav neviena noslÄ“puma. creation=Pievienot noslÄ“pumu +creation.description=Apraksts creation.name_placeholder=reÄ£istr-nejÅ«tÄ«gs, tikai burti, cipari un apakÅ¡svÄ«tras, nevar sÄkties ar GITEA_ vai GITHUB_ creation.value_placeholder=Ievadiet jebkÄdu saturu. Atstarpes sÄkumÄ un beigÄ tiks noņemtas. creation.success=NoslÄ“pums "%s" tika pievienots. diff --git a/options/locale/locale_nl-NL.ini b/options/locale/locale_nl-NL.ini index 650e8d4e23..77bf2d2d59 100644 --- a/options/locale/locale_nl-NL.ini +++ b/options/locale/locale_nl-NL.ini @@ -322,7 +322,6 @@ oauth_signup_submit=Account voltooien oauth_signin_tab=Bestaand account koppelen oauth_signin_title=Inloggen om het gekoppelde account te machtigen oauth_signin_submit=Account koppelen -oauth.signin.error=Er is een fout opgetreden bij het verwerken van het autorisatieverzoek. Als deze fout zich blijft voordoen, neem dan contact op met de sitebeheerder. oauth.signin.error.access_denied=Het autorisatieverzoek is geweigerd. oauth.signin.error.temporarily_unavailable=Autorisatie mislukt omdat de verificatieserver tijdelijk niet beschikbaar is. Probeer het later opnieuw. openid_connect_submit=Verbinden @@ -1142,7 +1141,6 @@ issues.filter_milestone=Mijlpaal issues.filter_project=Project issues.filter_project_none=Geen project issues.filter_assignee=Aangewezene -issues.filter_assginee_no_select=Alle toegewezen personen issues.filter_assginee_no_assignee=Geen verantwoordelijke issues.filter_poster=Auteur issues.filter_type=Type @@ -2523,6 +2521,7 @@ settings.link.button=Repository link bijwerken owner.settings.cleanuprules.enabled=Ingeschakeld [secrets] +creation.description=Omschrijving [actions] diff --git a/options/locale/locale_pl-PL.ini b/options/locale/locale_pl-PL.ini index 55a82e9629..f64b8771ac 100644 --- a/options/locale/locale_pl-PL.ini +++ b/options/locale/locale_pl-PL.ini @@ -1054,7 +1054,6 @@ issues.filter_label_no_select=Wszystkie etykiety issues.filter_milestone=KamieÅ„ milowy issues.filter_project_none=Brak projektu issues.filter_assignee=Przypisany -issues.filter_assginee_no_select=Wszyscy przypisani issues.filter_assginee_no_assignee=Brak przypisania issues.filter_type=Typ issues.filter_type.all_issues=Wszystkie zgÅ‚oszenia @@ -2412,6 +2411,7 @@ conan.details.repository=Repozytorium owner.settings.cleanuprules.enabled=WÅ‚Ä…czone [secrets] +creation.description=Opis [actions] diff --git a/options/locale/locale_pt-BR.ini b/options/locale/locale_pt-BR.ini index f4b479344d..204fedc311 100644 --- a/options/locale/locale_pt-BR.ini +++ b/options/locale/locale_pt-BR.ini @@ -390,7 +390,6 @@ oauth_signup_submit=Completar conta oauth_signin_tab=Vincular à uma conta existente oauth_signin_title=Acesse com uma conta vinculada oauth_signin_submit=Vincular conta -oauth.signin.error=Ocorreu um erro durante o processamento do pedido de autorização. Se este erro persistir, contate o administrador. oauth.signin.error.access_denied=O pedido de autorização foi negado. oauth.signin.error.temporarily_unavailable=A autorização falhou porque o servidor de autenticação está temporariamente indisponÃvel. Por favor, tente novamente mais tarde. openid_connect_submit=Conectar @@ -1379,8 +1378,6 @@ issues.filter_project=Projeto issues.filter_project_all=Todos os projetos issues.filter_project_none=Sem projeto issues.filter_assignee=AtribuÃdo -issues.filter_assginee_no_select=Todos os responsáveis -issues.filter_assginee_no_assignee=Sem responsável issues.filter_poster=Autor issues.filter_type=Tipo issues.filter_type.all_issues=Todas as issues @@ -2194,7 +2191,7 @@ settings.protect_check_status_contexts_list=Verificações de status encontradas settings.protect_required_approvals=Aprovações necessárias: settings.dismiss_stale_approvals=Descartar aprovações obsoletas settings.dismiss_stale_approvals_desc=Quando novos commits que mudam o conteúdo do pull request são enviados para o branch, as antigas aprovações serão descartadas. -settings.require_signed_commits=Exibir commits assinados +settings.require_signed_commits=Exigir commits assinados settings.require_signed_commits_desc=Rejeitar pushes para este branch se não estiverem assinados ou não forem validáveis. settings.protect_branch_name_pattern=Padrão de Nome de Branch Protegida settings.protect_patterns=Padrões @@ -3278,6 +3275,7 @@ secrets=Segredos description=Os segredos serão passados a certas ações e não poderão ser lidos de outra forma. none=Não há segredos ainda. creation=Adicionar Segredo +creation.description=Descrição creation.name_placeholder=apenas caracteres alfanuméricos ou underline (_), não pode começar com GITEA_ ou GITHUB_ creation.value_placeholder=Insira qualquer conteúdo. Espaços em branco no inÃcio e no fim serão omitidos. creation.success=O segredo "%s" foi adicionado. diff --git a/options/locale/locale_pt-PT.ini b/options/locale/locale_pt-PT.ini index 7dd8985cec..0b0ddbee56 100644 --- a/options/locale/locale_pt-PT.ini +++ b/options/locale/locale_pt-PT.ini @@ -457,7 +457,7 @@ oauth_signup_submit=Completar conta oauth_signin_tab=Vincular a uma conta existente oauth_signin_title=Inicie a sessão para autorizar a vinculação à conta oauth_signin_submit=Vincular conta -oauth.signin.error=Ocorreu um erro durante o processamento do pedido de autorização. Se este erro persistir, contacte o administrador. +oauth.signin.error.general=Ocorreu um erro durante o processamento do pedido de autorização: %s: Se este erro persistir, contacte o administrador. oauth.signin.error.access_denied=O pedido de autorização foi negado. oauth.signin.error.temporarily_unavailable=A autorização falhou porque o servidor de autenticação está temporariamente indisponÃvel. Tente mais tarde. oauth_callback_unable_auto_reg=O registo automático está habilitado, mas o fornecedor OAuth2 %[1]s sinalizou campos em falta: %[2]s, por isso não foi possÃvel criar uma conta automaticamente. Crie ou vincule uma conta ou contacte o administrador do sÃtio. @@ -926,6 +926,9 @@ permission_not_set=Não definido permission_no_access=Sem acesso permission_read=Lidas permission_write=Leitura e escrita +permission_anonymous_read=Leitura anónima +permission_everyone_read=Leitura pública +permission_everyone_write=Escrita pública access_token_desc=As permissões dos códigos escolhidos limitam a autorização apenas à s rotas da <a %s>API</a> correspondentes. Leia a <a %s>documentação</a> para obter mais informação. at_least_one_permission=Tem que escolher pelo menos uma permissão para criar um código permissions_list=Permissões: @@ -1138,6 +1141,7 @@ transfer.no_permission_to_reject=Você não tem permissão para rejeitar esta tr desc.private=Privado desc.public=Público +desc.public_access=Acesso público desc.template=Modelo desc.internal=Interno desc.archived=Arquivado @@ -1546,8 +1550,8 @@ issues.filter_project=Planeamento issues.filter_project_all=Todos os planeamentos issues.filter_project_none=Nenhum planeamento issues.filter_assignee=Encarregado -issues.filter_assginee_no_select=Todos os encarregados issues.filter_assginee_no_assignee=Sem encarregado +issues.filter_assignee_any_assignee=AtribuÃdo a qualquer pessoa issues.filter_poster=Autor(a) issues.filter_user_placeholder=Procurar utilizadores issues.filter_user_no_select=Todos os utilizadores @@ -2132,6 +2136,12 @@ contributors.contribution_type.deletions=Eliminações settings=Configurações settings.desc=Configurações é onde pode gerir as configurações do repositório settings.options=Repositório +settings.public_access=Acesso público +settings.public_access_desc=Configurar as permissões de acesso público do visitante para substituir os valores predefinidos deste repositório. +settings.public_access.docs.not_set=Não definido: nenhuma permissão extra de acesso público. As permissões do visitante seguem a visibilidade e as permissões de membro do repositório. +settings.public_access.docs.anonymous_read=Leitura anónima: utilizadores sem sessão iniciada podem consultar a unidade. +settings.public_access.docs.everyone_read=Leitura pública: todos os utilizadores com sessão iniciada podem aceder à unidade com permissão de leitura. Permissão de leitura das unidades de questões / pedidos de integração também significa que os utilizadores podem criar novas questões / pedidos de integração. +settings.public_access.docs.everyone_write=Escrita pública: Todos os utilizadores com sessão iniciada têm permissão de escrita na unidade. Apenas a unidade Wiki suporta esta permissão. settings.collaboration=Colaboradores settings.collaboration.admin=Administrador settings.collaboration.write=Escrita @@ -2178,7 +2188,6 @@ settings.advanced_settings=Configurações avançadas settings.wiki_desc=Habilitar wiki do repositório settings.use_internal_wiki=Usar o wiki integrado settings.default_wiki_branch_name=Nome do ramo predefinido do wiki -settings.default_permission_everyone_access=Permissão de acesso predefinida para todos os utilizadores registados: settings.failed_to_change_default_wiki_branch=Falhou ao mudar o nome do ramo predefinido do wiki. settings.use_external_wiki=Usar um wiki externo settings.external_wiki_url=URL do wiki externo @@ -3711,8 +3720,10 @@ secrets=Segredos description=Os segredos serão transmitidos a certas operações e não poderão ser lidos de outra forma. none=Ainda não há segredos. creation=Adicionar segredo +creation.description=Descrição creation.name_placeholder=Só sublinhados ou alfanuméricos sem distinguir maiúsculas, sem começar com GITEA_ nem GITHUB_ creation.value_placeholder=Insira um conteúdo qualquer. Espaços em branco no inÃcio ou no fim serão omitidos. +creation.description_placeholder=Escreva uma descrição curta (opcional). creation.success=O segredo "%s" foi adicionado. creation.failed=Falhou ao adicionar o segredo. deletion=Remover segredo diff --git a/options/locale/locale_ru-RU.ini b/options/locale/locale_ru-RU.ini index b8126089e9..fe52165837 100644 --- a/options/locale/locale_ru-RU.ini +++ b/options/locale/locale_ru-RU.ini @@ -388,7 +388,6 @@ oauth_signup_submit=ÐŸÐ¾Ð»Ð½Ð°Ñ ÑƒÑ‡Ñ‘Ñ‚Ð½Ð°Ñ Ð·Ð°Ð¿Ð¸ÑÑŒ oauth_signin_tab=СÑылка на ÑущеÑтвующую учётную запиÑÑŒ oauth_signin_title=Войдите, чтобы авторизовать ÑвÑзанную учётную запиÑÑŒ oauth_signin_submit=ПривÑзать учётную запиÑÑŒ -oauth.signin.error=Произошла ошибка при обработке запроÑа авторизации. ЕÑли Ñта ошибка повторÑетÑÑ, обратитеÑÑŒ к админиÑтратору Ñайта. oauth.signin.error.access_denied=Ð—Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° авторизацию был отклонен. oauth.signin.error.temporarily_unavailable=Произошла ошибка авторизации, так как Ñервер аутентификации временно недоÑтупен. ПожалуйÑта, повторите попытку позже. openid_connect_submit=Подключить @@ -1356,7 +1355,6 @@ issues.filter_project=Проект issues.filter_project_all=Ð’Ñе проекты issues.filter_project_none=Ðет проекта issues.filter_assignee=Ðазначено -issues.filter_assginee_no_select=Ð’Ñе Ð½Ð°Ð·Ð½Ð°Ñ‡ÐµÐ½Ð¸Ñ issues.filter_assginee_no_assignee=Ðет ответÑтвенного issues.filter_poster=Ðвтор issues.filter_type=Тип @@ -3275,6 +3273,7 @@ secrets=Секреты description=Секреты будут передаватьÑÑ Ð¾Ð¿Ñ€ÐµÐ´ÐµÐ»ÐµÐ½Ð½Ñ‹Ð¼ дейÑтвиÑм и не могут быть прочитаны иначе. none=Секретов пока нет. creation=Добавить Ñекрет +creation.description=ОпиÑание creation.name_placeholder=региÑÑ‚Ñ€ не важен, только алфавитно-цифровые Ñимволы и подчёркиваниÑ, не может начинатьÑÑ Ñ GITEA_ или GITHUB_ creation.value_placeholder=Введите любое Ñодержимое. Пробельные Ñимволы в начале и конце будут опущены. creation.success=Секрет «%s» добавлен. diff --git a/options/locale/locale_si-LK.ini b/options/locale/locale_si-LK.ini index 4857fa8d88..2cd7fb29b9 100644 --- a/options/locale/locale_si-LK.ini +++ b/options/locale/locale_si-LK.ini @@ -1025,7 +1025,6 @@ issues.filter_label_no_select=සියලු ලේබල issues.filter_milestone=සන්ධිස්ථà·à¶±à¶º issues.filter_project_none=ව්â€à¶ºà·à¶´à·˜à¶à·’ නà·à¶ issues.filter_assignee=අස්ගිනී -issues.filter_assginee_no_select=සියලුම ඇග්රි issues.filter_assginee_no_assignee=කිසිදු අස්වà·à¶¯à·Šà¶¯à·”මක් issues.filter_type=වර්ගය issues.filter_type.all_issues=සියලු ගà·à¶§à·…à·” @@ -2454,6 +2453,7 @@ conan.details.repository=කà·à·‚්ඨය owner.settings.cleanuprules.enabled=සබල කර ඇච[secrets] +creation.description=සවිස්à¶à¶»à¶º [actions] diff --git a/options/locale/locale_sk-SK.ini b/options/locale/locale_sk-SK.ini index 53ea17b43e..20f26db801 100644 --- a/options/locale/locale_sk-SK.ini +++ b/options/locale/locale_sk-SK.ini @@ -377,7 +377,6 @@ oauth_signup_submit=DokonÄiÅ¥ úÄet oauth_signin_tab=PrepojiÅ¥ s existujúcim úÄtom oauth_signin_title=Prihláste sa na overenie prepojeného úÄtu oauth_signin_submit=PrepojiÅ¥ úÄet -oauth.signin.error=Vyskytla sa chyba poÄas spracovania vaÅ¡ej autorizaÄnej žiadosti. Ak chyba pretrváva, kontaktujte, prosÃm, správcu. oauth.signin.error.access_denied=ŽiadosÅ¥ o autorizáciu bola zamietnutá. oauth.signin.error.temporarily_unavailable=Autorizácia zlyhala, pretože overovacà server je doÄasne nedostupný. Skúste to prosÃm neskôr. openid_connect_submit=PripojiÅ¥ diff --git a/options/locale/locale_sv-SE.ini b/options/locale/locale_sv-SE.ini index 0d3d0f5fc4..dbe53aaadf 100644 --- a/options/locale/locale_sv-SE.ini +++ b/options/locale/locale_sv-SE.ini @@ -873,7 +873,6 @@ issues.filter_label_no_select=Alla etiketter issues.filter_milestone=Milsten issues.filter_project_none=Inget projekt issues.filter_assignee=Förvärvare -issues.filter_assginee_no_select=Alla tilldelade issues.filter_assginee_no_assignee=Ingen tilldelad issues.filter_type=Typ issues.filter_type.all_issues=Alla ärenden @@ -1989,6 +1988,7 @@ conan.details.repository=Utvecklingskatalog owner.settings.cleanuprules.enabled=Aktiv [secrets] +creation.description=Beskrivning [actions] diff --git a/options/locale/locale_tr-TR.ini b/options/locale/locale_tr-TR.ini index b420e562c4..1a044fca83 100644 --- a/options/locale/locale_tr-TR.ini +++ b/options/locale/locale_tr-TR.ini @@ -441,7 +441,6 @@ oauth_signup_submit=Hesabı Tamamla oauth_signin_tab=Mevcut Hesaba BaÄŸla oauth_signin_title=BaÄŸlantılı Hesabı Yetkilendirmek için GiriÅŸ Yapın oauth_signin_submit=Hesabı BaÄŸla -oauth.signin.error=Yetkilendirme isteÄŸini iÅŸlerken bir hata oluÅŸtu. EÄŸer hata devam ederse lütfen site yöneticisiyle baÄŸlantıya geçin. oauth.signin.error.access_denied=Yetkilendirme isteÄŸi reddedildi. oauth.signin.error.temporarily_unavailable=Yetkilendirme sunucusu geçici olarak eriÅŸilemez olduÄŸu için yetkilendirme baÅŸarısız oldu. Lütfen daha sonra tekrar deneyin. oauth_callback_unable_auto_reg=Otomatik kayıt etkin ancak OAuth2 SaÄŸlayıcı %[1] eksik sahalar döndürdü: %[2]s, otomatik olarak hesap oluÅŸturulamıyor, lütfen bir hesap oluÅŸturun veya baÄŸlantı verin, veya site yöneticisiyle iletiÅŸim kurun. @@ -1485,7 +1484,6 @@ issues.filter_project=Proje issues.filter_project_all=Tüm projeler issues.filter_project_none=Proje yok issues.filter_assignee=Atanan -issues.filter_assginee_no_select=Tüm atananlar issues.filter_assginee_no_assignee=Atanan yok issues.filter_poster=Yazar issues.filter_type=Tür @@ -3534,6 +3532,7 @@ secrets=Gizlilikler description=Gizlilikler belirli iÅŸlemlere aktarılacaktır, bunun dışında okunamaz. none=Henüz gizlilik yok. creation=Gizlilik Ekle +creation.description=Açıklama creation.name_placeholder=küçük-büyük harfe duyarlı deÄŸil, alfanümerik karakterler veya sadece alt tire, GITEA_ veya GITHUB_ ile baÅŸlayamaz creation.value_placeholder=Herhangi bir içerik girin. BaÅŸtaki ve sondaki boÅŸluklar ihmal edilecektir. creation.success=Gizlilik "%s" eklendi. diff --git a/options/locale/locale_uk-UA.ini b/options/locale/locale_uk-UA.ini index 4071659304..8c8911ceb6 100644 --- a/options/locale/locale_uk-UA.ini +++ b/options/locale/locale_uk-UA.ini @@ -1071,7 +1071,6 @@ issues.filter_milestone=Етап issues.filter_project=Проєкт issues.filter_project_none=Проєкт відÑутній issues.filter_assignee=Виконавець -issues.filter_assginee_no_select=Ð’ÑÑ– виконавці issues.filter_assginee_no_assignee=Ðемає Ð²Ð¸ÐºÐ¾Ð½Ð°Ð²Ñ†Ñ issues.filter_type=Тип issues.filter_type.all_issues=Ð’ÑÑ– задачі @@ -2524,6 +2523,7 @@ conan.details.repository=Репозиторій owner.settings.cleanuprules.enabled=Увімкнено [secrets] +creation.description=ÐžÐ¿Ð¸Ñ [actions] diff --git a/options/locale/locale_zh-CN.ini b/options/locale/locale_zh-CN.ini index 2bd355c09e..f36789921e 100644 --- a/options/locale/locale_zh-CN.ini +++ b/options/locale/locale_zh-CN.ini @@ -446,7 +446,6 @@ oauth_signup_submit=完æˆè´¦å· oauth_signin_tab=绑定到现有å¸å· oauth_signin_title=登录以授æƒç»‘定å¸æˆ· oauth_signin_submit=ç»‘å®šè´¦å· -oauth.signin.error=处ç†æŽˆæƒè¯·æ±‚时出错。 如果æ¤é”™è¯¯ä»ç„¶å˜â€‹â€‹åœ¨ï¼Œè¯·è”系站点管ç†å‘˜ã€‚ oauth.signin.error.access_denied=授æƒè¯·æ±‚被拒ç»ã€‚ oauth.signin.error.temporarily_unavailable=授æƒå¤±è´¥ï¼Œå› 为认è¯æœåŠ¡å™¨æš‚æ—¶ä¸å¯ç”¨ã€‚请ç¨åŽå†è¯•ã€‚ oauth_callback_unable_auto_reg=自动注册已å¯ç”¨ï¼Œä½†OAuth2 æ供商 %[1]s 返回缺失的å—段:%[2]sï¼Œæ— æ³•è‡ªåŠ¨åˆ›å»ºå¸æˆ·ï¼Œè¯·åˆ›å»ºæˆ–链接到一个å¸æˆ·ï¼Œæˆ–è”系站点管ç†å‘˜ã€‚ @@ -1525,8 +1524,6 @@ issues.filter_project=项目 issues.filter_project_all=所有项目 issues.filter_project_none=æš‚æ— é¡¹ç›® issues.filter_assignee=指派人ç›é€‰ -issues.filter_assginee_no_select=所有指派æˆå‘˜ -issues.filter_assginee_no_assignee=未指派 issues.filter_poster=作者 issues.filter_user_placeholder=æœç´¢ç”¨æˆ· issues.filter_user_no_select=所有用户 @@ -3658,6 +3655,7 @@ secrets=密钥 description=Secrets å°†è¢«ä¼ ç»™ç‰¹å®šçš„ Actions,其它情况将ä¸èƒ½è¯»å– none=还没有密钥。 creation=æ·»åŠ å¯†é’¥ +creation.description=组织æè¿° creation.name_placeholder=ä¸åŒºåˆ†å¤§å°å†™ï¼Œå—æ¯æ•°å—或下划线ä¸èƒ½ä»¥GITEA_ 或 GITHUB_ 开头。 creation.value_placeholder=输入任何内容,开头和结尾的空白都会被çœç•¥ creation.success=您的密钥 '%s' æ·»åŠ æˆåŠŸã€‚ diff --git a/options/locale/locale_zh-HK.ini b/options/locale/locale_zh-HK.ini index 3733d95ec8..b924faba09 100644 --- a/options/locale/locale_zh-HK.ini +++ b/options/locale/locale_zh-HK.ini @@ -963,6 +963,7 @@ conan.details.repository=儲å˜åº« owner.settings.cleanuprules.enabled=已啟用 [secrets] +creation.description=組織æè¿° [actions] diff --git a/options/locale/locale_zh-TW.ini b/options/locale/locale_zh-TW.ini index 4bc3259586..10d6676ac5 100644 --- a/options/locale/locale_zh-TW.ini +++ b/options/locale/locale_zh-TW.ini @@ -444,7 +444,6 @@ oauth_signup_submit=完æˆå¸³æˆ¶ oauth_signin_tab=連çµåˆ°ç¾æœ‰å¸³æˆ¶ oauth_signin_title=登入以授權連çµå¸³æˆ¶ oauth_signin_submit=連çµå¸³æˆ¶ -oauth.signin.error=處ç†æŽˆæ¬Šè«‹æ±‚時發生錯誤。如果這個å•é¡ŒæŒçºŒç™¼ç”Ÿï¼Œè«‹è¯çµ¡ç¶²ç«™ç®¡ç†å“¡ã€‚ oauth.signin.error.access_denied=授權請求被拒絕。 oauth.signin.error.temporarily_unavailable=æŽˆæ¬Šå¤±æ•—ï¼Œå› ç‚ºèªè‰ä¼ºæœå™¨æš«æ™‚無法使用。請ç¨å¾Œå†è©¦ã€‚ oauth_callback_unable_auto_reg=自助註冊已啟用,但是 OAuth2 æ供者 %[1]s 回傳的çµæžœç¼ºå°‘欄ä½ï¼š%[2]s,導致無法自動建立帳號。請建立新帳號或是連çµè‡³æ—¢å˜çš„帳號,或是è¯çµ¡ç¶²ç«™ç®¡ç†è€…。 @@ -1518,8 +1517,6 @@ issues.filter_project=專案 issues.filter_project_all=所有專案 issues.filter_project_none=未é¸æ“‡å°ˆæ¡ˆ issues.filter_assignee=è² è²¬äºº -issues.filter_assginee_no_select=æ‰€æœ‰è² è²¬äºº -issues.filter_assginee_no_assignee=æ²’æœ‰è² è²¬äºº issues.filter_poster=作者 issues.filter_user_placeholder=æœå°‹ä½¿ç”¨è€… issues.filter_user_no_select=所有使用者 @@ -3646,6 +3643,7 @@ secrets=Secret description=Secret 會被傳給特定的 Action,其他情æ³ç„¡æ³•è®€å–。 none=還沒有 Secret。 creation=åŠ å…¥ Secret +creation.description=æè¿° creation.name_placeholder=ä¸å€åˆ†å¤§å°å¯«ï¼Œåªèƒ½åŒ…å«è‹±æ–‡å—æ¯ã€æ•¸å—ã€åº•ç·š ('_'),ä¸èƒ½ä»¥ GITEA_ 或 GITHUB_ é–‹é 。 creation.value_placeholder=輸入任何內容,é 尾的空白都會被忽略。 creation.success=已新增 Secret「%sã€ã€‚ diff --git a/routers/api/actions/artifactsv4.go b/routers/api/actions/artifactsv4.go index 665156d936..3d992ca2dd 100644 --- a/routers/api/actions/artifactsv4.go +++ b/routers/api/actions/artifactsv4.go @@ -162,8 +162,8 @@ func (r artifactV4Routes) buildSignature(endp, expires, artifactName string, tas mac.Write([]byte(endp)) mac.Write([]byte(expires)) mac.Write([]byte(artifactName)) - mac.Write([]byte(fmt.Sprint(taskID))) - mac.Write([]byte(fmt.Sprint(artifactID))) + fmt.Fprint(mac, taskID) + fmt.Fprint(mac, artifactID) return mac.Sum(nil) } diff --git a/routers/api/packages/api.go b/routers/api/packages/api.go index b64306037f..72db15dc26 100644 --- a/routers/api/packages/api.go +++ b/routers/api/packages/api.go @@ -46,13 +46,14 @@ func reqPackageAccess(accessMode perm.AccessMode) func(ctx *context.Context) { if ok { // it's a personal access token but not oauth2 token scopeMatched := false var err error - if accessMode == perm.AccessModeRead { + switch accessMode { + case perm.AccessModeRead: scopeMatched, err = scope.HasScope(auth_model.AccessTokenScopeReadPackage) if err != nil { ctx.HTTPError(http.StatusInternalServerError, "HasScope", err.Error()) return } - } else if accessMode == perm.AccessModeWrite { + case perm.AccessModeWrite: scopeMatched, err = scope.HasScope(auth_model.AccessTokenScopeWritePackage) if err != nil { ctx.HTTPError(http.StatusInternalServerError, "HasScope", err.Error()) @@ -703,13 +704,14 @@ func ContainerRoutes() *web.Router { g.MatchPath("POST", "/<image:*>/blobs/uploads", reqPackageAccess(perm.AccessModeWrite), container.VerifyImageName, container.InitiateUploadBlob) g.MatchPath("GET", "/<image:*>/tags/list", container.VerifyImageName, container.GetTagList) g.MatchPath("GET,PATCH,PUT,DELETE", `/<image:*>/blobs/uploads/<uuid:[-.=\w]+>`, reqPackageAccess(perm.AccessModeWrite), container.VerifyImageName, func(ctx *context.Context) { - if ctx.Req.Method == http.MethodGet { + switch ctx.Req.Method { + case http.MethodGet: container.GetUploadBlob(ctx) - } else if ctx.Req.Method == http.MethodPatch { + case http.MethodPatch: container.UploadBlob(ctx) - } else if ctx.Req.Method == http.MethodPut { + case http.MethodPut: container.EndUploadBlob(ctx) - } else /* DELETE */ { + default: /* DELETE */ container.CancelUploadBlob(ctx) } }) diff --git a/routers/api/packages/cargo/cargo.go b/routers/api/packages/cargo/cargo.go index 42ef13476c..710c614c6e 100644 --- a/routers/api/packages/cargo/cargo.go +++ b/routers/api/packages/cargo/cargo.go @@ -51,7 +51,7 @@ func apiError(ctx *context.Context, status int, obj any) { // https://rust-lang.github.io/rfcs/2789-sparse-index.html func RepositoryConfig(ctx *context.Context) { - ctx.JSON(http.StatusOK, cargo_service.BuildConfig(ctx.Package.Owner, setting.Service.RequireSignInView || ctx.Package.Owner.Visibility != structs.VisibleTypePublic)) + ctx.JSON(http.StatusOK, cargo_service.BuildConfig(ctx.Package.Owner, setting.Service.RequireSignInViewStrict || ctx.Package.Owner.Visibility != structs.VisibleTypePublic)) } func EnumeratePackageVersions(ctx *context.Context) { diff --git a/routers/api/packages/container/container.go b/routers/api/packages/container/container.go index bb14db9db7..6ef1655235 100644 --- a/routers/api/packages/container/container.go +++ b/routers/api/packages/container/container.go @@ -126,7 +126,7 @@ func apiUnauthorizedError(ctx *context.Context) { // ReqContainerAccess is a middleware which checks the current user valid (real user or ghost if anonymous access is enabled) func ReqContainerAccess(ctx *context.Context) { - if ctx.Doer == nil || (setting.Service.RequireSignInView && ctx.Doer.IsGhost()) { + if ctx.Doer == nil || (setting.Service.RequireSignInViewStrict && ctx.Doer.IsGhost()) { apiUnauthorizedError(ctx) } } @@ -152,7 +152,7 @@ func Authenticate(ctx *context.Context) { u := ctx.Doer packageScope := auth_service.GetAccessScope(ctx.Data) if u == nil { - if setting.Service.RequireSignInView { + if setting.Service.RequireSignInViewStrict { apiUnauthorizedError(ctx) return } diff --git a/routers/api/v1/admin/hooks.go b/routers/api/v1/admin/hooks.go index fb1ea4eab6..a687541be5 100644 --- a/routers/api/v1/admin/hooks.go +++ b/routers/api/v1/admin/hooks.go @@ -51,9 +51,10 @@ func ListHooks(ctx *context.APIContext) { // for compatibility the default value is true isSystemWebhook := optional.Some(true) typeValue := ctx.FormString("type") - if typeValue == "default" { + switch typeValue { + case "default": isSystemWebhook = optional.Some(false) - } else if typeValue == "all" { + case "all": isSystemWebhook = optional.None[bool]() } diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index bc76b5285e..3dcb87261b 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -355,7 +355,7 @@ func reqToken() func(ctx *context.APIContext) { func reqExploreSignIn() func(ctx *context.APIContext) { return func(ctx *context.APIContext) { - if (setting.Service.RequireSignInView || setting.Service.Explore.RequireSigninView) && !ctx.IsSigned { + if (setting.Service.RequireSignInViewStrict || setting.Service.Explore.RequireSigninView) && !ctx.IsSigned { ctx.APIError(http.StatusUnauthorized, "you must be signed in to search for users") } } @@ -842,13 +842,13 @@ func verifyAuthWithOptions(options *common.VerifyOptions) func(ctx *context.APIC func individualPermsChecker(ctx *context.APIContext) { // org permissions have been checked in context.OrgAssignment(), but individual permissions haven't been checked. if ctx.ContextUser.IsIndividual() { - switch { - case ctx.ContextUser.Visibility == api.VisibleTypePrivate: + switch ctx.ContextUser.Visibility { + case api.VisibleTypePrivate: if ctx.Doer == nil || (ctx.ContextUser.ID != ctx.Doer.ID && !ctx.Doer.IsAdmin) { ctx.APIErrorNotFound("Visit Project", nil) return } - case ctx.ContextUser.Visibility == api.VisibleTypeLimited: + case api.VisibleTypeLimited: if ctx.Doer == nil { ctx.APIErrorNotFound("Visit Project", nil) return @@ -886,7 +886,7 @@ func Routes() *web.Router { m.Use(apiAuth(buildAuthGroup())) m.Use(verifyAuthWithOptions(&common.VerifyOptions{ - SignInRequired: setting.Service.RequireSignInView, + SignInRequired: setting.Service.RequireSignInViewStrict, })) addActionsRoutes := func( @@ -1168,6 +1168,10 @@ func Routes() *web.Router { m.Post("/{workflow_id}/dispatches", reqRepoWriter(unit.TypeActions), bind(api.CreateActionWorkflowDispatch{}), repo.ActionsDispatchWorkflow) }, context.ReferencesGitRepo(), reqToken(), reqRepoReader(unit.TypeActions)) + m.Group("/actions/jobs", func() { + m.Get("/{job_id}/logs", repo.DownloadActionsRunJobLogs) + }, reqToken(), reqRepoReader(unit.TypeActions)) + m.Group("/hooks/git", func() { m.Combo("").Get(repo.ListGitHooks) m.Group("/{id}", func() { diff --git a/routers/api/v1/repo/action.go b/routers/api/v1/repo/action.go index 2ace9fa295..ed2017a372 100644 --- a/routers/api/v1/repo/action.go +++ b/routers/api/v1/repo/action.go @@ -1103,8 +1103,8 @@ func DeleteArtifact(ctx *context.APIContext) { func buildSignature(endp string, expires, artifactID int64) []byte { mac := hmac.New(sha256.New, setting.GetGeneralTokenSigningSecret()) mac.Write([]byte(endp)) - mac.Write([]byte(fmt.Sprint(expires))) - mac.Write([]byte(fmt.Sprint(artifactID))) + fmt.Fprint(mac, expires) + fmt.Fprint(mac, artifactID) return mac.Sum(nil) } diff --git a/routers/api/v1/repo/actions_run.go b/routers/api/v1/repo/actions_run.go new file mode 100644 index 0000000000..c6d18af6aa --- /dev/null +++ b/routers/api/v1/repo/actions_run.go @@ -0,0 +1,64 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repo + +import ( + "errors" + + actions_model "code.gitea.io/gitea/models/actions" + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/routers/common" + "code.gitea.io/gitea/services/context" +) + +func DownloadActionsRunJobLogs(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/actions/jobs/{job_id}/logs repository downloadActionsRunJobLogs + // --- + // summary: Downloads the job logs for a workflow run + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: name of the owner + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repository + // type: string + // required: true + // - name: job_id + // in: path + // description: id of the job + // type: integer + // required: true + // responses: + // "200": + // description: output blob content + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + + jobID := ctx.PathParamInt64("job_id") + curJob, err := actions_model.GetRunJobByID(ctx, jobID) + if err != nil { + ctx.APIErrorInternal(err) + return + } + if err = curJob.LoadRepo(ctx); err != nil { + ctx.APIErrorInternal(err) + return + } + + err = common.DownloadActionsRunJobLogs(ctx.Base, ctx.Repo.Repository, curJob) + if err != nil { + if errors.Is(err, util.ErrNotExist) { + ctx.APIErrorNotFound(err) + } else { + ctx.APIErrorInternal(err) + } + } +} diff --git a/routers/api/v1/repo/issue.go b/routers/api/v1/repo/issue.go index c9575ff98a..e678db5262 100644 --- a/routers/api/v1/repo/issue.go +++ b/routers/api/v1/repo/issue.go @@ -290,10 +290,10 @@ func SearchIssues(ctx *context.APIContext) { if ctx.IsSigned { ctxUserID := ctx.Doer.ID if ctx.FormBool("created") { - searchOpt.PosterID = optional.Some(ctxUserID) + searchOpt.PosterID = strconv.FormatInt(ctxUserID, 10) } if ctx.FormBool("assigned") { - searchOpt.AssigneeID = optional.Some(ctxUserID) + searchOpt.AssigneeID = strconv.FormatInt(ctxUserID, 10) } if ctx.FormBool("mentioned") { searchOpt.MentionID = optional.Some(ctxUserID) @@ -538,10 +538,10 @@ func ListIssues(ctx *context.APIContext) { } if createdByID > 0 { - searchOpt.PosterID = optional.Some(createdByID) + searchOpt.PosterID = strconv.FormatInt(createdByID, 10) } if assignedByID > 0 { - searchOpt.AssigneeID = optional.Some(assignedByID) + searchOpt.AssigneeID = strconv.FormatInt(assignedByID, 10) } if mentionedByID > 0 { searchOpt.MentionID = optional.Some(mentionedByID) diff --git a/routers/api/v1/repo/wiki.go b/routers/api/v1/repo/wiki.go index 8d73383f76..67dd6c913d 100644 --- a/routers/api/v1/repo/wiki.go +++ b/routers/api/v1/repo/wiki.go @@ -476,7 +476,7 @@ func findEntryForFile(commit *git.Commit, target string) (*git.TreeEntry, error) // findWikiRepoCommit opens the wiki repo and returns the latest commit, writing to context on error. // The caller is responsible for closing the returned repo again func findWikiRepoCommit(ctx *context.APIContext) (*git.Repository, *git.Commit) { - wikiRepo, err := gitrepo.OpenWikiRepository(ctx, ctx.Repo.Repository) + wikiRepo, err := gitrepo.OpenRepository(ctx, ctx.Repo.Repository.WikiStorageRepo()) if err != nil { if git.IsErrNotExist(err) || err.Error() == "no such file or directory" { ctx.APIErrorNotFound(err) diff --git a/routers/api/v1/user/key.go b/routers/api/v1/user/key.go index 6295f4753b..04854f2092 100644 --- a/routers/api/v1/user/key.go +++ b/routers/api/v1/user/key.go @@ -24,9 +24,10 @@ import ( // appendPrivateInformation appends the owner and key type information to api.PublicKey func appendPrivateInformation(ctx std_ctx.Context, apiKey *api.PublicKey, key *asymkey_model.PublicKey, defaultUser *user_model.User) (*api.PublicKey, error) { - if key.Type == asymkey_model.KeyTypeDeploy { + switch key.Type { + case asymkey_model.KeyTypeDeploy: apiKey.KeyType = "deploy" - } else if key.Type == asymkey_model.KeyTypeUser { + case asymkey_model.KeyTypeUser: apiKey.KeyType = "user" if defaultUser.ID == key.OwnerID { @@ -38,7 +39,7 @@ func appendPrivateInformation(ctx std_ctx.Context, apiKey *api.PublicKey, key *a } apiKey.Owner = convert.ToUser(ctx, user, user) } - } else { + default: apiKey.KeyType = "unknown" } apiKey.ReadOnly = key.Mode == perm.AccessModeRead diff --git a/routers/common/actions.go b/routers/common/actions.go new file mode 100644 index 0000000000..a4eabb6ba2 --- /dev/null +++ b/routers/common/actions.go @@ -0,0 +1,71 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package common + +import ( + "fmt" + "strings" + + actions_model "code.gitea.io/gitea/models/actions" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/modules/actions" + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/services/context" +) + +func DownloadActionsRunJobLogsWithIndex(ctx *context.Base, ctxRepo *repo_model.Repository, runID, jobIndex int64) error { + runJobs, err := actions_model.GetRunJobsByRunID(ctx, runID) + if err != nil { + return fmt.Errorf("GetRunJobsByRunID: %w", err) + } + if err = runJobs.LoadRepos(ctx); err != nil { + return fmt.Errorf("LoadRepos: %w", err) + } + if jobIndex < 0 || jobIndex >= int64(len(runJobs)) { + return util.NewNotExistErrorf("job index is out of range: %d", jobIndex) + } + return DownloadActionsRunJobLogs(ctx, ctxRepo, runJobs[jobIndex]) +} + +func DownloadActionsRunJobLogs(ctx *context.Base, ctxRepo *repo_model.Repository, curJob *actions_model.ActionRunJob) error { + if curJob.Repo.ID != ctxRepo.ID { + return util.NewNotExistErrorf("job not found") + } + + if curJob.TaskID == 0 { + return util.NewNotExistErrorf("job not started") + } + + if err := curJob.LoadRun(ctx); err != nil { + return fmt.Errorf("LoadRun: %w", err) + } + + task, err := actions_model.GetTaskByID(ctx, curJob.TaskID) + if err != nil { + return fmt.Errorf("GetTaskByID: %w", err) + } + + if task.LogExpired { + return util.NewNotExistErrorf("logs have been cleaned up") + } + + reader, err := actions.OpenLogs(ctx, task.LogInStorage, task.LogFilename) + if err != nil { + return fmt.Errorf("OpenLogs: %w", err) + } + defer reader.Close() + + workflowName := curJob.Run.WorkflowID + if p := strings.Index(workflowName, "."); p > 0 { + workflowName = workflowName[0:p] + } + ctx.ServeContent(reader, &context.ServeHeaderOptions{ + Filename: fmt.Sprintf("%v-%v-%v.log", workflowName, curJob.Name, task.ID), + ContentLength: &task.LogSize, + ContentType: "text/plain", + ContentTypeCharset: "utf-8", + Disposition: "attachment", + }) + return nil +} diff --git a/routers/common/blockexpensive.go b/routers/common/blockexpensive.go new file mode 100644 index 0000000000..f52aa2b709 --- /dev/null +++ b/routers/common/blockexpensive.go @@ -0,0 +1,90 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package common + +import ( + "net/http" + "strings" + + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/reqctx" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/web/middleware" + + "github.com/go-chi/chi/v5" +) + +func BlockExpensive() func(next http.Handler) http.Handler { + if !setting.Service.BlockAnonymousAccessExpensive { + return nil + } + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + ret := determineRequestPriority(reqctx.FromContext(req.Context())) + if !ret.SignedIn { + if ret.Expensive || ret.LongPolling { + http.Redirect(w, req, setting.AppSubURL+"/user/login", http.StatusSeeOther) + return + } + } + next.ServeHTTP(w, req) + }) + } +} + +func isRoutePathExpensive(routePattern string) bool { + if strings.HasPrefix(routePattern, "/user/") || strings.HasPrefix(routePattern, "/login/") { + return false + } + + expensivePaths := []string{ + // code related + "/{username}/{reponame}/archive/", + "/{username}/{reponame}/blame/", + "/{username}/{reponame}/commit/", + "/{username}/{reponame}/commits/", + "/{username}/{reponame}/graph", + "/{username}/{reponame}/media/", + "/{username}/{reponame}/raw/", + "/{username}/{reponame}/src/", + + // issue & PR related (no trailing slash) + "/{username}/{reponame}/issues", + "/{username}/{reponame}/{type:issues}", + "/{username}/{reponame}/pulls", + "/{username}/{reponame}/{type:pulls}", + + // wiki + "/{username}/{reponame}/wiki/", + + // activity + "/{username}/{reponame}/activity/", + } + for _, path := range expensivePaths { + if strings.HasPrefix(routePattern, path) { + return true + } + } + return false +} + +func isRoutePathForLongPolling(routePattern string) bool { + return routePattern == "/user/events" +} + +func determineRequestPriority(reqCtx reqctx.RequestContext) (ret struct { + SignedIn bool + Expensive bool + LongPolling bool +}, +) { + chiRoutePath := chi.RouteContext(reqCtx).RoutePattern() + if _, ok := reqCtx.GetData()[middleware.ContextDataKeySignedUser].(*user_model.User); ok { + ret.SignedIn = true + } else { + ret.Expensive = isRoutePathExpensive(chiRoutePath) + ret.LongPolling = isRoutePathForLongPolling(chiRoutePath) + } + return ret +} diff --git a/routers/common/blockexpensive_test.go b/routers/common/blockexpensive_test.go new file mode 100644 index 0000000000..db5c0db7dd --- /dev/null +++ b/routers/common/blockexpensive_test.go @@ -0,0 +1,30 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package common + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestBlockExpensive(t *testing.T) { + cases := []struct { + expensive bool + routePath string + }{ + {false, "/user/xxx"}, + {false, "/login/xxx"}, + {true, "/{username}/{reponame}/archive/xxx"}, + {true, "/{username}/{reponame}/graph"}, + {true, "/{username}/{reponame}/src/xxx"}, + {true, "/{username}/{reponame}/wiki/xxx"}, + {true, "/{username}/{reponame}/activity/xxx"}, + } + for _, c := range cases { + assert.Equal(t, c.expensive, isRoutePathExpensive(c.routePath), "routePath: %s", c.routePath) + } + + assert.True(t, isRoutePathForLongPolling("/user/events")) +} diff --git a/routers/common/pagetmpl.go b/routers/common/pagetmpl.go new file mode 100644 index 0000000000..52c9fceba3 --- /dev/null +++ b/routers/common/pagetmpl.go @@ -0,0 +1,75 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package common + +import ( + goctx "context" + "errors" + + activities_model "code.gitea.io/gitea/models/activities" + "code.gitea.io/gitea/models/db" + issues_model "code.gitea.io/gitea/models/issues" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/services/context" +) + +// StopwatchTmplInfo is a view on a stopwatch specifically for template rendering +type StopwatchTmplInfo struct { + IssueLink string + RepoSlug string + IssueIndex int64 + Seconds int64 +} + +func getActiveStopwatch(goCtx goctx.Context) *StopwatchTmplInfo { + ctx := context.GetWebContext(goCtx) + if ctx.Doer == nil { + return nil + } + + _, sw, issue, err := issues_model.HasUserStopwatch(ctx, ctx.Doer.ID) + if err != nil { + if !errors.Is(err, goctx.Canceled) { + log.Error("Unable to HasUserStopwatch for user:%-v: %v", ctx.Doer, err) + } + return nil + } + + if sw == nil || sw.ID == 0 { + return nil + } + + return &StopwatchTmplInfo{ + issue.Link(), + issue.Repo.FullName(), + issue.Index, + sw.Seconds() + 1, // ensure time is never zero in ui + } +} + +func notificationUnreadCount(goCtx goctx.Context) int64 { + ctx := context.GetWebContext(goCtx) + if ctx.Doer == nil { + return 0 + } + count, err := db.Count[activities_model.Notification](ctx, activities_model.FindNotificationOptions{ + UserID: ctx.Doer.ID, + Status: []activities_model.NotificationStatus{activities_model.NotificationStatusUnread}, + }) + if err != nil { + if !errors.Is(err, goctx.Canceled) { + log.Error("Unable to find notification for user:%-v: %v", ctx.Doer, err) + } + return 0 + } + return count +} + +func PageTmplFunctions(ctx *context.Context) { + if ctx.IsSigned { + // defer the function call to the last moment when the tmpl renders + ctx.Data["NotificationUnreadCount"] = notificationUnreadCount + ctx.Data["GetActiveStopwatch"] = getActiveStopwatch + } +} diff --git a/routers/install/install.go b/routers/install/install.go index b81a5680d3..e0613f12aa 100644 --- a/routers/install/install.go +++ b/routers/install/install.go @@ -151,7 +151,7 @@ func Install(ctx *context.Context) { form.DisableRegistration = setting.Service.DisableRegistration form.AllowOnlyExternalRegistration = setting.Service.AllowOnlyExternalRegistration form.EnableCaptcha = setting.Service.EnableCaptcha - form.RequireSignInView = setting.Service.RequireSignInView + form.RequireSignInView = setting.Service.RequireSignInViewStrict form.DefaultKeepEmailPrivate = setting.Service.DefaultKeepEmailPrivate form.DefaultAllowCreateOrganization = setting.Service.DefaultAllowCreateOrganization form.DefaultEnableTimetracking = setting.Service.DefaultEnableTimetracking diff --git a/routers/private/hook_post_receive.go b/routers/private/hook_post_receive.go index dba6aef9a3..442d0a76c9 100644 --- a/routers/private/hook_post_receive.go +++ b/routers/private/hook_post_receive.go @@ -303,14 +303,11 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) { } if pr == nil { - if repo.IsFork { - branch = fmt.Sprintf("%s:%s", repo.OwnerName, branch) - } results = append(results, private.HookPostReceiveBranchResult{ Message: setting.Git.PullRequestPushMessage && baseRepo.AllowsPulls(ctx), Create: true, Branch: branch, - URL: fmt.Sprintf("%s/compare/%s...%s", baseRepo.HTMLURL(), util.PathEscapeSegments(baseRepo.DefaultBranch), util.PathEscapeSegments(branch)), + URL: fmt.Sprintf("%s/pulls/new/%s", repo.HTMLURL(), util.PathEscapeSegments(branch)), }) } else { results = append(results, private.HookPostReceiveBranchResult{ diff --git a/routers/private/hook_pre_receive.go b/routers/private/hook_pre_receive.go index ae23abc542..48fe591bbd 100644 --- a/routers/private/hook_pre_receive.go +++ b/routers/private/hook_pre_receive.go @@ -447,10 +447,7 @@ func preReceiveFor(ctx *preReceiveContext, refFullName git.RefName) { baseBranchName := refFullName.ForBranchName() - baseBranchExist := false - if gitrepo.IsBranchExist(ctx, ctx.Repo.Repository, baseBranchName) { - baseBranchExist = true - } + baseBranchExist := gitrepo.IsBranchExist(ctx, ctx.Repo.Repository, baseBranchName) if !baseBranchExist { for p, v := range baseBranchName { diff --git a/routers/private/serv.go b/routers/private/serv.go index ecff3b7a53..37fbc0730c 100644 --- a/routers/private/serv.go +++ b/routers/private/serv.go @@ -286,7 +286,7 @@ func ServCommand(ctx *context.PrivateContext) { repo.IsPrivate || owner.Visibility.IsPrivate() || (user != nil && user.IsRestricted) || // user will be nil if the key is a deploykey - setting.Service.RequireSignInView) { + setting.Service.RequireSignInViewStrict) { if key.Type == asymkey_model.KeyTypeDeploy { if deployKey.Mode < mode { ctx.JSON(http.StatusUnauthorized, private.Response{ diff --git a/routers/web/admin/users.go b/routers/web/admin/users.go index f6a3af1c86..e42cbb316c 100644 --- a/routers/web/admin/users.go +++ b/routers/web/admin/users.go @@ -22,7 +22,6 @@ import ( "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/templates" - "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/routers/web/explore" user_setting "code.gitea.io/gitea/routers/web/user/setting" @@ -72,11 +71,11 @@ func Users(ctx *context.Context) { PageSize: setting.UI.Admin.UserPagingNum, }, SearchByEmail: true, - IsActive: util.OptionalBoolParse(statusFilterMap["is_active"]), - IsAdmin: util.OptionalBoolParse(statusFilterMap["is_admin"]), - IsRestricted: util.OptionalBoolParse(statusFilterMap["is_restricted"]), - IsTwoFactorEnabled: util.OptionalBoolParse(statusFilterMap["is_2fa_enabled"]), - IsProhibitLogin: util.OptionalBoolParse(statusFilterMap["is_prohibit_login"]), + IsActive: optional.ParseBool(statusFilterMap["is_active"]), + IsAdmin: optional.ParseBool(statusFilterMap["is_admin"]), + IsRestricted: optional.ParseBool(statusFilterMap["is_restricted"]), + IsTwoFactorEnabled: optional.ParseBool(statusFilterMap["is_2fa_enabled"]), + IsProhibitLogin: optional.ParseBool(statusFilterMap["is_prohibit_login"]), IncludeReserved: true, // administrator needs to list all accounts include reserved, bot, remote ones }, tplUsers) } diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go index f07ef98931..1de8d7e8a3 100644 --- a/routers/web/auth/auth.go +++ b/routers/web/auth/auth.go @@ -534,7 +534,8 @@ func createUserInContext(ctx *context.Context, tpl templates.TplName, form any, } if err := user_model.CreateUser(ctx, u, meta, overwrites); err != nil { if allowLink && (user_model.IsErrUserAlreadyExist(err) || user_model.IsErrEmailAlreadyUsed(err)) { - if setting.OAuth2Client.AccountLinking == setting.OAuth2AccountLinkingAuto { + switch setting.OAuth2Client.AccountLinking { + case setting.OAuth2AccountLinkingAuto: var user *user_model.User user = &user_model.User{Name: u.Name} hasUser, err := user_model.GetUser(ctx, user) @@ -550,7 +551,7 @@ func createUserInContext(ctx *context.Context, tpl templates.TplName, form any, // TODO: probably we should respect 'remember' user's choice... linkAccount(ctx, user, *gothUser, true) return false // user is already created here, all redirects are handled - } else if setting.OAuth2Client.AccountLinking == setting.OAuth2AccountLinkingLogin { + case setting.OAuth2AccountLinkingLogin: showLinkingLogin(ctx, *gothUser) return false // user will be created only after linking login } diff --git a/routers/web/auth/auth_test.go b/routers/web/auth/auth_test.go index cbcb2a5222..e238125407 100644 --- a/routers/web/auth/auth_test.go +++ b/routers/web/auth/auth_test.go @@ -61,23 +61,35 @@ func TestUserLogin(t *testing.T) { assert.Equal(t, "/", test.RedirectURL(resp)) } -func TestSignUpOAuth2ButMissingFields(t *testing.T) { +func TestSignUpOAuth2Login(t *testing.T) { defer test.MockVariableValue(&setting.OAuth2Client.EnableAutoRegistration, true)() - defer test.MockVariableValue(&gothic.CompleteUserAuth, func(res http.ResponseWriter, req *http.Request) (goth.User, error) { - return goth.User{Provider: "dummy-auth-source", UserID: "dummy-user"}, nil - })() addOAuth2Source(t, "dummy-auth-source", oauth2.Source{}) - mockOpt := contexttest.MockContextOption{SessionStore: session.NewMockStore("dummy-sid")} - ctx, resp := contexttest.MockContext(t, "/user/oauth2/dummy-auth-source/callback?code=dummy-code", mockOpt) - ctx.SetPathParam("provider", "dummy-auth-source") - SignInOAuthCallback(ctx) - assert.Equal(t, http.StatusSeeOther, resp.Code) - assert.Equal(t, "/user/link_account", test.RedirectURL(resp)) + t.Run("OAuth2MissingField", func(t *testing.T) { + defer test.MockVariableValue(&gothic.CompleteUserAuth, func(res http.ResponseWriter, req *http.Request) (goth.User, error) { + return goth.User{Provider: "dummy-auth-source", UserID: "dummy-user"}, nil + })() + mockOpt := contexttest.MockContextOption{SessionStore: session.NewMockStore("dummy-sid")} + ctx, resp := contexttest.MockContext(t, "/user/oauth2/dummy-auth-source/callback?code=dummy-code", mockOpt) + ctx.SetPathParam("provider", "dummy-auth-source") + SignInOAuthCallback(ctx) + assert.Equal(t, http.StatusSeeOther, resp.Code) + assert.Equal(t, "/user/link_account", test.RedirectURL(resp)) + + // then the user will be redirected to the link account page, and see a message about the missing fields + ctx, _ = contexttest.MockContext(t, "/user/link_account", mockOpt) + LinkAccount(ctx) + assert.EqualValues(t, "auth.oauth_callback_unable_auto_reg:dummy-auth-source,email", ctx.Data["AutoRegistrationFailedPrompt"]) + }) - // then the user will be redirected to the link account page, and see a message about the missing fields - ctx, _ = contexttest.MockContext(t, "/user/link_account", mockOpt) - LinkAccount(ctx) - assert.EqualValues(t, "auth.oauth_callback_unable_auto_reg:dummy-auth-source,email", ctx.Data["AutoRegistrationFailedPrompt"]) + t.Run("OAuth2CallbackError", func(t *testing.T) { + mockOpt := contexttest.MockContextOption{SessionStore: session.NewMockStore("dummy-sid")} + ctx, resp := contexttest.MockContext(t, "/user/oauth2/dummy-auth-source/callback", mockOpt) + ctx.SetPathParam("provider", "dummy-auth-source") + SignInOAuthCallback(ctx) + assert.Equal(t, http.StatusSeeOther, resp.Code) + assert.Equal(t, "/user/login", test.RedirectURL(resp)) + assert.Contains(t, ctx.Flash.ErrorMsg, "auth.oauth.signin.error.general") + }) } diff --git a/routers/web/auth/oauth.go b/routers/web/auth/oauth.go index 7a9721cf56..94a8bec565 100644 --- a/routers/web/auth/oauth.go +++ b/routers/web/auth/oauth.go @@ -115,7 +115,7 @@ func SignInOAuthCallback(ctx *context.Context) { case "temporarily_unavailable": ctx.Flash.Error(ctx.Tr("auth.oauth.signin.error.temporarily_unavailable")) default: - ctx.Flash.Error(ctx.Tr("auth.oauth.signin.error")) + ctx.Flash.Error(ctx.Tr("auth.oauth.signin.error.general", callbackErr.Description)) } ctx.Redirect(setting.AppSubURL + "/user/login") return @@ -155,9 +155,10 @@ func SignInOAuthCallback(ctx *context.Context) { return } if uname == "" { - if setting.OAuth2Client.Username == setting.OAuth2UsernameNickname { + switch setting.OAuth2Client.Username { + case setting.OAuth2UsernameNickname: missingFields = append(missingFields, "nickname") - } else if setting.OAuth2Client.Username == setting.OAuth2UsernamePreferredUsername { + case setting.OAuth2UsernamePreferredUsername: missingFields = append(missingFields, "preferred_username") } // else: "UserID" and "Email" have been handled above separately } @@ -431,8 +432,10 @@ func oAuth2UserLoginCallback(ctx *context.Context, authSource *auth.Source, requ gothUser, err := oauth2Source.Callback(request, response) if err != nil { if err.Error() == "securecookie: the value is too long" || strings.Contains(err.Error(), "Data too long") { - log.Error("OAuth2 Provider %s returned too long a token. Current max: %d. Either increase the [OAuth2] MAX_TOKEN_LENGTH or reduce the information returned from the OAuth2 provider", authSource.Name, setting.OAuth2.MaxTokenLength) err = fmt.Errorf("OAuth2 Provider %s returned too long a token. Current max: %d. Either increase the [OAuth2] MAX_TOKEN_LENGTH or reduce the information returned from the OAuth2 provider", authSource.Name, setting.OAuth2.MaxTokenLength) + log.Error("oauth2Source.Callback failed: %v", err) + } else { + err = errCallback{Code: "internal", Description: err.Error()} } return nil, goth.User{}, err } diff --git a/routers/web/auth/oauth2_provider.go b/routers/web/auth/oauth2_provider.go index 00b5b2db52..ff571fbf2c 100644 --- a/routers/web/auth/oauth2_provider.go +++ b/routers/web/auth/oauth2_provider.go @@ -249,7 +249,7 @@ func AuthorizeOAuth(ctx *context.Context) { }, form.RedirectURI) return } - if err := ctx.Session.Set("CodeChallengeMethod", form.CodeChallenge); err != nil { + if err := ctx.Session.Set("CodeChallenge", form.CodeChallenge); err != nil { handleAuthorizeError(ctx, AuthorizeError{ ErrorCode: ErrorCodeServerError, ErrorDescription: "cannot set code challenge", diff --git a/routers/web/devtest/devtest.go b/routers/web/devtest/devtest.go index 1ea1398173..063ff42409 100644 --- a/routers/web/devtest/devtest.go +++ b/routers/web/devtest/devtest.go @@ -4,16 +4,21 @@ package devtest import ( + "html/template" "net/http" "path" + "strconv" "strings" "time" + "unicode" "code.gitea.io/gitea/models/asymkey" "code.gitea.io/gitea/models/db" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/badge" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/templates" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/services/context" ) @@ -45,84 +50,121 @@ func FetchActionTest(ctx *context.Context) { ctx.JSONRedirect("") } -func prepareMockData(ctx *context.Context) { - if ctx.Req.URL.Path == "/devtest/gitea-ui" { - now := time.Now() - ctx.Data["TimeNow"] = now - ctx.Data["TimePast5s"] = now.Add(-5 * time.Second) - ctx.Data["TimeFuture5s"] = now.Add(5 * time.Second) - ctx.Data["TimePast2m"] = now.Add(-2 * time.Minute) - ctx.Data["TimeFuture2m"] = now.Add(2 * time.Minute) - ctx.Data["TimePast1y"] = now.Add(-1 * 366 * 86400 * time.Second) - ctx.Data["TimeFuture1y"] = now.Add(1 * 366 * 86400 * time.Second) +func prepareMockDataGiteaUI(ctx *context.Context) { + now := time.Now() + ctx.Data["TimeNow"] = now + ctx.Data["TimePast5s"] = now.Add(-5 * time.Second) + ctx.Data["TimeFuture5s"] = now.Add(5 * time.Second) + ctx.Data["TimePast2m"] = now.Add(-2 * time.Minute) + ctx.Data["TimeFuture2m"] = now.Add(2 * time.Minute) + ctx.Data["TimePast1y"] = now.Add(-1 * 366 * 86400 * time.Second) + ctx.Data["TimeFuture1y"] = now.Add(1 * 366 * 86400 * time.Second) +} + +func prepareMockDataBadgeCommitSign(ctx *context.Context) { + var commits []*asymkey.SignCommit + mockUsers, _ := db.Find[user_model.User](ctx, user_model.SearchUserOptions{ListOptions: db.ListOptions{PageSize: 1}}) + mockUser := mockUsers[0] + commits = append(commits, &asymkey.SignCommit{ + Verification: &asymkey.CommitVerification{}, + UserCommit: &user_model.UserCommit{ + Commit: &git.Commit{ID: git.Sha1ObjectFormat.EmptyObjectID()}, + }, + }) + commits = append(commits, &asymkey.SignCommit{ + Verification: &asymkey.CommitVerification{ + Verified: true, + Reason: "name / key-id", + SigningUser: mockUser, + SigningKey: &asymkey.GPGKey{KeyID: "12345678"}, + TrustStatus: "trusted", + }, + UserCommit: &user_model.UserCommit{ + User: mockUser, + Commit: &git.Commit{ID: git.Sha1ObjectFormat.EmptyObjectID()}, + }, + }) + commits = append(commits, &asymkey.SignCommit{ + Verification: &asymkey.CommitVerification{ + Verified: true, + Reason: "name / key-id", + SigningUser: mockUser, + SigningSSHKey: &asymkey.PublicKey{Fingerprint: "aa:bb:cc:dd:ee"}, + TrustStatus: "untrusted", + }, + UserCommit: &user_model.UserCommit{ + User: mockUser, + Commit: &git.Commit{ID: git.Sha1ObjectFormat.EmptyObjectID()}, + }, + }) + commits = append(commits, &asymkey.SignCommit{ + Verification: &asymkey.CommitVerification{ + Verified: true, + Reason: "name / key-id", + SigningUser: mockUser, + SigningSSHKey: &asymkey.PublicKey{Fingerprint: "aa:bb:cc:dd:ee"}, + TrustStatus: "other(unmatch)", + }, + UserCommit: &user_model.UserCommit{ + User: mockUser, + Commit: &git.Commit{ID: git.Sha1ObjectFormat.EmptyObjectID()}, + }, + }) + commits = append(commits, &asymkey.SignCommit{ + Verification: &asymkey.CommitVerification{ + Warning: true, + Reason: "gpg.error", + SigningEmail: "test@example.com", + }, + UserCommit: &user_model.UserCommit{ + User: mockUser, + Commit: &git.Commit{ID: git.Sha1ObjectFormat.EmptyObjectID()}, + }, + }) + + ctx.Data["MockCommits"] = commits +} + +func prepareMockDataBadgeActionsSvg(ctx *context.Context) { + fontFamilyNames := strings.Split(badge.DefaultFontFamily, ",") + selectedFontFamilyName := ctx.FormString("font", fontFamilyNames[0]) + var badges []badge.Badge + badges = append(badges, badge.GenerateBadge("å•Šå•Šå•Šå•Šå•Šå•Šå•Šå•Šå•Šå•Šå•Šå•Š", "🌞🌞🌞🌞🌞", "green")) + for r := rune(0); r < 256; r++ { + if unicode.IsPrint(r) { + s := strings.Repeat(string(r), 15) + badges = append(badges, badge.GenerateBadge(s, util.TruncateRunes(s, 7), "green")) + } } - if ctx.Req.URL.Path == "/devtest/commit-sign-badge" { - var commits []*asymkey.SignCommit - mockUsers, _ := db.Find[user_model.User](ctx, user_model.SearchUserOptions{ListOptions: db.ListOptions{PageSize: 1}}) - mockUser := mockUsers[0] - commits = append(commits, &asymkey.SignCommit{ - Verification: &asymkey.CommitVerification{}, - UserCommit: &user_model.UserCommit{ - Commit: &git.Commit{ID: git.Sha1ObjectFormat.EmptyObjectID()}, - }, - }) - commits = append(commits, &asymkey.SignCommit{ - Verification: &asymkey.CommitVerification{ - Verified: true, - Reason: "name / key-id", - SigningUser: mockUser, - SigningKey: &asymkey.GPGKey{KeyID: "12345678"}, - TrustStatus: "trusted", - }, - UserCommit: &user_model.UserCommit{ - User: mockUser, - Commit: &git.Commit{ID: git.Sha1ObjectFormat.EmptyObjectID()}, - }, - }) - commits = append(commits, &asymkey.SignCommit{ - Verification: &asymkey.CommitVerification{ - Verified: true, - Reason: "name / key-id", - SigningUser: mockUser, - SigningSSHKey: &asymkey.PublicKey{Fingerprint: "aa:bb:cc:dd:ee"}, - TrustStatus: "untrusted", - }, - UserCommit: &user_model.UserCommit{ - User: mockUser, - Commit: &git.Commit{ID: git.Sha1ObjectFormat.EmptyObjectID()}, - }, - }) - commits = append(commits, &asymkey.SignCommit{ - Verification: &asymkey.CommitVerification{ - Verified: true, - Reason: "name / key-id", - SigningUser: mockUser, - SigningSSHKey: &asymkey.PublicKey{Fingerprint: "aa:bb:cc:dd:ee"}, - TrustStatus: "other(unmatch)", - }, - UserCommit: &user_model.UserCommit{ - User: mockUser, - Commit: &git.Commit{ID: git.Sha1ObjectFormat.EmptyObjectID()}, - }, - }) - commits = append(commits, &asymkey.SignCommit{ - Verification: &asymkey.CommitVerification{ - Warning: true, - Reason: "gpg.error", - SigningEmail: "test@example.com", - }, - UserCommit: &user_model.UserCommit{ - User: mockUser, - Commit: &git.Commit{ID: git.Sha1ObjectFormat.EmptyObjectID()}, - }, - }) + var badgeSVGs []template.HTML + for i, b := range badges { + b.IDPrefix = "devtest-" + strconv.FormatInt(int64(i), 10) + "-" + b.FontFamily = selectedFontFamilyName + h, err := ctx.RenderToHTML("shared/actions/runner_badge", map[string]any{"Badge": b}) + if err != nil { + ctx.ServerError("RenderToHTML", err) + return + } + badgeSVGs = append(badgeSVGs, h) + } + ctx.Data["BadgeSVGs"] = badgeSVGs + ctx.Data["BadgeFontFamilyNames"] = fontFamilyNames + ctx.Data["SelectedFontFamilyName"] = selectedFontFamilyName +} - ctx.Data["MockCommits"] = commits +func prepareMockData(ctx *context.Context) { + switch ctx.Req.URL.Path { + case "/devtest/gitea-ui": + prepareMockDataGiteaUI(ctx) + case "/devtest/badge-commit-sign": + prepareMockDataBadgeCommitSign(ctx) + case "/devtest/badge-actions-svg": + prepareMockDataBadgeActionsSvg(ctx) } } -func Tmpl(ctx *context.Context) { +func TmplCommon(ctx *context.Context) { prepareMockData(ctx) if ctx.Req.Method == "POST" { _ = ctx.Req.ParseForm() diff --git a/routers/web/githttp.go b/routers/web/githttp.go index 8597ffe795..06de811f16 100644 --- a/routers/web/githttp.go +++ b/routers/web/githttp.go @@ -4,26 +4,12 @@ package web import ( - "net/http" - - "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/routers/web/repo" "code.gitea.io/gitea/services/context" ) func addOwnerRepoGitHTTPRouters(m *web.Router) { - reqGitSignIn := func(ctx *context.Context) { - if !setting.Service.RequireSignInView { - return - } - // rely on the results of Contexter - if !ctx.IsSigned { - // TODO: support digit auth - which would be Authorization header with digit - ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm="Gitea"`) - ctx.HTTPError(http.StatusUnauthorized) - } - } m.Group("/{username}/{reponame}", func() { m.Methods("POST,OPTIONS", "/git-upload-pack", repo.ServiceUploadPack) m.Methods("POST,OPTIONS", "/git-receive-pack", repo.ServiceReceivePack) @@ -36,5 +22,5 @@ func addOwnerRepoGitHTTPRouters(m *web.Router) { m.Methods("GET,OPTIONS", "/objects/{head:[0-9a-f]{2}}/{hash:[0-9a-f]{38,62}}", repo.GetLooseObject) m.Methods("GET,OPTIONS", "/objects/pack/pack-{file:[0-9a-f]{40,64}}.pack", repo.GetPackFile) m.Methods("GET,OPTIONS", "/objects/pack/pack-{file:[0-9a-f]{40,64}}.idx", repo.GetIdxFile) - }, optSignInIgnoreCsrf, reqGitSignIn, repo.HTTPGitEnabledHandler, repo.CorsHandler(), context.UserAssignmentWeb()) + }, optSignInIgnoreCsrf, repo.HTTPGitEnabledHandler, repo.CorsHandler(), context.UserAssignmentWeb()) } diff --git a/routers/web/org/projects.go b/routers/web/org/projects.go index 985fd2ca45..49f4792772 100644 --- a/routers/web/org/projects.go +++ b/routers/web/org/projects.go @@ -347,11 +347,11 @@ func ViewProject(ctx *context.Context) { if ctx.Written() { return } - assigneeID := ctx.FormInt64("assignee") // TODO: use "optional" but not 0 in the future + assigneeID := ctx.FormString("assignee") opts := issues_model.IssuesOptions{ LabelIDs: labelIDs, - AssigneeID: optional.Some(assigneeID), + AssigneeID: assigneeID, Owner: project.Owner, Doer: ctx.Doer, } diff --git a/routers/web/org/worktime.go b/routers/web/org/worktime.go index 2336984825..a576dd9a11 100644 --- a/routers/web/org/worktime.go +++ b/routers/web/org/worktime.go @@ -55,13 +55,14 @@ func Worktime(ctx *context.Context) { var worktimeSumResult any var err error - if worktimeBy == "milestones" { + switch worktimeBy { + case "milestones": worktimeSumResult, err = organization.GetWorktimeByMilestones(ctx.Org.Organization, unixFrom, unixTo) ctx.Data["WorktimeByMilestones"] = true - } else if worktimeBy == "members" { + case "members": worktimeSumResult, err = organization.GetWorktimeByMembers(ctx.Org.Organization, unixFrom, unixTo) ctx.Data["WorktimeByMembers"] = true - } else /* by repos */ { + default: /* by repos */ worktimeSumResult, err = organization.GetWorktimeByRepos(ctx.Org.Organization, unixFrom, unixTo) ctx.Data["WorktimeByRepos"] = true } diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go index 41f0d2d0ec..eb6fc6ded6 100644 --- a/routers/web/repo/actions/view.go +++ b/routers/web/repo/actions/view.go @@ -14,7 +14,6 @@ import ( "net/http" "net/url" "strconv" - "strings" "time" actions_model "code.gitea.io/gitea/models/actions" @@ -31,6 +30,7 @@ import ( "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/routers/common" actions_service "code.gitea.io/gitea/services/actions" context_module "code.gitea.io/gitea/services/context" notify_service "code.gitea.io/gitea/services/notify" @@ -469,49 +469,19 @@ func Logs(ctx *context_module.Context) { runIndex := getRunIndex(ctx) jobIndex := ctx.PathParamInt64("job") - job, _ := getRunJobs(ctx, runIndex, jobIndex) - if ctx.Written() { - return - } - if job.TaskID == 0 { - ctx.HTTPError(http.StatusNotFound, "job is not started") - return - } - - err := job.LoadRun(ctx) - if err != nil { - ctx.HTTPError(http.StatusInternalServerError, err.Error()) - return - } - - task, err := actions_model.GetTaskByID(ctx, job.TaskID) - if err != nil { - ctx.HTTPError(http.StatusInternalServerError, err.Error()) - return - } - if task.LogExpired { - ctx.HTTPError(http.StatusNotFound, "logs have been cleaned up") - return - } - - reader, err := actions.OpenLogs(ctx, task.LogInStorage, task.LogFilename) + run, err := actions_model.GetRunByIndex(ctx, ctx.Repo.Repository.ID, runIndex) if err != nil { - ctx.HTTPError(http.StatusInternalServerError, err.Error()) + ctx.NotFoundOrServerError("GetRunByIndex", func(err error) bool { + return errors.Is(err, util.ErrNotExist) + }, err) return } - defer reader.Close() - workflowName := job.Run.WorkflowID - if p := strings.Index(workflowName, "."); p > 0 { - workflowName = workflowName[0:p] + if err = common.DownloadActionsRunJobLogsWithIndex(ctx.Base, ctx.Repo.Repository, run.ID, jobIndex); err != nil { + ctx.NotFoundOrServerError("DownloadActionsRunJobLogsWithIndex", func(err error) bool { + return errors.Is(err, util.ErrNotExist) + }, err) } - ctx.ServeContent(reader, &context_module.ServeHeaderOptions{ - Filename: fmt.Sprintf("%v-%v-%v.log", workflowName, job.Name, task.ID), - ContentLength: &task.LogSize, - ContentType: "text/plain", - ContentTypeCharset: "utf-8", - Disposition: "attachment", - }) } func Cancel(ctx *context_module.Context) { diff --git a/routers/web/repo/code_frequency.go b/routers/web/repo/code_frequency.go index e212d3b60c..2b2dd5744a 100644 --- a/routers/web/repo/code_frequency.go +++ b/routers/web/repo/code_frequency.go @@ -34,7 +34,7 @@ func CodeFrequencyData(ctx *context.Context) { ctx.Status(http.StatusAccepted) return } - ctx.ServerError("GetCodeFrequencyData", err) + ctx.ServerError("GetContributorStats", err) } else { ctx.JSON(http.StatusOK, contributorStats["total"].Weeks) } diff --git a/routers/web/repo/commit.go b/routers/web/repo/commit.go index bbdcf9875e..3fd1eacb58 100644 --- a/routers/web/repo/commit.go +++ b/routers/web/repo/commit.go @@ -284,7 +284,7 @@ func Diff(ctx *context.Context) { ) if ctx.Data["PageIsWiki"] != nil { - gitRepo, err = gitrepo.OpenWikiRepository(ctx, ctx.Repo.Repository) + gitRepo, err = gitrepo.OpenRepository(ctx, ctx.Repo.Repository.WikiStorageRepo()) if err != nil { ctx.ServerError("Repo.GitRepo.GetCommit", err) return @@ -417,7 +417,7 @@ func Diff(ctx *context.Context) { func RawDiff(ctx *context.Context) { var gitRepo *git.Repository if ctx.Data["PageIsWiki"] != nil { - wikiRepo, err := gitrepo.OpenWikiRepository(ctx, ctx.Repo.Repository) + wikiRepo, err := gitrepo.OpenRepository(ctx, ctx.Repo.Repository.WikiStorageRepo()) if err != nil { ctx.ServerError("OpenRepository", err) return diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go index 6cea95e387..2c36477e6a 100644 --- a/routers/web/repo/compare.go +++ b/routers/web/repo/compare.go @@ -569,19 +569,13 @@ func PrepareCompareDiff( ctx *context.Context, ci *common.CompareInfo, whitespaceBehavior git.TrustedCmdArgs, -) bool { - var ( - repo = ctx.Repo.Repository - err error - title string - ) - - // Get diff information. - ctx.Data["CommitRepoLink"] = ci.HeadRepo.Link() - +) (nothingToCompare bool) { + repo := ctx.Repo.Repository headCommitID := ci.CompareInfo.HeadCommitID + ctx.Data["CommitRepoLink"] = ci.HeadRepo.Link() ctx.Data["AfterCommitID"] = headCommitID + ctx.Data["ExpandNewPrForm"] = ctx.FormBool("expand") if (headCommitID == ci.CompareInfo.MergeBase && !ci.DirectComparison) || headCommitID == ci.CompareInfo.BaseCommitID { @@ -670,6 +664,7 @@ func PrepareCompareDiff( ctx.Data["Commits"] = commits ctx.Data["CommitCount"] = len(commits) + title := ci.HeadBranch if len(commits) == 1 { c := commits[0] title = strings.TrimSpace(c.UserCommit.Summary()) @@ -678,9 +673,8 @@ func PrepareCompareDiff( if len(body) > 1 { ctx.Data["content"] = strings.Join(body[1:], "\n") } - } else { - title = ci.HeadBranch } + if len(title) > 255 { var trailer string title, trailer = util.EllipsisDisplayStringX(title, 255) @@ -745,8 +739,7 @@ func CompareDiff(ctx *context.Context) { ctx.Data["OtherCompareSeparator"] = "..." } - nothingToCompare := PrepareCompareDiff(ctx, ci, - gitdiff.GetWhitespaceFlag(ctx.Data["WhitespaceBehavior"].(string))) + nothingToCompare := PrepareCompareDiff(ctx, ci, gitdiff.GetWhitespaceFlag(ctx.Data["WhitespaceBehavior"].(string))) if ctx.Written() { return } @@ -885,7 +878,7 @@ func ExcerptBlob(ctx *context.Context) { gitRepo := ctx.Repo.GitRepo if ctx.Data["PageIsWiki"] == true { var err error - gitRepo, err = gitrepo.OpenWikiRepository(ctx, ctx.Repo.Repository) + gitRepo, err = gitrepo.OpenRepository(ctx, ctx.Repo.Repository.WikiStorageRepo()) if err != nil { ctx.ServerError("OpenRepository", err) return @@ -945,9 +938,10 @@ func ExcerptBlob(ctx *context.Context) { RightHunkSize: rightHunkSize, }, } - if direction == "up" { + switch direction { + case "up": section.Lines = append([]*gitdiff.DiffLine{lineSection}, section.Lines...) - } else if direction == "down" { + case "down": section.Lines = append(section.Lines, lineSection) } } diff --git a/routers/web/repo/githttp.go b/routers/web/repo/githttp.go index f93d7fc66a..89001ddf57 100644 --- a/routers/web/repo/githttp.go +++ b/routers/web/repo/githttp.go @@ -127,7 +127,7 @@ func httpBase(ctx *context.Context) *serviceHandler { // Only public pull don't need auth. isPublicPull := repoExist && !repo.IsPrivate && isPull var ( - askAuth = !isPublicPull || setting.Service.RequireSignInView + askAuth = !isPublicPull || setting.Service.RequireSignInViewStrict environ []string ) diff --git a/routers/web/repo/issue_content_history.go b/routers/web/repo/issue_content_history.go index c2c208736c..3602f4ec8a 100644 --- a/routers/web/repo/issue_content_history.go +++ b/routers/web/repo/issue_content_history.go @@ -157,15 +157,16 @@ func GetContentHistoryDetail(ctx *context.Context) { diffHTMLBuf := bytes.Buffer{} diffHTMLBuf.WriteString("<pre class='chroma'>") for _, it := range diff { - if it.Type == diffmatchpatch.DiffInsert { + switch it.Type { + case diffmatchpatch.DiffInsert: diffHTMLBuf.WriteString("<span class='gi'>") diffHTMLBuf.WriteString(html.EscapeString(it.Text)) diffHTMLBuf.WriteString("</span>") - } else if it.Type == diffmatchpatch.DiffDelete { + case diffmatchpatch.DiffDelete: diffHTMLBuf.WriteString("<span class='gd'>") diffHTMLBuf.WriteString(html.EscapeString(it.Text)) diffHTMLBuf.WriteString("</span>") - } else { + default: diffHTMLBuf.WriteString(html.EscapeString(it.Text)) } } diff --git a/routers/web/repo/issue_list.go b/routers/web/repo/issue_list.go index a65ae77795..5dc9e8a6b5 100644 --- a/routers/web/repo/issue_list.go +++ b/routers/web/repo/issue_list.go @@ -208,10 +208,10 @@ func SearchIssues(ctx *context.Context) { if ctx.IsSigned { ctxUserID := ctx.Doer.ID if ctx.FormBool("created") { - searchOpt.PosterID = optional.Some(ctxUserID) + searchOpt.PosterID = strconv.FormatInt(ctxUserID, 10) } if ctx.FormBool("assigned") { - searchOpt.AssigneeID = optional.Some(ctxUserID) + searchOpt.AssigneeID = strconv.FormatInt(ctxUserID, 10) } if ctx.FormBool("mentioned") { searchOpt.MentionID = optional.Some(ctxUserID) @@ -373,10 +373,10 @@ func SearchRepoIssuesJSON(ctx *context.Context) { } if createdByID > 0 { - searchOpt.PosterID = optional.Some(createdByID) + searchOpt.PosterID = strconv.FormatInt(createdByID, 10) } if assignedByID > 0 { - searchOpt.AssigneeID = optional.Some(assignedByID) + searchOpt.AssigneeID = strconv.FormatInt(assignedByID, 10) } if mentionedByID > 0 { searchOpt.MentionID = optional.Some(mentionedByID) @@ -490,7 +490,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt viewType = "all" } - assigneeID := ctx.FormInt64("assignee") // TODO: use "optional" but not 0 in the future + assigneeID := ctx.FormString("assignee") posterUsername := ctx.FormString("poster") posterUserID := shared_user.GetFilterUserIDByName(ctx, posterUsername) var mentionedID, reviewRequestedID, reviewedID int64 @@ -498,11 +498,11 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt if ctx.IsSigned { switch viewType { case "created_by": - posterUserID = optional.Some(ctx.Doer.ID) + posterUserID = strconv.FormatInt(ctx.Doer.ID, 10) case "mentioned": mentionedID = ctx.Doer.ID case "assigned": - assigneeID = ctx.Doer.ID + assigneeID = fmt.Sprint(ctx.Doer.ID) case "review_requested": reviewRequestedID = ctx.Doer.ID case "reviewed_by": @@ -532,7 +532,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt LabelIDs: labelIDs, MilestoneIDs: mileIDs, ProjectID: projectID, - AssigneeID: optional.Some(assigneeID), + AssigneeID: assigneeID, MentionedID: mentionedID, PosterID: posterUserID, ReviewRequestedID: reviewRequestedID, @@ -613,7 +613,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt PageSize: setting.UI.IssuePagingNum, }, RepoIDs: []int64{repo.ID}, - AssigneeID: optional.Some(assigneeID), + AssigneeID: assigneeID, PosterID: posterUserID, MentionedID: mentionedID, ReviewRequestedID: reviewRequestedID, @@ -696,9 +696,10 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt return 0 } reviewTyp := issues_model.ReviewTypeApprove - if typ == "reject" { + switch typ { + case "reject": reviewTyp = issues_model.ReviewTypeReject - } else if typ == "waiting" { + case "waiting": reviewTyp = issues_model.ReviewTypeRequest } for _, count := range counts { diff --git a/routers/web/repo/issue_stopwatch.go b/routers/web/repo/issue_stopwatch.go index 73e279e0a6..5a8d203771 100644 --- a/routers/web/repo/issue_stopwatch.go +++ b/routers/web/repo/issue_stopwatch.go @@ -4,8 +4,6 @@ package repo import ( - "strings" - "code.gitea.io/gitea/models/db" issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/modules/eventsource" @@ -72,39 +70,3 @@ func CancelStopwatch(c *context.Context) { c.JSONRedirect("") } - -// GetActiveStopwatch is the middleware that sets .ActiveStopwatch on context -func GetActiveStopwatch(ctx *context.Context) { - if strings.HasPrefix(ctx.Req.URL.Path, "/api") { - return - } - - if !ctx.IsSigned { - return - } - - _, sw, issue, err := issues_model.HasUserStopwatch(ctx, ctx.Doer.ID) - if err != nil { - ctx.ServerError("HasUserStopwatch", err) - return - } - - if sw == nil || sw.ID == 0 { - return - } - - ctx.Data["ActiveStopwatch"] = StopwatchTmplInfo{ - issue.Link(), - issue.Repo.FullName(), - issue.Index, - sw.Seconds() + 1, // ensure time is never zero in ui - } -} - -// StopwatchTmplInfo is a view on a stopwatch specifically for template rendering -type StopwatchTmplInfo struct { - IssueLink string - RepoSlug string - IssueIndex int64 - Seconds int64 -} diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go index 5b81a5e4d1..6810025c6f 100644 --- a/routers/web/repo/projects.go +++ b/routers/web/repo/projects.go @@ -315,12 +315,12 @@ func ViewProject(ctx *context.Context) { labelIDs := issue.PrepareFilterIssueLabels(ctx, ctx.Repo.Repository.ID, ctx.Repo.Owner) - assigneeID := ctx.FormInt64("assignee") // TODO: use "optional" but not 0 in the future + assigneeID := ctx.FormString("assignee") issuesMap, err := project_service.LoadIssuesFromProject(ctx, project, &issues_model.IssuesOptions{ RepoIDs: []int64{ctx.Repo.Repository.ID}, LabelIDs: labelIDs, - AssigneeID: optional.Some(assigneeID), + AssigneeID: assigneeID, }) if err != nil { ctx.ServerError("LoadIssuesOfColumns", err) diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go index e12798f93d..c72664f8e9 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -1269,6 +1269,21 @@ func stopTimerIfAvailable(ctx *context.Context, user *user_model.User, issue *is return nil } +func PullsNewRedirect(ctx *context.Context) { + branch := ctx.PathParam("*") + redirectRepo := ctx.Repo.Repository + repo := ctx.Repo.Repository + if repo.IsFork { + if err := repo.GetBaseRepo(ctx); err != nil { + ctx.ServerError("GetBaseRepo", err) + return + } + redirectRepo = repo.BaseRepo + branch = fmt.Sprintf("%s:%s", repo.OwnerName, branch) + } + ctx.Redirect(fmt.Sprintf("%s/compare/%s...%s?expand=1", redirectRepo.Link(), util.PathEscapeSegments(redirectRepo.DefaultBranch), util.PathEscapeSegments(branch))) +} + // CompareAndPullRequestPost response for creating pull request func CompareAndPullRequestPost(ctx *context.Context) { form := web.GetForm(ctx).(*forms.CreateIssueForm) diff --git a/routers/web/repo/pull_review.go b/routers/web/repo/pull_review.go index fb92d24394..929e131d61 100644 --- a/routers/web/repo/pull_review.go +++ b/routers/web/repo/pull_review.go @@ -209,11 +209,12 @@ func renderConversation(ctx *context.Context, comment *issues_model.Comment, ori return user_service.CanBlockUser(ctx, ctx.Doer, blocker, blockee) } - if origin == "diff" { + switch origin { + case "diff": ctx.HTML(http.StatusOK, tplDiffConversation) - } else if origin == "timeline" { + case "timeline": ctx.HTML(http.StatusOK, tplTimelineConversation) - } else { + default: ctx.HTTPError(http.StatusBadRequest, "Unknown origin: "+origin) } } diff --git a/routers/web/repo/recent_commits.go b/routers/web/repo/recent_commits.go index 228eb0dbac..2660116062 100644 --- a/routers/web/repo/recent_commits.go +++ b/routers/web/repo/recent_commits.go @@ -4,12 +4,10 @@ package repo import ( - "errors" "net/http" "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/services/context" - contributors_service "code.gitea.io/gitea/services/repository" ) const ( @@ -26,16 +24,3 @@ func RecentCommits(ctx *context.Context) { ctx.HTML(http.StatusOK, tplRecentCommits) } - -// RecentCommitsData returns JSON of recent commits data -func RecentCommitsData(ctx *context.Context) { - if contributorStats, err := contributors_service.GetContributorStats(ctx, ctx.Cache, ctx.Repo.Repository, ctx.Repo.Repository.DefaultBranch); err != nil { - if errors.Is(err, contributors_service.ErrAwaitGeneration) { - ctx.Status(http.StatusAccepted) - return - } - ctx.ServerError("RecentCommitsData", err) - } else { - ctx.JSON(http.StatusOK, contributorStats["total"].Weeks) - } -} diff --git a/routers/web/repo/repo.go b/routers/web/repo/repo.go index 73baf683ed..54b7448a89 100644 --- a/routers/web/repo/repo.go +++ b/routers/web/repo/repo.go @@ -154,8 +154,8 @@ func createCommon(ctx *context.Context) { ctx.Data["Licenses"] = repo_module.Licenses ctx.Data["Readmes"] = repo_module.Readmes ctx.Data["IsForcedPrivate"] = setting.Repository.ForcePrivate - ctx.Data["CanCreateRepo"] = ctx.Doer.CanCreateRepo() - ctx.Data["MaxCreationLimit"] = ctx.Doer.MaxCreationLimit() + ctx.Data["CanCreateRepoInDoer"] = ctx.Doer.CanCreateRepo() + ctx.Data["MaxCreationLimitOfDoer"] = ctx.Doer.MaxCreationLimit() ctx.Data["SupportedObjectFormats"] = git.DefaultFeatures().SupportedObjectFormats ctx.Data["DefaultObjectFormat"] = git.Sha1ObjectFormat } diff --git a/routers/web/repo/setting/public_access.go b/routers/web/repo/setting/public_access.go new file mode 100644 index 0000000000..368d34294a --- /dev/null +++ b/routers/web/repo/setting/public_access.go @@ -0,0 +1,155 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package setting + +import ( + "net/http" + "slices" + "strconv" + + "code.gitea.io/gitea/models/perm" + "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unit" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/templates" + "code.gitea.io/gitea/services/context" +) + +const tplRepoSettingsPublicAccess templates.TplName = "repo/settings/public_access" + +func parsePublicAccessMode(permission string, allowed []string) (ret struct { + AnonymousAccessMode, EveryoneAccessMode perm.AccessMode +}, +) { + ret.AnonymousAccessMode = perm.AccessModeNone + ret.EveryoneAccessMode = perm.AccessModeNone + + // if site admin forces repositories to be private, then do not allow any other access mode, + // otherwise the "force private" setting would be bypassed + if setting.Repository.ForcePrivate { + return ret + } + if !slices.Contains(allowed, permission) { + return ret + } + switch permission { + case paAnonymousRead: + ret.AnonymousAccessMode = perm.AccessModeRead + case paEveryoneRead: + ret.EveryoneAccessMode = perm.AccessModeRead + case paEveryoneWrite: + ret.EveryoneAccessMode = perm.AccessModeWrite + } + return ret +} + +const ( + paNotSet = "not-set" + paAnonymousRead = "anonymous-read" + paEveryoneRead = "everyone-read" + paEveryoneWrite = "everyone-write" +) + +type repoUnitPublicAccess struct { + UnitType unit.Type + FormKey string + DisplayName string + PublicAccessTypes []string + UnitPublicAccess string +} + +func repoUnitPublicAccesses(ctx *context.Context) []*repoUnitPublicAccess { + accesses := []*repoUnitPublicAccess{ + { + UnitType: unit.TypeCode, + DisplayName: ctx.Locale.TrString("repo.code"), + PublicAccessTypes: []string{paAnonymousRead, paEveryoneRead}, + }, + { + UnitType: unit.TypeIssues, + DisplayName: ctx.Locale.TrString("issues"), + PublicAccessTypes: []string{paAnonymousRead, paEveryoneRead}, + }, + { + UnitType: unit.TypePullRequests, + DisplayName: ctx.Locale.TrString("pull_requests"), + PublicAccessTypes: []string{paAnonymousRead, paEveryoneRead}, + }, + { + UnitType: unit.TypeReleases, + DisplayName: ctx.Locale.TrString("repo.releases"), + PublicAccessTypes: []string{paAnonymousRead, paEveryoneRead}, + }, + { + UnitType: unit.TypeWiki, + DisplayName: ctx.Locale.TrString("repo.wiki"), + PublicAccessTypes: []string{paAnonymousRead, paEveryoneRead, paEveryoneWrite}, + }, + { + UnitType: unit.TypeProjects, + DisplayName: ctx.Locale.TrString("repo.projects"), + PublicAccessTypes: []string{paAnonymousRead, paEveryoneRead}, + }, + { + UnitType: unit.TypePackages, + DisplayName: ctx.Locale.TrString("repo.packages"), + PublicAccessTypes: []string{paAnonymousRead, paEveryoneRead}, + }, + { + UnitType: unit.TypeActions, + DisplayName: ctx.Locale.TrString("repo.actions"), + PublicAccessTypes: []string{paAnonymousRead, paEveryoneRead}, + }, + } + for _, ua := range accesses { + ua.FormKey = "repo-unit-access-" + strconv.Itoa(int(ua.UnitType)) + for _, u := range ctx.Repo.Repository.Units { + if u.Type == ua.UnitType { + ua.UnitPublicAccess = paNotSet + switch { + case u.EveryoneAccessMode == perm.AccessModeWrite: + ua.UnitPublicAccess = paEveryoneWrite + case u.EveryoneAccessMode == perm.AccessModeRead: + ua.UnitPublicAccess = paEveryoneRead + case u.AnonymousAccessMode == perm.AccessModeRead: + ua.UnitPublicAccess = paAnonymousRead + } + break + } + } + } + return slices.DeleteFunc(accesses, func(ua *repoUnitPublicAccess) bool { + return ua.UnitPublicAccess == "" + }) +} + +func PublicAccess(ctx *context.Context) { + ctx.Data["PageIsSettingsPublicAccess"] = true + ctx.Data["RepoUnitPublicAccesses"] = repoUnitPublicAccesses(ctx) + ctx.Data["GlobalForcePrivate"] = setting.Repository.ForcePrivate + if setting.Repository.ForcePrivate { + ctx.Flash.Error(ctx.Tr("form.repository_force_private"), true) + } + ctx.HTML(http.StatusOK, tplRepoSettingsPublicAccess) +} + +func PublicAccessPost(ctx *context.Context) { + accesses := repoUnitPublicAccesses(ctx) + for _, ua := range accesses { + formVal := ctx.FormString(ua.FormKey) + parsed := parsePublicAccessMode(formVal, ua.PublicAccessTypes) + err := repo.UpdateRepoUnitPublicAccess(ctx, &repo.RepoUnit{ + RepoID: ctx.Repo.Repository.ID, + Type: ua.UnitType, + AnonymousAccessMode: parsed.AnonymousAccessMode, + EveryoneAccessMode: parsed.EveryoneAccessMode, + }) + if err != nil { + ctx.ServerError("UpdateRepoUnitPublicAccess", err) + return + } + } + ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success")) + ctx.Redirect(ctx.Repo.Repository.Link() + "/settings/public_access") +} diff --git a/routers/web/repo/setting/setting.go b/routers/web/repo/setting/setting.go index ac7eb768fa..a0edb1e11a 100644 --- a/routers/web/repo/setting/setting.go +++ b/routers/web/repo/setting/setting.go @@ -13,7 +13,6 @@ import ( "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/organization" - "code.gitea.io/gitea/models/perm" repo_model "code.gitea.io/gitea/models/repo" unit_model "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" @@ -37,6 +36,8 @@ import ( mirror_service "code.gitea.io/gitea/services/mirror" repo_service "code.gitea.io/gitea/services/repository" wiki_service "code.gitea.io/gitea/services/wiki" + + "xorm.io/xorm/convert" ) const ( @@ -48,15 +49,6 @@ const ( tplDeployKeys templates.TplName = "repo/settings/deploy_keys" ) -func parseEveryoneAccessMode(permission string, allowed ...perm.AccessMode) perm.AccessMode { - // if site admin forces repositories to be private, then do not allow any other access mode, - // otherwise the "force private" setting would be bypassed - if setting.Repository.ForcePrivate { - return perm.AccessModeNone - } - return perm.ParseAccessMode(permission, allowed...) -} - // SettingsCtxData is a middleware that sets all the general context data for the // settings template. func SettingsCtxData(ctx *context.Context) { @@ -105,8 +97,6 @@ func Settings(ctx *context.Context) { // SettingsPost response for changes of a repository func SettingsPost(ctx *context.Context) { - form := web.GetForm(ctx).(*forms.RepoSettingForm) - ctx.Data["ForcePrivate"] = setting.Repository.ForcePrivate ctx.Data["MirrorsEnabled"] = setting.Mirror.Enabled ctx.Data["DisableNewPullMirrors"] = setting.Mirror.DisableNewPull @@ -119,867 +109,937 @@ func SettingsPost(ctx *context.Context) { ctx.Data["SigningSettings"] = setting.Repository.Signing ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled - repo := ctx.Repo.Repository - switch ctx.FormString("action") { case "update": - if ctx.HasError() { - ctx.HTML(http.StatusOK, tplSettingsOptions) - return - } + handleSettingsPostUpdate(ctx) + case "mirror": + handleSettingsPostMirror(ctx) + case "mirror-sync": + handleSettingsPostMirrorSync(ctx) + case "push-mirror-sync": + handleSettingsPostPushMirrorSync(ctx) + case "push-mirror-update": + handleSettingsPostPushMirrorUpdate(ctx) + case "push-mirror-remove": + handleSettingsPostPushMirrorRemove(ctx) + case "push-mirror-add": + handleSettingsPostPushMirrorAdd(ctx) + case "advanced": + handleSettingsPostAdvanced(ctx) + case "signing": + handleSettingsPostSigning(ctx) + case "admin": + handleSettingsPostAdmin(ctx) + case "admin_index": + handleSettingsPostAdminIndex(ctx) + case "convert": + handleSettingsPostConvert(ctx) + case "convert_fork": + handleSettingsPostConvertFork(ctx) + case "transfer": + handleSettingsPostTransfer(ctx) + case "cancel_transfer": + handleSettingsPostCancelTransfer(ctx) + case "delete": + handleSettingsPostDelete(ctx) + case "delete-wiki": + handleSettingsPostDeleteWiki(ctx) + case "archive": + handleSettingsPostArchive(ctx) + case "unarchive": + handleSettingsPostUnarchive(ctx) + case "visibility": + handleSettingsPostVisibility(ctx) + default: + ctx.NotFound(nil) + } +} - newRepoName := form.RepoName - // Check if repository name has been changed. - if repo.LowerName != strings.ToLower(newRepoName) { - // Close the GitRepo if open - if ctx.Repo.GitRepo != nil { - ctx.Repo.GitRepo.Close() - ctx.Repo.GitRepo = nil - } - if err := repo_service.ChangeRepositoryName(ctx, ctx.Doer, repo, newRepoName); err != nil { +func handleSettingsPostUpdate(ctx *context.Context) { + form := web.GetForm(ctx).(*forms.RepoSettingForm) + repo := ctx.Repo.Repository + if ctx.HasError() { + ctx.HTML(http.StatusOK, tplSettingsOptions) + return + } + + newRepoName := form.RepoName + // Check if repository name has been changed. + if repo.LowerName != strings.ToLower(newRepoName) { + // Close the GitRepo if open + if ctx.Repo.GitRepo != nil { + ctx.Repo.GitRepo.Close() + ctx.Repo.GitRepo = nil + } + if err := repo_service.ChangeRepositoryName(ctx, ctx.Doer, repo, newRepoName); err != nil { + ctx.Data["Err_RepoName"] = true + switch { + case repo_model.IsErrRepoAlreadyExist(err): + ctx.RenderWithErr(ctx.Tr("form.repo_name_been_taken"), tplSettingsOptions, &form) + case db.IsErrNameReserved(err): + ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name), tplSettingsOptions, &form) + case repo_model.IsErrRepoFilesAlreadyExist(err): ctx.Data["Err_RepoName"] = true switch { - case repo_model.IsErrRepoAlreadyExist(err): - ctx.RenderWithErr(ctx.Tr("form.repo_name_been_taken"), tplSettingsOptions, &form) - case db.IsErrNameReserved(err): - ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name), tplSettingsOptions, &form) - case repo_model.IsErrRepoFilesAlreadyExist(err): - ctx.Data["Err_RepoName"] = true - switch { - case ctx.IsUserSiteAdmin() || (setting.Repository.AllowAdoptionOfUnadoptedRepositories && setting.Repository.AllowDeleteOfUnadoptedRepositories): - ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt_or_delete"), tplSettingsOptions, form) - case setting.Repository.AllowAdoptionOfUnadoptedRepositories: - ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt"), tplSettingsOptions, form) - case setting.Repository.AllowDeleteOfUnadoptedRepositories: - ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.delete"), tplSettingsOptions, form) - default: - ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist"), tplSettingsOptions, form) - } - case db.IsErrNamePatternNotAllowed(err): - ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tplSettingsOptions, &form) + case ctx.IsUserSiteAdmin() || (setting.Repository.AllowAdoptionOfUnadoptedRepositories && setting.Repository.AllowDeleteOfUnadoptedRepositories): + ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt_or_delete"), tplSettingsOptions, form) + case setting.Repository.AllowAdoptionOfUnadoptedRepositories: + ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt"), tplSettingsOptions, form) + case setting.Repository.AllowDeleteOfUnadoptedRepositories: + ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.delete"), tplSettingsOptions, form) default: - ctx.ServerError("ChangeRepositoryName", err) + ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist"), tplSettingsOptions, form) } - return + case db.IsErrNamePatternNotAllowed(err): + ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tplSettingsOptions, &form) + default: + ctx.ServerError("ChangeRepositoryName", err) } - - log.Trace("Repository name changed: %s/%s -> %s", ctx.Repo.Owner.Name, repo.Name, newRepoName) - } - // In case it's just a case change. - repo.Name = newRepoName - repo.LowerName = strings.ToLower(newRepoName) - repo.Description = form.Description - repo.Website = form.Website - repo.IsTemplate = form.Template - - // Visibility of forked repository is forced sync with base repository. - if repo.IsFork { - form.Private = repo.BaseRepo.IsPrivate || repo.BaseRepo.Owner.Visibility == structs.VisibleTypePrivate - } - - if err := repo_service.UpdateRepository(ctx, repo, false); err != nil { - ctx.ServerError("UpdateRepository", err) return } - log.Trace("Repository basic settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name) - ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success")) - ctx.Redirect(repo.Link() + "/settings") + log.Trace("Repository name changed: %s/%s -> %s", ctx.Repo.Owner.Name, repo.Name, newRepoName) + } + // In case it's just a case change. + repo.Name = newRepoName + repo.LowerName = strings.ToLower(newRepoName) + repo.Description = form.Description + repo.Website = form.Website + repo.IsTemplate = form.Template + + // Visibility of forked repository is forced sync with base repository. + if repo.IsFork { + form.Private = repo.BaseRepo.IsPrivate || repo.BaseRepo.Owner.Visibility == structs.VisibleTypePrivate + } - case "mirror": - if !setting.Mirror.Enabled || !repo.IsMirror || repo.IsArchived { - ctx.NotFound(nil) - return - } + if err := repo_service.UpdateRepository(ctx, repo, false); err != nil { + ctx.ServerError("UpdateRepository", err) + return + } + log.Trace("Repository basic settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name) - pullMirror, err := repo_model.GetMirrorByRepoID(ctx, ctx.Repo.Repository.ID) - if err == repo_model.ErrMirrorNotExist { - ctx.NotFound(nil) - return - } - if err != nil { - ctx.ServerError("GetMirrorByRepoID", err) - return - } - // This section doesn't require repo_name/RepoName to be set in the form, don't show it - // as an error on the UI for this action - ctx.Data["Err_RepoName"] = nil - - interval, err := time.ParseDuration(form.Interval) - if err != nil || (interval != 0 && interval < setting.Mirror.MinInterval) { - ctx.Data["Err_Interval"] = true - ctx.RenderWithErr(ctx.Tr("repo.mirror_interval_invalid"), tplSettingsOptions, &form) - return - } + ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success")) + ctx.Redirect(repo.Link() + "/settings") +} - pullMirror.EnablePrune = form.EnablePrune - pullMirror.Interval = interval - pullMirror.ScheduleNextUpdate() - if err := repo_model.UpdateMirror(ctx, pullMirror); err != nil { - ctx.ServerError("UpdateMirror", err) - return - } +func handleSettingsPostMirror(ctx *context.Context) { + form := web.GetForm(ctx).(*forms.RepoSettingForm) + repo := ctx.Repo.Repository + if !setting.Mirror.Enabled || !repo.IsMirror || repo.IsArchived { + ctx.NotFound(nil) + return + } - u, err := git.GetRemoteURL(ctx, ctx.Repo.Repository.RepoPath(), pullMirror.GetRemoteName()) - if err != nil { - ctx.Data["Err_MirrorAddress"] = true - handleSettingRemoteAddrError(ctx, err, form) - return - } - if u.User != nil && form.MirrorPassword == "" && form.MirrorUsername == u.User.Username() { - form.MirrorPassword, _ = u.User.Password() - } + pullMirror, err := repo_model.GetMirrorByRepoID(ctx, ctx.Repo.Repository.ID) + if err == repo_model.ErrMirrorNotExist { + ctx.NotFound(nil) + return + } + if err != nil { + ctx.ServerError("GetMirrorByRepoID", err) + return + } + // This section doesn't require repo_name/RepoName to be set in the form, don't show it + // as an error on the UI for this action + ctx.Data["Err_RepoName"] = nil + + interval, err := time.ParseDuration(form.Interval) + if err != nil || (interval != 0 && interval < setting.Mirror.MinInterval) { + ctx.Data["Err_Interval"] = true + ctx.RenderWithErr(ctx.Tr("repo.mirror_interval_invalid"), tplSettingsOptions, &form) + return + } - address, err := git.ParseRemoteAddr(form.MirrorAddress, form.MirrorUsername, form.MirrorPassword) - if err == nil { - err = migrations.IsMigrateURLAllowed(address, ctx.Doer) - } - if err != nil { - ctx.Data["Err_MirrorAddress"] = true - handleSettingRemoteAddrError(ctx, err, form) - return - } + pullMirror.EnablePrune = form.EnablePrune + pullMirror.Interval = interval + pullMirror.ScheduleNextUpdate() + if err := repo_model.UpdateMirror(ctx, pullMirror); err != nil { + ctx.ServerError("UpdateMirror", err) + return + } - if err := mirror_service.UpdateAddress(ctx, pullMirror, address); err != nil { - ctx.ServerError("UpdateAddress", err) - return - } + u, err := git.GetRemoteURL(ctx, ctx.Repo.Repository.RepoPath(), pullMirror.GetRemoteName()) + if err != nil { + ctx.Data["Err_MirrorAddress"] = true + handleSettingRemoteAddrError(ctx, err, form) + return + } + if u.User != nil && form.MirrorPassword == "" && form.MirrorUsername == u.User.Username() { + form.MirrorPassword, _ = u.User.Password() + } - remoteAddress, err := util.SanitizeURL(form.MirrorAddress) - if err != nil { - ctx.Data["Err_MirrorAddress"] = true - handleSettingRemoteAddrError(ctx, err, form) - return - } - pullMirror.RemoteAddress = remoteAddress + address, err := git.ParseRemoteAddr(form.MirrorAddress, form.MirrorUsername, form.MirrorPassword) + if err == nil { + err = migrations.IsMigrateURLAllowed(address, ctx.Doer) + } + if err != nil { + ctx.Data["Err_MirrorAddress"] = true + handleSettingRemoteAddrError(ctx, err, form) + return + } - form.LFS = form.LFS && setting.LFS.StartServer + if err := mirror_service.UpdateAddress(ctx, pullMirror, address); err != nil { + ctx.ServerError("UpdateAddress", err) + return + } - if len(form.LFSEndpoint) > 0 { - ep := lfs.DetermineEndpoint("", form.LFSEndpoint) - if ep == nil { - ctx.Data["Err_LFSEndpoint"] = true - ctx.RenderWithErr(ctx.Tr("repo.migrate.invalid_lfs_endpoint"), tplSettingsOptions, &form) - return - } - err = migrations.IsMigrateURLAllowed(ep.String(), ctx.Doer) - if err != nil { - ctx.Data["Err_LFSEndpoint"] = true - handleSettingRemoteAddrError(ctx, err, form) - return - } - } + remoteAddress, err := util.SanitizeURL(form.MirrorAddress) + if err != nil { + ctx.Data["Err_MirrorAddress"] = true + handleSettingRemoteAddrError(ctx, err, form) + return + } + pullMirror.RemoteAddress = remoteAddress + + form.LFS = form.LFS && setting.LFS.StartServer - pullMirror.LFS = form.LFS - pullMirror.LFSEndpoint = form.LFSEndpoint - if err := repo_model.UpdateMirror(ctx, pullMirror); err != nil { - ctx.ServerError("UpdateMirror", err) + if len(form.LFSEndpoint) > 0 { + ep := lfs.DetermineEndpoint("", form.LFSEndpoint) + if ep == nil { + ctx.Data["Err_LFSEndpoint"] = true + ctx.RenderWithErr(ctx.Tr("repo.migrate.invalid_lfs_endpoint"), tplSettingsOptions, &form) return } - - ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success")) - ctx.Redirect(repo.Link() + "/settings") - - case "mirror-sync": - if !setting.Mirror.Enabled || !repo.IsMirror || repo.IsArchived { - ctx.NotFound(nil) + err = migrations.IsMigrateURLAllowed(ep.String(), ctx.Doer) + if err != nil { + ctx.Data["Err_LFSEndpoint"] = true + handleSettingRemoteAddrError(ctx, err, form) return } + } - mirror_service.AddPullMirrorToQueue(repo.ID) + pullMirror.LFS = form.LFS + pullMirror.LFSEndpoint = form.LFSEndpoint + if err := repo_model.UpdateMirror(ctx, pullMirror); err != nil { + ctx.ServerError("UpdateMirror", err) + return + } - ctx.Flash.Info(ctx.Tr("repo.settings.pull_mirror_sync_in_progress", repo.OriginalURL)) - ctx.Redirect(repo.Link() + "/settings") + ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success")) + ctx.Redirect(repo.Link() + "/settings") +} - case "push-mirror-sync": - if !setting.Mirror.Enabled { - ctx.NotFound(nil) - return - } +func handleSettingsPostMirrorSync(ctx *context.Context) { + repo := ctx.Repo.Repository + if !setting.Mirror.Enabled || !repo.IsMirror || repo.IsArchived { + ctx.NotFound(nil) + return + } - m, _, _ := repo_model.GetPushMirrorByIDAndRepoID(ctx, form.PushMirrorID, repo.ID) - if m == nil { - ctx.NotFound(nil) - return - } + mirror_service.AddPullMirrorToQueue(repo.ID) - mirror_service.AddPushMirrorToQueue(m.ID) + ctx.Flash.Info(ctx.Tr("repo.settings.pull_mirror_sync_in_progress", repo.OriginalURL)) + ctx.Redirect(repo.Link() + "/settings") +} - ctx.Flash.Info(ctx.Tr("repo.settings.push_mirror_sync_in_progress", m.RemoteAddress)) - ctx.Redirect(repo.Link() + "/settings") +func handleSettingsPostPushMirrorSync(ctx *context.Context) { + form := web.GetForm(ctx).(*forms.RepoSettingForm) + repo := ctx.Repo.Repository - case "push-mirror-update": - if !setting.Mirror.Enabled || repo.IsArchived { - ctx.NotFound(nil) - return - } + if !setting.Mirror.Enabled { + ctx.NotFound(nil) + return + } - // This section doesn't require repo_name/RepoName to be set in the form, don't show it - // as an error on the UI for this action - ctx.Data["Err_RepoName"] = nil + m, _, _ := repo_model.GetPushMirrorByIDAndRepoID(ctx, form.PushMirrorID, repo.ID) + if m == nil { + ctx.NotFound(nil) + return + } - interval, err := time.ParseDuration(form.PushMirrorInterval) - if err != nil || (interval != 0 && interval < setting.Mirror.MinInterval) { - ctx.RenderWithErr(ctx.Tr("repo.mirror_interval_invalid"), tplSettingsOptions, &forms.RepoSettingForm{}) - return - } + mirror_service.AddPushMirrorToQueue(m.ID) - m, _, _ := repo_model.GetPushMirrorByIDAndRepoID(ctx, form.PushMirrorID, repo.ID) - if m == nil { - ctx.NotFound(nil) - return - } + ctx.Flash.Info(ctx.Tr("repo.settings.push_mirror_sync_in_progress", m.RemoteAddress)) + ctx.Redirect(repo.Link() + "/settings") +} - m.Interval = interval - if err := repo_model.UpdatePushMirrorInterval(ctx, m); err != nil { - ctx.ServerError("UpdatePushMirrorInterval", err) - return - } - // Background why we are adding it to Queue - // If we observed its implementation in the context of `push-mirror-sync` where it - // is evident that pushing to the queue is necessary for updates. - // So, there are updates within the given interval, it is necessary to update the queue accordingly. - if !ctx.FormBool("push_mirror_defer_sync") { - // push_mirror_defer_sync is mainly for testing purpose, we do not really want to sync the push mirror immediately - mirror_service.AddPushMirrorToQueue(m.ID) - } - ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success")) - ctx.Redirect(repo.Link() + "/settings") +func handleSettingsPostPushMirrorUpdate(ctx *context.Context) { + form := web.GetForm(ctx).(*forms.RepoSettingForm) + repo := ctx.Repo.Repository - case "push-mirror-remove": - if !setting.Mirror.Enabled || repo.IsArchived { - ctx.NotFound(nil) - return - } + if !setting.Mirror.Enabled || repo.IsArchived { + ctx.NotFound(nil) + return + } - // This section doesn't require repo_name/RepoName to be set in the form, don't show it - // as an error on the UI for this action - ctx.Data["Err_RepoName"] = nil + // This section doesn't require repo_name/RepoName to be set in the form, don't show it + // as an error on the UI for this action + ctx.Data["Err_RepoName"] = nil - m, _, _ := repo_model.GetPushMirrorByIDAndRepoID(ctx, form.PushMirrorID, repo.ID) - if m == nil { - ctx.NotFound(nil) - return - } + interval, err := time.ParseDuration(form.PushMirrorInterval) + if err != nil || (interval != 0 && interval < setting.Mirror.MinInterval) { + ctx.RenderWithErr(ctx.Tr("repo.mirror_interval_invalid"), tplSettingsOptions, &forms.RepoSettingForm{}) + return + } - if err := mirror_service.RemovePushMirrorRemote(ctx, m); err != nil { - ctx.ServerError("RemovePushMirrorRemote", err) - return - } + m, _, _ := repo_model.GetPushMirrorByIDAndRepoID(ctx, form.PushMirrorID, repo.ID) + if m == nil { + ctx.NotFound(nil) + return + } - if err := repo_model.DeletePushMirrors(ctx, repo_model.PushMirrorOptions{ID: m.ID, RepoID: m.RepoID}); err != nil { - ctx.ServerError("DeletePushMirrorByID", err) - return - } + m.Interval = interval + if err := repo_model.UpdatePushMirrorInterval(ctx, m); err != nil { + ctx.ServerError("UpdatePushMirrorInterval", err) + return + } + // Background why we are adding it to Queue + // If we observed its implementation in the context of `push-mirror-sync` where it + // is evident that pushing to the queue is necessary for updates. + // So, there are updates within the given interval, it is necessary to update the queue accordingly. + if !ctx.FormBool("push_mirror_defer_sync") { + // push_mirror_defer_sync is mainly for testing purpose, we do not really want to sync the push mirror immediately + mirror_service.AddPushMirrorToQueue(m.ID) + } + ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success")) + ctx.Redirect(repo.Link() + "/settings") +} - ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success")) - ctx.Redirect(repo.Link() + "/settings") +func handleSettingsPostPushMirrorRemove(ctx *context.Context) { + form := web.GetForm(ctx).(*forms.RepoSettingForm) + repo := ctx.Repo.Repository - case "push-mirror-add": - if setting.Mirror.DisableNewPush || repo.IsArchived { - ctx.NotFound(nil) - return - } + if !setting.Mirror.Enabled || repo.IsArchived { + ctx.NotFound(nil) + return + } - // This section doesn't require repo_name/RepoName to be set in the form, don't show it - // as an error on the UI for this action - ctx.Data["Err_RepoName"] = nil + // This section doesn't require repo_name/RepoName to be set in the form, don't show it + // as an error on the UI for this action + ctx.Data["Err_RepoName"] = nil - interval, err := time.ParseDuration(form.PushMirrorInterval) - if err != nil || (interval != 0 && interval < setting.Mirror.MinInterval) { - ctx.Data["Err_PushMirrorInterval"] = true - ctx.RenderWithErr(ctx.Tr("repo.mirror_interval_invalid"), tplSettingsOptions, &form) - return - } + m, _, _ := repo_model.GetPushMirrorByIDAndRepoID(ctx, form.PushMirrorID, repo.ID) + if m == nil { + ctx.NotFound(nil) + return + } - address, err := git.ParseRemoteAddr(form.PushMirrorAddress, form.PushMirrorUsername, form.PushMirrorPassword) - if err == nil { - err = migrations.IsMigrateURLAllowed(address, ctx.Doer) - } - if err != nil { - ctx.Data["Err_PushMirrorAddress"] = true - handleSettingRemoteAddrError(ctx, err, form) - return - } + if err := mirror_service.RemovePushMirrorRemote(ctx, m); err != nil { + ctx.ServerError("RemovePushMirrorRemote", err) + return + } - remoteSuffix, err := util.CryptoRandomString(10) - if err != nil { - ctx.ServerError("RandomString", err) - return - } + if err := repo_model.DeletePushMirrors(ctx, repo_model.PushMirrorOptions{ID: m.ID, RepoID: m.RepoID}); err != nil { + ctx.ServerError("DeletePushMirrorByID", err) + return + } - remoteAddress, err := util.SanitizeURL(form.PushMirrorAddress) - if err != nil { - ctx.Data["Err_PushMirrorAddress"] = true - handleSettingRemoteAddrError(ctx, err, form) - return - } + ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success")) + ctx.Redirect(repo.Link() + "/settings") +} - m := &repo_model.PushMirror{ - RepoID: repo.ID, - Repo: repo, - RemoteName: fmt.Sprintf("remote_mirror_%s", remoteSuffix), - SyncOnCommit: form.PushMirrorSyncOnCommit, - Interval: interval, - RemoteAddress: remoteAddress, - } - if err := db.Insert(ctx, m); err != nil { - ctx.ServerError("InsertPushMirror", err) - return - } +func handleSettingsPostPushMirrorAdd(ctx *context.Context) { + form := web.GetForm(ctx).(*forms.RepoSettingForm) + repo := ctx.Repo.Repository - if err := mirror_service.AddPushMirrorRemote(ctx, m, address); err != nil { - if err := repo_model.DeletePushMirrors(ctx, repo_model.PushMirrorOptions{ID: m.ID, RepoID: m.RepoID}); err != nil { - log.Error("DeletePushMirrors %v", err) - } - ctx.ServerError("AddPushMirrorRemote", err) - return - } + if setting.Mirror.DisableNewPush || repo.IsArchived { + ctx.NotFound(nil) + return + } - ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success")) - ctx.Redirect(repo.Link() + "/settings") + // This section doesn't require repo_name/RepoName to be set in the form, don't show it + // as an error on the UI for this action + ctx.Data["Err_RepoName"] = nil - case "advanced": - var repoChanged bool - var units []repo_model.RepoUnit - var deleteUnitTypes []unit_model.Type + interval, err := time.ParseDuration(form.PushMirrorInterval) + if err != nil || (interval != 0 && interval < setting.Mirror.MinInterval) { + ctx.Data["Err_PushMirrorInterval"] = true + ctx.RenderWithErr(ctx.Tr("repo.mirror_interval_invalid"), tplSettingsOptions, &form) + return + } - // This section doesn't require repo_name/RepoName to be set in the form, don't show it - // as an error on the UI for this action - ctx.Data["Err_RepoName"] = nil + address, err := git.ParseRemoteAddr(form.PushMirrorAddress, form.PushMirrorUsername, form.PushMirrorPassword) + if err == nil { + err = migrations.IsMigrateURLAllowed(address, ctx.Doer) + } + if err != nil { + ctx.Data["Err_PushMirrorAddress"] = true + handleSettingRemoteAddrError(ctx, err, form) + return + } - if repo.CloseIssuesViaCommitInAnyBranch != form.EnableCloseIssuesViaCommitInAnyBranch { - repo.CloseIssuesViaCommitInAnyBranch = form.EnableCloseIssuesViaCommitInAnyBranch - repoChanged = true - } + remoteSuffix, err := util.CryptoRandomString(10) + if err != nil { + ctx.ServerError("RandomString", err) + return + } - if form.EnableCode && !unit_model.TypeCode.UnitGlobalDisabled() { - units = append(units, repo_model.RepoUnit{ - RepoID: repo.ID, - Type: unit_model.TypeCode, - EveryoneAccessMode: parseEveryoneAccessMode(form.DefaultCodeEveryoneAccess, perm.AccessModeNone, perm.AccessModeRead), - }) - } else if !unit_model.TypeCode.UnitGlobalDisabled() { - deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeCode) - } + remoteAddress, err := util.SanitizeURL(form.PushMirrorAddress) + if err != nil { + ctx.Data["Err_PushMirrorAddress"] = true + handleSettingRemoteAddrError(ctx, err, form) + return + } - if form.EnableWiki && form.EnableExternalWiki && !unit_model.TypeExternalWiki.UnitGlobalDisabled() { - if !validation.IsValidExternalURL(form.ExternalWikiURL) { - ctx.Flash.Error(ctx.Tr("repo.settings.external_wiki_url_error")) - ctx.Redirect(repo.Link() + "/settings") - return - } + m := &repo_model.PushMirror{ + RepoID: repo.ID, + Repo: repo, + RemoteName: fmt.Sprintf("remote_mirror_%s", remoteSuffix), + SyncOnCommit: form.PushMirrorSyncOnCommit, + Interval: interval, + RemoteAddress: remoteAddress, + } + if err := db.Insert(ctx, m); err != nil { + ctx.ServerError("InsertPushMirror", err) + return + } - units = append(units, repo_model.RepoUnit{ - RepoID: repo.ID, - Type: unit_model.TypeExternalWiki, - Config: &repo_model.ExternalWikiConfig{ - ExternalWikiURL: form.ExternalWikiURL, - }, - }) - deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeWiki) - } else if form.EnableWiki && !form.EnableExternalWiki && !unit_model.TypeWiki.UnitGlobalDisabled() { - units = append(units, repo_model.RepoUnit{ - RepoID: repo.ID, - Type: unit_model.TypeWiki, - Config: new(repo_model.UnitConfig), - EveryoneAccessMode: parseEveryoneAccessMode(form.DefaultWikiEveryoneAccess, perm.AccessModeNone, perm.AccessModeRead, perm.AccessModeWrite), - }) - deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalWiki) - } else { - if !unit_model.TypeExternalWiki.UnitGlobalDisabled() { - deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalWiki) - } - if !unit_model.TypeWiki.UnitGlobalDisabled() { - deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeWiki) - } + if err := mirror_service.AddPushMirrorRemote(ctx, m, address); err != nil { + if err := repo_model.DeletePushMirrors(ctx, repo_model.PushMirrorOptions{ID: m.ID, RepoID: m.RepoID}); err != nil { + log.Error("DeletePushMirrors %v", err) } + ctx.ServerError("AddPushMirrorRemote", err) + return + } - if form.DefaultWikiBranch != "" { - if err := wiki_service.ChangeDefaultWikiBranch(ctx, repo, form.DefaultWikiBranch); err != nil { - log.Error("ChangeDefaultWikiBranch failed, err: %v", err) - ctx.Flash.Warning(ctx.Tr("repo.settings.failed_to_change_default_wiki_branch")) - } - } + ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success")) + ctx.Redirect(repo.Link() + "/settings") +} - if form.EnableIssues && form.EnableExternalTracker && !unit_model.TypeExternalTracker.UnitGlobalDisabled() { - if !validation.IsValidExternalURL(form.ExternalTrackerURL) { - ctx.Flash.Error(ctx.Tr("repo.settings.external_tracker_url_error")) - ctx.Redirect(repo.Link() + "/settings") - return - } - if len(form.TrackerURLFormat) != 0 && !validation.IsValidExternalTrackerURLFormat(form.TrackerURLFormat) { - ctx.Flash.Error(ctx.Tr("repo.settings.tracker_url_format_error")) - ctx.Redirect(repo.Link() + "/settings") - return - } - units = append(units, repo_model.RepoUnit{ - RepoID: repo.ID, - Type: unit_model.TypeExternalTracker, - Config: &repo_model.ExternalTrackerConfig{ - ExternalTrackerURL: form.ExternalTrackerURL, - ExternalTrackerFormat: form.TrackerURLFormat, - ExternalTrackerStyle: form.TrackerIssueStyle, - ExternalTrackerRegexpPattern: form.ExternalTrackerRegexpPattern, - }, - }) - deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeIssues) - } else if form.EnableIssues && !form.EnableExternalTracker && !unit_model.TypeIssues.UnitGlobalDisabled() { - units = append(units, repo_model.RepoUnit{ - RepoID: repo.ID, - Type: unit_model.TypeIssues, - Config: &repo_model.IssuesConfig{ - EnableTimetracker: form.EnableTimetracker, - AllowOnlyContributorsToTrackTime: form.AllowOnlyContributorsToTrackTime, - EnableDependencies: form.EnableIssueDependencies, - }, - EveryoneAccessMode: parseEveryoneAccessMode(form.DefaultIssuesEveryoneAccess, perm.AccessModeNone, perm.AccessModeRead), - }) - deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalTracker) - } else { - if !unit_model.TypeExternalTracker.UnitGlobalDisabled() { - deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalTracker) - } - if !unit_model.TypeIssues.UnitGlobalDisabled() { - deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeIssues) - } +func newRepoUnit(repo *repo_model.Repository, unitType unit_model.Type, config convert.Conversion) repo_model.RepoUnit { + repoUnit := repo_model.RepoUnit{RepoID: repo.ID, Type: unitType, Config: config} + for _, u := range repo.Units { + if u.Type == unitType { + repoUnit.EveryoneAccessMode = u.EveryoneAccessMode + repoUnit.AnonymousAccessMode = u.AnonymousAccessMode } + } + return repoUnit +} - if form.EnableProjects && !unit_model.TypeProjects.UnitGlobalDisabled() { - units = append(units, repo_model.RepoUnit{ - RepoID: repo.ID, - Type: unit_model.TypeProjects, - Config: &repo_model.ProjectsConfig{ - ProjectsMode: repo_model.ProjectsMode(form.ProjectsMode), - }, - }) - } else if !unit_model.TypeProjects.UnitGlobalDisabled() { - deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeProjects) - } +func handleSettingsPostAdvanced(ctx *context.Context) { + form := web.GetForm(ctx).(*forms.RepoSettingForm) + repo := ctx.Repo.Repository + var repoChanged bool + var units []repo_model.RepoUnit + var deleteUnitTypes []unit_model.Type - if form.EnableReleases && !unit_model.TypeReleases.UnitGlobalDisabled() { - units = append(units, repo_model.RepoUnit{ - RepoID: repo.ID, - Type: unit_model.TypeReleases, - }) - } else if !unit_model.TypeReleases.UnitGlobalDisabled() { - deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeReleases) - } + // This section doesn't require repo_name/RepoName to be set in the form, don't show it + // as an error on the UI for this action + ctx.Data["Err_RepoName"] = nil + + if repo.CloseIssuesViaCommitInAnyBranch != form.EnableCloseIssuesViaCommitInAnyBranch { + repo.CloseIssuesViaCommitInAnyBranch = form.EnableCloseIssuesViaCommitInAnyBranch + repoChanged = true + } + + if form.EnableCode && !unit_model.TypeCode.UnitGlobalDisabled() { + units = append(units, newRepoUnit(repo, unit_model.TypeCode, nil)) + } else if !unit_model.TypeCode.UnitGlobalDisabled() { + deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeCode) + } - if form.EnablePackages && !unit_model.TypePackages.UnitGlobalDisabled() { - units = append(units, repo_model.RepoUnit{ - RepoID: repo.ID, - Type: unit_model.TypePackages, - }) - } else if !unit_model.TypePackages.UnitGlobalDisabled() { - deleteUnitTypes = append(deleteUnitTypes, unit_model.TypePackages) + if form.EnableWiki && form.EnableExternalWiki && !unit_model.TypeExternalWiki.UnitGlobalDisabled() { + if !validation.IsValidExternalURL(form.ExternalWikiURL) { + ctx.Flash.Error(ctx.Tr("repo.settings.external_wiki_url_error")) + ctx.Redirect(repo.Link() + "/settings") + return } - if form.EnableActions && !unit_model.TypeActions.UnitGlobalDisabled() { - units = append(units, repo_model.RepoUnit{ - RepoID: repo.ID, - Type: unit_model.TypeActions, - }) - } else if !unit_model.TypeActions.UnitGlobalDisabled() { - deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeActions) + units = append(units, newRepoUnit(repo, unit_model.TypeExternalWiki, &repo_model.ExternalWikiConfig{ + ExternalWikiURL: form.ExternalWikiURL, + })) + deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeWiki) + } else if form.EnableWiki && !form.EnableExternalWiki && !unit_model.TypeWiki.UnitGlobalDisabled() { + units = append(units, newRepoUnit(repo, unit_model.TypeWiki, new(repo_model.UnitConfig))) + deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalWiki) + } else { + if !unit_model.TypeExternalWiki.UnitGlobalDisabled() { + deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalWiki) + } + if !unit_model.TypeWiki.UnitGlobalDisabled() { + deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeWiki) } + } - if form.EnablePulls && !unit_model.TypePullRequests.UnitGlobalDisabled() { - units = append(units, repo_model.RepoUnit{ - RepoID: repo.ID, - Type: unit_model.TypePullRequests, - Config: &repo_model.PullRequestsConfig{ - IgnoreWhitespaceConflicts: form.PullsIgnoreWhitespace, - AllowMerge: form.PullsAllowMerge, - AllowRebase: form.PullsAllowRebase, - AllowRebaseMerge: form.PullsAllowRebaseMerge, - AllowSquash: form.PullsAllowSquash, - AllowFastForwardOnly: form.PullsAllowFastForwardOnly, - AllowManualMerge: form.PullsAllowManualMerge, - AutodetectManualMerge: form.EnableAutodetectManualMerge, - AllowRebaseUpdate: form.PullsAllowRebaseUpdate, - DefaultDeleteBranchAfterMerge: form.DefaultDeleteBranchAfterMerge, - DefaultMergeStyle: repo_model.MergeStyle(form.PullsDefaultMergeStyle), - DefaultAllowMaintainerEdit: form.DefaultAllowMaintainerEdit, - }, - }) - } else if !unit_model.TypePullRequests.UnitGlobalDisabled() { - deleteUnitTypes = append(deleteUnitTypes, unit_model.TypePullRequests) + if form.DefaultWikiBranch != "" { + if err := wiki_service.ChangeDefaultWikiBranch(ctx, repo, form.DefaultWikiBranch); err != nil { + log.Error("ChangeDefaultWikiBranch failed, err: %v", err) + ctx.Flash.Warning(ctx.Tr("repo.settings.failed_to_change_default_wiki_branch")) } + } - if len(units) == 0 { - ctx.Flash.Error(ctx.Tr("repo.settings.update_settings_no_unit")) - ctx.Redirect(ctx.Repo.RepoLink + "/settings") + if form.EnableIssues && form.EnableExternalTracker && !unit_model.TypeExternalTracker.UnitGlobalDisabled() { + if !validation.IsValidExternalURL(form.ExternalTrackerURL) { + ctx.Flash.Error(ctx.Tr("repo.settings.external_tracker_url_error")) + ctx.Redirect(repo.Link() + "/settings") return } - - if err := repo_service.UpdateRepositoryUnits(ctx, repo, units, deleteUnitTypes); err != nil { - ctx.ServerError("UpdateRepositoryUnits", err) + if len(form.TrackerURLFormat) != 0 && !validation.IsValidExternalTrackerURLFormat(form.TrackerURLFormat) { + ctx.Flash.Error(ctx.Tr("repo.settings.tracker_url_format_error")) + ctx.Redirect(repo.Link() + "/settings") return } - if repoChanged { - if err := repo_service.UpdateRepository(ctx, repo, false); err != nil { - ctx.ServerError("UpdateRepository", err) - return - } + units = append(units, newRepoUnit(repo, unit_model.TypeExternalTracker, &repo_model.ExternalTrackerConfig{ + ExternalTrackerURL: form.ExternalTrackerURL, + ExternalTrackerFormat: form.TrackerURLFormat, + ExternalTrackerStyle: form.TrackerIssueStyle, + ExternalTrackerRegexpPattern: form.ExternalTrackerRegexpPattern, + })) + deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeIssues) + } else if form.EnableIssues && !form.EnableExternalTracker && !unit_model.TypeIssues.UnitGlobalDisabled() { + units = append(units, newRepoUnit(repo, unit_model.TypeIssues, &repo_model.IssuesConfig{ + EnableTimetracker: form.EnableTimetracker, + AllowOnlyContributorsToTrackTime: form.AllowOnlyContributorsToTrackTime, + EnableDependencies: form.EnableIssueDependencies, + })) + deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalTracker) + } else { + if !unit_model.TypeExternalTracker.UnitGlobalDisabled() { + deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalTracker) } - log.Trace("Repository advanced settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name) + if !unit_model.TypeIssues.UnitGlobalDisabled() { + deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeIssues) + } + } - ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success")) - ctx.Redirect(ctx.Repo.RepoLink + "/settings") + if form.EnableProjects && !unit_model.TypeProjects.UnitGlobalDisabled() { + units = append(units, newRepoUnit(repo, unit_model.TypeProjects, &repo_model.ProjectsConfig{ + ProjectsMode: repo_model.ProjectsMode(form.ProjectsMode), + })) + } else if !unit_model.TypeProjects.UnitGlobalDisabled() { + deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeProjects) + } - case "signing": - changed := false - trustModel := repo_model.ToTrustModel(form.TrustModel) - if trustModel != repo.TrustModel { - repo.TrustModel = trustModel - changed = true - } + if form.EnableReleases && !unit_model.TypeReleases.UnitGlobalDisabled() { + units = append(units, newRepoUnit(repo, unit_model.TypeReleases, nil)) + } else if !unit_model.TypeReleases.UnitGlobalDisabled() { + deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeReleases) + } - if changed { - if err := repo_service.UpdateRepository(ctx, repo, false); err != nil { - ctx.ServerError("UpdateRepository", err) - return - } - } - log.Trace("Repository signing settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name) + if form.EnablePackages && !unit_model.TypePackages.UnitGlobalDisabled() { + units = append(units, newRepoUnit(repo, unit_model.TypePackages, nil)) + } else if !unit_model.TypePackages.UnitGlobalDisabled() { + deleteUnitTypes = append(deleteUnitTypes, unit_model.TypePackages) + } - ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success")) - ctx.Redirect(ctx.Repo.RepoLink + "/settings") + if form.EnableActions && !unit_model.TypeActions.UnitGlobalDisabled() { + units = append(units, newRepoUnit(repo, unit_model.TypeActions, nil)) + } else if !unit_model.TypeActions.UnitGlobalDisabled() { + deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeActions) + } - case "admin": - if !ctx.Doer.IsAdmin { - ctx.HTTPError(http.StatusForbidden) - return - } + if form.EnablePulls && !unit_model.TypePullRequests.UnitGlobalDisabled() { + units = append(units, newRepoUnit(repo, unit_model.TypePullRequests, &repo_model.PullRequestsConfig{ + IgnoreWhitespaceConflicts: form.PullsIgnoreWhitespace, + AllowMerge: form.PullsAllowMerge, + AllowRebase: form.PullsAllowRebase, + AllowRebaseMerge: form.PullsAllowRebaseMerge, + AllowSquash: form.PullsAllowSquash, + AllowFastForwardOnly: form.PullsAllowFastForwardOnly, + AllowManualMerge: form.PullsAllowManualMerge, + AutodetectManualMerge: form.EnableAutodetectManualMerge, + AllowRebaseUpdate: form.PullsAllowRebaseUpdate, + DefaultDeleteBranchAfterMerge: form.DefaultDeleteBranchAfterMerge, + DefaultMergeStyle: repo_model.MergeStyle(form.PullsDefaultMergeStyle), + DefaultAllowMaintainerEdit: form.DefaultAllowMaintainerEdit, + })) + } else if !unit_model.TypePullRequests.UnitGlobalDisabled() { + deleteUnitTypes = append(deleteUnitTypes, unit_model.TypePullRequests) + } - if repo.IsFsckEnabled != form.EnableHealthCheck { - repo.IsFsckEnabled = form.EnableHealthCheck - } + if len(units) == 0 { + ctx.Flash.Error(ctx.Tr("repo.settings.update_settings_no_unit")) + ctx.Redirect(ctx.Repo.RepoLink + "/settings") + return + } + if err := repo_service.UpdateRepositoryUnits(ctx, repo, units, deleteUnitTypes); err != nil { + ctx.ServerError("UpdateRepositoryUnits", err) + return + } + if repoChanged { if err := repo_service.UpdateRepository(ctx, repo, false); err != nil { ctx.ServerError("UpdateRepository", err) return } + } + log.Trace("Repository advanced settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name) - log.Trace("Repository admin settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name) + ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success")) + ctx.Redirect(ctx.Repo.RepoLink + "/settings") +} - ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success")) - ctx.Redirect(ctx.Repo.RepoLink + "/settings") +func handleSettingsPostSigning(ctx *context.Context) { + form := web.GetForm(ctx).(*forms.RepoSettingForm) + repo := ctx.Repo.Repository + changed := false + trustModel := repo_model.ToTrustModel(form.TrustModel) + if trustModel != repo.TrustModel { + repo.TrustModel = trustModel + changed = true + } - case "admin_index": - if !ctx.Doer.IsAdmin { - ctx.HTTPError(http.StatusForbidden) + if changed { + if err := repo_service.UpdateRepository(ctx, repo, false); err != nil { + ctx.ServerError("UpdateRepository", err) return } + } + log.Trace("Repository signing settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name) - switch form.RequestReindexType { - case "stats": - if err := stats.UpdateRepoIndexer(ctx.Repo.Repository); err != nil { - ctx.ServerError("UpdateStatsRepondexer", err) - return - } - case "code": - if !setting.Indexer.RepoIndexerEnabled { - ctx.HTTPError(http.StatusForbidden) - return - } - code.UpdateRepoIndexer(ctx.Repo.Repository) - default: - ctx.NotFound(nil) - return - } + ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success")) + ctx.Redirect(ctx.Repo.RepoLink + "/settings") +} - log.Trace("Repository reindex for %s requested: %s/%s", form.RequestReindexType, ctx.Repo.Owner.Name, repo.Name) +func handleSettingsPostAdmin(ctx *context.Context) { + form := web.GetForm(ctx).(*forms.RepoSettingForm) + repo := ctx.Repo.Repository + if !ctx.Doer.IsAdmin { + ctx.HTTPError(http.StatusForbidden) + return + } - ctx.Flash.Success(ctx.Tr("repo.settings.reindex_requested")) - ctx.Redirect(ctx.Repo.RepoLink + "/settings") + if repo.IsFsckEnabled != form.EnableHealthCheck { + repo.IsFsckEnabled = form.EnableHealthCheck + } - case "convert": - if !ctx.Repo.IsOwner() { - ctx.HTTPError(http.StatusNotFound) - return - } - if repo.Name != form.RepoName { - ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil) - return - } + if err := repo_service.UpdateRepository(ctx, repo, false); err != nil { + ctx.ServerError("UpdateRepository", err) + return + } - if !repo.IsMirror { - ctx.HTTPError(http.StatusNotFound) - return - } - repo.IsMirror = false + log.Trace("Repository admin settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name) - if _, err := repo_service.CleanUpMigrateInfo(ctx, repo); err != nil { - ctx.ServerError("CleanUpMigrateInfo", err) - return - } else if err = repo_model.DeleteMirrorByRepoID(ctx, ctx.Repo.Repository.ID); err != nil { - ctx.ServerError("DeleteMirrorByRepoID", err) - return - } - log.Trace("Repository converted from mirror to regular: %s", repo.FullName()) - ctx.Flash.Success(ctx.Tr("repo.settings.convert_succeed")) - ctx.Redirect(repo.Link()) + ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success")) + ctx.Redirect(ctx.Repo.RepoLink + "/settings") +} - case "convert_fork": - if !ctx.Repo.IsOwner() { - ctx.HTTPError(http.StatusNotFound) - return - } - if err := repo.LoadOwner(ctx); err != nil { - ctx.ServerError("Convert Fork", err) +func handleSettingsPostAdminIndex(ctx *context.Context) { + form := web.GetForm(ctx).(*forms.RepoSettingForm) + repo := ctx.Repo.Repository + if !ctx.Doer.IsAdmin { + ctx.HTTPError(http.StatusForbidden) + return + } + + switch form.RequestReindexType { + case "stats": + if err := stats.UpdateRepoIndexer(ctx.Repo.Repository); err != nil { + ctx.ServerError("UpdateStatsRepondexer", err) return } - if repo.Name != form.RepoName { - ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil) + case "code": + if !setting.Indexer.RepoIndexerEnabled { + ctx.HTTPError(http.StatusForbidden) return } + code.UpdateRepoIndexer(ctx.Repo.Repository) + default: + ctx.NotFound(nil) + return + } - if !repo.IsFork { - ctx.HTTPError(http.StatusNotFound) - return - } + log.Trace("Repository reindex for %s requested: %s/%s", form.RequestReindexType, ctx.Repo.Owner.Name, repo.Name) - if !ctx.Repo.Owner.CanCreateRepo() { - maxCreationLimit := ctx.Repo.Owner.MaxCreationLimit() - msg := ctx.TrN(maxCreationLimit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", maxCreationLimit) - ctx.Flash.Error(msg) - ctx.Redirect(repo.Link() + "/settings") - return - } + ctx.Flash.Success(ctx.Tr("repo.settings.reindex_requested")) + ctx.Redirect(ctx.Repo.RepoLink + "/settings") +} - if err := repo_service.ConvertForkToNormalRepository(ctx, repo); err != nil { - log.Error("Unable to convert repository %-v from fork. Error: %v", repo, err) - ctx.ServerError("Convert Fork", err) - return - } +func handleSettingsPostConvert(ctx *context.Context) { + form := web.GetForm(ctx).(*forms.RepoSettingForm) + repo := ctx.Repo.Repository + if !ctx.Repo.IsOwner() { + ctx.HTTPError(http.StatusNotFound) + return + } + if repo.Name != form.RepoName { + ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil) + return + } - log.Trace("Repository converted from fork to regular: %s", repo.FullName()) - ctx.Flash.Success(ctx.Tr("repo.settings.convert_fork_succeed")) - ctx.Redirect(repo.Link()) + if !repo.IsMirror { + ctx.HTTPError(http.StatusNotFound) + return + } + repo.IsMirror = false - case "transfer": - if !ctx.Repo.IsOwner() { - ctx.HTTPError(http.StatusNotFound) - return - } - if repo.Name != form.RepoName { - ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil) - return - } + if _, err := repo_service.CleanUpMigrateInfo(ctx, repo); err != nil { + ctx.ServerError("CleanUpMigrateInfo", err) + return + } else if err = repo_model.DeleteMirrorByRepoID(ctx, ctx.Repo.Repository.ID); err != nil { + ctx.ServerError("DeleteMirrorByRepoID", err) + return + } + log.Trace("Repository converted from mirror to regular: %s", repo.FullName()) + ctx.Flash.Success(ctx.Tr("repo.settings.convert_succeed")) + ctx.Redirect(repo.Link()) +} - newOwner, err := user_model.GetUserByName(ctx, ctx.FormString("new_owner_name")) - if err != nil { - if user_model.IsErrUserNotExist(err) { - ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_owner_name"), tplSettingsOptions, nil) - return - } - ctx.ServerError("IsUserExist", err) - return - } +func handleSettingsPostConvertFork(ctx *context.Context) { + form := web.GetForm(ctx).(*forms.RepoSettingForm) + repo := ctx.Repo.Repository + if !ctx.Repo.IsOwner() { + ctx.HTTPError(http.StatusNotFound) + return + } + if err := repo.LoadOwner(ctx); err != nil { + ctx.ServerError("Convert Fork", err) + return + } + if repo.Name != form.RepoName { + ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil) + return + } - if newOwner.Type == user_model.UserTypeOrganization { - if !ctx.Doer.IsAdmin && newOwner.Visibility == structs.VisibleTypePrivate && !organization.OrgFromUser(newOwner).HasMemberWithUserID(ctx, ctx.Doer.ID) { - // The user shouldn't know about this organization - ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_owner_name"), tplSettingsOptions, nil) - return - } - } + if !repo.IsFork { + ctx.HTTPError(http.StatusNotFound) + return + } - // Close the GitRepo if open - if ctx.Repo.GitRepo != nil { - ctx.Repo.GitRepo.Close() - ctx.Repo.GitRepo = nil - } + if !ctx.Repo.Owner.CanCreateRepo() { + maxCreationLimit := ctx.Repo.Owner.MaxCreationLimit() + msg := ctx.TrN(maxCreationLimit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", maxCreationLimit) + ctx.Flash.Error(msg) + ctx.Redirect(repo.Link() + "/settings") + return + } - oldFullname := repo.FullName() - if err := repo_service.StartRepositoryTransfer(ctx, ctx.Doer, newOwner, repo, nil); err != nil { - if repo_model.IsErrRepoAlreadyExist(err) { - ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplSettingsOptions, nil) - } else if repo_model.IsErrRepoTransferInProgress(err) { - ctx.RenderWithErr(ctx.Tr("repo.settings.transfer_in_progress"), tplSettingsOptions, nil) - } else if errors.Is(err, user_model.ErrBlockedUser) { - ctx.RenderWithErr(ctx.Tr("repo.settings.transfer.blocked_user"), tplSettingsOptions, nil) - } else { - ctx.ServerError("TransferOwnership", err) - } + if err := repo_service.ConvertForkToNormalRepository(ctx, repo); err != nil { + log.Error("Unable to convert repository %-v from fork. Error: %v", repo, err) + ctx.ServerError("Convert Fork", err) + return + } - return - } + log.Trace("Repository converted from fork to regular: %s", repo.FullName()) + ctx.Flash.Success(ctx.Tr("repo.settings.convert_fork_succeed")) + ctx.Redirect(repo.Link()) +} - if ctx.Repo.Repository.Status == repo_model.RepositoryPendingTransfer { - log.Trace("Repository transfer process was started: %s/%s -> %s", ctx.Repo.Owner.Name, repo.Name, newOwner) - ctx.Flash.Success(ctx.Tr("repo.settings.transfer_started", newOwner.DisplayName())) - } else { - log.Trace("Repository transferred: %s -> %s", oldFullname, ctx.Repo.Repository.FullName()) - ctx.Flash.Success(ctx.Tr("repo.settings.transfer_succeed")) - } - ctx.Redirect(repo.Link() + "/settings") +func handleSettingsPostTransfer(ctx *context.Context) { + form := web.GetForm(ctx).(*forms.RepoSettingForm) + repo := ctx.Repo.Repository + if !ctx.Repo.IsOwner() { + ctx.HTTPError(http.StatusNotFound) + return + } + if repo.Name != form.RepoName { + ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil) + return + } - case "cancel_transfer": - if !ctx.Repo.IsOwner() { - ctx.HTTPError(http.StatusNotFound) + newOwner, err := user_model.GetUserByName(ctx, ctx.FormString("new_owner_name")) + if err != nil { + if user_model.IsErrUserNotExist(err) { + ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_owner_name"), tplSettingsOptions, nil) return } + ctx.ServerError("IsUserExist", err) + return + } - repoTransfer, err := repo_model.GetPendingRepositoryTransfer(ctx, ctx.Repo.Repository) - if err != nil { - if repo_model.IsErrNoPendingTransfer(err) { - ctx.Flash.Error("repo.settings.transfer_abort_invalid") - ctx.Redirect(repo.Link() + "/settings") - } else { - ctx.ServerError("GetPendingRepositoryTransfer", err) - } + if newOwner.Type == user_model.UserTypeOrganization { + if !ctx.Doer.IsAdmin && newOwner.Visibility == structs.VisibleTypePrivate && !organization.OrgFromUser(newOwner).HasMemberWithUserID(ctx, ctx.Doer.ID) { + // The user shouldn't know about this organization + ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_owner_name"), tplSettingsOptions, nil) return } + } - if err := repo_service.CancelRepositoryTransfer(ctx, repoTransfer, ctx.Doer); err != nil { - ctx.ServerError("CancelRepositoryTransfer", err) - return + // Close the GitRepo if open + if ctx.Repo.GitRepo != nil { + ctx.Repo.GitRepo.Close() + ctx.Repo.GitRepo = nil + } + + oldFullname := repo.FullName() + if err := repo_service.StartRepositoryTransfer(ctx, ctx.Doer, newOwner, repo, nil); err != nil { + if repo_model.IsErrRepoAlreadyExist(err) { + ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplSettingsOptions, nil) + } else if repo_model.IsErrRepoTransferInProgress(err) { + ctx.RenderWithErr(ctx.Tr("repo.settings.transfer_in_progress"), tplSettingsOptions, nil) + } else if errors.Is(err, user_model.ErrBlockedUser) { + ctx.RenderWithErr(ctx.Tr("repo.settings.transfer.blocked_user"), tplSettingsOptions, nil) + } else { + ctx.ServerError("TransferOwnership", err) } - log.Trace("Repository transfer process was cancelled: %s/%s ", ctx.Repo.Owner.Name, repo.Name) - ctx.Flash.Success(ctx.Tr("repo.settings.transfer_abort_success", repoTransfer.Recipient.Name)) - ctx.Redirect(repo.Link() + "/settings") + return + } - case "delete": - if !ctx.Repo.IsOwner() { - ctx.HTTPError(http.StatusNotFound) - return - } - if repo.Name != form.RepoName { - ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil) - return - } + if ctx.Repo.Repository.Status == repo_model.RepositoryPendingTransfer { + log.Trace("Repository transfer process was started: %s/%s -> %s", ctx.Repo.Owner.Name, repo.Name, newOwner) + ctx.Flash.Success(ctx.Tr("repo.settings.transfer_started", newOwner.DisplayName())) + } else { + log.Trace("Repository transferred: %s -> %s", oldFullname, ctx.Repo.Repository.FullName()) + ctx.Flash.Success(ctx.Tr("repo.settings.transfer_succeed")) + } + ctx.Redirect(repo.Link() + "/settings") +} - // Close the gitrepository before doing this. - if ctx.Repo.GitRepo != nil { - ctx.Repo.GitRepo.Close() - } +func handleSettingsPostCancelTransfer(ctx *context.Context) { + repo := ctx.Repo.Repository + if !ctx.Repo.IsOwner() { + ctx.HTTPError(http.StatusNotFound) + return + } - if err := repo_service.DeleteRepository(ctx, ctx.Doer, ctx.Repo.Repository, true); err != nil { - ctx.ServerError("DeleteRepository", err) - return + repoTransfer, err := repo_model.GetPendingRepositoryTransfer(ctx, ctx.Repo.Repository) + if err != nil { + if repo_model.IsErrNoPendingTransfer(err) { + ctx.Flash.Error("repo.settings.transfer_abort_invalid") + ctx.Redirect(repo.Link() + "/settings") + } else { + ctx.ServerError("GetPendingRepositoryTransfer", err) } - log.Trace("Repository deleted: %s/%s", ctx.Repo.Owner.Name, repo.Name) + return + } - ctx.Flash.Success(ctx.Tr("repo.settings.deletion_success")) - ctx.Redirect(ctx.Repo.Owner.DashboardLink()) + if err := repo_service.CancelRepositoryTransfer(ctx, repoTransfer, ctx.Doer); err != nil { + ctx.ServerError("CancelRepositoryTransfer", err) + return + } - case "delete-wiki": - if !ctx.Repo.IsOwner() { - ctx.HTTPError(http.StatusNotFound) - return - } - if repo.Name != form.RepoName { - ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil) - return - } + log.Trace("Repository transfer process was cancelled: %s/%s ", ctx.Repo.Owner.Name, repo.Name) + ctx.Flash.Success(ctx.Tr("repo.settings.transfer_abort_success", repoTransfer.Recipient.Name)) + ctx.Redirect(repo.Link() + "/settings") +} - err := wiki_service.DeleteWiki(ctx, repo) - if err != nil { - log.Error("Delete Wiki: %v", err.Error()) - } - log.Trace("Repository wiki deleted: %s/%s", ctx.Repo.Owner.Name, repo.Name) +func handleSettingsPostDelete(ctx *context.Context) { + form := web.GetForm(ctx).(*forms.RepoSettingForm) + repo := ctx.Repo.Repository + if !ctx.Repo.IsOwner() { + ctx.HTTPError(http.StatusNotFound) + return + } + if repo.Name != form.RepoName { + ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil) + return + } - ctx.Flash.Success(ctx.Tr("repo.settings.wiki_deletion_success")) - ctx.Redirect(ctx.Repo.RepoLink + "/settings") + // Close the gitrepository before doing this. + if ctx.Repo.GitRepo != nil { + ctx.Repo.GitRepo.Close() + } - case "archive": - if !ctx.Repo.IsOwner() { - ctx.HTTPError(http.StatusForbidden) - return - } + if err := repo_service.DeleteRepository(ctx, ctx.Doer, ctx.Repo.Repository, true); err != nil { + ctx.ServerError("DeleteRepository", err) + return + } + log.Trace("Repository deleted: %s/%s", ctx.Repo.Owner.Name, repo.Name) - if repo.IsMirror { - ctx.Flash.Error(ctx.Tr("repo.settings.archive.error_ismirror")) - ctx.Redirect(ctx.Repo.RepoLink + "/settings") - return - } + ctx.Flash.Success(ctx.Tr("repo.settings.deletion_success")) + ctx.Redirect(ctx.Repo.Owner.DashboardLink()) +} - if err := repo_model.SetArchiveRepoState(ctx, repo, true); err != nil { - log.Error("Tried to archive a repo: %s", err) - ctx.Flash.Error(ctx.Tr("repo.settings.archive.error")) - ctx.Redirect(ctx.Repo.RepoLink + "/settings") - return - } +func handleSettingsPostDeleteWiki(ctx *context.Context) { + form := web.GetForm(ctx).(*forms.RepoSettingForm) + repo := ctx.Repo.Repository + if !ctx.Repo.IsOwner() { + ctx.HTTPError(http.StatusNotFound) + return + } + if repo.Name != form.RepoName { + ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil) + return + } - if err := actions_service.CleanRepoScheduleTasks(ctx, repo); err != nil { - log.Error("CleanRepoScheduleTasks for archived repo %s/%s: %v", ctx.Repo.Owner.Name, repo.Name, err) - } + err := wiki_service.DeleteWiki(ctx, repo) + if err != nil { + log.Error("Delete Wiki: %v", err.Error()) + } + log.Trace("Repository wiki deleted: %s/%s", ctx.Repo.Owner.Name, repo.Name) - // update issue indexer - issue_indexer.UpdateRepoIndexer(ctx, repo.ID) + ctx.Flash.Success(ctx.Tr("repo.settings.wiki_deletion_success")) + ctx.Redirect(ctx.Repo.RepoLink + "/settings") +} - ctx.Flash.Success(ctx.Tr("repo.settings.archive.success")) +func handleSettingsPostArchive(ctx *context.Context) { + repo := ctx.Repo.Repository + if !ctx.Repo.IsOwner() { + ctx.HTTPError(http.StatusForbidden) + return + } - log.Trace("Repository was archived: %s/%s", ctx.Repo.Owner.Name, repo.Name) + if repo.IsMirror { + ctx.Flash.Error(ctx.Tr("repo.settings.archive.error_ismirror")) ctx.Redirect(ctx.Repo.RepoLink + "/settings") + return + } - case "unarchive": - if !ctx.Repo.IsOwner() { - ctx.HTTPError(http.StatusForbidden) - return - } + if err := repo_model.SetArchiveRepoState(ctx, repo, true); err != nil { + log.Error("Tried to archive a repo: %s", err) + ctx.Flash.Error(ctx.Tr("repo.settings.archive.error")) + ctx.Redirect(ctx.Repo.RepoLink + "/settings") + return + } - if err := repo_model.SetArchiveRepoState(ctx, repo, false); err != nil { - log.Error("Tried to unarchive a repo: %s", err) - ctx.Flash.Error(ctx.Tr("repo.settings.unarchive.error")) - ctx.Redirect(ctx.Repo.RepoLink + "/settings") - return - } + if err := actions_service.CleanRepoScheduleTasks(ctx, repo); err != nil { + log.Error("CleanRepoScheduleTasks for archived repo %s/%s: %v", ctx.Repo.Owner.Name, repo.Name, err) + } - if ctx.Repo.Repository.UnitEnabled(ctx, unit_model.TypeActions) { - if err := actions_service.DetectAndHandleSchedules(ctx, repo); err != nil { - log.Error("DetectAndHandleSchedules for un-archived repo %s/%s: %v", ctx.Repo.Owner.Name, repo.Name, err) - } - } + // update issue indexer + issue_indexer.UpdateRepoIndexer(ctx, repo.ID) - // update issue indexer - issue_indexer.UpdateRepoIndexer(ctx, repo.ID) + ctx.Flash.Success(ctx.Tr("repo.settings.archive.success")) - ctx.Flash.Success(ctx.Tr("repo.settings.unarchive.success")) + log.Trace("Repository was archived: %s/%s", ctx.Repo.Owner.Name, repo.Name) + ctx.Redirect(ctx.Repo.RepoLink + "/settings") +} + +func handleSettingsPostUnarchive(ctx *context.Context) { + repo := ctx.Repo.Repository + if !ctx.Repo.IsOwner() { + ctx.HTTPError(http.StatusForbidden) + return + } - log.Trace("Repository was un-archived: %s/%s", ctx.Repo.Owner.Name, repo.Name) + if err := repo_model.SetArchiveRepoState(ctx, repo, false); err != nil { + log.Error("Tried to unarchive a repo: %s", err) + ctx.Flash.Error(ctx.Tr("repo.settings.unarchive.error")) ctx.Redirect(ctx.Repo.RepoLink + "/settings") + return + } - case "visibility": - if repo.IsFork { - ctx.Flash.Error(ctx.Tr("repo.settings.visibility.fork_error")) - ctx.Redirect(ctx.Repo.RepoLink + "/settings") - return + if ctx.Repo.Repository.UnitEnabled(ctx, unit_model.TypeActions) { + if err := actions_service.DetectAndHandleSchedules(ctx, repo); err != nil { + log.Error("DetectAndHandleSchedules for un-archived repo %s/%s: %v", ctx.Repo.Owner.Name, repo.Name, err) } + } - var err error + // update issue indexer + issue_indexer.UpdateRepoIndexer(ctx, repo.ID) - // when ForcePrivate enabled, you could change public repo to private, but only admin users can change private to public - if setting.Repository.ForcePrivate && repo.IsPrivate && !ctx.Doer.IsAdmin { - ctx.RenderWithErr(ctx.Tr("form.repository_force_private"), tplSettingsOptions, form) - return - } + ctx.Flash.Success(ctx.Tr("repo.settings.unarchive.success")) - if repo.IsPrivate { - err = repo_service.MakeRepoPublic(ctx, repo) - } else { - err = repo_service.MakeRepoPrivate(ctx, repo) - } + log.Trace("Repository was un-archived: %s/%s", ctx.Repo.Owner.Name, repo.Name) + ctx.Redirect(ctx.Repo.RepoLink + "/settings") +} - if err != nil { - log.Error("Tried to change the visibility of the repo: %s", err) - ctx.Flash.Error(ctx.Tr("repo.settings.visibility.error")) - ctx.Redirect(ctx.Repo.RepoLink + "/settings") - return - } +func handleSettingsPostVisibility(ctx *context.Context) { + form := web.GetForm(ctx).(*forms.RepoSettingForm) + repo := ctx.Repo.Repository + if repo.IsFork { + ctx.Flash.Error(ctx.Tr("repo.settings.visibility.fork_error")) + ctx.Redirect(ctx.Repo.RepoLink + "/settings") + return + } - ctx.Flash.Success(ctx.Tr("repo.settings.visibility.success")) + var err error - log.Trace("Repository visibility changed: %s/%s", ctx.Repo.Owner.Name, repo.Name) - ctx.Redirect(ctx.Repo.RepoLink + "/settings") + // when ForcePrivate enabled, you could change public repo to private, but only admin users can change private to public + if setting.Repository.ForcePrivate && repo.IsPrivate && !ctx.Doer.IsAdmin { + ctx.RenderWithErr(ctx.Tr("form.repository_force_private"), tplSettingsOptions, form) + return + } - default: - ctx.NotFound(nil) + if repo.IsPrivate { + err = repo_service.MakeRepoPublic(ctx, repo) + } else { + err = repo_service.MakeRepoPrivate(ctx, repo) + } + + if err != nil { + log.Error("Tried to change the visibility of the repo: %s", err) + ctx.Flash.Error(ctx.Tr("repo.settings.visibility.error")) + ctx.Redirect(ctx.Repo.RepoLink + "/settings") + return } + + ctx.Flash.Success(ctx.Tr("repo.settings.visibility.success")) + + log.Trace("Repository visibility changed: %s/%s", ctx.Repo.Owner.Name, repo.Name) + ctx.Redirect(ctx.Repo.RepoLink + "/settings") } func handleSettingRemoteAddrError(ctx *context.Context, err error, form *forms.RepoSettingForm) { diff --git a/routers/web/repo/wiki.go b/routers/web/repo/wiki.go index 0f8e1223c6..20c8c2b406 100644 --- a/routers/web/repo/wiki.go +++ b/routers/web/repo/wiki.go @@ -96,7 +96,7 @@ func findEntryForFile(commit *git.Commit, target string) (*git.TreeEntry, error) } func findWikiRepoCommit(ctx *context.Context) (*git.Repository, *git.Commit, error) { - wikiGitRepo, errGitRepo := gitrepo.OpenWikiRepository(ctx, ctx.Repo.Repository) + wikiGitRepo, errGitRepo := gitrepo.OpenRepository(ctx, ctx.Repo.Repository.WikiStorageRepo()) if errGitRepo != nil { ctx.ServerError("OpenRepository", errGitRepo) return nil, nil, errGitRepo @@ -105,7 +105,7 @@ func findWikiRepoCommit(ctx *context.Context) (*git.Repository, *git.Commit, err commit, errCommit := wikiGitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultWikiBranch) if git.IsErrNotExist(errCommit) { // if the default branch recorded in database is out of sync, then re-sync it - gitRepoDefaultBranch, errBranch := gitrepo.GetWikiDefaultBranch(ctx, ctx.Repo.Repository) + gitRepoDefaultBranch, errBranch := gitrepo.GetDefaultBranch(ctx, ctx.Repo.Repository.WikiStorageRepo()) if errBranch != nil { return wikiGitRepo, nil, errBranch } diff --git a/routers/web/repo/wiki_test.go b/routers/web/repo/wiki_test.go index 99114c93e0..e44cf46ba8 100644 --- a/routers/web/repo/wiki_test.go +++ b/routers/web/repo/wiki_test.go @@ -29,7 +29,7 @@ const ( ) func wikiEntry(t *testing.T, repo *repo_model.Repository, wikiName wiki_service.WebPath) *git.TreeEntry { - wikiRepo, err := gitrepo.OpenWikiRepository(git.DefaultContext, repo) + wikiRepo, err := gitrepo.OpenRepository(git.DefaultContext, repo.WikiStorageRepo()) assert.NoError(t, err) defer wikiRepo.Close() commit, err := wikiRepo.GetBranchCommit("master") diff --git a/routers/web/shared/user/helper.go b/routers/web/shared/user/helper.go index b82181a1df..3fc39fd3ab 100644 --- a/routers/web/shared/user/helper.go +++ b/routers/web/shared/user/helper.go @@ -8,9 +8,7 @@ import ( "slices" "strconv" - "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/modules/optional" ) func MakeSelfOnTop(doer *user.User, users []*user.User) []*user.User { @@ -34,19 +32,20 @@ func MakeSelfOnTop(doer *user.User, users []*user.User) []*user.User { // So it's better to make it work like GitHub: users could input username directly. // Since it only converts the username to ID directly and is only used internally (to search issues), so no permission check is needed. // Return values: -// * nil: no filter -// * some(id): match the id, the id could be -1 to match the issues without assignee -// * some(NonExistingID): match no issue (due to the user doesn't exist) -func GetFilterUserIDByName(ctx context.Context, name string) optional.Option[int64] { +// * "": no filter +// * "{the-id}": match the id +// * "(none)": match no issue (due to the user doesn't exist) +func GetFilterUserIDByName(ctx context.Context, name string) string { if name == "" { - return optional.None[int64]() + return "" } u, err := user.GetUserByName(ctx, name) if err != nil { if id, err := strconv.ParseInt(name, 10, 64); err == nil { - return optional.Some(id) + return strconv.FormatInt(id, 10) } - return optional.Some(db.NonExistingID) + // The "(none)" is for internal usage only: when doer tries to search non-existing user, use "(none)" to return empty result. + return "(none)" } - return optional.Some(u.ID) + return strconv.FormatInt(u.ID, 10) } diff --git a/routers/web/user/home.go b/routers/web/user/home.go index 8e030a62a2..f90d9df897 100644 --- a/routers/web/user/home.go +++ b/routers/web/user/home.go @@ -119,7 +119,7 @@ func Dashboard(ctx *context.Context) { ctx.Data["HeatmapTotalContributions"] = activities_model.GetTotalContributionsInHeatmap(data) } - feeds, count, err := feed_service.GetFeeds(ctx, activities_model.GetFeedsOptions{ + feeds, count, err := feed_service.GetFeedsForDashboard(ctx, activities_model.GetFeedsOptions{ RequestedUser: ctxUser, RequestedTeam: ctx.Org.Team, Actor: ctx.Doer, @@ -137,11 +137,10 @@ func Dashboard(ctx *context.Context) { return } - ctx.Data["Feeds"] = feeds - - pager := context.NewPagination(int(count), setting.UI.FeedPagingNum, page, 5) + pager := context.NewPagination(count, setting.UI.FeedPagingNum, page, 5).WithCurRows(len(feeds)) pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager + ctx.Data["Feeds"] = feeds ctx.HTML(http.StatusOK, tplDashboard) } @@ -501,9 +500,9 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) { case issues_model.FilterModeAll: case issues_model.FilterModeYourRepositories: case issues_model.FilterModeAssign: - opts.AssigneeID = optional.Some(ctx.Doer.ID) + opts.AssigneeID = strconv.FormatInt(ctx.Doer.ID, 10) case issues_model.FilterModeCreate: - opts.PosterID = optional.Some(ctx.Doer.ID) + opts.PosterID = strconv.FormatInt(ctx.Doer.ID, 10) case issues_model.FilterModeMention: opts.MentionedID = ctx.Doer.ID case issues_model.FilterModeReviewRequested: @@ -618,9 +617,10 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) { return 0 } reviewTyp := issues_model.ReviewTypeApprove - if typ == "reject" { + switch typ { + case "reject": reviewTyp = issues_model.ReviewTypeReject - } else if typ == "waiting" { + case "waiting": reviewTyp = issues_model.ReviewTypeRequest } for _, count := range counts { @@ -792,9 +792,9 @@ func getUserIssueStats(ctx *context.Context, ctxUser *user_model.User, filterMod case issues_model.FilterModeYourRepositories: openClosedOpts.AllPublic = false case issues_model.FilterModeAssign: - openClosedOpts.AssigneeID = optional.Some(doerID) + openClosedOpts.AssigneeID = strconv.FormatInt(doerID, 10) case issues_model.FilterModeCreate: - openClosedOpts.PosterID = optional.Some(doerID) + openClosedOpts.PosterID = strconv.FormatInt(doerID, 10) case issues_model.FilterModeMention: openClosedOpts.MentionID = optional.Some(doerID) case issues_model.FilterModeReviewRequested: @@ -816,8 +816,8 @@ func getUserIssueStats(ctx *context.Context, ctxUser *user_model.User, filterMod // Below stats are for the left sidebar opts = opts.Copy(func(o *issue_indexer.SearchOptions) { - o.AssigneeID = nil - o.PosterID = nil + o.AssigneeID = "" + o.PosterID = "" o.MentionID = nil o.ReviewRequestedID = nil o.ReviewedID = nil @@ -827,11 +827,11 @@ func getUserIssueStats(ctx *context.Context, ctxUser *user_model.User, filterMod if err != nil { return nil, err } - ret.AssignCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.AssigneeID = optional.Some(doerID) })) + ret.AssignCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.AssigneeID = strconv.FormatInt(doerID, 10) })) if err != nil { return nil, err } - ret.CreateCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.PosterID = optional.Some(doerID) })) + ret.CreateCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.PosterID = strconv.FormatInt(doerID, 10) })) if err != nil { return nil, err } diff --git a/routers/web/user/notification.go b/routers/web/user/notification.go index 1c91ff6364..89f3c6956f 100644 --- a/routers/web/user/notification.go +++ b/routers/web/user/notification.go @@ -4,7 +4,6 @@ package user import ( - goctx "context" "errors" "fmt" "net/http" @@ -35,32 +34,6 @@ const ( tplNotificationSubscriptions templates.TplName = "user/notification/notification_subscriptions" ) -// GetNotificationCount is the middleware that sets the notification count in the context -func GetNotificationCount(ctx *context.Context) { - if strings.HasPrefix(ctx.Req.URL.Path, "/api") { - return - } - - if !ctx.IsSigned { - return - } - - ctx.Data["NotificationUnreadCount"] = func() int64 { - count, err := db.Count[activities_model.Notification](ctx, activities_model.FindNotificationOptions{ - UserID: ctx.Doer.ID, - Status: []activities_model.NotificationStatus{activities_model.NotificationStatusUnread}, - }) - if err != nil { - if err != goctx.Canceled { - log.Error("Unable to GetNotificationCount for user:%-v: %v", ctx.Doer, err) - } - return -1 - } - - return count - } -} - // Notifications is the notifications page func Notifications(ctx *context.Context) { getNotifications(ctx) @@ -335,9 +308,10 @@ func NotificationSubscriptions(ctx *context.Context) { return 0 } reviewTyp := issues_model.ReviewTypeApprove - if typ == "reject" { + switch typ { + case "reject": reviewTyp = issues_model.ReviewTypeReject - } else if typ == "waiting" { + case "waiting": reviewTyp = issues_model.ReviewTypeRequest } for _, count := range counts { diff --git a/routers/web/web.go b/routers/web/web.go index f4bd3ef4bc..84043e0bfb 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -280,28 +280,26 @@ func Routes() *web.Router { routes.Get("/api/swagger", append(mid, misc.Swagger)...) // Render V1 by default } - // TODO: These really seem like things that could be folded into Contexter or as helper functions - mid = append(mid, user.GetNotificationCount) - mid = append(mid, repo.GetActiveStopwatch) mid = append(mid, goGet) + mid = append(mid, common.PageTmplFunctions) - others := web.NewRouter() - others.Use(mid...) - registerRoutes(others) - routes.Mount("", others) + webRoutes := web.NewRouter() + webRoutes.Use(mid...) + webRoutes.Group("", func() { registerWebRoutes(webRoutes) }, common.BlockExpensive()) + routes.Mount("", webRoutes) return routes } var optSignInIgnoreCsrf = verifyAuthWithOptions(&common.VerifyOptions{DisableCSRF: true}) -// registerRoutes register routes -func registerRoutes(m *web.Router) { +// registerWebRoutes register routes +func registerWebRoutes(m *web.Router) { // required to be signed in or signed out reqSignIn := verifyAuthWithOptions(&common.VerifyOptions{SignInRequired: true}) reqSignOut := verifyAuthWithOptions(&common.VerifyOptions{SignOutRequired: true}) // optional sign in (if signed in, use the user as doer, if not, no doer) - optSignIn := verifyAuthWithOptions(&common.VerifyOptions{SignInRequired: setting.Service.RequireSignInView}) - optExploreSignIn := verifyAuthWithOptions(&common.VerifyOptions{SignInRequired: setting.Service.RequireSignInView || setting.Service.Explore.RequireSigninView}) + optSignIn := verifyAuthWithOptions(&common.VerifyOptions{SignInRequired: setting.Service.RequireSignInViewStrict}) + optExploreSignIn := verifyAuthWithOptions(&common.VerifyOptions{SignInRequired: setting.Service.RequireSignInViewStrict || setting.Service.Explore.RequireSigninView}) validation.AddBindingRules() @@ -856,13 +854,13 @@ func registerRoutes(m *web.Router) { individualPermsChecker := func(ctx *context.Context) { // org permissions have been checked in context.OrgAssignment(), but individual permissions haven't been checked. if ctx.ContextUser.IsIndividual() { - switch { - case ctx.ContextUser.Visibility == structs.VisibleTypePrivate: + switch ctx.ContextUser.Visibility { + case structs.VisibleTypePrivate: if ctx.Doer == nil || (ctx.ContextUser.ID != ctx.Doer.ID && !ctx.Doer.IsAdmin) { ctx.NotFound(nil) return } - case ctx.ContextUser.Visibility == structs.VisibleTypeLimited: + case structs.VisibleTypeLimited: if ctx.Doer == nil { ctx.NotFound(nil) return @@ -1080,6 +1078,8 @@ func registerRoutes(m *web.Router) { m.Post("/avatar", web.Bind(forms.AvatarForm{}), repo_setting.SettingsAvatar) m.Post("/avatar/delete", repo_setting.SettingsDeleteAvatar) + m.Combo("/public_access").Get(repo_setting.PublicAccess).Post(repo_setting.PublicAccessPost) + m.Group("/collaboration", func() { m.Combo("").Get(repo_setting.Collaboration).Post(repo_setting.CollaborationPost) m.Post("/access_mode", repo_setting.ChangeCollaborationAccessMode) @@ -1185,6 +1185,7 @@ func registerRoutes(m *web.Router) { m.Combo("/compare/*", repo.MustBeNotEmpty, repo.SetEditorconfigIfExists). Get(repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.CompareDiff). Post(reqSignIn, context.RepoMustNotBeArchived(), reqUnitPullsReader, repo.MustAllowPulls, web.Bind(forms.CreateIssueForm{}), repo.SetWhitespaceBehavior, repo.CompareAndPullRequestPost) + m.Get("/pulls/new/*", repo.PullsNewRedirect) }, optSignIn, context.RepoAssignment, reqUnitCodeReader) // end "/{username}/{reponame}": repo code: find, compare, list @@ -1489,7 +1490,7 @@ func registerRoutes(m *web.Router) { }) m.Group("/recent-commits", func() { m.Get("", repo.RecentCommits) - m.Get("/data", repo.RecentCommitsData) + m.Get("/data", repo.CodeFrequencyData) // "recent-commits" also uses the same data as "code-frequency" }) }, reqUnitCodeReader) }, @@ -1640,7 +1641,7 @@ func registerRoutes(m *web.Router) { m.Group("/devtest", func() { m.Any("", devtest.List) m.Any("/fetch-action-test", devtest.FetchActionTest) - m.Any("/{sub}", devtest.Tmpl) + m.Any("/{sub}", devtest.TmplCommon) m.Get("/repo-action-view/{run}/{job}", devtest.MockActionsView) m.Post("/actions-mock/runs/{run}/jobs/{job}", web.Bind(actions.ViewRequest{}), devtest.MockActionsRunsJobs) }) diff --git a/services/asymkey/sign.go b/services/asymkey/sign.go index da265dec27..2216bca54a 100644 --- a/services/asymkey/sign.go +++ b/services/asymkey/sign.go @@ -204,7 +204,7 @@ Loop: return false, "", nil, &ErrWontSign{twofa} } case parentSigned: - gitRepo, err := gitrepo.OpenWikiRepository(ctx, repo) + gitRepo, err := gitrepo.OpenRepository(ctx, repo.WikiStorageRepo()) if err != nil { return false, "", nil, err } diff --git a/services/auth/oauth2_test.go b/services/auth/oauth2_test.go index 9edf18d58e..f003742a94 100644 --- a/services/auth/oauth2_test.go +++ b/services/auth/oauth2_test.go @@ -26,7 +26,7 @@ func TestUserIDFromToken(t *testing.T) { o := OAuth2{} uid := o.userIDFromToken(t.Context(), token, ds) - assert.Equal(t, int64(user_model.ActionsUserID), uid) + assert.Equal(t, user_model.ActionsUserID, uid) assert.Equal(t, true, ds["IsActionsToken"]) assert.Equal(t, ds["ActionsTaskID"], int64(RunningTaskID)) }) diff --git a/services/context/package.go b/services/context/package.go index 64bb4f3ecd..8b722932b1 100644 --- a/services/context/package.go +++ b/services/context/package.go @@ -93,7 +93,7 @@ func packageAssignment(ctx *packageAssignmentCtx, errCb func(int, any)) *Package } func determineAccessMode(ctx *Base, pkg *Package, doer *user_model.User) (perm.AccessMode, error) { - if setting.Service.RequireSignInView && (doer == nil || doer.IsGhost()) { + if setting.Service.RequireSignInViewStrict && (doer == nil || doer.IsGhost()) { return perm.AccessModeNone, nil } diff --git a/services/context/pagination.go b/services/context/pagination.go index d33dd217d0..25a9298e01 100644 --- a/services/context/pagination.go +++ b/services/context/pagination.go @@ -21,12 +21,18 @@ type Pagination struct { // NewPagination creates a new instance of the Pagination struct. // "pagingNum" is "page size" or "limit", "current" is "page" +// total=-1 means only showing prev/next func NewPagination(total, pagingNum, current, numPages int) *Pagination { p := &Pagination{} p.Paginater = paginator.New(total, pagingNum, current, numPages) return p } +func (p *Pagination) WithCurRows(n int) *Pagination { + p.Paginater.SetCurRows(n) + return p +} + func (p *Pagination) AddParamFromRequest(req *http.Request) { for key, values := range req.URL.Query() { if key == "page" || len(values) == 0 || (len(values) == 1 && values[0] == "") { diff --git a/services/context/repo.go b/services/context/repo.go index 6eccd1312a..7d0b44c42f 100644 --- a/services/context/repo.go +++ b/services/context/repo.go @@ -328,7 +328,9 @@ func RedirectToRepo(ctx *Base, redirectRepoID int64) { if ctx.Req.URL.RawQuery != "" { redirectPath += "?" + ctx.Req.URL.RawQuery } - ctx.Redirect(path.Join(setting.AppSubURL, redirectPath), http.StatusTemporaryRedirect) + // Git client needs a 301 redirect by default to follow the new location + // It's not documentated in git documentation, but it's the behavior of git client + ctx.Redirect(path.Join(setting.AppSubURL, redirectPath), http.StatusMovedPermanently) } func repoAssignment(ctx *Context, repo *repo_model.Repository) { @@ -344,7 +346,7 @@ func repoAssignment(ctx *Context, repo *repo_model.Repository) { return } - if !ctx.Repo.Permission.HasAnyUnitAccessOrEveryoneAccess() && !canWriteAsMaintainer(ctx) { + if !ctx.Repo.Permission.HasAnyUnitAccessOrPublicAccess() && !canWriteAsMaintainer(ctx) { if ctx.FormString("go-get") == "1" { EarlyResponseForGoGetMeta(ctx) return diff --git a/services/context/upload/upload.go b/services/context/upload/upload.go index da4370a433..12aa485aa7 100644 --- a/services/context/upload/upload.go +++ b/services/context/upload/upload.go @@ -87,14 +87,15 @@ func Verify(buf []byte, fileName, allowedTypesStr string) error { // AddUploadContext renders template values for dropzone func AddUploadContext(ctx *context.Context, uploadType string) { - if uploadType == "release" { + switch uploadType { + case "release": ctx.Data["UploadUrl"] = ctx.Repo.RepoLink + "/releases/attachments" ctx.Data["UploadRemoveUrl"] = ctx.Repo.RepoLink + "/releases/attachments/remove" ctx.Data["UploadLinkUrl"] = ctx.Repo.RepoLink + "/releases/attachments" ctx.Data["UploadAccepts"] = strings.ReplaceAll(setting.Repository.Release.AllowedTypes, "|", ",") ctx.Data["UploadMaxFiles"] = setting.Attachment.MaxFiles ctx.Data["UploadMaxSize"] = setting.Attachment.MaxSize - } else if uploadType == "comment" { + case "comment": ctx.Data["UploadUrl"] = ctx.Repo.RepoLink + "/issues/attachments" ctx.Data["UploadRemoveUrl"] = ctx.Repo.RepoLink + "/issues/attachments/remove" if len(ctx.PathParam("index")) > 0 { @@ -105,7 +106,7 @@ func AddUploadContext(ctx *context.Context, uploadType string) { ctx.Data["UploadAccepts"] = strings.ReplaceAll(setting.Attachment.AllowedTypes, "|", ",") ctx.Data["UploadMaxFiles"] = setting.Attachment.MaxFiles ctx.Data["UploadMaxSize"] = setting.Attachment.MaxSize - } else if uploadType == "repo" { + case "repo": ctx.Data["UploadUrl"] = ctx.Repo.RepoLink + "/upload-file" ctx.Data["UploadRemoveUrl"] = ctx.Repo.RepoLink + "/upload-remove" ctx.Data["UploadLinkUrl"] = ctx.Repo.RepoLink + "/upload-file" diff --git a/services/doctor/fix16961_test.go b/services/doctor/fix16961_test.go index 498ed9c8d5..d5eded1117 100644 --- a/services/doctor/fix16961_test.go +++ b/services/doctor/fix16961_test.go @@ -19,12 +19,6 @@ func Test_fixUnitConfig_16961(t *testing.T) { wantErr bool }{ { - name: "empty", - bs: "", - wantFixed: true, - wantErr: false, - }, - { name: "normal: {}", bs: "{}", wantFixed: false, diff --git a/services/doctor/misc.go b/services/doctor/misc.go index 260a28ec4c..1269d088c3 100644 --- a/services/doctor/misc.go +++ b/services/doctor/misc.go @@ -8,7 +8,7 @@ import ( "fmt" "os" "os/exec" - "path" + "path/filepath" "strings" "code.gitea.io/gitea/models" @@ -49,14 +49,14 @@ func checkScriptType(ctx context.Context, logger log.Logger, autofix bool) error func checkHooks(ctx context.Context, logger log.Logger, autofix bool) error { if err := iterateRepositories(ctx, func(repo *repo_model.Repository) error { - results, err := gitrepo.CheckDelegateHooksForRepo(ctx, repo) + results, err := gitrepo.CheckDelegateHooks(ctx, repo) if err != nil { logger.Critical("Unable to check delegate hooks for repo %-v. ERROR: %v", repo, err) return fmt.Errorf("Unable to check delegate hooks for repo %-v. ERROR: %w", repo, err) } if len(results) > 0 && autofix { logger.Warn("Regenerated hooks for %s", repo.FullName()) - if err := gitrepo.CreateDelegateHooksForRepo(ctx, repo); err != nil { + if err := gitrepo.CreateDelegateHooks(ctx, repo); err != nil { logger.Critical("Unable to recreate delegate hooks for %-v. ERROR: %v", repo, err) return fmt.Errorf("Unable to recreate delegate hooks for %-v. ERROR: %w", repo, err) } @@ -148,7 +148,7 @@ func checkDaemonExport(ctx context.Context, logger log.Logger, autofix bool) err } // Create/Remove git-daemon-export-ok for git-daemon... - daemonExportFile := path.Join(repo.RepoPath(), `git-daemon-export-ok`) + daemonExportFile := filepath.Join(repo.RepoPath(), `git-daemon-export-ok`) isExist, err := util.IsExist(daemonExportFile) if err != nil { log.Error("Unable to check if %s exists. Error: %v", daemonExportFile, err) @@ -196,7 +196,7 @@ func checkCommitGraph(ctx context.Context, logger log.Logger, autofix bool) erro commitGraphExists := func() (bool, error) { // Check commit-graph exists - commitGraphFile := path.Join(repo.RepoPath(), `objects/info/commit-graph`) + commitGraphFile := filepath.Join(repo.RepoPath(), `objects/info/commit-graph`) isExist, err := util.IsExist(commitGraphFile) if err != nil { logger.Error("Unable to check if %s exists. Error: %v", commitGraphFile, err) @@ -204,7 +204,7 @@ func checkCommitGraph(ctx context.Context, logger log.Logger, autofix bool) erro } if !isExist { - commitGraphsDir := path.Join(repo.RepoPath(), `objects/info/commit-graphs`) + commitGraphsDir := filepath.Join(repo.RepoPath(), `objects/info/commit-graphs`) isExist, err = util.IsExist(commitGraphsDir) if err != nil { logger.Error("Unable to check if %s exists. Error: %v", commitGraphsDir, err) diff --git a/services/doctor/storage.go b/services/doctor/storage.go index 3f3b562c37..77fc6d65df 100644 --- a/services/doctor/storage.go +++ b/services/doctor/storage.go @@ -121,7 +121,7 @@ func checkStorage(opts *checkStorageOptions) func(ctx context.Context, logger lo storer: storage.LFS, isOrphaned: func(path string, obj storage.Object, stat fs.FileInfo) (bool, error) { // The oid of an LFS stored object is the name but with all the path.Separators removed - oid := strings.ReplaceAll(path, "/", "") + oid := strings.ReplaceAll(strings.ReplaceAll(path, "\\", ""), "/", "") exists, err := git.ExistsLFSObject(ctx, oid) return !exists, err }, diff --git a/services/feed/feed.go b/services/feed/feed.go index a1c327fb51..214e9b5765 100644 --- a/services/feed/feed.go +++ b/services/feed/feed.go @@ -13,9 +13,21 @@ import ( 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/cache" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" ) +func userFeedCacheKey(userID int64) string { + return fmt.Sprintf("user_feed_%d", userID) +} + +func GetFeedsForDashboard(ctx context.Context, opts activities_model.GetFeedsOptions) (activities_model.ActionList, int, error) { + opts.DontCount = opts.RequestedTeam == nil && opts.Date == "" + results, cnt, err := activities_model.GetFeeds(ctx, opts) + return results, util.Iif(opts.DontCount, -1, int(cnt)), err +} + // GetFeeds returns actions according to the provided options func GetFeeds(ctx context.Context, opts activities_model.GetFeedsOptions) (activities_model.ActionList, int64, error) { return activities_model.GetFeeds(ctx, opts) @@ -68,6 +80,13 @@ func notifyWatchers(ctx context.Context, act *activities_model.Action, watchers if err := db.Insert(ctx, act); err != nil { return fmt.Errorf("insert new action: %w", err) } + + total, err := activities_model.CountUserFeeds(ctx, act.UserID) + if err != nil { + return fmt.Errorf("count user feeds: %w", err) + } + + _ = cache.GetCache().Put(userFeedCacheKey(act.UserID), fmt.Sprintf("%d", total), setting.CacheService.TTLSeconds()) } return nil diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go index 1366d30b1f..d20220b784 100644 --- a/services/forms/repo_form.go +++ b/services/forms/repo_form.go @@ -110,17 +110,14 @@ type RepoSettingForm struct { EnablePrune bool // Advanced settings - EnableCode bool - DefaultCodeEveryoneAccess string + EnableCode bool - EnableWiki bool - EnableExternalWiki bool - DefaultWikiBranch string - DefaultWikiEveryoneAccess string - ExternalWikiURL string + EnableWiki bool + EnableExternalWiki bool + DefaultWikiBranch string + ExternalWikiURL string EnableIssues bool - DefaultIssuesEveryoneAccess string EnableExternalTracker bool ExternalTrackerURL string TrackerURLFormat string diff --git a/services/gitdiff/highlightdiff.go b/services/gitdiff/highlightdiff.go index 6e18651d83..e8be063e69 100644 --- a/services/gitdiff/highlightdiff.go +++ b/services/gitdiff/highlightdiff.go @@ -14,13 +14,14 @@ import ( // token is a html tag or entity, eg: "<span ...>", "</span>", "<" func extractHTMLToken(s string) (before, token, after string, valid bool) { for pos1 := 0; pos1 < len(s); pos1++ { - if s[pos1] == '<' { + switch s[pos1] { + case '<': pos2 := strings.IndexByte(s[pos1:], '>') if pos2 == -1 { return "", "", s, false } return s[:pos1], s[pos1 : pos1+pos2+1], s[pos1+pos2+1:], true - } else if s[pos1] == '&' { + case '&': pos2 := strings.IndexByte(s[pos1:], ';') if pos2 == -1 { return "", "", s, false diff --git a/services/lfs/server.go b/services/lfs/server.go index c4866edaab..1e7608b781 100644 --- a/services/lfs/server.go +++ b/services/lfs/server.go @@ -164,11 +164,12 @@ func BatchHandler(ctx *context.Context) { } var isUpload bool - if br.Operation == "upload" { + switch br.Operation { + case "upload": isUpload = true - } else if br.Operation == "download" { + case "download": isUpload = false - } else { + default: log.Trace("Attempt to BATCH with invalid operation: %s", br.Operation) writeStatus(ctx, http.StatusBadRequest) return diff --git a/services/mailer/notify.go b/services/mailer/notify.go index a27177e8f5..77c366fe31 100644 --- a/services/mailer/notify.go +++ b/services/mailer/notify.go @@ -31,15 +31,16 @@ func (m *mailNotifier) CreateIssueComment(ctx context.Context, doer *user_model. issue *issues_model.Issue, comment *issues_model.Comment, mentions []*user_model.User, ) { var act activities_model.ActionType - if comment.Type == issues_model.CommentTypeClose { + switch comment.Type { + case issues_model.CommentTypeClose: act = activities_model.ActionCloseIssue - } else if comment.Type == issues_model.CommentTypeReopen { + case issues_model.CommentTypeReopen: act = activities_model.ActionReopenIssue - } else if comment.Type == issues_model.CommentTypeComment { + case issues_model.CommentTypeComment: act = activities_model.ActionCommentIssue - } else if comment.Type == issues_model.CommentTypeCode { + case issues_model.CommentTypeCode: act = activities_model.ActionCommentIssue - } else if comment.Type == issues_model.CommentTypePullRequestPush { + case issues_model.CommentTypePullRequestPush: act = 0 } @@ -95,11 +96,12 @@ func (m *mailNotifier) NewPullRequest(ctx context.Context, pr *issues_model.Pull func (m *mailNotifier) PullRequestReview(ctx context.Context, pr *issues_model.PullRequest, r *issues_model.Review, comment *issues_model.Comment, mentions []*user_model.User) { var act activities_model.ActionType - if comment.Type == issues_model.CommentTypeClose { + switch comment.Type { + case issues_model.CommentTypeClose: act = activities_model.ActionCloseIssue - } else if comment.Type == issues_model.CommentTypeReopen { + case issues_model.CommentTypeReopen: act = activities_model.ActionReopenIssue - } else if comment.Type == issues_model.CommentTypeComment { + case issues_model.CommentTypeComment: act = activities_model.ActionCommentPull } if err := MailParticipantsComment(ctx, comment, act, pr.Issue, mentions); err != nil { diff --git a/services/migrations/gitlab.go b/services/migrations/gitlab.go index efc5b960cf..4bed8e2f6c 100644 --- a/services/migrations/gitlab.go +++ b/services/migrations/gitlab.go @@ -16,6 +16,7 @@ import ( "time" issues_model "code.gitea.io/gitea/models/issues" + "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/log" base "code.gitea.io/gitea/modules/migration" @@ -535,11 +536,15 @@ func (g *GitlabDownloader) GetComments(ctx context.Context, commentable base.Com } for _, stateEvent := range stateEvents { + posterUserID, posterUsername := user.GhostUserID, user.GhostUserName + if stateEvent.User != nil { + posterUserID, posterUsername = int64(stateEvent.User.ID), stateEvent.User.Username + } comment := &base.Comment{ IssueIndex: commentable.GetLocalIndex(), Index: int64(stateEvent.ID), - PosterID: int64(stateEvent.User.ID), - PosterName: stateEvent.User.Username, + PosterID: posterUserID, + PosterName: posterUsername, Content: "", Created: *stateEvent.CreatedAt, } diff --git a/services/mirror/mirror_pull.go b/services/mirror/mirror_pull.go index 658747e7c8..fa5b9934ec 100644 --- a/services/mirror/mirror_pull.go +++ b/services/mirror/mirror_pull.go @@ -235,6 +235,24 @@ func pruneBrokenReferences(ctx context.Context, return pruneErr } +// checkRecoverableSyncError takes an error message from a git fetch command and returns false if it should be a fatal/blocking error +func checkRecoverableSyncError(stderrMessage string) bool { + switch { + case strings.Contains(stderrMessage, "unable to resolve reference") && strings.Contains(stderrMessage, "reference broken"): + return true + case strings.Contains(stderrMessage, "remote error") && strings.Contains(stderrMessage, "not our ref"): + return true + case strings.Contains(stderrMessage, "cannot lock ref") && strings.Contains(stderrMessage, "but expected"): + return true + case strings.Contains(stderrMessage, "cannot lock ref") && strings.Contains(stderrMessage, "unable to resolve reference"): + return true + case strings.Contains(stderrMessage, "Unable to create") && strings.Contains(stderrMessage, ".lock"): + return true + default: + return false + } +} + // runSync returns true if sync finished without error. func runSync(ctx context.Context, m *repo_model.Mirror) ([]*mirrorSyncResult, bool) { repoPath := m.Repo.RepoPath() @@ -275,7 +293,7 @@ func runSync(ctx context.Context, m *repo_model.Mirror) ([]*mirrorSyncResult, bo stdoutMessage := util.SanitizeCredentialURLs(stdout) // Now check if the error is a resolve reference due to broken reference - if strings.Contains(stderr, "unable to resolve reference") && strings.Contains(stderr, "reference broken") { + if checkRecoverableSyncError(stderr) { log.Warn("SyncMirrors [repo: %-v]: failed to update mirror repository due to broken references:\nStdout: %s\nStderr: %s\nErr: %v\nAttempting Prune", m.Repo, stdoutMessage, stderrMessage, err) err = nil @@ -324,6 +342,15 @@ func runSync(ctx context.Context, m *repo_model.Mirror) ([]*mirrorSyncResult, bo return nil, false } + if m.LFS && setting.LFS.StartServer { + log.Trace("SyncMirrors [repo: %-v]: syncing LFS objects...", m.Repo) + endpoint := lfs.DetermineEndpoint(remoteURL.String(), m.LFSEndpoint) + lfsClient := lfs.NewClient(endpoint, nil) + if err = repo_module.StoreMissingLfsObjectsInRepository(ctx, m.Repo, gitRepo, lfsClient); err != nil { + log.Error("SyncMirrors [repo: %-v]: failed to synchronize LFS objects for repository: %v", m.Repo, err) + } + } + log.Trace("SyncMirrors [repo: %-v]: syncing branches...", m.Repo) if _, err = repo_module.SyncRepoBranchesWithRepo(ctx, m.Repo, gitRepo, 0); err != nil { log.Error("SyncMirrors [repo: %-v]: failed to synchronize branches: %v", m.Repo, err) @@ -333,15 +360,6 @@ func runSync(ctx context.Context, m *repo_model.Mirror) ([]*mirrorSyncResult, bo if err = repo_module.SyncReleasesWithTags(ctx, m.Repo, gitRepo); err != nil { log.Error("SyncMirrors [repo: %-v]: failed to synchronize tags to releases: %v", m.Repo, err) } - - if m.LFS && setting.LFS.StartServer { - log.Trace("SyncMirrors [repo: %-v]: syncing LFS objects...", m.Repo) - endpoint := lfs.DetermineEndpoint(remoteURL.String(), m.LFSEndpoint) - lfsClient := lfs.NewClient(endpoint, nil) - if err = repo_module.StoreMissingLfsObjectsInRepository(ctx, m.Repo, gitRepo, lfsClient); err != nil { - log.Error("SyncMirrors [repo: %-v]: failed to synchronize LFS objects for repository: %v", m.Repo, err) - } - } gitRepo.Close() log.Trace("SyncMirrors [repo: %-v]: updating size of repository", m.Repo) @@ -368,7 +386,7 @@ func runSync(ctx context.Context, m *repo_model.Mirror) ([]*mirrorSyncResult, bo stdoutMessage := util.SanitizeCredentialURLs(stdout) // Now check if the error is a resolve reference due to broken reference - if strings.Contains(stderrMessage, "unable to resolve reference") && strings.Contains(stderrMessage, "reference broken") { + if checkRecoverableSyncError(stderrMessage) { log.Warn("SyncMirrors [repo: %-v Wiki]: failed to update mirror wiki repository due to broken references:\nStdout: %s\nStderr: %s\nErr: %v\nAttempting Prune", m.Repo, stdoutMessage, stderrMessage, err) err = nil diff --git a/services/mirror/mirror_test.go b/services/mirror/mirror_pull_test.go index 76632b6872..67ef37c954 100644 --- a/services/mirror/mirror_test.go +++ b/services/mirror/mirror_pull_test.go @@ -64,3 +64,31 @@ func Test_parseRemoteUpdateOutput(t *testing.T) { assert.EqualValues(t, "1c97ebc746", results[9].oldCommitID) assert.EqualValues(t, "976d27d52f", results[9].newCommitID) } + +func Test_checkRecoverableSyncError(t *testing.T) { + cases := []struct { + recoverable bool + message string + }{ + // A race condition in http git-fetch where certain refs were listed on the remote and are no longer there, would exit status 128 + {true, "fatal: remote error: upload-pack: not our ref 988881adc9fc3655077dc2d4d757d480b5ea0e11"}, + // A race condition where a local gc/prune removes a named ref during a git-fetch would exit status 1 + {true, "cannot lock ref 'refs/pull/123456/merge': unable to resolve reference 'refs/pull/134153/merge'"}, + // A race condition in http git-fetch where named refs were listed on the remote and are no longer there + {true, "error: cannot lock ref 'refs/remotes/origin/foo': unable to resolve reference 'refs/remotes/origin/foo': reference broken"}, + // A race condition in http git-fetch where named refs were force-pushed during the update, would exit status 128 + {true, "error: cannot lock ref 'refs/pull/123456/merge': is at 988881adc9fc3655077dc2d4d757d480b5ea0e11 but expected 7f894307ffc9553edbd0b671cab829786866f7b2"}, + // A race condition with other local git operations, such as git-maintenance, would exit status 128 (well, "Unable" the "U" is uppercase) + {true, "fatal: Unable to create '/data/gitea-repositories/foo-org/bar-repo.git/./objects/info/commit-graphs/commit-graph-chain.lock': File exists."}, + // Missing or unauthorized credentials, would exit status 128 + {false, "fatal: Authentication failed for 'https://example.com/foo-does-not-exist/bar.git/'"}, + // A non-existent remote repository, would exit status 128 + {false, "fatal: Could not read from remote repository."}, + // A non-functioning proxy, would exit status 128 + {false, "fatal: unable to access 'https://example.com/foo-does-not-exist/bar.git/': Failed to connect to configured-https-proxy port 1080 after 0 ms: Couldn't connect to server"}, + } + + for _, c := range cases { + assert.Equal(t, c.recoverable, checkRecoverableSyncError(c.message), "test case: %s", c.message) + } +} diff --git a/services/mirror/mirror_push.go b/services/mirror/mirror_push.go index 6e72876893..9b57427d98 100644 --- a/services/mirror/mirror_push.go +++ b/services/mirror/mirror_push.go @@ -143,7 +143,7 @@ func runPushSync(ctx context.Context, m *repo_model.PushMirror) error { var gitRepo *git.Repository if isWiki { - gitRepo, err = gitrepo.OpenWikiRepository(ctx, repo) + gitRepo, err = gitrepo.OpenRepository(ctx, repo.WikiStorageRepo()) } else { gitRepo, err = gitrepo.OpenRepository(ctx, repo) } diff --git a/services/packages/cargo/index.go b/services/packages/cargo/index.go index 0c8a98c40f..decb224b85 100644 --- a/services/packages/cargo/index.go +++ b/services/packages/cargo/index.go @@ -247,7 +247,7 @@ func createOrUpdateConfigFile(ctx context.Context, repo *repo_model.Repository, "Initialize Cargo Config", func(t *files_service.TemporaryUploadRepository) error { var b bytes.Buffer - err := json.NewEncoder(&b).Encode(BuildConfig(owner, setting.Service.RequireSignInView || owner.Visibility != structs.VisibleTypePublic || repo.IsPrivate)) + err := json.NewEncoder(&b).Encode(BuildConfig(owner, setting.Service.RequireSignInViewStrict || owner.Visibility != structs.VisibleTypePublic || repo.IsPrivate)) if err != nil { return err } diff --git a/services/repository/adopt.go b/services/repository/adopt.go index ea4f9a1920..b7321156d9 100644 --- a/services/repository/adopt.go +++ b/services/repository/adopt.go @@ -115,7 +115,7 @@ func adoptRepository(ctx context.Context, repo *repo_model.Repository, defaultBr return fmt.Errorf("adoptRepository: path does not already exist: %s", repo.FullName()) } - if err := gitrepo.CreateDelegateHooksForRepo(ctx, repo); err != nil { + if err := gitrepo.CreateDelegateHooks(ctx, repo); err != nil { return fmt.Errorf("createDelegateHooks: %w", err) } diff --git a/services/repository/adopt_test.go b/services/repository/adopt_test.go index 123cedc1f2..294185ea1f 100644 --- a/services/repository/adopt_test.go +++ b/services/repository/adopt_test.go @@ -71,7 +71,7 @@ func TestListUnadoptedRepositories_ListOptions(t *testing.T) { username := "user2" unadoptedList := []string{path.Join(username, "unadopted1"), path.Join(username, "unadopted2")} for _, unadopted := range unadoptedList { - _ = os.Mkdir(path.Join(setting.RepoRootPath, unadopted+".git"), 0o755) + _ = os.Mkdir(filepath.Join(setting.RepoRootPath, unadopted+".git"), 0o755) } opts := db.ListOptions{Page: 1, PageSize: 1} diff --git a/services/repository/create.go b/services/repository/create.go index 1a6a68b35a..af4e897151 100644 --- a/services/repository/create.go +++ b/services/repository/create.go @@ -384,7 +384,8 @@ func CreateRepositoryByExample(ctx context.Context, doer, u *user_model.User, re } units := make([]repo_model.RepoUnit, 0, len(defaultUnits)) for _, tp := range defaultUnits { - if tp == unit.TypeIssues { + switch tp { + case unit.TypeIssues: units = append(units, repo_model.RepoUnit{ RepoID: repo.ID, Type: tp, @@ -394,7 +395,7 @@ func CreateRepositoryByExample(ctx context.Context, doer, u *user_model.User, re EnableDependencies: setting.Service.DefaultEnableDependencies, }, }) - } else if tp == unit.TypePullRequests { + case unit.TypePullRequests: units = append(units, repo_model.RepoUnit{ RepoID: repo.ID, Type: tp, @@ -404,13 +405,13 @@ func CreateRepositoryByExample(ctx context.Context, doer, u *user_model.User, re AllowRebaseUpdate: true, }, }) - } else if tp == unit.TypeProjects { + case unit.TypeProjects: units = append(units, repo_model.RepoUnit{ RepoID: repo.ID, Type: tp, Config: &repo_model.ProjectsConfig{ProjectsMode: repo_model.ProjectsModeAll}, }) - } else { + default: units = append(units, repo_model.RepoUnit{ RepoID: repo.ID, Type: tp, diff --git a/services/repository/fork.go b/services/repository/fork.go index 7f7364acfc..5b1ba7a418 100644 --- a/services/repository/fork.go +++ b/services/repository/fork.go @@ -170,7 +170,7 @@ func ForkRepository(ctx context.Context, doer, owner *user_model.User, opts Fork return fmt.Errorf("git update-server-info: %w", err) } - if err = gitrepo.CreateDelegateHooksForRepo(ctx, repo); err != nil { + if err = gitrepo.CreateDelegateHooks(ctx, repo); err != nil { return fmt.Errorf("createDelegateHooks: %w", err) } diff --git a/services/repository/hooks.go b/services/repository/hooks.go index 2b3eb79153..c13b272550 100644 --- a/services/repository/hooks.go +++ b/services/repository/hooks.go @@ -31,11 +31,11 @@ func SyncRepositoryHooks(ctx context.Context) error { default: } - if err := gitrepo.CreateDelegateHooksForRepo(ctx, repo); err != nil { + if err := gitrepo.CreateDelegateHooks(ctx, repo); err != nil { return fmt.Errorf("SyncRepositoryHook: %w", err) } if repo.HasWiki() { - if err := gitrepo.CreateDelegateHooksForWiki(ctx, repo); err != nil { + if err := gitrepo.CreateDelegateHooks(ctx, repo.WikiStorageRepo()); err != nil { return fmt.Errorf("SyncRepositoryHook: %w", err) } } diff --git a/services/repository/migrate.go b/services/repository/migrate.go index 1969b16a2d..9a5c6ffb0f 100644 --- a/services/repository/migrate.go +++ b/services/repository/migrate.go @@ -13,6 +13,7 @@ import ( "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/organization" repo_model "code.gitea.io/gitea/models/repo" + unit_model "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/gitrepo" @@ -246,6 +247,19 @@ func MigrateRepositoryGitData(ctx context.Context, u *user_model.User, } } + var enableRepoUnits []repo_model.RepoUnit + if opts.Releases && !unit_model.TypeReleases.UnitGlobalDisabled() { + enableRepoUnits = append(enableRepoUnits, repo_model.RepoUnit{RepoID: repo.ID, Type: unit_model.TypeReleases}) + } + if opts.Wiki && !unit_model.TypeWiki.UnitGlobalDisabled() { + enableRepoUnits = append(enableRepoUnits, repo_model.RepoUnit{RepoID: repo.ID, Type: unit_model.TypeWiki}) + } + if len(enableRepoUnits) > 0 { + err = UpdateRepositoryUnits(ctx, repo, enableRepoUnits, nil) + if err != nil { + return nil, err + } + } return repo, committer.Commit() } @@ -265,11 +279,11 @@ func cleanUpMigrateGitConfig(ctx context.Context, repoPath string) error { // CleanUpMigrateInfo finishes migrating repository and/or wiki with things that don't need to be done for mirrors. func CleanUpMigrateInfo(ctx context.Context, repo *repo_model.Repository) (*repo_model.Repository, error) { - if err := gitrepo.CreateDelegateHooksForRepo(ctx, repo); err != nil { + if err := gitrepo.CreateDelegateHooks(ctx, repo); err != nil { return repo, fmt.Errorf("createDelegateHooks: %w", err) } if repo.HasWiki() { - if err := gitrepo.CreateDelegateHooksForWiki(ctx, repo); err != nil { + if err := gitrepo.CreateDelegateHooks(ctx, repo.WikiStorageRepo()); err != nil { return repo, fmt.Errorf("createDelegateHooks.(wiki): %w", err) } } diff --git a/services/repository/push.go b/services/repository/push.go index c40333f0a8..6d3b9dd252 100644 --- a/services/repository/push.go +++ b/services/repository/push.go @@ -167,8 +167,9 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error { } } - branch := opts.RefFullName.BranchName() if !opts.IsDelRef() { + branch := opts.RefFullName.BranchName() + log.Trace("TriggerTask '%s/%s' by %s", repo.Name, branch, pusher.Name) newCommit, err := gitRepo.GetCommit(opts.NewCommitID) @@ -176,60 +177,15 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error { return fmt.Errorf("gitRepo.GetCommit(%s) in %s/%s[%d]: %w", opts.NewCommitID, repo.OwnerName, repo.Name, repo.ID, err) } - refName := opts.RefName() - // Push new branch. var l []*git.Commit if opts.IsNewRef() { - if repo.IsEmpty { // Change default branch and empty status only if pushed ref is non-empty branch. - repo.DefaultBranch = refName - repo.IsEmpty = false - if repo.DefaultBranch != setting.Repository.DefaultBranch { - if err := gitrepo.SetDefaultBranch(ctx, repo, repo.DefaultBranch); err != nil { - return err - } - } - // Update the is empty and default_branch columns - if err := repo_model.UpdateRepositoryCols(ctx, repo, "default_branch", "is_empty"); err != nil { - return fmt.Errorf("UpdateRepositoryCols: %w", err) - } - } - - l, err = newCommit.CommitsBeforeLimit(10) - if err != nil { - return fmt.Errorf("newCommit.CommitsBeforeLimit: %w", err) - } - notify_service.CreateRef(ctx, pusher, repo, opts.RefFullName, opts.NewCommitID) + l, err = pushNewBranch(ctx, repo, pusher, opts, newCommit) } else { - l, err = newCommit.CommitsBeforeUntil(opts.OldCommitID) - if err != nil { - return fmt.Errorf("newCommit.CommitsBeforeUntil: %w", err) - } - - isForcePush, err := newCommit.IsForcePush(opts.OldCommitID) - if err != nil { - log.Error("IsForcePush %s:%s failed: %v", repo.FullName(), branch, err) - } - - // only update branch can trigger pull request task because the pull request hasn't been created yet when creaing a branch - go pull_service.AddTestPullRequestTask(pull_service.TestPullRequestOptions{ - RepoID: repo.ID, - Doer: pusher, - Branch: branch, - IsSync: true, - IsForcePush: isForcePush, - OldCommitID: opts.OldCommitID, - NewCommitID: opts.NewCommitID, - }) - - if isForcePush { - log.Trace("Push %s is a force push", opts.NewCommitID) - - cache.Remove(repo.GetCommitsCountCacheKey(opts.RefName(), true)) - } else { - // TODO: increment update the commit count cache but not remove - cache.Remove(repo.GetCommitsCountCacheKey(opts.RefName(), true)) - } + l, err = pushUpdateBranch(ctx, repo, pusher, opts, newCommit) + } + if err != nil { + return err } // delete cache for divergence @@ -246,36 +202,11 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error { commits := repo_module.GitToPushCommits(l) commits.HeadCommit = repo_module.CommitToPushCommit(newCommit) - if err := issue_service.UpdateIssuesCommit(ctx, pusher, repo, commits.Commits, refName); err != nil { + if err := issue_service.UpdateIssuesCommit(ctx, pusher, repo, commits.Commits, opts.RefName()); err != nil { log.Error("updateIssuesCommit: %v", err) } - oldCommitID := opts.OldCommitID - if oldCommitID == objectFormat.EmptyObjectID().String() && len(commits.Commits) > 0 { - oldCommit, err := gitRepo.GetCommit(commits.Commits[len(commits.Commits)-1].Sha1) - if err != nil && !git.IsErrNotExist(err) { - log.Error("unable to GetCommit %s from %-v: %v", oldCommitID, repo, err) - } - if oldCommit != nil { - for i := 0; i < oldCommit.ParentCount(); i++ { - commitID, _ := oldCommit.ParentID(i) - if !commitID.IsZero() { - oldCommitID = commitID.String() - break - } - } - } - } - - if oldCommitID == objectFormat.EmptyObjectID().String() && repo.DefaultBranch != branch { - oldCommitID = repo.DefaultBranch - } - - if oldCommitID != objectFormat.EmptyObjectID().String() { - commits.CompareURL = repo.ComposeCompareURL(oldCommitID, opts.NewCommitID) - } else { - commits.CompareURL = "" - } + commits.CompareURL = getCompareURL(repo, gitRepo, objectFormat, commits.Commits, opts) if len(commits.Commits) > setting.UI.FeedMaxCommitNum { commits.Commits = commits.Commits[:setting.UI.FeedMaxCommitNum] @@ -288,12 +219,7 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error { log.Error("repo_module.CacheRef %s/%s failed: %v", repo.ID, branch, err) } } else { - notify_service.DeleteRef(ctx, pusher, repo, opts.RefFullName) - - if err := pull_service.AdjustPullsCausedByBranchDeleted(ctx, pusher, repo, branch); err != nil { - // close all related pulls - log.Error("close related pull request failed: %v", err) - } + pushDeleteBranch(ctx, repo, pusher, opts) } // Even if user delete a branch on a repository which he didn't watch, he will be watch that. @@ -304,8 +230,11 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error { log.Trace("Non-tag and non-branch commits pushed.") } } - if err := PushUpdateAddDeleteTags(ctx, repo, gitRepo, addTags, delTags); err != nil { - return fmt.Errorf("PushUpdateAddDeleteTags: %w", err) + + if len(addTags)+len(delTags) > 0 { + if err := PushUpdateAddDeleteTags(ctx, repo, gitRepo, addTags, delTags); err != nil { + return fmt.Errorf("PushUpdateAddDeleteTags: %w", err) + } } // Change repository last updated time. @@ -316,6 +245,102 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error { return nil } +func getCompareURL(repo *repo_model.Repository, gitRepo *git.Repository, objectFormat git.ObjectFormat, commits []*repo_module.PushCommit, opts *repo_module.PushUpdateOptions) string { + oldCommitID := opts.OldCommitID + if oldCommitID == objectFormat.EmptyObjectID().String() && len(commits) > 0 { + oldCommit, err := gitRepo.GetCommit(commits[len(commits)-1].Sha1) + if err != nil && !git.IsErrNotExist(err) { + log.Error("unable to GetCommit %s from %-v: %v", oldCommitID, repo, err) + } + if oldCommit != nil { + for i := 0; i < oldCommit.ParentCount(); i++ { + commitID, _ := oldCommit.ParentID(i) + if !commitID.IsZero() { + oldCommitID = commitID.String() + break + } + } + } + } + + if oldCommitID == objectFormat.EmptyObjectID().String() && repo.DefaultBranch != opts.RefFullName.BranchName() { + oldCommitID = repo.DefaultBranch + } + + if oldCommitID != objectFormat.EmptyObjectID().String() { + return repo.ComposeCompareURL(oldCommitID, opts.NewCommitID) + } + return "" +} + +func pushNewBranch(ctx context.Context, repo *repo_model.Repository, pusher *user_model.User, opts *repo_module.PushUpdateOptions, newCommit *git.Commit) ([]*git.Commit, error) { + if repo.IsEmpty { // Change default branch and empty status only if pushed ref is non-empty branch. + repo.DefaultBranch = opts.RefName() + repo.IsEmpty = false + if repo.DefaultBranch != setting.Repository.DefaultBranch { + if err := gitrepo.SetDefaultBranch(ctx, repo, repo.DefaultBranch); err != nil { + return nil, err + } + } + // Update the is empty and default_branch columns + if err := repo_model.UpdateRepositoryCols(ctx, repo, "default_branch", "is_empty"); err != nil { + return nil, fmt.Errorf("UpdateRepositoryCols: %w", err) + } + } + + l, err := newCommit.CommitsBeforeLimit(10) + if err != nil { + return nil, fmt.Errorf("newCommit.CommitsBeforeLimit: %w", err) + } + notify_service.CreateRef(ctx, pusher, repo, opts.RefFullName, opts.NewCommitID) + return l, nil +} + +func pushUpdateBranch(_ context.Context, repo *repo_model.Repository, pusher *user_model.User, opts *repo_module.PushUpdateOptions, newCommit *git.Commit) ([]*git.Commit, error) { + l, err := newCommit.CommitsBeforeUntil(opts.OldCommitID) + if err != nil { + return nil, fmt.Errorf("newCommit.CommitsBeforeUntil: %w", err) + } + + branch := opts.RefFullName.BranchName() + + isForcePush, err := newCommit.IsForcePush(opts.OldCommitID) + if err != nil { + log.Error("IsForcePush %s:%s failed: %v", repo.FullName(), branch, err) + } + + // only update branch can trigger pull request task because the pull request hasn't been created yet when creating a branch + go pull_service.AddTestPullRequestTask(pull_service.TestPullRequestOptions{ + RepoID: repo.ID, + Doer: pusher, + Branch: branch, + IsSync: true, + IsForcePush: isForcePush, + OldCommitID: opts.OldCommitID, + NewCommitID: opts.NewCommitID, + }) + + if isForcePush { + log.Trace("Push %s is a force push", opts.NewCommitID) + + cache.Remove(repo.GetCommitsCountCacheKey(opts.RefName(), true)) + } else { + // TODO: increment update the commit count cache but not remove + cache.Remove(repo.GetCommitsCountCacheKey(opts.RefName(), true)) + } + + return l, nil +} + +func pushDeleteBranch(ctx context.Context, repo *repo_model.Repository, pusher *user_model.User, opts *repo_module.PushUpdateOptions) { + notify_service.DeleteRef(ctx, pusher, repo, opts.RefFullName) + + if err := pull_service.AdjustPullsCausedByBranchDeleted(ctx, pusher, repo, opts.RefFullName.BranchName()); err != nil { + // close all related pulls + log.Error("close related pull request failed: %v", err) + } +} + // PushUpdateAddDeleteTags updates a number of added and delete tags func PushUpdateAddDeleteTags(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, addTags, delTags []string) error { return db.WithTx(ctx, func(ctx context.Context) error { diff --git a/services/repository/transfer.go b/services/repository/transfer.go index 3940b2a142..a589bc469d 100644 --- a/services/repository/transfer.go +++ b/services/repository/transfer.go @@ -331,12 +331,13 @@ func changeRepositoryName(ctx context.Context, repo *repo_model.Repository, newR return fmt.Errorf("IsRepositoryExist: %w", err) } else if has { return repo_model.ErrRepoAlreadyExist{ - Uname: repo.Owner.Name, + Uname: repo.OwnerName, Name: newRepoName, } } - if err = gitrepo.RenameRepository(ctx, repo, newRepoName); err != nil { + if err = gitrepo.RenameRepository(ctx, repo, + repo_model.StorageRepo(repo_model.RelativePath(repo.OwnerName, newRepoName))); err != nil { return fmt.Errorf("rename repository directory: %w", err) } diff --git a/services/webhook/general.go b/services/webhook/general.go index ea75038faf..c58f83354d 100644 --- a/services/webhook/general.go +++ b/services/webhook/general.go @@ -39,11 +39,12 @@ func getPullRequestInfo(p *api.PullRequestPayload) (title, link, by, operator, o for i, user := range assignList { assignStringList[i] = user.UserName } - if p.Action == api.HookIssueAssigned { + switch p.Action { + case api.HookIssueAssigned: operateResult = fmt.Sprintf("%s assign this to %s", p.Sender.UserName, assignList[len(assignList)-1].UserName) - } else if p.Action == api.HookIssueUnassigned { + case api.HookIssueUnassigned: operateResult = fmt.Sprintf("%s unassigned this for someone", p.Sender.UserName) - } else if p.Action == api.HookIssueMilestoned { + case api.HookIssueMilestoned: operateResult = fmt.Sprintf("%s/milestone/%d", p.Repository.HTMLURL, p.PullRequest.Milestone.ID) } link = p.PullRequest.HTMLURL @@ -64,11 +65,12 @@ func getIssuesInfo(p *api.IssuePayload) (issueTitle, link, by, operator, operate for i, user := range assignList { assignStringList[i] = user.UserName } - if p.Action == api.HookIssueAssigned { + switch p.Action { + case api.HookIssueAssigned: operateResult = fmt.Sprintf("%s assign this to %s", p.Sender.UserName, assignList[len(assignList)-1].UserName) - } else if p.Action == api.HookIssueUnassigned { + case api.HookIssueUnassigned: operateResult = fmt.Sprintf("%s unassigned this for someone", p.Sender.UserName) - } else if p.Action == api.HookIssueMilestoned { + case api.HookIssueMilestoned: operateResult = fmt.Sprintf("%s/milestone/%d", p.Repository.HTMLURL, p.Issue.Milestone.ID) } link = p.Issue.HTMLURL diff --git a/services/wiki/wiki.go b/services/wiki/wiki.go index a3fe07927d..b21f46639d 100644 --- a/services/wiki/wiki.go +++ b/services/wiki/wiki.go @@ -41,7 +41,7 @@ func InitWiki(ctx context.Context, repo *repo_model.Repository) error { if err := git.InitRepository(ctx, repo.WikiPath(), true, repo.ObjectFormatName); err != nil { return fmt.Errorf("InitRepository: %w", err) - } else if err = gitrepo.CreateDelegateHooksForWiki(ctx, repo); err != nil { + } else if err = gitrepo.CreateDelegateHooks(ctx, repo.WikiStorageRepo()); err != nil { return fmt.Errorf("createDelegateHooks: %w", err) } else if _, _, err = git.NewCommand("symbolic-ref", "HEAD").AddDynamicArguments(git.BranchPrefix+repo.DefaultWikiBranch).RunStdString(ctx, &git.RunOpts{Dir: repo.WikiPath()}); err != nil { return fmt.Errorf("unable to set default wiki branch to %q: %w", repo.DefaultWikiBranch, err) @@ -100,7 +100,7 @@ func updateWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model return fmt.Errorf("InitWiki: %w", err) } - hasDefaultBranch := gitrepo.IsWikiBranchExist(ctx, repo, repo.DefaultWikiBranch) + hasDefaultBranch := gitrepo.IsBranchExist(ctx, repo.WikiStorageRepo(), repo.DefaultWikiBranch) basePath, err := repo_module.CreateTemporaryPath("update-wiki") if err != nil { @@ -381,7 +381,7 @@ func ChangeDefaultWikiBranch(ctx context.Context, repo *repo_model.Repository, n return nil } - oldDefBranch, err := gitrepo.GetWikiDefaultBranch(ctx, repo) + oldDefBranch, err := gitrepo.GetDefaultBranch(ctx, repo.WikiStorageRepo()) if err != nil { return fmt.Errorf("unable to get default branch: %w", err) } @@ -389,7 +389,7 @@ func ChangeDefaultWikiBranch(ctx context.Context, repo *repo_model.Repository, n return nil } - gitRepo, err := gitrepo.OpenWikiRepository(ctx, repo) + gitRepo, err := gitrepo.OpenRepository(ctx, repo.WikiStorageRepo()) if errors.Is(err, util.ErrNotExist) { return nil // no git repo on storage, no need to do anything else } else if err != nil { diff --git a/services/wiki/wiki_test.go b/services/wiki/wiki_test.go index e8b89f5e97..288d258279 100644 --- a/services/wiki/wiki_test.go +++ b/services/wiki/wiki_test.go @@ -166,7 +166,7 @@ func TestRepository_AddWikiPage(t *testing.T) { webPath := UserTitleToWebPath("", userTitle) assert.NoError(t, AddWikiPage(git.DefaultContext, doer, repo, webPath, wikiContent, commitMsg)) // Now need to show that the page has been added: - gitRepo, err := gitrepo.OpenWikiRepository(git.DefaultContext, repo) + gitRepo, err := gitrepo.OpenRepository(git.DefaultContext, repo.WikiStorageRepo()) require.NoError(t, err) defer gitRepo.Close() @@ -213,7 +213,7 @@ func TestRepository_EditWikiPage(t *testing.T) { assert.NoError(t, EditWikiPage(git.DefaultContext, doer, repo, "Home", webPath, newWikiContent, commitMsg)) // Now need to show that the page has been added: - gitRepo, err := gitrepo.OpenWikiRepository(git.DefaultContext, repo) + gitRepo, err := gitrepo.OpenRepository(git.DefaultContext, repo.WikiStorageRepo()) assert.NoError(t, err) masterTree, err := gitRepo.GetTree(repo.DefaultWikiBranch) assert.NoError(t, err) @@ -237,7 +237,7 @@ func TestRepository_DeleteWikiPage(t *testing.T) { assert.NoError(t, DeleteWikiPage(git.DefaultContext, doer, repo, "Home")) // Now need to show that the page has been added: - gitRepo, err := gitrepo.OpenWikiRepository(git.DefaultContext, repo) + gitRepo, err := gitrepo.OpenRepository(git.DefaultContext, repo.WikiStorageRepo()) require.NoError(t, err) defer gitRepo.Close() @@ -251,7 +251,7 @@ func TestRepository_DeleteWikiPage(t *testing.T) { func TestPrepareWikiFileName(t *testing.T) { unittest.PrepareTestEnv(t) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) - gitRepo, err := gitrepo.OpenWikiRepository(git.DefaultContext, repo) + gitRepo, err := gitrepo.OpenRepository(git.DefaultContext, repo.WikiStorageRepo()) require.NoError(t, err) defer gitRepo.Close() diff --git a/templates/admin/config.tmpl b/templates/admin/config.tmpl index 29a5e1b473..88dadeb3ee 100644 --- a/templates/admin/config.tmpl +++ b/templates/admin/config.tmpl @@ -148,7 +148,7 @@ <dt>{{ctx.Locale.Tr "admin.config.enable_openid_signin"}}</dt> <dd>{{svg (Iif .Service.EnableOpenIDSignIn "octicon-check" "octicon-x")}}</dd> <dt>{{ctx.Locale.Tr "admin.config.require_sign_in_view"}}</dt> - <dd>{{svg (Iif .Service.RequireSignInView "octicon-check" "octicon-x")}}</dd> + <dd>{{svg (Iif .Service.RequireSignInViewStrict "octicon-check" "octicon-x")}}</dd> <dt>{{ctx.Locale.Tr "admin.config.mail_notify"}}</dt> <dd>{{svg (Iif .Service.EnableNotifyMail "octicon-check" "octicon-x")}}</dd> <dt>{{ctx.Locale.Tr "admin.config.enable_captcha"}}</dt> diff --git a/templates/base/head_navbar.tmpl b/templates/base/head_navbar.tmpl index eab6c00840..35e14d38d3 100644 --- a/templates/base/head_navbar.tmpl +++ b/templates/base/head_navbar.tmpl @@ -1,8 +1,11 @@ {{$notificationUnreadCount := 0}} {{if and .IsSigned .NotificationUnreadCount}} - {{$notificationUnreadCount = call .NotificationUnreadCount}} + {{$notificationUnreadCount = call .NotificationUnreadCount ctx}} +{{end}} +{{$activeStopwatch := NIL}} +{{if and .IsSigned EnableTimetracking .GetActiveStopwatch}} + {{$activeStopwatch = call .GetActiveStopwatch ctx}} {{end}} - <nav id="navbar" aria-label="{{ctx.Locale.Tr "aria.navbar"}}"> <div class="navbar-left"> <!-- the logo --> @@ -12,8 +15,8 @@ <!-- mobile right menu, it must be here because in mobile view, each item is a flex column, the first item is a full row column --> <div class="ui secondary menu navbar-mobile-right only-mobile"> - {{if and .IsSigned EnableTimetracking .ActiveStopwatch}} - <a id="mobile-stopwatch-icon" class="active-stopwatch item" href="{{.ActiveStopwatch.IssueLink}}" title="{{ctx.Locale.Tr "active_stopwatch"}}" data-seconds="{{.ActiveStopwatch.Seconds}}"> + {{if $activeStopwatch}} + <a id="mobile-stopwatch-icon" class="active-stopwatch item" href="{{$activeStopwatch.IssueLink}}" title="{{ctx.Locale.Tr "active_stopwatch"}}" data-seconds="{{$activeStopwatch.Seconds}}"> <div class="tw-relative"> {{svg "octicon-stopwatch"}} <span class="header-stopwatch-dot"></span> @@ -82,8 +85,8 @@ </div><!-- end content avatar menu --> </div><!-- end dropdown avatar menu --> {{else if .IsSigned}} - {{if and EnableTimetracking .ActiveStopwatch}} - <a class="item not-mobile active-stopwatch" href="{{.ActiveStopwatch.IssueLink}}" title="{{ctx.Locale.Tr "active_stopwatch"}}" data-seconds="{{.ActiveStopwatch.Seconds}}"> + {{if $activeStopwatch}} + <a class="item not-mobile active-stopwatch" href="{{$activeStopwatch.IssueLink}}" title="{{ctx.Locale.Tr "active_stopwatch"}}" data-seconds="{{$activeStopwatch.Seconds}}"> <div class="tw-relative"> {{svg "octicon-stopwatch"}} <span class="header-stopwatch-dot"></span> @@ -186,15 +189,15 @@ {{end}} </div><!-- end full right menu --> - {{if and .IsSigned EnableTimetracking .ActiveStopwatch}} + {{if $activeStopwatch}} <div class="active-stopwatch-popup tippy-target"> <div class="tw-flex tw-items-center tw-gap-2 tw-p-3"> - <a class="stopwatch-link tw-flex tw-items-center tw-gap-2 muted" href="{{.ActiveStopwatch.IssueLink}}"> + <a class="stopwatch-link tw-flex tw-items-center tw-gap-2 muted" href="{{$activeStopwatch.IssueLink}}"> {{svg "octicon-issue-opened" 16}} - <span class="stopwatch-issue">{{.ActiveStopwatch.RepoSlug}}#{{.ActiveStopwatch.IssueIndex}}</span> + <span class="stopwatch-issue">{{$activeStopwatch.RepoSlug}}#{{$activeStopwatch.IssueIndex}}</span> </a> <div class="tw-flex tw-gap-1"> - <form class="stopwatch-commit form-fetch-action" method="post" action="{{.ActiveStopwatch.IssueLink}}/times/stopwatch/toggle"> + <form class="stopwatch-commit form-fetch-action" method="post" action="{{$activeStopwatch.IssueLink}}/times/stopwatch/toggle"> {{.CsrfTokenHtml}} <button type="submit" @@ -202,7 +205,7 @@ data-tooltip-content="{{ctx.Locale.Tr "repo.issues.stop_tracking"}}" >{{svg "octicon-square-fill"}}</button> </form> - <form class="stopwatch-cancel form-fetch-action" method="post" action="{{.ActiveStopwatch.IssueLink}}/times/stopwatch/cancel"> + <form class="stopwatch-cancel form-fetch-action" method="post" action="{{$activeStopwatch.IssueLink}}/times/stopwatch/cancel"> {{.CsrfTokenHtml}} <button type="submit" diff --git a/templates/base/paginate.tmpl b/templates/base/paginate.tmpl index 253892c009..f6c1785ccf 100644 --- a/templates/base/paginate.tmpl +++ b/templates/base/paginate.tmpl @@ -2,32 +2,42 @@ {{$paginationLink := $.Link}} {{if eq $paginationLink AppSubUrl}}{{$paginationLink = print $paginationLink "/"}}{{end}} {{with .Page.Paginater}} - {{if gt .TotalPages 1}} + {{if or (eq .TotalPages -1) (gt .TotalPages 1)}} + {{$showFirstLast := gt .TotalPages 1}} <div class="center page buttons"> <div class="ui borderless pagination menu"> + {{if $showFirstLast}} <a class="{{if .IsFirst}}disabled{{end}} item navigation" {{if not .IsFirst}}href="{{$paginationLink}}{{if $paginationParams}}?{{$paginationParams}}{{end}}"{{end}}> {{svg "gitea-double-chevron-left" 16 "tw-mr-1"}} <span class="navigation_label">{{ctx.Locale.Tr "admin.first_page"}}</span> </a> + {{end}} + <a class="{{if not .HasPrevious}}disabled{{end}} item navigation" {{if .HasPrevious}}href="{{$paginationLink}}?page={{.Previous}}{{if $paginationParams}}&{{$paginationParams}}{{end}}"{{end}}> {{svg "octicon-chevron-left" 16 "tw-mr-1"}} <span class="navigation_label">{{ctx.Locale.Tr "repo.issues.previous"}}</span> </a> - {{range .Pages}} + {{$pages := .Pages}} + {{$pagesLen := len $pages}} + {{range $pages}} {{if eq .Num -1}} <a class="disabled item">...</a> {{else}} - <a class="{{if .IsCurrent}}active {{end}}item" {{if not .IsCurrent}}href="{{$paginationLink}}?page={{.Num}}{{if $paginationParams}}&{{$paginationParams}}{{end}}"{{end}}>{{.Num}}</a> + {{/* do not highlight the current page if there is only one page */}} + <a class="{{if and .IsCurrent (gt $pagesLen 1)}}active {{end}}item" {{if not .IsCurrent}}href="{{$paginationLink}}?page={{.Num}}{{if $paginationParams}}&{{$paginationParams}}{{end}}"{{end}}>{{.Num}}</a> {{end}} {{end}} <a class="{{if not .HasNext}}disabled{{end}} item navigation" {{if .HasNext}}href="{{$paginationLink}}?page={{.Next}}{{if $paginationParams}}&{{$paginationParams}}{{end}}"{{end}}> <span class="navigation_label">{{ctx.Locale.Tr "repo.issues.next"}}</span> {{svg "octicon-chevron-right" 16 "tw-ml-1"}} </a> + + {{if $showFirstLast}} <a class="{{if .IsLast}}disabled{{end}} item navigation" {{if not .IsLast}}href="{{$paginationLink}}?page={{.TotalPages}}{{if $paginationParams}}&{{$paginationParams}}{{end}}"{{end}}> <span class="navigation_label">{{ctx.Locale.Tr "admin.last_page"}}</span> {{svg "gitea-double-chevron-right" 16 "tw-ml-1"}} </a> + {{end}} </div> </div> {{end}} diff --git a/templates/devtest/badge-actions-svg.tmpl b/templates/devtest/badge-actions-svg.tmpl new file mode 100644 index 0000000000..8125793bb3 --- /dev/null +++ b/templates/devtest/badge-actions-svg.tmpl @@ -0,0 +1,18 @@ +{{template "devtest/devtest-header"}} +<div class="page-content devtest ui container"> + <div> + <h1>Actions SVG</h1> + <form class="tw-my-3"> + {{range $fontName := .BadgeFontFamilyNames}} + <label><input name="font" type="radio" value="{{$fontName}}" {{Iif (eq $.SelectedFontFamilyName $fontName) "checked"}}>{{$fontName}}</label> + {{end}} + <button>submit</button> + </form> + <div class="flex-text-block tw-flex-wrap"> + {{range $badgeSVG := .BadgeSVGs}} + <div>{{$badgeSVG}}</div> + {{end}} + </div> + </div> +</div> +{{template "devtest/devtest-footer"}} diff --git a/templates/devtest/commit-sign-badge.tmpl b/templates/devtest/badge-commit-sign.tmpl index a6677c4495..a6677c4495 100644 --- a/templates/devtest/commit-sign-badge.tmpl +++ b/templates/devtest/badge-commit-sign.tmpl diff --git a/templates/projects/view.tmpl b/templates/projects/view.tmpl index 3cbd651059..79925e69cc 100644 --- a/templates/projects/view.tmpl +++ b/templates/projects/view.tmpl @@ -1,25 +1,22 @@ {{$canWriteProject := and .CanWriteProjects (or (not .Repository) (not .Repository.IsArchived))}} <div class="ui container tw-max-w-full"> - <div class="tw-flex tw-justify-between tw-items-center tw-mb-4 tw-gap-3"> - <h2 class="tw-mb-0 tw-flex-1 tw-break-anywhere">{{.Project.Title}}</h2> - <div class="project-toolbar-right"> - <div class="ui secondary filter menu labels"> - {{$queryLink := QueryBuild "?" "labels" .SelectLabels "assignee" $.AssigneeID "archived_labels" (Iif $.ShowArchivedLabels "true")}} - - {{template "repo/issue/filter_item_label" dict "Labels" .Labels "QueryLink" $queryLink "SupportArchivedLabel" true}} - - {{template "repo/issue/filter_item_user_assign" dict - "QueryParamKey" "assignee" - "QueryLink" $queryLink - "UserSearchList" $.Assignees - "SelectedUserId" $.AssigneeID - "TextFilterTitle" (ctx.Locale.Tr "repo.issues.filter_assignee") - "TextZeroValue" (ctx.Locale.Tr "repo.issues.filter_assginee_no_select") - "TextNegativeOne" (ctx.Locale.Tr "repo.issues.filter_assginee_no_assignee") - }} - </div> - </div> + <div class="flex-text-block tw-flex-wrap tw-mb-4"> + <h2 class="tw-mb-0">{{.Project.Title}}</h2> + <div class="tw-flex-1"></div> + <div class="ui secondary menu tw-m-0"> + {{$queryLink := QueryBuild "?" "labels" .SelectLabels "assignee" $.AssigneeID "archived_labels" (Iif $.ShowArchivedLabels "true")}} + {{template "repo/issue/filter_item_label" dict "Labels" .Labels "QueryLink" $queryLink "SupportArchivedLabel" true}} + {{template "repo/issue/filter_item_user_assign" dict + "QueryParamKey" "assignee" + "QueryLink" $queryLink + "UserSearchList" $.Assignees + "SelectedUserId" $.AssigneeID + "TextFilterTitle" (ctx.Locale.Tr "repo.issues.filter_assignee") + "TextFilterMatchNone" (ctx.Locale.Tr "repo.issues.filter_assginee_no_assignee") + "TextFilterMatchAny" (ctx.Locale.Tr "repo.issues.filter_assignee_any_assignee") + }} + </div> {{if $canWriteProject}} <div class="ui compact mini menu"> <a class="item" href="{{.Link}}/edit?redirect=project"> diff --git a/templates/repo/branch/list.tmpl b/templates/repo/branch/list.tmpl index f4f3c2e5c5..c0accf16fa 100644 --- a/templates/repo/branch/list.tmpl +++ b/templates/repo/branch/list.tmpl @@ -128,13 +128,13 @@ {{svg "octicon-git-pull-request"}} {{ctx.Locale.Tr "repo.branch.included"}} </span> {{else if and (not .DBBranch.IsDeleted) $.AllowsPulls (gt .CommitsAhead 0)}} - <a href="{{$.RepoLink}}/compare/{{PathEscapeSegments $.DefaultBranchBranch.DBBranch.Name}}...{{if ne $.Repository.Owner.Name $.Owner.Name}}{{PathEscape $.Owner.Name}}:{{end}}{{PathEscapeSegments .DBBranch.Name}}"> + <a href="{{$.RepoLink}}/compare/{{PathEscapeSegments $.DefaultBranchBranch.DBBranch.Name}}...{{if ne $.Repository.Owner.Name $.Owner.Name}}{{PathEscape $.Owner.Name}}:{{end}}{{PathEscapeSegments .DBBranch.Name}}?expand=1"> <button id="new-pull-request" class="ui compact basic button tw-mr-0">{{if $.CanPull}}{{ctx.Locale.Tr "repo.pulls.compare_changes"}}{{else}}{{ctx.Locale.Tr "action.compare_branch"}}{{end}}</button> </a> {{end}} {{else if and .LatestPullRequest.HasMerged .MergeMovedOn}} {{if and (not .DBBranch.IsDeleted) $.AllowsPulls (gt .CommitsAhead 0)}} - <a href="{{$.RepoLink}}/compare/{{PathEscapeSegments $.DefaultBranchBranch.DBBranch.Name}}...{{if ne $.Repository.Owner.Name $.Owner.Name}}{{PathEscape $.Owner.Name}}:{{end}}{{PathEscapeSegments .DBBranch.Name}}"> + <a href="{{$.RepoLink}}/compare/{{PathEscapeSegments $.DefaultBranchBranch.DBBranch.Name}}...{{if ne $.Repository.Owner.Name $.Owner.Name}}{{PathEscape $.Owner.Name}}:{{end}}{{PathEscapeSegments .DBBranch.Name}}?expand=1"> <button id="new-pull-request" class="ui compact basic button tw-mr-0">{{if $.CanPull}}{{ctx.Locale.Tr "repo.pulls.compare_changes"}}{{else}}{{ctx.Locale.Tr "action.compare_branch"}}{{end}}</button> </a> {{end}} diff --git a/templates/repo/code/recently_pushed_new_branches.tmpl b/templates/repo/code/recently_pushed_new_branches.tmpl index f0edf6065b..4a864ba756 100644 --- a/templates/repo/code/recently_pushed_new_branches.tmpl +++ b/templates/repo/code/recently_pushed_new_branches.tmpl @@ -5,7 +5,7 @@ {{$branchLink := HTMLFormat `<a href="%s">%s</a>` .BranchLink .BranchDisplayName}} {{ctx.Locale.Tr "repo.pulls.recently_pushed_new_branches" $branchLink $timeSince}} </div> - <a role="button" class="ui compact green button tw-m-0" href="{{.BranchCompareURL}}"> + <a role="button" class="ui compact green button tw-m-0" href="{{QueryBuild .BranchCompareURL "expand" 1}}"> {{ctx.Locale.Tr "repo.pulls.compare_changes"}} </a> </div> diff --git a/templates/repo/create.tmpl b/templates/repo/create.tmpl index ad308c857c..a90c26b423 100644 --- a/templates/repo/create.tmpl +++ b/templates/repo/create.tmpl @@ -7,25 +7,21 @@ <div class="ui attached segment"> {{template "base/alert" .}} {{template "repo/create_helper" .}} - - {{if not .CanCreateRepo}} - <div class="ui negative message"> - <p>{{ctx.Locale.TrN .MaxCreationLimit "repo.form.reach_limit_of_creation_1" "repo.form.reach_limit_of_creation_n" .MaxCreationLimit}}</p> - </div> - {{end}} <form class="ui form left-right-form new-repo-form" action="{{.Link}}" method="post"> {{.CsrfTokenHtml}} + <div id="create-repo-error-message" class="ui negative message tw-text-center tw-hidden"></div> <div class="inline required field {{if .Err_Owner}}error{{end}}"> <label>{{ctx.Locale.Tr "repo.owner"}}</label> - <div class="ui selection owner dropdown"> - <input type="hidden" id="uid" name="uid" value="{{.ContextUser.ID}}" required> - <span class="text truncated-item-container" title="{{.ContextUser.Name}}"> - {{ctx.AvatarUtils.Avatar .ContextUser 28 "mini"}} - <span class="truncated-item-name">{{.ContextUser.ShortName 40}}</span> - </span> + <div class="ui selection dropdown" id="repo_owner_dropdown"> + <input type="hidden" name="uid" value="{{.ContextUser.ID}}"> + <span class="text truncated-item-name"></span> {{svg "octicon-triangle-down" 14 "dropdown icon"}} <div class="menu"> - <div class="item truncated-item-container" data-value="{{.SignedUser.ID}}" title="{{.SignedUser.Name}}"> + <div class="item truncated-item-container" data-value="{{.SignedUser.ID}}" title="{{.SignedUser.Name}}" + {{if not .CanCreateRepoInDoer}} + data-create-repo-disallowed-prompt="{{ctx.Locale.TrN .MaxCreationLimit "repo.form.reach_limit_of_creation_1" "repo.form.reach_limit_of_creation_n" .MaxCreationLimitOfDoer}}" + {{end}} + > {{ctx.AvatarUtils.Avatar .SignedUser 28 "mini"}} <span class="truncated-item-name">{{.SignedUser.ShortName 40}}</span> </div> @@ -212,7 +208,7 @@ <br> <div class="inline field"> <label></label> - <button class="ui primary button{{if not .CanCreateRepo}} disabled{{end}}"> + <button class="ui primary button"> {{ctx.Locale.Tr "repo.create_repo"}} </button> </div> diff --git a/templates/repo/diff/compare.tmpl b/templates/repo/diff/compare.tmpl index 9a7a04a328..05cfffd2b7 100644 --- a/templates/repo/diff/compare.tmpl +++ b/templates/repo/diff/compare.tmpl @@ -205,10 +205,10 @@ {{end}} </div> {{else if $allowCreatePR}} - <div class="ui info message pullrequest-form-toggle {{if .Flash}}tw-hidden{{end}}"> + <div class="ui info message pullrequest-form-toggle {{if .ExpandNewPrForm}}tw-hidden{{end}}"> <button class="ui button primary show-panel toggle" data-panel=".pullrequest-form-toggle, .pullrequest-form">{{ctx.Locale.Tr "repo.pulls.new"}}</button> </div> - <div class="pullrequest-form {{if not .Flash}}tw-hidden{{end}}"> + <div class="pullrequest-form {{if not .ExpandNewPrForm}}tw-hidden{{end}}"> {{template "repo/issue/new_form" .}} </div> {{end}} diff --git a/templates/repo/header.tmpl b/templates/repo/header.tmpl index 8c2a0da8d0..c7c53b4f5d 100644 --- a/templates/repo/header.tmpl +++ b/templates/repo/header.tmpl @@ -25,6 +25,9 @@ <div class="repo-icon only-mobile" data-tooltip-content="{{ctx.Locale.Tr "repo.desc.internal"}}">{{svg "octicon-shield-lock" 18}}</div> {{end}} {{end}} + {{if $.Permission.HasAnyUnitPublicAccess}} + <span class="ui basic orange label">{{ctx.Locale.Tr "repo.desc.public_access"}}</span> + {{end}} {{if .IsTemplate}} <span class="ui basic label not-mobile">{{ctx.Locale.Tr "repo.desc.template"}}</span> <div class="repo-icon only-mobile" data-tooltip-content="{{ctx.Locale.Tr "repo.desc.template"}}">{{svg "octicon-repo-template" 18}}</div> @@ -208,7 +211,7 @@ </a> {{end}} - {{if and (.Permission.CanReadAny ctx.Consts.RepoUnitTypePullRequests ctx.Consts.RepoUnitTypeIssues ctx.Consts.RepoUnitTypeReleases) (not .IsEmptyRepo)}} + {{if and (.Permission.CanReadAny ctx.Consts.RepoUnitTypePullRequests ctx.Consts.RepoUnitTypeIssues ctx.Consts.RepoUnitTypeReleases ctx.Consts.RepoUnitTypeCode) (not .IsEmptyRepo)}} <a class="{{if .PageIsActivity}}active {{end}}item" href="{{.RepoLink}}/activity"> {{svg "octicon-pulse"}} {{ctx.Locale.Tr "repo.activity"}} </a> diff --git a/templates/repo/icon.tmpl b/templates/repo/icon.tmpl index e5e0bd68e7..3747d3a6f5 100644 --- a/templates/repo/icon.tmpl +++ b/templates/repo/icon.tmpl @@ -1,6 +1,6 @@ {{$avatarLink := (.RelAvatarLink ctx)}} {{if $avatarLink}} - <img class="ui avatar tw-align-middle" src="{{$avatarLink}}" width="24" height="24" alt="{{.FullName}}"> + <img class="ui avatar tw-align-middle" src="{{$avatarLink}}" width="24" height="24" alt="" aria-hidden="true"> {{else if $.IsMirror}} {{svg "octicon-mirror" 24}} {{else if $.IsFork}} diff --git a/templates/repo/issue/filter_item_user_assign.tmpl b/templates/repo/issue/filter_item_user_assign.tmpl index 4f1db71d57..42886edaa0 100644 --- a/templates/repo/issue/filter_item_user_assign.tmpl +++ b/templates/repo/issue/filter_item_user_assign.tmpl @@ -4,8 +4,8 @@ * UserSearchList * SelectedUserId: 0 or empty means default, -1 means "no user is set" * TextFilterTitle -* TextZeroValue: the text for "all issues" -* TextNegativeOne: the text for "issues with no assignee" +* TextFilterMatchNone: the text for "issues with no assignee" +* TextFilterMatchAny: the text for "issues with any assignee" */}} {{$queryLink := .QueryLink}} <div class="item ui dropdown jump {{if not .UserSearchList}}disabled{{end}}"> @@ -15,16 +15,24 @@ <i class="icon">{{svg "octicon-search" 16}}</i> <input type="text" placeholder="{{ctx.Locale.Tr "repo.issues.filter_user_placeholder"}}"> </div> - {{if $.TextZeroValue}} - <a class="item {{if not .SelectedUserId}}selected{{end}}" href="{{QueryBuild $queryLink $.QueryParamKey NIL}}">{{$.TextZeroValue}}</a> + {{if $.TextFilterMatchNone}} + {{$isSelected := eq .SelectedUserId "(none)"}} + <a class="item" href="{{QueryBuild $queryLink $.QueryParamKey (Iif $isSelected NIL "(none)")}}"> + {{svg "octicon-check" 14 (Iif $isSelected "" "tw-invisible")}} {{$.TextFilterMatchNone}} + </a> {{end}} - {{if $.TextNegativeOne}} - <a class="item {{if eq .SelectedUserId -1}}selected{{end}}" href="{{QueryBuild $queryLink $.QueryParamKey -1}}">{{$.TextNegativeOne}}</a> + {{if $.TextFilterMatchAny}} + {{$isSelected := eq .SelectedUserId "(any)"}} + <a class="item" href="{{QueryBuild $queryLink $.QueryParamKey (Iif $isSelected NIL "(any)")}}"> + {{svg "octicon-check" 14 (Iif $isSelected "" "tw-invisible")}} {{$.TextFilterMatchAny}} + </a> {{end}} <div class="divider"></div> - {{range .UserSearchList}} - <a class="item {{if eq $.SelectedUserId .ID}}selected{{end}}" href="{{QueryBuild $queryLink $.QueryParamKey .ID}}"> - {{ctx.AvatarUtils.Avatar . 20}}{{template "repo/search_name" .}} + {{range $user := .UserSearchList}} + {{$isSelected := eq $.SelectedUserId (print $user.ID)}} + <a class="item" href="{{QueryBuild $queryLink $.QueryParamKey (Iif $isSelected NIL $user.ID)}}"> + {{svg "octicon-check" 14 (Iif $isSelected "" "tw-invisible")}} + {{ctx.AvatarUtils.Avatar $user 20}}{{template "repo/search_name" .}} </a> {{end}} </div> diff --git a/templates/repo/issue/filter_list.tmpl b/templates/repo/issue/filter_list.tmpl index 7612d93b21..58ca4a7c00 100644 --- a/templates/repo/issue/filter_list.tmpl +++ b/templates/repo/issue/filter_list.tmpl @@ -94,8 +94,8 @@ "UserSearchList" $.Assignees "SelectedUserId" $.AssigneeID "TextFilterTitle" (ctx.Locale.Tr "repo.issues.filter_assignee") - "TextZeroValue" (ctx.Locale.Tr "repo.issues.filter_assginee_no_select") - "TextNegativeOne" (ctx.Locale.Tr "repo.issues.filter_assginee_no_assignee") + "TextFilterMatchNone" (ctx.Locale.Tr "repo.issues.filter_assginee_no_assignee") + "TextFilterMatchAny" (ctx.Locale.Tr "repo.issues.filter_assignee_any_assignee") }} {{if .IsSigned}} diff --git a/templates/repo/issue/list.tmpl b/templates/repo/issue/list.tmpl index 53d0eca171..0ab761e038 100644 --- a/templates/repo/issue/list.tmpl +++ b/templates/repo/issue/list.tmpl @@ -14,9 +14,10 @@ </div> {{end}} - <div class="list-header"> - {{template "repo/issue/navbar" .}} + <div class="list-header flex-text-block"> {{template "repo/issue/search" .}} + <a class="ui small button" href="{{.RepoLink}}/labels">{{ctx.Locale.Tr "repo.labels"}}</a> + <a class="ui small button" href="{{.RepoLink}}/milestones">{{ctx.Locale.Tr "repo.milestones"}}</a> {{if not .Repository.IsArchived}} {{if .PageIsIssueList}} <a class="ui small primary button issue-list-new" href="{{.RepoLink}}/issues/new{{if .NewIssueChooseTemplate}}/choose{{end}}">{{ctx.Locale.Tr "repo.issues.new"}}</a> diff --git a/templates/repo/navbar.tmpl b/templates/repo/navbar.tmpl index b2471dc17e..e004c5254b 100644 --- a/templates/repo/navbar.tmpl +++ b/templates/repo/navbar.tmpl @@ -1,14 +1,19 @@ +{{$canReadCode := $.Permission.CanRead ctx.Consts.RepoUnitTypeCode}} + <div class="ui fluid vertical menu"> + {{/* the default activity page "pulse" could work with any permission: code, issue, pr, release*/}} <a class="{{if .PageIsPulse}}active {{end}}item" href="{{.RepoLink}}/activity"> {{ctx.Locale.Tr "repo.activity.navbar.pulse"}} </a> - <a class="{{if .PageIsContributors}}active {{end}}item" href="{{.RepoLink}}/activity/contributors"> - {{ctx.Locale.Tr "repo.activity.navbar.contributors"}} - </a> - <a class="{{if .PageIsCodeFrequency}}active{{end}} item" href="{{.RepoLink}}/activity/code-frequency"> - {{ctx.Locale.Tr "repo.activity.navbar.code_frequency"}} - </a> - <a class="{{if .PageIsRecentCommits}}active{{end}} item" href="{{.RepoLink}}/activity/recent-commits"> - {{ctx.Locale.Tr "repo.activity.navbar.recent_commits"}} - </a> + {{if $canReadCode}} + <a class="{{if .PageIsContributors}}active {{end}}item" href="{{.RepoLink}}/activity/contributors"> + {{ctx.Locale.Tr "repo.activity.navbar.contributors"}} + </a> + <a class="{{if .PageIsCodeFrequency}}active{{end}} item" href="{{.RepoLink}}/activity/code-frequency"> + {{ctx.Locale.Tr "repo.activity.navbar.code_frequency"}} + </a> + <a class="{{if .PageIsRecentCommits}}active{{end}} item" href="{{.RepoLink}}/activity/recent-commits"> + {{ctx.Locale.Tr "repo.activity.navbar.recent_commits"}} + </a> + {{end}} </div> diff --git a/templates/repo/projects/view.tmpl b/templates/repo/projects/view.tmpl index 05ad7264bf..7267a99b1d 100644 --- a/templates/repo/projects/view.tmpl +++ b/templates/repo/projects/view.tmpl @@ -2,8 +2,9 @@ <div role="main" aria-label="{{.Title}}" class="page-content repository projects view-project"> {{template "repo/header" .}} <div class="ui container padded"> - <div class="tw-flex tw-justify-between tw-items-center tw-mb-4"> - {{template "repo/issue/navbar" .}} + <div class="flex-text-block tw-justify-end tw-mb-4"> + <a class="ui small button" href="{{.RepoLink}}/labels">{{ctx.Locale.Tr "repo.labels"}}</a> + <a class="ui small button" href="{{.RepoLink}}/milestones">{{ctx.Locale.Tr "repo.milestones"}}</a> <a class="ui small primary button" href="{{.RepoLink}}/issues/new/choose?project={{.Project.ID}}">{{ctx.Locale.Tr "repo.issues.new"}}</a> </div> </div> diff --git a/templates/repo/settings/navbar.tmpl b/templates/repo/settings/navbar.tmpl index 3e127ccbb3..3dd86d1f6a 100644 --- a/templates/repo/settings/navbar.tmpl +++ b/templates/repo/settings/navbar.tmpl @@ -4,6 +4,11 @@ <a class="{{if .PageIsSettingsOptions}}active {{end}}item" href="{{.RepoLink}}/settings"> {{ctx.Locale.Tr "repo.settings.options"}} </a> + {{if or .Repository.IsPrivate .Permission.HasAnyUnitPublicAccess}} + <a class="{{if .PageIsSettingsPublicAccess}}active {{end}}item" href="{{.RepoLink}}/settings/public_access"> + {{ctx.Locale.Tr "repo.settings.public_access"}} + </a> + {{end}} <a class="{{if .PageIsSettingsCollaboration}}active {{end}}item" href="{{.RepoLink}}/settings/collaboration"> {{ctx.Locale.Tr "repo.settings.collaboration"}} </a> diff --git a/templates/repo/settings/options.tmpl b/templates/repo/settings/options.tmpl index aade734a1d..202be3fce7 100644 --- a/templates/repo/settings/options.tmpl +++ b/templates/repo/settings/options.tmpl @@ -310,15 +310,6 @@ <input class="enable-system" name="enable_code" type="checkbox"{{if $isCodeEnabled}} checked{{end}}> <label>{{ctx.Locale.Tr "repo.code.desc"}}</label> </div> - <div class="inline field tw-pl-4"> - {{$unitCode := .Repository.MustGetUnit ctx ctx.Consts.RepoUnitTypeCode}} - <label>{{ctx.Locale.Tr "repo.settings.default_permission_everyone_access"}}</label> - <select name="default_code_everyone_access" class="ui selection dropdown"> - {{/* everyone access mode is different from others, none means it is unset and won't be applied */}} - <option value="none" {{Iif (eq $unitCode.EveryoneAccessMode 0) "selected"}}>{{ctx.Locale.Tr "settings.permission_not_set"}}</option> - <option value="read" {{Iif (eq $unitCode.EveryoneAccessMode 1) "selected"}}>{{ctx.Locale.Tr "settings.permission_read"}}</option> - </select> - </div> </div> {{$isInternalWikiEnabled := .Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypeWiki}} @@ -346,16 +337,6 @@ <label>{{ctx.Locale.Tr "repo.settings.default_wiki_branch_name"}}</label> <input name="default_wiki_branch" value="{{.Repository.DefaultWikiBranch}}"> </div> - <div class="inline field"> - {{$unitInternalWiki := .Repository.MustGetUnit ctx ctx.Consts.RepoUnitTypeWiki}} - <label>{{ctx.Locale.Tr "repo.settings.default_permission_everyone_access"}}</label> - <select name="default_wiki_everyone_access" class="ui selection dropdown"> - {{/* everyone access mode is different from others, none means it is unset and won't be applied */}} - <option value="none" {{Iif (eq $unitInternalWiki.EveryoneAccessMode 0) "selected"}}>{{ctx.Locale.Tr "settings.permission_not_set"}}</option> - <option value="read" {{Iif (eq $unitInternalWiki.EveryoneAccessMode 1) "selected"}}>{{ctx.Locale.Tr "settings.permission_read"}}</option> - <option value="write" {{Iif (eq $unitInternalWiki.EveryoneAccessMode 2) "selected"}}>{{ctx.Locale.Tr "settings.permission_write"}}</option> - </select> - </div> </div> <div class="field"> <div class="ui radio checkbox{{if $isExternalWikiGlobalDisabled}} disabled{{end}}"{{if $isExternalWikiGlobalDisabled}} data-tooltip-content="{{ctx.Locale.Tr "repo.unit_disabled"}}"{{end}}> @@ -391,15 +372,6 @@ </div> </div> <div class="field tw-pl-4 {{if (.Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypeExternalTracker)}}disabled{{end}}" id="internal_issue_box"> - <div class="inline field"> - {{$unitIssue := .Repository.MustGetUnit ctx ctx.Consts.RepoUnitTypeIssues}} - <label>{{ctx.Locale.Tr "repo.settings.default_permission_everyone_access"}}</label> - <select name="default_issues_everyone_access" class="ui selection dropdown"> - {{/* everyone access mode is different from others, none means it is unset and won't be applied */}} - <option value="none" {{Iif (eq $unitIssue.EveryoneAccessMode 0) "selected"}}>{{ctx.Locale.Tr "settings.permission_not_set"}}</option> - <option value="read" {{Iif (eq $unitIssue.EveryoneAccessMode 1) "selected"}}>{{ctx.Locale.Tr "settings.permission_read"}}</option> - </select> - </div> {{if .Repository.CanEnableTimetracker}} <div class="field"> <div class="ui checkbox"> diff --git a/templates/repo/settings/public_access.tmpl b/templates/repo/settings/public_access.tmpl new file mode 100644 index 0000000000..c1c198bcce --- /dev/null +++ b/templates/repo/settings/public_access.tmpl @@ -0,0 +1,54 @@ +{{template "repo/settings/layout_head" (dict "ctxData" . "pageClass" "repository settings")}} +<div class="repo-setting-content"> + <h4 class="ui top attached header"> + {{ctx.Locale.Tr "repo.settings.public_access"}} + </h4> + <div class="ui attached segment"> + <p> + {{ctx.Locale.Tr "repo.settings.public_access_desc"}} + </p> + {{$paNotSet := "not-set"}} + {{$paAnonymousRead := "anonymous-read"}} + {{$paEveryoneRead := "everyone-read"}} + {{$paEveryoneWrite := "everyone-write"}} + <form class="ui form" method="post"> + {{.CsrfTokenHtml}} + <table class="ui table unstackable tw-my-2"> + <thead> + <tr> + <th>{{ctx.Locale.Tr "units.unit"}}</th> + <th class="tw-text-center">{{ctx.Locale.Tr "settings.permission_not_set"}}</th> + <th class="tw-text-center">{{ctx.Locale.Tr "settings.permission_anonymous_read"}}</th> + <th class="tw-text-center">{{ctx.Locale.Tr "settings.permission_everyone_read"}}</th> + <th class="tw-text-center">{{ctx.Locale.Tr "settings.permission_everyone_write"}}</th> + </tr> + </thead> + <tbody> + {{range $ua := .RepoUnitPublicAccesses}} + <tr> + <td>{{$ua.DisplayName}}</td> + <td class="tw-text-center"><label><input type="radio" name="{{$ua.FormKey}}" value="{{$paNotSet}}" {{Iif (eq $paNotSet $ua.UnitPublicAccess) "checked"}}></label></td> + <td class="tw-text-center"><label><input type="radio" name="{{$ua.FormKey}}" value="{{$paAnonymousRead}}" {{Iif (eq $paAnonymousRead $ua.UnitPublicAccess) "checked"}}></label></td> + <td class="tw-text-center"><label><input type="radio" name="{{$ua.FormKey}}" value="{{$paEveryoneRead}}" {{Iif (eq $paEveryoneRead $ua.UnitPublicAccess) "checked"}}></label></td> + <td class="tw-text-center"> + {{if SliceUtils.Contains $ua.PublicAccessTypes $paEveryoneWrite}} + <label><input type="radio" name="{{$ua.FormKey}}" value="{{$paEveryoneWrite}}" {{Iif (eq $paEveryoneWrite $ua.UnitPublicAccess) "checked"}}></label> + {{else}} + - + {{end}} + </td> + </tr> + {{end}} + </tbody> + </table> + <ul class="tw-my-3 tw-pl-5 tw-flex tw-flex-col tw-gap-3"> + <li>{{ctx.Locale.Tr "repo.settings.public_access.docs.not_set"}}</li> + <li>{{ctx.Locale.Tr "repo.settings.public_access.docs.anonymous_read"}}</li> + <li>{{ctx.Locale.Tr "repo.settings.public_access.docs.everyone_read"}}</li> + <li>{{ctx.Locale.Tr "repo.settings.public_access.docs.everyone_write"}}</li> + </ul> + <button class="ui primary button {{if .GlobalForcePrivate}}disabled{{end}}">{{ctx.Locale.Tr "repo.settings.update_settings"}}</button> + </form> + </div> +</div> +{{template "repo/settings/layout_footer" .}} diff --git a/templates/repo/view_content.tmpl b/templates/repo/view_content.tmpl index 06e9f8515c..292a2f878c 100644 --- a/templates/repo/view_content.tmpl +++ b/templates/repo/view_content.tmpl @@ -30,7 +30,7 @@ {{end}} {{$cmpBranch = print $cmpBranch (.BranchName|PathEscapeSegments)}} {{$compareLink := printf "%s/compare/%s...%s" .BaseRepo.Link (.BaseRepo.DefaultBranch|PathEscapeSegments) $cmpBranch}} - <a id="new-pull-request" role="button" class="ui compact basic button" href="{{$compareLink}}" + <a id="new-pull-request" role="button" class="ui compact basic button" href="{{QueryBuild $compareLink "expand" 1}}" data-tooltip-content="{{if .PullRequestCtx.Allowed}}{{ctx.Locale.Tr "repo.pulls.compare_changes"}}{{else}}{{ctx.Locale.Tr "action.compare_branch"}}{{end}}"> {{svg "octicon-git-pull-request"}} </a> diff --git a/templates/repo/view_list.tmpl b/templates/repo/view_list.tmpl index b4d27fb1e3..4745110dd2 100644 --- a/templates/repo/view_list.tmpl +++ b/templates/repo/view_list.tmpl @@ -14,19 +14,21 @@ {{$entry := $item.Entry}} {{$commit := $item.Commit}} {{$submoduleFile := $item.SubmoduleFile}} - <div class="repo-file-cell name {{if not $commit}}notready{{end}}"> + <div class="repo-file-cell name muted-links {{if not $commit}}notready{{end}}"> {{ctx.RenderUtils.RenderFileIcon $entry}} {{if $entry.IsSubModule}} {{$submoduleLink := $submoduleFile.SubmoduleWebLink ctx}} {{if $submoduleLink}} - <a class="muted" href="{{$submoduleLink.RepoWebLink}}">{{$entry.Name}}</a> <span class="at">@</span> <a href="{{$submoduleLink.CommitWebLink}}">{{ShortSha $submoduleFile.RefID}}</a> + <a class="entry-name" href="{{$submoduleLink.RepoWebLink}}" title="{{$entry.Name}}">{{$entry.Name}}</a> + @ <a class="text primary" href="{{$submoduleLink.CommitWebLink}}">{{ShortSha $submoduleFile.RefID}}</a> {{else}} - {{$entry.Name}} <span class="at">@</span> {{ShortSha $submoduleFile.RefID}} + <span class="entry-name" title="{{$entry.Name}}">{{$entry.Name}}</span> + @ {{ShortSha $submoduleFile.RefID}} {{end}} {{else}} {{if $entry.IsDir}} {{$subJumpablePathName := $entry.GetSubJumpablePathName}} - <a class="muted" href="{{$.TreeLink}}/{{PathEscapeSegments $subJumpablePathName}}" title="{{$subJumpablePathName}}"> + <a class="entry-name" href="{{$.TreeLink}}/{{PathEscapeSegments $subJumpablePathName}}" title="{{$subJumpablePathName}}"> {{$subJumpablePathFields := StringUtils.Split $subJumpablePathName "/"}} {{$subJumpablePathFieldLast := (Eval (len $subJumpablePathFields) "-" 1)}} {{if eq $subJumpablePathFieldLast 0}} @@ -37,7 +39,7 @@ {{end}} </a> {{else}} - <a class="muted" href="{{$.TreeLink}}/{{PathEscapeSegments $entry.Name}}" title="{{$entry.Name}}">{{$entry.Name}}</a> + <a class="entry-name" href="{{$.TreeLink}}/{{PathEscapeSegments $entry.Name}}" title="{{$entry.Name}}">{{$entry.Name}}</a> {{end}} {{end}} </div> diff --git a/templates/shared/actions/runner_badge.tmpl b/templates/shared/actions/runner_badge.tmpl index 816e87e177..1ba9be09fb 100644 --- a/templates/shared/actions/runner_badge.tmpl +++ b/templates/shared/actions/runner_badge.tmpl @@ -1,25 +1,27 @@ -<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="{{.Badge.Width}}" height="18" +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="{{.Badge.Width}}" height="20" role="img" aria-label="{{.Badge.Label.Text}}: {{.Badge.Message.Text}}"> <title>{{.Badge.Label.Text}}: {{.Badge.Message.Text}}</title> - <linearGradient id="s" x2="0" y2="100%"> - <stop offset="0" stop-color="#fff" stop-opacity=".7" /> - <stop offset=".1" stop-color="#aaa" stop-opacity=".1" /> - <stop offset=".9" stop-color="#000" stop-opacity=".3" /> - <stop offset="1" stop-color="#000" stop-opacity=".5" /> + <linearGradient id="{{.Badge.IDPrefix}}s" x2="0" y2="100%"> + <stop offset="0" stop-color="#bbb" stop-opacity=".1" /> + <stop offset="1" stop-opacity=".1" /> </linearGradient> - <clipPath id="r"> - <rect width="{{.Badge.Width}}" height="18" rx="4" fill="#fff" /> + <clipPath id="{{.Badge.IDPrefix}}r"> + <rect width="{{.Badge.Width}}" height="20" rx="3" fill="#fff" /> </clipPath> - <g clip-path="url(#r)"> - <rect width="{{.Badge.Label.Width}}" height="18" fill="#555" /> - <rect x="{{.Badge.Label.Width}}" width="{{.Badge.Message.Width}}" height="18" fill="{{.Badge.Color}}" /> - <rect width="{{.Badge.Width}}" height="18" fill="url(#s)" /> + <g clip-path="url(#{{.Badge.IDPrefix}}r)"> + <rect width="{{.Badge.Label.Width}}" height="20" fill="#555" /> + <rect x="{{.Badge.Label.Width}}" width="{{.Badge.Message.Width}}" height="20" fill="{{.Badge.Color}}" /> + <rect width="{{.Badge.Width}}" height="20" fill="url(#{{.Badge.IDPrefix}}s)" /> + </g> + <g fill="#fff" text-anchor="middle" font-family="{{.Badge.FontFamily}}" + text-rendering="geometricPrecision" font-size="{{.Badge.FontSize}}"> + <text aria-hidden="true" x="{{.Badge.Label.X}}" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" + textLength="{{.Badge.Label.TextLength}}">{{.Badge.Label.Text}}</text> + <text x="{{.Badge.Label.X}}" y="140" + transform="scale(.1)" fill="#fff" textLength="{{.Badge.Label.TextLength}}">{{.Badge.Label.Text}}</text> + <text aria-hidden="true" x="{{.Badge.Message.X}}" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" + textLength="{{.Badge.Message.TextLength}}">{{.Badge.Message.Text}}</text> + <text x="{{.Badge.Message.X}}" y="140" transform="scale(.1)" fill="#fff" + textLength="{{.Badge.Message.TextLength}}">{{.Badge.Message.Text}}</text> </g> - <g fill="#fff" text-anchor="middle" font-family="Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" - font-size="{{.Badge.FontSize}}"><text aria-hidden="true" x="{{.Badge.Label.X}}" y="140" fill="#010101" fill-opacity=".3" - transform="scale(.1)" textLength="{{.Badge.Label.TextLength}}">{{.Badge.Label.Text}}</text><text x="{{.Badge.Label.X}}" y="130" - transform="scale(.1)" fill="#fff" textLength="{{.Badge.Label.TextLength}}">{{.Badge.Label.Text}}</text><text aria-hidden="true" - x="{{.Badge.Message.X}}" y="140" fill="#010101" fill-opacity=".3" transform="scale(.1)" - textLength="{{.Badge.Message.TextLength}}">{{.Badge.Message.Text}}</text><text x="{{.Badge.Message.X}}" y="130" transform="scale(.1)" - fill="#fff" textLength="{{.Badge.Message.TextLength}}">{{.Badge.Message.Text}}</text></g> </svg> diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 1efaf1a875..de7c8dc6f0 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -4187,6 +4187,52 @@ } } }, + "/repos/{owner}/{repo}/actions/jobs/{job_id}/logs": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Downloads the job logs for a workflow run", + "operationId": "downloadActionsRunJobLogs", + "parameters": [ + { + "type": "string", + "description": "name of the owner", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repository", + "name": "repo", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "id of the job", + "name": "job_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "output blob content" + }, + "400": { + "$ref": "#/responses/error" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + } + }, "/repos/{owner}/{repo}/actions/runners/registration-token": { "get": { "produces": [ diff --git a/templates/user/auth/grant.tmpl b/templates/user/auth/grant.tmpl index 7a6f156e36..e56241b0f8 100644 --- a/templates/user/auth/grant.tmpl +++ b/templates/user/auth/grant.tmpl @@ -1,35 +1,33 @@ {{template "base/head" .}} -<div role="main" aria-label="{{.Title}}" class="page-content ui one column stackable tw-text-center page grid oauth2-authorize-application-box"> - <div class="column seven wide"> - <div class="ui middle centered raised segments"> - <h3 class="ui top attached header"> - {{ctx.Locale.Tr "auth.authorize_title" .Application.Name}} - </h3> - <div class="ui attached segment"> - {{template "base/alert" .}} - <p> - {{if not .AdditionalScopes}} - <b>{{ctx.Locale.Tr "auth.authorize_application_description"}}</b><br> - {{end}} - {{ctx.Locale.Tr "auth.authorize_application_created_by" .ApplicationCreatorLinkHTML}}<br> - {{ctx.Locale.Tr "auth.authorize_application_with_scopes" (HTMLFormat "<b>%s</b>" .Scope)}} - </p> - </div> - <div class="ui attached segment"> - <p>{{ctx.Locale.Tr "auth.authorize_redirect_notice" .ApplicationRedirectDomainHTML}}</p> - </div> - <div class="ui attached segment"> - <form method="post" action="{{AppSubUrl}}/login/oauth/grant"> - {{.CsrfTokenHtml}} - <input type="hidden" name="client_id" value="{{.Application.ClientID}}"> - <input type="hidden" name="state" value="{{.State}}"> - <input type="hidden" name="scope" value="{{.Scope}}"> - <input type="hidden" name="nonce" value="{{.Nonce}}"> - <input type="hidden" name="redirect_uri" value="{{.RedirectURI}}"> - <button type="submit" id="authorize-app" name="granted" value="true" class="ui red inline button">{{ctx.Locale.Tr "auth.authorize_application"}}</button> - <button type="submit" name="granted" value="false" class="ui basic primary inline button">{{ctx.Locale.Tr "cancel"}}</button> - </form> - </div> +<div role="main" aria-label="{{.Title}}" class="page-content oauth2-authorize-application-box"> + <div class="ui container tw-max-w-[500px]"> + <h3 class="ui top attached header"> + {{ctx.Locale.Tr "auth.authorize_title" .Application.Name}} + </h3> + <div class="ui attached segment"> + {{template "base/alert" .}} + <p> + {{if not .AdditionalScopes}} + <b>{{ctx.Locale.Tr "auth.authorize_application_description"}}</b><br> + {{end}} + {{ctx.Locale.Tr "auth.authorize_application_created_by" .ApplicationCreatorLinkHTML}}<br> + {{ctx.Locale.Tr "auth.authorize_application_with_scopes" (HTMLFormat "<b>%s</b>" .Scope)}} + </p> + </div> + <div class="ui attached segment"> + <p>{{ctx.Locale.Tr "auth.authorize_redirect_notice" .ApplicationRedirectDomainHTML}}</p> + </div> + <div class="ui attached segment tw-text-center"> + <form method="post" action="{{AppSubUrl}}/login/oauth/grant"> + {{.CsrfTokenHtml}} + <input type="hidden" name="client_id" value="{{.Application.ClientID}}"> + <input type="hidden" name="state" value="{{.State}}"> + <input type="hidden" name="scope" value="{{.Scope}}"> + <input type="hidden" name="nonce" value="{{.Nonce}}"> + <input type="hidden" name="redirect_uri" value="{{.RedirectURI}}"> + <button type="submit" id="authorize-app" name="granted" value="true" class="ui red inline button">{{ctx.Locale.Tr "auth.authorize_application"}}</button> + <button type="submit" name="granted" value="false" class="ui basic primary inline button">{{ctx.Locale.Tr "cancel"}}</button> + </form> </div> </div> </div> diff --git a/templates/user/auth/grant_error.tmpl b/templates/user/auth/grant_error.tmpl index e37c4f6544..7a4521d331 100644 --- a/templates/user/auth/grant_error.tmpl +++ b/templates/user/auth/grant_error.tmpl @@ -1,15 +1,12 @@ {{template "base/head" .}} -<div role="main" aria-label="{{.Title}}" class="page-content ui one column stackable tw-text-center page grid oauth2-authorize-application-box {{if .IsRepo}}repository{{end}}"> - {{if .IsRepo}}{{template "repo/header" .}}{{end}} - <div class="column seven wide"> - <div class="ui middle centered raised segments"> - <h1 class="ui top attached header"> - {{ctx.Locale.Tr "auth.authorization_failed"}} - </h1> - <h3 class="ui attached segment">{{.Error.ErrorDescription}}</h3> - <div class="ui attached segment"> - <p>{{ctx.Locale.Tr "auth.authorization_failed_desc"}}</p> - </div> +<div role="main" aria-label="{{.Title}}" class="page-content oauth2-authorize-application-box"> + <div class="ui container tw-max-w-[500px]"> + <h1 class="ui top attached header"> + {{ctx.Locale.Tr "auth.authorization_failed"}} + </h1> + <h3 class="ui attached segment">{{.Error.ErrorDescription}}</h3> + <div class="ui attached segment"> + <p>{{ctx.Locale.Tr "auth.authorization_failed_desc"}}</p> </div> </div> </div> diff --git a/templates/user/dashboard/dashboard.tmpl b/templates/user/dashboard/dashboard.tmpl index 3ce3c1eb73..666dd78073 100644 --- a/templates/user/dashboard/dashboard.tmpl +++ b/templates/user/dashboard/dashboard.tmpl @@ -5,7 +5,7 @@ <div class="flex-container-main"> {{template "base/alert" .}} {{template "user/heatmap" .}} - {{if .Feeds}} + {{if .Page.Paginater.TotalPages}} {{template "user/dashboard/feeds" .}} {{else}} {{template "user/dashboard/guide" .}} diff --git a/templates/user/notification/notification_div.tmpl b/templates/user/notification/notification_div.tmpl index 0d2371a358..9af2cd53b3 100644 --- a/templates/user/notification/notification_div.tmpl +++ b/templates/user/notification/notification_div.tmpl @@ -1,6 +1,6 @@ <div role="main" aria-label="{{.Title}}" class="page-content user notification" id="notification_div" data-sequence-number="{{.SequenceNumber}}"> <div class="ui container"> - {{$notificationUnreadCount := call .NotificationUnreadCount}} + {{$notificationUnreadCount := call .NotificationUnreadCount ctx}} <div class="tw-flex tw-items-center tw-justify-between tw-mb-[--page-spacing]"> <div class="small-menu-items ui compact tiny menu"> <a class="{{if eq .Status 1}}active {{end}}item" href="{{AppSubUrl}}/notifications?q=unread"> diff --git a/tests/integration/actions_log_test.go b/tests/integration/actions_log_test.go index 5e99e5026d..cd20604b84 100644 --- a/tests/integration/actions_log_test.go +++ b/tests/integration/actions_log_test.go @@ -7,10 +7,12 @@ import ( "fmt" "net/http" "net/url" + "strconv" "strings" "testing" "time" + actions_model "code.gitea.io/gitea/models/actions" auth_model "code.gitea.io/gitea/models/auth" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" @@ -29,7 +31,7 @@ func TestDownloadTaskLogs(t *testing.T) { testCases := []struct { treePath string fileContent string - outcome *mockTaskOutcome + outcome []*mockTaskOutcome zstdEnabled bool }{ { @@ -44,21 +46,44 @@ jobs: runs-on: ubuntu-latest steps: - run: echo job1 with zstd enabled + job2: + runs-on: ubuntu-latest + steps: + - run: echo job2 with zstd enabled `, - outcome: &mockTaskOutcome{ - result: runnerv1.Result_RESULT_SUCCESS, - logRows: []*runnerv1.LogRow{ - { - Time: timestamppb.New(now.Add(1 * time.Second)), - Content: " \U0001F433 docker create image", + outcome: []*mockTaskOutcome{ + { + result: runnerv1.Result_RESULT_SUCCESS, + logRows: []*runnerv1.LogRow{ + { + Time: timestamppb.New(now.Add(1 * time.Second)), + Content: " \U0001F433 docker create image", + }, + { + Time: timestamppb.New(now.Add(2 * time.Second)), + Content: "job1 zstd enabled", + }, + { + Time: timestamppb.New(now.Add(3 * time.Second)), + Content: "\U0001F3C1 Job succeeded", + }, }, - { - Time: timestamppb.New(now.Add(2 * time.Second)), - Content: "job1 zstd enabled", - }, - { - Time: timestamppb.New(now.Add(3 * time.Second)), - Content: "\U0001F3C1 Job succeeded", + }, + { + result: runnerv1.Result_RESULT_SUCCESS, + logRows: []*runnerv1.LogRow{ + { + Time: timestamppb.New(now.Add(1 * time.Second)), + Content: " \U0001F433 docker create image", + }, + { + Time: timestamppb.New(now.Add(2 * time.Second)), + Content: "job2 zstd enabled", + }, + { + Time: timestamppb.New(now.Add(3 * time.Second)), + Content: "\U0001F3C1 Job succeeded", + }, }, }, }, @@ -76,21 +101,44 @@ jobs: runs-on: ubuntu-latest steps: - run: echo job1 with zstd disabled + job2: + runs-on: ubuntu-latest + steps: + - run: echo job2 with zstd disabled `, - outcome: &mockTaskOutcome{ - result: runnerv1.Result_RESULT_SUCCESS, - logRows: []*runnerv1.LogRow{ - { - Time: timestamppb.New(now.Add(4 * time.Second)), - Content: " \U0001F433 docker create image", - }, - { - Time: timestamppb.New(now.Add(5 * time.Second)), - Content: "job1 zstd disabled", + outcome: []*mockTaskOutcome{ + { + result: runnerv1.Result_RESULT_SUCCESS, + logRows: []*runnerv1.LogRow{ + { + Time: timestamppb.New(now.Add(4 * time.Second)), + Content: " \U0001F433 docker create image", + }, + { + Time: timestamppb.New(now.Add(5 * time.Second)), + Content: "job1 zstd disabled", + }, + { + Time: timestamppb.New(now.Add(6 * time.Second)), + Content: "\U0001F3C1 Job succeeded", + }, }, - { - Time: timestamppb.New(now.Add(6 * time.Second)), - Content: "\U0001F3C1 Job succeeded", + }, + { + result: runnerv1.Result_RESULT_SUCCESS, + logRows: []*runnerv1.LogRow{ + { + Time: timestamppb.New(now.Add(4 * time.Second)), + Content: " \U0001F433 docker create image", + }, + { + Time: timestamppb.New(now.Add(5 * time.Second)), + Content: "job2 zstd disabled", + }, + { + Time: timestamppb.New(now.Add(6 * time.Second)), + Content: "\U0001F3C1 Job succeeded", + }, }, }, }, @@ -122,33 +170,55 @@ jobs: opts := getWorkflowCreateFileOptions(user2, repo.DefaultBranch, fmt.Sprintf("create %s", tc.treePath), tc.fileContent) createWorkflowFile(t, token, user2.Name, repo.Name, tc.treePath, opts) - // fetch and execute task - task := runner.fetchTask(t) - runner.execTask(t, task, tc.outcome) + // fetch and execute tasks + for jobIndex, outcome := range tc.outcome { + task := runner.fetchTask(t) + runner.execTask(t, task, outcome) - // check whether the log file exists - logFileName := fmt.Sprintf("%s/%02x/%d.log", repo.FullName(), task.Id%256, task.Id) - if setting.Actions.LogCompression.IsZstd() { - logFileName += ".zst" - } - _, err := storage.Actions.Stat(logFileName) - assert.NoError(t, err) + // check whether the log file exists + logFileName := fmt.Sprintf("%s/%02x/%d.log", repo.FullName(), task.Id%256, task.Id) + if setting.Actions.LogCompression.IsZstd() { + logFileName += ".zst" + } + _, err := storage.Actions.Stat(logFileName) + assert.NoError(t, err) - // download task logs and check content - runIndex := task.Context.GetFields()["run_number"].GetStringValue() - req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%s/jobs/0/logs", user2.Name, repo.Name, runIndex)). - AddTokenAuth(token) - resp := MakeRequest(t, req, http.StatusOK) - logTextLines := strings.Split(strings.TrimSpace(resp.Body.String()), "\n") - assert.Len(t, logTextLines, len(tc.outcome.logRows)) - for idx, lr := range tc.outcome.logRows { - assert.Equal( - t, - fmt.Sprintf("%s %s", lr.Time.AsTime().Format("2006-01-02T15:04:05.0000000Z07:00"), lr.Content), - logTextLines[idx], - ) - } + // download task logs and check content + runIndex := task.Context.GetFields()["run_number"].GetStringValue() + req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%s/jobs/%d/logs", user2.Name, repo.Name, runIndex, jobIndex)). + AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) + logTextLines := strings.Split(strings.TrimSpace(resp.Body.String()), "\n") + assert.Len(t, logTextLines, len(outcome.logRows)) + for idx, lr := range outcome.logRows { + assert.Equal( + t, + fmt.Sprintf("%s %s", lr.Time.AsTime().Format("2006-01-02T15:04:05.0000000Z07:00"), lr.Content), + logTextLines[idx], + ) + } + runID, _ := strconv.ParseInt(task.Context.GetFields()["run_id"].GetStringValue(), 10, 64) + + jobs, err := actions_model.GetRunJobsByRunID(t.Context(), runID) + assert.NoError(t, err) + assert.Len(t, jobs, len(tc.outcome)) + jobID := jobs[jobIndex].ID + + // download task logs from API and check content + req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/actions/jobs/%d/logs", user2.Name, repo.Name, jobID)). + AddTokenAuth(token) + resp = MakeRequest(t, req, http.StatusOK) + logTextLines = strings.Split(strings.TrimSpace(resp.Body.String()), "\n") + assert.Len(t, logTextLines, len(outcome.logRows)) + for idx, lr := range outcome.logRows { + assert.Equal( + t, + fmt.Sprintf("%s %s", lr.Time.AsTime().Format("2006-01-02T15:04:05.0000000Z07:00"), lr.Content), + logTextLines[idx], + ) + } + } resetFunc() }) } diff --git a/tests/integration/api_org_test.go b/tests/integration/api_org_test.go index d766b1e8be..a3bb04c55a 100644 --- a/tests/integration/api_org_test.go +++ b/tests/integration/api_org_test.go @@ -156,7 +156,7 @@ func TestAPIOrgEditBadVisibility(t *testing.T) { func TestAPIOrgDeny(t *testing.T) { defer tests.PrepareTestEnv(t)() - defer test.MockVariableValue(&setting.Service.RequireSignInView, true)() + defer test.MockVariableValue(&setting.Service.RequireSignInViewStrict, true)() orgName := "user1_org" req := NewRequestf(t, "GET", "/api/v1/orgs/%s", orgName) diff --git a/tests/integration/api_packages_container_test.go b/tests/integration/api_packages_container_test.go index 3905ad1b70..cc9bf11f13 100644 --- a/tests/integration/api_packages_container_test.go +++ b/tests/integration/api_packages_container_test.go @@ -111,7 +111,7 @@ func TestPackageContainer(t *testing.T) { AddTokenAuth(anonymousToken) MakeRequest(t, req, http.StatusOK) - defer test.MockVariableValue(&setting.Service.RequireSignInView, true)() + defer test.MockVariableValue(&setting.Service.RequireSignInViewStrict, true)() req = NewRequest(t, "GET", fmt.Sprintf("%sv2", setting.AppURL)) MakeRequest(t, req, http.StatusUnauthorized) diff --git a/tests/integration/api_packages_generic_test.go b/tests/integration/api_packages_generic_test.go index baa8dd66c8..5f410fc470 100644 --- a/tests/integration/api_packages_generic_test.go +++ b/tests/integration/api_packages_generic_test.go @@ -15,6 +15,7 @@ import ( "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/test" "code.gitea.io/gitea/tests" "github.com/stretchr/testify/assert" @@ -131,11 +132,7 @@ func TestPackageGeneric(t *testing.T) { t.Run("RequireSignInView", func(t *testing.T) { defer tests.PrintCurrentTest(t)() - - setting.Service.RequireSignInView = true - defer func() { - setting.Service.RequireSignInView = false - }() + defer test.MockVariableValue(&setting.Service.RequireSignInViewStrict, true)() req = NewRequest(t, "GET", url+"/dummy.bin") MakeRequest(t, req, http.StatusUnauthorized) diff --git a/tests/integration/git_helper_for_declarative_test.go b/tests/integration/git_helper_for_declarative_test.go index 435253c9c7..fce9b8f247 100644 --- a/tests/integration/git_helper_for_declarative_test.go +++ b/tests/integration/git_helper_for_declarative_test.go @@ -10,7 +10,6 @@ import ( "net/http" "net/url" "os" - "path" "path/filepath" "strconv" "testing" @@ -35,12 +34,12 @@ func withKeyFile(t *testing.T, keyname string, callback func(string)) { err = ssh.GenKeyPair(keyFile) assert.NoError(t, err) - err = os.WriteFile(path.Join(tmpDir, "ssh"), []byte("#!/bin/bash\n"+ + err = os.WriteFile(filepath.Join(tmpDir, "ssh"), []byte("#!/bin/bash\n"+ "ssh -o \"UserKnownHostsFile=/dev/null\" -o \"StrictHostKeyChecking=no\" -o \"IdentitiesOnly=yes\" -i \""+keyFile+"\" \"$@\""), 0o700) assert.NoError(t, err) // Setup ssh wrapper - t.Setenv("GIT_SSH", path.Join(tmpDir, "ssh")) + t.Setenv("GIT_SSH", filepath.Join(tmpDir, "ssh")) t.Setenv("GIT_SSH_COMMAND", "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o IdentitiesOnly=yes -i \""+keyFile+"\"") t.Setenv("GIT_SSH_VARIANT", "ssh") diff --git a/tests/integration/git_smart_http_test.go b/tests/integration/git_smart_http_test.go index 55d647672a..fc8ca11847 100644 --- a/tests/integration/git_smart_http_test.go +++ b/tests/integration/git_smart_http_test.go @@ -9,6 +9,8 @@ import ( "net/url" "testing" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/modules/util" "github.com/stretchr/testify/assert" @@ -16,7 +18,10 @@ import ( ) func TestGitSmartHTTP(t *testing.T) { - onGiteaRun(t, testGitSmartHTTP) + onGiteaRun(t, func(t *testing.T, u *url.URL) { + testGitSmartHTTP(t, u) + testRenamedRepoRedirect(t) + }) } func testGitSmartHTTP(t *testing.T, u *url.URL) { @@ -73,3 +78,21 @@ func testGitSmartHTTP(t *testing.T, u *url.URL) { }) } } + +func testRenamedRepoRedirect(t *testing.T) { + defer test.MockVariableValue(&setting.Service.RequireSignInViewStrict, true)() + + // git client requires to get a 301 redirect response before 401 unauthorized response + req := NewRequest(t, "GET", "/user2/oldrepo1/info/refs") + resp := MakeRequest(t, req, http.StatusMovedPermanently) + redirect := resp.Header().Get("Location") + assert.Equal(t, "/user2/repo1/info/refs", redirect) + + req = NewRequest(t, "GET", redirect) + resp = MakeRequest(t, req, http.StatusUnauthorized) + assert.Equal(t, "Unauthorized\n", resp.Body.String()) + + req = NewRequest(t, "GET", redirect).AddBasicAuth("user2") + resp = MakeRequest(t, req, http.StatusOK) + assert.Contains(t, resp.Body.String(), "65f1bf27bc3bf70f64657658635e66094edbcb4d\trefs/tags/v1.1") +} diff --git a/tests/integration/migration-test/migration_test.go b/tests/integration/migration-test/migration_test.go index 1b42f00604..c319fd20a7 100644 --- a/tests/integration/migration-test/migration_test.go +++ b/tests/integration/migration-test/migration_test.go @@ -38,7 +38,7 @@ var currentEngine *xorm.Engine func initMigrationTest(t *testing.T) func() { testlogger.Init() giteaRoot := test.SetupGiteaRoot() - setting.AppPath = path.Join(giteaRoot, "gitea") + setting.AppPath = filepath.Join(giteaRoot, "gitea") if _, err := os.Stat(setting.AppPath); err != nil { testlogger.Fatalf(fmt.Sprintf("Could not find gitea binary at %s\n", setting.AppPath)) } @@ -47,7 +47,7 @@ func initMigrationTest(t *testing.T) func() { if giteaConf == "" { testlogger.Fatalf("Environment variable $GITEA_CONF not set\n") } else if !path.IsAbs(giteaConf) { - setting.CustomConf = path.Join(giteaRoot, giteaConf) + setting.CustomConf = filepath.Join(giteaRoot, giteaConf) } else { setting.CustomConf = giteaConf } @@ -55,7 +55,7 @@ func initMigrationTest(t *testing.T) func() { unittest.InitSettings() assert.NotEmpty(t, setting.RepoRootPath) - assert.NoError(t, unittest.SyncDirs(path.Join(filepath.Dir(setting.AppPath), "tests/gitea-repositories-meta"), setting.RepoRootPath)) + assert.NoError(t, unittest.SyncDirs(filepath.Join(filepath.Dir(setting.AppPath), "tests/gitea-repositories-meta"), setting.RepoRootPath)) assert.NoError(t, git.InitFull(t.Context())) setting.LoadDBSetting() setting.InitLoggersForTest() diff --git a/tests/integration/mirror_pull_test.go b/tests/integration/mirror_pull_test.go index cf6faa7704..7ab8f72b4a 100644 --- a/tests/integration/mirror_pull_test.go +++ b/tests/integration/mirror_pull_test.go @@ -4,10 +4,12 @@ package integration import ( + "slices" "testing" "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" @@ -19,11 +21,13 @@ import ( "code.gitea.io/gitea/tests" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestMirrorPull(t *testing.T) { defer tests.PrepareTestEnv(t)() + ctx := t.Context() user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) repoPath := repo_model.RepoPath(user.Name, repo.Name) @@ -35,10 +39,10 @@ func TestMirrorPull(t *testing.T) { Mirror: true, CloneAddr: repoPath, Wiki: true, - Releases: false, + Releases: true, } - mirrorRepo, err := repo_service.CreateRepositoryDirectly(db.DefaultContext, user, user, repo_service.CreateRepoOptions{ + mirrorRepo, err := repo_service.CreateRepositoryDirectly(ctx, user, user, repo_service.CreateRepoOptions{ Name: opts.RepoName, Description: opts.Description, IsPrivate: opts.Private, @@ -48,11 +52,15 @@ func TestMirrorPull(t *testing.T) { assert.NoError(t, err) assert.True(t, mirrorRepo.IsMirror, "expected pull-mirror repo to be marked as a mirror immediately after its creation") - ctx := t.Context() - - mirror, err := repo_service.MigrateRepositoryGitData(ctx, user, mirrorRepo, opts, nil) + mirrorRepo, err = repo_service.MigrateRepositoryGitData(ctx, user, mirrorRepo, opts, nil) assert.NoError(t, err) + // these units should have been enabled + mirrorRepo.Units = nil + require.NoError(t, mirrorRepo.LoadUnits(ctx)) + assert.True(t, slices.ContainsFunc(mirrorRepo.Units, func(u *repo_model.RepoUnit) bool { return u.Type == unit.TypeReleases })) + assert.True(t, slices.ContainsFunc(mirrorRepo.Units, func(u *repo_model.RepoUnit) bool { return u.Type == unit.TypeWiki })) + gitRepo, err := gitrepo.OpenRepository(git.DefaultContext, repo) assert.NoError(t, err) defer gitRepo.Close() @@ -60,10 +68,11 @@ func TestMirrorPull(t *testing.T) { findOptions := repo_model.FindReleasesOptions{ IncludeDrafts: true, IncludeTags: true, - RepoID: mirror.ID, + RepoID: mirrorRepo.ID, } initCount, err := db.Count[repo_model.Release](db.DefaultContext, findOptions) assert.NoError(t, err) + assert.Zero(t, initCount) // no sync yet, so even though there is a tag in source repo, the mirror's release table is still empty assert.NoError(t, release_service.CreateRelease(gitRepo, &repo_model.Release{ RepoID: repo.ID, @@ -79,12 +88,15 @@ func TestMirrorPull(t *testing.T) { IsTag: true, }, nil, "")) - _, err = repo_model.GetMirrorByRepoID(ctx, mirror.ID) + _, err = repo_model.GetMirrorByRepoID(ctx, mirrorRepo.ID) assert.NoError(t, err) - ok := mirror_service.SyncPullMirror(ctx, mirror.ID) + ok := mirror_service.SyncPullMirror(ctx, mirrorRepo.ID) assert.True(t, ok) + // actually there is a tag in the source repo, so after "sync", that tag will also come into the mirror + initCount++ + count, err := db.Count[repo_model.Release](db.DefaultContext, findOptions) assert.NoError(t, err) assert.EqualValues(t, initCount+1, count) @@ -93,7 +105,7 @@ func TestMirrorPull(t *testing.T) { assert.NoError(t, err) assert.NoError(t, release_service.DeleteReleaseByID(ctx, repo, release, user, true)) - ok = mirror_service.SyncPullMirror(ctx, mirror.ID) + ok = mirror_service.SyncPullMirror(ctx, mirrorRepo.ID) assert.True(t, ok) count, err = db.Count[repo_model.Release](db.DefaultContext, findOptions) diff --git a/tests/integration/oauth_test.go b/tests/integration/oauth_test.go index d7ef103506..d2228bae79 100644 --- a/tests/integration/oauth_test.go +++ b/tests/integration/oauth_test.go @@ -19,7 +19,7 @@ import ( "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" - oauth2_provider "code.gitea.io/gitea/services/oauth2_provider" + "code.gitea.io/gitea/services/oauth2_provider" "code.gitea.io/gitea/tests" "github.com/stretchr/testify/assert" diff --git a/tests/integration/pull_compare_test.go b/tests/integration/pull_compare_test.go index 106774aa54..4ac5be18be 100644 --- a/tests/integration/pull_compare_test.go +++ b/tests/integration/pull_compare_test.go @@ -24,23 +24,38 @@ import ( func TestPullCompare(t *testing.T) { defer tests.PrepareTestEnv(t)() - session := loginUser(t, "user2") - req := NewRequest(t, "GET", "/user2/repo1/pulls") - resp := session.MakeRequest(t, req, http.StatusOK) - htmlDoc := NewHTMLParser(t, resp.Body) - link, exists := htmlDoc.doc.Find(".new-pr-button").Attr("href") - assert.True(t, exists, "The template has changed") - - req = NewRequest(t, "GET", link) - resp = session.MakeRequest(t, req, http.StatusOK) - assert.EqualValues(t, http.StatusOK, resp.Code) - - // test the edit button in the PR diff view - req = NewRequest(t, "GET", "/user2/repo1/pulls/3/files") - resp = session.MakeRequest(t, req, http.StatusOK) - doc := NewHTMLParser(t, resp.Body) - editButtonCount := doc.doc.Find(".diff-file-header-actions a[href*='/_edit/']").Length() - assert.Positive(t, editButtonCount, "Expected to find a button to edit a file in the PR diff view but there were none") + t.Run("PullsNewRedirect", func(t *testing.T) { + req := NewRequest(t, "GET", "/user2/repo1/pulls/new/foo") + resp := MakeRequest(t, req, http.StatusSeeOther) + redirect := test.RedirectURL(resp) + assert.Equal(t, "/user2/repo1/compare/master...foo?expand=1", redirect) + + req = NewRequest(t, "GET", "/user13/repo11/pulls/new/foo") + resp = MakeRequest(t, req, http.StatusSeeOther) + redirect = test.RedirectURL(resp) + assert.Equal(t, "/user12/repo10/compare/master...user13:foo?expand=1", redirect) + }) + + t.Run("ButtonsExist", func(t *testing.T) { + session := loginUser(t, "user2") + + // test the "New PR" button + req := NewRequest(t, "GET", "/user2/repo1/pulls") + resp := session.MakeRequest(t, req, http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + link, exists := htmlDoc.doc.Find(".new-pr-button").Attr("href") + assert.True(t, exists, "The template has changed") + req = NewRequest(t, "GET", link) + resp = session.MakeRequest(t, req, http.StatusOK) + assert.EqualValues(t, http.StatusOK, resp.Code) + + // test the edit button in the PR diff view + req = NewRequest(t, "GET", "/user2/repo1/pulls/3/files") + resp = session.MakeRequest(t, req, http.StatusOK) + doc := NewHTMLParser(t, resp.Body) + editButtonCount := doc.doc.Find(".diff-file-header-actions a[href*='/_edit/']").Length() + assert.Positive(t, editButtonCount, "Expected to find a button to edit a file in the PR diff view but there were none") + }) onGiteaRun(t, func(t *testing.T, u *url.URL) { defer tests.PrepareTestEnv(t)() @@ -54,8 +69,8 @@ func TestPullCompare(t *testing.T) { repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user2", Name: "repo1"}) issueIndex := unittest.AssertExistsAndLoadBean(t, &issues_model.IssueIndex{GroupID: repo1.ID}, unittest.OrderBy("group_id ASC")) prFilesURL := fmt.Sprintf("/user2/repo1/pulls/%d/files", issueIndex.MaxIndex) - req = NewRequest(t, "GET", prFilesURL) - resp = session.MakeRequest(t, req, http.StatusOK) + req := NewRequest(t, "GET", prFilesURL) + resp := session.MakeRequest(t, req, http.StatusOK) doc := NewHTMLParser(t, resp.Body) editButtonCount := doc.doc.Find(".diff-file-header-actions a[href*='/_edit/']").Length() assert.Positive(t, editButtonCount, "Expected to find a button to edit a file in the PR diff view but there were none") diff --git a/tests/integration/repo_generate_test.go b/tests/integration/repo_generate_test.go index ff2aa220d3..f5645d62bc 100644 --- a/tests/integration/repo_generate_test.go +++ b/tests/integration/repo_generate_test.go @@ -31,16 +31,16 @@ func testRepoGenerate(t *testing.T, session *TestSession, templateID, templateOw // Step2: click the "Use this template" button htmlDoc := NewHTMLParser(t, resp.Body) - link, exists := htmlDoc.doc.Find("a.ui.button[href^=\"/repo/create\"]").Attr("href") + link, exists := htmlDoc.doc.Find(`a.ui.button[href^="/repo/create"]`).Attr("href") assert.True(t, exists, "The template has changed") req = NewRequest(t, "GET", link) resp = session.MakeRequest(t, req, http.StatusOK) - // Step3: fill the form of the create + // Step3: fill the form on the "create" page htmlDoc = NewHTMLParser(t, resp.Body) - link, exists = htmlDoc.doc.Find("form.ui.form[action^=\"/repo/create\"]").Attr("action") + link, exists = htmlDoc.doc.Find(`form.ui.form[action^="/repo/create"]`).Attr("action") assert.True(t, exists, "The template has changed") - _, exists = htmlDoc.doc.Find(fmt.Sprintf(".owner.dropdown .item[data-value=\"%d\"]", generateOwner.ID)).Attr("data-value") + _, exists = htmlDoc.doc.Find(fmt.Sprintf(`#repo_owner_dropdown .item[data-value="%d"]`, generateOwner.ID)).Attr("data-value") assert.True(t, exists, "Generate owner '%s' is not present in select box", generateOwnerName) req = NewRequestWithValues(t, "POST", link, map[string]string{ "_csrf": htmlDoc.GetCSRF(), diff --git a/tests/integration/repo_test.go b/tests/integration/repo_test.go index a5728ffcbd..9a3cb988a6 100644 --- a/tests/integration/repo_test.go +++ b/tests/integration/repo_test.go @@ -7,10 +7,12 @@ import ( "fmt" "net/http" "path" + "strconv" "strings" "testing" "time" + "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/tests" @@ -19,8 +21,26 @@ import ( "github.com/stretchr/testify/assert" ) -func TestViewRepo(t *testing.T) { +func TestRepoView(t *testing.T) { defer tests.PrepareTestEnv(t)() + t.Run("ViewRepoPublic", testViewRepoPublic) + t.Run("ViewRepoWithCache", testViewRepoWithCache) + t.Run("ViewRepoPrivate", testViewRepoPrivate) + t.Run("ViewRepo1CloneLinkAnonymous", testViewRepo1CloneLinkAnonymous) + t.Run("ViewRepo1CloneLinkAuthorized", testViewRepo1CloneLinkAuthorized) + t.Run("ViewRepoWithSymlinks", testViewRepoWithSymlinks) + t.Run("ViewFileInRepo", testViewFileInRepo) + t.Run("BlameFileInRepo", testBlameFileInRepo) + t.Run("ViewRepoDirectory", testViewRepoDirectory) + t.Run("ViewRepoDirectoryReadme", testViewRepoDirectoryReadme) + t.Run("MarkDownReadmeImage", testMarkDownReadmeImage) + t.Run("MarkDownReadmeImageSubfolder", testMarkDownReadmeImageSubfolder) + t.Run("GeneratedSourceLink", testGeneratedSourceLink) + t.Run("ViewCommit", testViewCommit) +} + +func testViewRepoPublic(t *testing.T) { + defer tests.PrintCurrentTest(t)() session := loginUser(t, "user2") @@ -41,87 +61,118 @@ func TestViewRepo(t *testing.T) { session.MakeRequest(t, req, http.StatusNotFound) } -func testViewRepo(t *testing.T) { - defer tests.PrepareTestEnv(t)() - - req := NewRequest(t, "GET", "/org3/repo3") - session := loginUser(t, "user2") - resp := session.MakeRequest(t, req, http.StatusOK) - - htmlDoc := NewHTMLParser(t, resp.Body) - files := htmlDoc.doc.Find("#repo-files-table .repo-file-item") - - type file struct { - fileName string - commitID string - commitMsg string - commitTime string - } +func testViewRepoWithCache(t *testing.T) { + defer tests.PrintCurrentTest(t)() + testView := func(t *testing.T) { + req := NewRequest(t, "GET", "/org3/repo3") + session := loginUser(t, "user2") + resp := session.MakeRequest(t, req, http.StatusOK) - var items []file - - files.Each(func(i int, s *goquery.Selection) { - tds := s.Find(".repo-file-cell") - var f file - tds.Each(func(i int, s *goquery.Selection) { - if i == 0 { - f.fileName = strings.TrimSpace(s.Text()) - } else if i == 1 { - a := s.Find("a") - f.commitMsg = strings.TrimSpace(a.Text()) - l, _ := a.Attr("href") - f.commitID = path.Base(l) - } + htmlDoc := NewHTMLParser(t, resp.Body) + files := htmlDoc.doc.Find("#repo-files-table .repo-file-item") + + type file struct { + fileName string + commitID string + commitMsg string + commitTime string + } + + var items []file + + files.Each(func(i int, s *goquery.Selection) { + tds := s.Find(".repo-file-cell") + var f file + tds.Each(func(i int, s *goquery.Selection) { + if i == 0 { + f.fileName = strings.TrimSpace(s.Text()) + } else if i == 1 { + a := s.Find("a") + f.commitMsg = strings.TrimSpace(a.Text()) + l, _ := a.Attr("href") + f.commitID = path.Base(l) + } + }) + + // convert "2017-06-14 21:54:21 +0800" to "Wed, 14 Jun 2017 13:54:21 UTC" + htmlTimeString, _ := s.Find("relative-time").Attr("datetime") + htmlTime, _ := time.Parse(time.RFC3339, htmlTimeString) + f.commitTime = htmlTime.In(time.Local).Format(time.RFC1123) + items = append(items, f) }) - // convert "2017-06-14 21:54:21 +0800" to "Wed, 14 Jun 2017 13:54:21 UTC" - htmlTimeString, _ := s.Find("relative-time").Attr("datetime") - htmlTime, _ := time.Parse(time.RFC3339, htmlTimeString) - f.commitTime = htmlTime.In(time.Local).Format(time.RFC1123) - items = append(items, f) - }) - - commitT := time.Date(2017, time.June, 14, 13, 54, 21, 0, time.UTC).In(time.Local).Format(time.RFC1123) - assert.EqualValues(t, []file{ - { - fileName: "doc", - commitID: "2a47ca4b614a9f5a43abbd5ad851a54a616ffee6", - commitMsg: "init project", - commitTime: commitT, - }, - { - fileName: "README.md", - commitID: "2a47ca4b614a9f5a43abbd5ad851a54a616ffee6", - commitMsg: "init project", - commitTime: commitT, - }, - }, items) -} + commitT := time.Date(2017, time.June, 14, 13, 54, 21, 0, time.UTC).In(time.Local).Format(time.RFC1123) + assert.EqualValues(t, []file{ + { + fileName: "doc", + commitID: "2a47ca4b614a9f5a43abbd5ad851a54a616ffee6", + commitMsg: "init project", + commitTime: commitT, + }, + { + fileName: "README.md", + commitID: "2a47ca4b614a9f5a43abbd5ad851a54a616ffee6", + commitMsg: "init project", + commitTime: commitT, + }, + }, items) + } -func TestViewRepo2(t *testing.T) { + // FIXME: these test don't seem quite right, no enough assert // no last commit cache - testViewRepo(t) - + testView(t) // enable last commit cache for all repositories oldCommitsCount := setting.CacheService.LastCommit.CommitsCount setting.CacheService.LastCommit.CommitsCount = 0 // first view will not hit the cache - testViewRepo(t) + testView(t) // second view will hit the cache - testViewRepo(t) + testView(t) setting.CacheService.LastCommit.CommitsCount = oldCommitsCount } -func TestViewRepo3(t *testing.T) { - defer tests.PrepareTestEnv(t)() +func testViewRepoPrivate(t *testing.T) { + defer tests.PrintCurrentTest(t)() req := NewRequest(t, "GET", "/org3/repo3") - session := loginUser(t, "user4") - session.MakeRequest(t, req, http.StatusOK) + MakeRequest(t, req, http.StatusNotFound) + + t.Run("OrgMemberAccess", func(t *testing.T) { + req = NewRequest(t, "GET", "/org3/repo3") + session := loginUser(t, "user4") + resp := session.MakeRequest(t, req, http.StatusOK) + assert.Contains(t, resp.Body.String(), `<div id="repo-files-table"`) + }) + + t.Run("PublicAccess-AnonymousAccess", func(t *testing.T) { + session := loginUser(t, "user1") + + // set unit code to "anonymous read" + req = NewRequestWithValues(t, "POST", "/org3/repo3/settings/public_access", map[string]string{ + "_csrf": GetUserCSRFToken(t, session), + "repo-unit-access-" + strconv.Itoa(int(unit.TypeCode)): "anonymous-read", + }) + session.MakeRequest(t, req, http.StatusSeeOther) + + // try to "anonymous read" (ok) + req = NewRequest(t, "GET", "/org3/repo3") + resp := MakeRequest(t, req, http.StatusOK) + assert.Contains(t, resp.Body.String(), `<span class="ui basic orange label">Public Access</span>`) + + // remove "anonymous read" + req = NewRequestWithValues(t, "POST", "/org3/repo3/settings/public_access", map[string]string{ + "_csrf": GetUserCSRFToken(t, session), + }) + session.MakeRequest(t, req, http.StatusSeeOther) + + // try to "anonymous read" (not found) + req = NewRequest(t, "GET", "/org3/repo3") + MakeRequest(t, req, http.StatusNotFound) + }) } -func TestViewRepo1CloneLinkAnonymous(t *testing.T) { - defer tests.PrepareTestEnv(t)() +func testViewRepo1CloneLinkAnonymous(t *testing.T) { + defer tests.PrintCurrentTest(t)() req := NewRequest(t, "GET", "/user2/repo1") resp := MakeRequest(t, req, http.StatusOK) @@ -139,8 +190,8 @@ func TestViewRepo1CloneLinkAnonymous(t *testing.T) { assert.Equal(t, "tea clone user2/repo1", link) } -func TestViewRepo1CloneLinkAuthorized(t *testing.T) { - defer tests.PrepareTestEnv(t)() +func testViewRepo1CloneLinkAuthorized(t *testing.T) { + defer tests.PrintCurrentTest(t)() session := loginUser(t, "user2") @@ -162,8 +213,8 @@ func TestViewRepo1CloneLinkAuthorized(t *testing.T) { assert.Equal(t, "tea clone user2/repo1", link) } -func TestViewRepoWithSymlinks(t *testing.T) { - defer tests.PrepareTestEnv(t)() +func testViewRepoWithSymlinks(t *testing.T) { + defer tests.PrintCurrentTest(t)() defer test.MockVariableValue(&setting.UI.FileIconTheme, "basic")() session := loginUser(t, "user2") @@ -186,8 +237,8 @@ func TestViewRepoWithSymlinks(t *testing.T) { } // TestViewFileInRepo repo description, topics and summary should not be displayed when viewing a file -func TestViewFileInRepo(t *testing.T) { - defer tests.PrepareTestEnv(t)() +func testViewFileInRepo(t *testing.T) { + defer tests.PrintCurrentTest(t)() session := loginUser(t, "user2") @@ -205,8 +256,8 @@ func TestViewFileInRepo(t *testing.T) { } // TestBlameFileInRepo repo description, topics and summary should not be displayed when running blame on a file -func TestBlameFileInRepo(t *testing.T) { - defer tests.PrepareTestEnv(t)() +func testBlameFileInRepo(t *testing.T) { + defer tests.PrintCurrentTest(t)() session := loginUser(t, "user2") @@ -224,8 +275,8 @@ func TestBlameFileInRepo(t *testing.T) { } // TestViewRepoDirectory repo description, topics and summary should not be displayed when within a directory -func TestViewRepoDirectory(t *testing.T) { - defer tests.PrepareTestEnv(t)() +func testViewRepoDirectory(t *testing.T) { + defer tests.PrintCurrentTest(t)() session := loginUser(t, "user2") @@ -246,8 +297,8 @@ func TestViewRepoDirectory(t *testing.T) { } // ensure that the all the different ways to find and render a README work -func TestViewRepoDirectoryReadme(t *testing.T) { - defer tests.PrepareTestEnv(t)() +func testViewRepoDirectoryReadme(t *testing.T) { + defer tests.PrintCurrentTest(t)() // there are many combinations: // - READMEs can be .md, .txt, or have no extension @@ -353,8 +404,8 @@ func TestViewRepoDirectoryReadme(t *testing.T) { missing("symlink-loop", "/user2/readme-test/src/branch/symlink-loop/") } -func TestMarkDownReadmeImage(t *testing.T) { - defer tests.PrepareTestEnv(t)() +func testMarkDownReadmeImage(t *testing.T) { + defer tests.PrintCurrentTest(t)() session := loginUser(t, "user2") @@ -375,8 +426,8 @@ func TestMarkDownReadmeImage(t *testing.T) { assert.Equal(t, "/user2/repo1/media/branch/home-md-img-check/test-fake-img.jpg", src) } -func TestMarkDownReadmeImageSubfolder(t *testing.T) { - defer tests.PrepareTestEnv(t)() +func testMarkDownReadmeImageSubfolder(t *testing.T) { + defer tests.PrintCurrentTest(t)() session := loginUser(t, "user2") @@ -398,8 +449,8 @@ func TestMarkDownReadmeImageSubfolder(t *testing.T) { assert.Equal(t, "/user2/repo1/media/branch/sub-home-md-img-check/docs/test-fake-img.jpg", src) } -func TestGeneratedSourceLink(t *testing.T) { - defer tests.PrepareTestEnv(t)() +func testGeneratedSourceLink(t *testing.T) { + defer tests.PrintCurrentTest(t)() t.Run("Rendered file", func(t *testing.T) { defer tests.PrintCurrentTest(t)() @@ -434,8 +485,8 @@ func TestGeneratedSourceLink(t *testing.T) { }) } -func TestViewCommit(t *testing.T) { - defer tests.PrepareTestEnv(t)() +func testViewCommit(t *testing.T) { + defer tests.PrintCurrentTest(t)() req := NewRequest(t, "GET", "/user2/repo1/commit/0123456789012345678901234567890123456789") req.Header.Add("Accept", "text/html") diff --git a/tests/integration/signin_test.go b/tests/integration/signin_test.go index 5c7555286e..3852c08032 100644 --- a/tests/integration/signin_test.go +++ b/tests/integration/signin_test.go @@ -16,6 +16,7 @@ import ( "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/modules/translation" "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/routers" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/tests" @@ -166,3 +167,32 @@ func TestEnablePasswordSignInFormAndEnablePasskeyAuth(t *testing.T) { AssertHTMLElement(t, doc, ".signin-passkey", true) }) } + +func TestRequireSignInView(t *testing.T) { + defer tests.PrepareTestEnv(t)() + t.Run("NoRequireSignInView", func(t *testing.T) { + require.False(t, setting.Service.RequireSignInViewStrict) + require.False(t, setting.Service.BlockAnonymousAccessExpensive) + req := NewRequest(t, "GET", "/user2/repo1/src/branch/master") + MakeRequest(t, req, http.StatusOK) + }) + t.Run("RequireSignInView", func(t *testing.T) { + defer test.MockVariableValue(&setting.Service.RequireSignInViewStrict, true)() + defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())() + req := NewRequest(t, "GET", "/user2/repo1/src/branch/master") + resp := MakeRequest(t, req, http.StatusSeeOther) + assert.Equal(t, "/user/login", resp.Header().Get("Location")) + }) + t.Run("BlockAnonymousAccessExpensive", func(t *testing.T) { + defer test.MockVariableValue(&setting.Service.RequireSignInViewStrict, false)() + defer test.MockVariableValue(&setting.Service.BlockAnonymousAccessExpensive, true)() + defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())() + + req := NewRequest(t, "GET", "/user2/repo1") + MakeRequest(t, req, http.StatusOK) + + req = NewRequest(t, "GET", "/user2/repo1/src/branch/master") + resp := MakeRequest(t, req, http.StatusSeeOther) + assert.Equal(t, "/user/login", resp.Header().Get("Location")) + }) +} diff --git a/web_src/css/base.css b/web_src/css/base.css index 47b4f44a66..98eb32bc13 100644 --- a/web_src/css/base.css +++ b/web_src/css/base.css @@ -989,14 +989,7 @@ table th[data-sortt-desc] .svg { box-shadow: 0 0 0 1px var(--color-secondary) inset; } -.emoji { - font-size: 1.25em; - line-height: var(--line-height-default); - font-style: normal !important; - font-weight: var(--font-weight-normal) !important; - vertical-align: -0.075em; -} - +/* for "image" emojis like ":git:" ":gitea:" and ":github:" (see CUSTOM_EMOJIS config option) */ .emoji img { border-width: 0 !important; margin: 0 !important; @@ -1155,6 +1148,11 @@ table th[data-sortt-desc] .svg { min-width: 0; } +.flex-text-block > .ui.button, +.flex-text-inline > .ui.button { + margin: 0; /* fomantic buttons have default margin, when we use them in a flex container with gap, we do not need these margins */ +} + /* to override Fomantic's default display: block for ".menu .item", and use a slightly larger gap for menu item content the "!important" is necessary to override Fomantic UI menu item styles, meanwhile we should keep the "hidden" items still hidden */ .ui.dropdown .menu.flex-items-menu > .item:not(.hidden, .filtered, .tw-hidden) { diff --git a/web_src/css/features/projects.css b/web_src/css/features/projects.css index 8763d3684e..72ef523913 100644 --- a/web_src/css/features/projects.css +++ b/web_src/css/features/projects.css @@ -8,18 +8,6 @@ margin: 0 0.5em; } -.project-toolbar-right .filter.menu { - flex-direction: row; - flex-wrap: wrap; -} - -@media (max-width: 767.98px) { - .project-toolbar-right .dropdown .menu { - left: auto !important; - right: auto !important; - } -} - .project-column { background-color: var(--color-project-column-bg) !important; border: 1px solid var(--color-secondary) !important; diff --git a/web_src/css/markup/content.css b/web_src/css/markup/content.css index 865ac0536a..6d985a76a7 100644 --- a/web_src/css/markup/content.css +++ b/web_src/css/markup/content.css @@ -336,11 +336,6 @@ padding-right: 28px; } -.markup .emoji { - max-width: none; - vertical-align: text-top; -} - .markup span.frame { display: block; overflow: hidden; diff --git a/web_src/css/repo/home-file-list.css b/web_src/css/repo/home-file-list.css index 189b6406d4..46128457ed 100644 --- a/web_src/css/repo/home-file-list.css +++ b/web_src/css/repo/home-file-list.css @@ -14,10 +14,6 @@ } } -#repo-files-table .repo-file-cell.name .svg { - margin-right: 2px; -} - #repo-files-table .svg.octicon-file-directory-fill, #repo-files-table .svg.octicon-file-submodule { color: var(--color-primary); @@ -70,11 +66,25 @@ } #repo-files-table .repo-file-cell.name { + display: flex; + align-items: center; + gap: 0.5em; + overflow: hidden; +} + +#repo-files-table .repo-file-cell.name > a, +#repo-files-table .repo-file-cell.name > span { + flex-shrink: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +#repo-files-table .repo-file-cell.name .entry-name { + flex-shrink: 1; + min-width: 3em; +} + @media (max-width: 767.98px) { #repo-files-table .repo-file-cell.name { max-width: 35vw; diff --git a/web_src/fomantic/build/components/dropdown.js b/web_src/fomantic/build/components/dropdown.js index d3a1f7dc24..163de9f751 100644 --- a/web_src/fomantic/build/components/dropdown.js +++ b/web_src/fomantic/build/components/dropdown.js @@ -752,6 +752,7 @@ $.fn.dropdown = function(parameters) { if(module.is.searchSelection() && module.can.show() && module.is.focusedOnSearch() ) { module.show(); } + settings.onAfterFiltered.call(element); // GITEA-PATCH: callback to correctly handle the filtered items } ; if(settings.useLabels && module.has.maxSelections()) { @@ -1130,7 +1131,11 @@ $.fn.dropdown = function(parameters) { icon: { click: function(event) { iconClicked=true; - if(module.has.search()) { + // GITEA-PATCH: official dropdown doesn't support the search input in menu + // so we need to make the menu could be shown when the search input is in menu and user clicks the icon + const searchInputInMenu = Boolean($menu.find('.search > input').length); + if(module.has.search() && !searchInputInMenu) { + // the search input is in the dropdown element (but not in the popup menu), try to focus it if(!module.is.active()) { if(settings.showOnFocus){ module.focusSearch(); @@ -3988,6 +3993,8 @@ $.fn.dropdown.settings = { onShow : function(){}, onHide : function(){}, + onAfterFiltered: function(){}, // GITEA-PATCH: callback to correctly handle the filtered items + /* Component */ name : 'Dropdown', namespace : 'dropdown', diff --git a/web_src/js/features/repo-common.test.ts b/web_src/js/features/repo-common.test.ts index 009dfc86b1..33a29ecb2c 100644 --- a/web_src/js/features/repo-common.test.ts +++ b/web_src/js/features/repo-common.test.ts @@ -1,7 +1,22 @@ -import {substituteRepoOpenWithUrl} from './repo-common.ts'; +import {sanitizeRepoName, substituteRepoOpenWithUrl} from './repo-common.ts'; test('substituteRepoOpenWithUrl', () => { // For example: "x-github-client://openRepo/https://github.com/go-gitea/gitea" expect(substituteRepoOpenWithUrl('proto://a/{url}', 'https://gitea')).toEqual('proto://a/https://gitea'); expect(substituteRepoOpenWithUrl('proto://a?link={url}', 'https://gitea')).toEqual('proto://a?link=https%3A%2F%2Fgitea'); }); + +test('sanitizeRepoName', () => { + expect(sanitizeRepoName(' a b ')).toEqual('a-b'); + expect(sanitizeRepoName('a-b_c.git ')).toEqual('a-b_c'); + expect(sanitizeRepoName('/x.git/')).toEqual('-x.git-'); + expect(sanitizeRepoName('.profile')).toEqual('.profile'); + expect(sanitizeRepoName('.profile.')).toEqual('.profile'); + expect(sanitizeRepoName('.pro..file')).toEqual('.pro.file'); + + expect(sanitizeRepoName('foo.rss.atom.git.wiki')).toEqual('foo'); + + expect(sanitizeRepoName('.')).toEqual(''); + expect(sanitizeRepoName('..')).toEqual(''); + expect(sanitizeRepoName('-')).toEqual(''); +}); diff --git a/web_src/js/features/repo-common.ts b/web_src/js/features/repo-common.ts index 4362a2c713..ebb6881c67 100644 --- a/web_src/js/features/repo-common.ts +++ b/web_src/js/features/repo-common.ts @@ -159,3 +159,19 @@ export async function updateIssuesMeta(url: string, action: string, issue_ids: s console.error(error); } } + +export function sanitizeRepoName(name: string): string { + name = name.trim().replace(/[^-.\w]/g, '-'); + for (let lastName = ''; lastName !== name;) { + lastName = name; + name = name.replace(/\.+$/g, ''); + name = name.replace(/\.{2,}/g, '.'); + for (const ext of ['.git', '.wiki', '.rss', '.atom']) { + if (name.endsWith(ext)) { + name = name.substring(0, name.length - ext.length); + } + } + } + if (['.', '..', '-'].includes(name)) name = ''; + return name; +} diff --git a/web_src/js/features/repo-migration.ts b/web_src/js/features/repo-migration.ts index fb9c822f98..4914e47267 100644 --- a/web_src/js/features/repo-migration.ts +++ b/web_src/js/features/repo-migration.ts @@ -1,4 +1,5 @@ import {hideElem, showElem, toggleElem} from '../utils/dom.ts'; +import {sanitizeRepoName} from './repo-common.ts'; const service = document.querySelector<HTMLInputElement>('#service_type'); const user = document.querySelector<HTMLInputElement>('#auth_username'); @@ -25,13 +26,19 @@ export function initRepoMigration() { }); lfs?.addEventListener('change', setLFSSettingsVisibility); - const cloneAddr = document.querySelector<HTMLInputElement>('#clone_addr'); - cloneAddr?.addEventListener('change', () => { - const repoName = document.querySelector<HTMLInputElement>('#repo_name'); - if (cloneAddr.value && !repoName?.value) { // Only modify if repo_name input is blank - repoName.value = /^(.*\/)?((.+?)(\.git)?)$/.exec(cloneAddr.value)[3]; - } - }); + const elCloneAddr = document.querySelector<HTMLInputElement>('#clone_addr'); + const elRepoName = document.querySelector<HTMLInputElement>('#repo_name'); + if (elCloneAddr && elRepoName) { + let repoNameChanged = false; + elRepoName.addEventListener('input', () => {repoNameChanged = true}); + elCloneAddr.addEventListener('input', () => { + if (repoNameChanged) return; + let repoNameFromUrl = elCloneAddr.value.split(/[?#]/)[0]; + repoNameFromUrl = /^(.*\/)?((.+?)\/?)$/.exec(repoNameFromUrl)[3]; + repoNameFromUrl = repoNameFromUrl.split(/[?#]/)[0]; + elRepoName.value = sanitizeRepoName(repoNameFromUrl); + }); + } } function checkAuth() { diff --git a/web_src/js/features/repo-new.ts b/web_src/js/features/repo-new.ts index f2c5eba62c..0e4d78872d 100644 --- a/web_src/js/features/repo-new.ts +++ b/web_src/js/features/repo-new.ts @@ -1,11 +1,14 @@ -import {hideElem, showElem, toggleElem} from '../utils/dom.ts'; +import {hideElem, querySingleVisibleElem, showElem, toggleElem} from '../utils/dom.ts'; import {htmlEscape} from 'escape-goat'; import {fomanticQuery} from '../modules/fomantic/base.ts'; +import {sanitizeRepoName} from './repo-common.ts'; const {appSubUrl} = window.config; function initRepoNewTemplateSearch(form: HTMLFormElement) { - const inputRepoOwnerUid = form.querySelector<HTMLInputElement>('#uid'); + const elSubmitButton = querySingleVisibleElem<HTMLInputElement>(form, '.ui.primary.button'); + const elCreateRepoErrorMessage = form.querySelector('#create-repo-error-message'); + const elRepoOwnerDropdown = form.querySelector('#repo_owner_dropdown'); const elRepoTemplateDropdown = form.querySelector<HTMLInputElement>('#repo_template_search'); const inputRepoTemplate = form.querySelector<HTMLInputElement>('#repo_template'); const elTemplateUnits = form.querySelector('#template_units'); @@ -18,11 +21,23 @@ function initRepoNewTemplateSearch(form: HTMLFormElement) { inputRepoTemplate.addEventListener('change', checkTemplate); checkTemplate(); - const $dropdown = fomanticQuery(elRepoTemplateDropdown); + const $repoOwnerDropdown = fomanticQuery(elRepoOwnerDropdown); + const $repoTemplateDropdown = fomanticQuery(elRepoTemplateDropdown); const onChangeOwner = function () { - $dropdown.dropdown('setting', { + const ownerId = $repoOwnerDropdown.dropdown('get value'); + const $ownerItem = $repoOwnerDropdown.dropdown('get item', ownerId); + hideElem(elCreateRepoErrorMessage); + elSubmitButton.disabled = false; + if ($ownerItem?.length) { + const elOwnerItem = $ownerItem[0]; + elCreateRepoErrorMessage.textContent = elOwnerItem.getAttribute('data-create-repo-disallowed-prompt') ?? ''; + const hasError = Boolean(elCreateRepoErrorMessage.textContent); + toggleElem(elCreateRepoErrorMessage, hasError); + elSubmitButton.disabled = hasError; + } + $repoTemplateDropdown.dropdown('setting', { apiSettings: { - url: `${appSubUrl}/repo/search?q={query}&template=true&priority_owner_id=${inputRepoOwnerUid.value}`, + url: `${appSubUrl}/repo/search?q={query}&template=true&priority_owner_id=${ownerId}`, onResponse(response: any) { const results = []; results.push({name: '', value: ''}); // empty item means not using template @@ -32,14 +47,14 @@ function initRepoNewTemplateSearch(form: HTMLFormElement) { value: String(tmplRepo.repository.id), }); } - $dropdown.fomanticExt.onResponseKeepSelectedItem($dropdown, inputRepoTemplate.value); + $repoTemplateDropdown.fomanticExt.onResponseKeepSelectedItem($repoTemplateDropdown, inputRepoTemplate.value); return {results}; }, cache: false, }, }); }; - inputRepoOwnerUid.addEventListener('change', onChangeOwner); + $repoOwnerDropdown.dropdown('setting', 'onChange', onChangeOwner); onChangeOwner(); } @@ -74,6 +89,10 @@ export function initRepoNew() { } }; inputRepoName.addEventListener('input', updateUiRepoName); + inputRepoName.addEventListener('change', () => { + inputRepoName.value = sanitizeRepoName(inputRepoName.value); + updateUiRepoName(); + }); updateUiRepoName(); initRepoNewTemplateSearch(form); diff --git a/web_src/js/features/repo-settings.ts b/web_src/js/features/repo-settings.ts index 80f897069e..27dc4e9bfe 100644 --- a/web_src/js/features/repo-settings.ts +++ b/web_src/js/features/repo-settings.ts @@ -1,6 +1,6 @@ import {minimatch} from 'minimatch'; import {createMonaco} from './codeeditor.ts'; -import {onInputDebounce, queryElems, toggleElem} from '../utils/dom.ts'; +import {onInputDebounce, queryElems, toggleClass, toggleElem} from '../utils/dom.ts'; import {POST} from '../modules/fetch.ts'; import {initAvatarUploaderWithCropper} from './comp/Cropper.ts'; import {initRepoSettingsBranchesDrag} from './repo-settings-branches.ts'; @@ -125,22 +125,14 @@ function initRepoSettingsOptions() { const pageContent = document.querySelector('.page-content.repository.settings.options'); if (!pageContent) return; - const toggleClass = (elems: NodeListOf<Element>, className: string, value: boolean) => { - for (const el of elems) el.classList.toggle(className, value); - }; - // Enable or select internal/external wiki system and issue tracker. queryElems<HTMLInputElement>(pageContent, '.enable-system', (el) => el.addEventListener('change', () => { - const elTargets = document.querySelectorAll(el.getAttribute('data-target')); - const elContexts = document.querySelectorAll(el.getAttribute('data-context')); - toggleClass(elTargets, 'disabled', !el.checked); - toggleClass(elContexts, 'disabled', el.checked); + toggleClass(el.getAttribute('data-target'), 'disabled', !el.checked); + toggleClass(el.getAttribute('data-context'), 'disabled', el.checked); })); queryElems<HTMLInputElement>(pageContent, '.enable-system-radio', (el) => el.addEventListener('change', () => { - const elTargets = document.querySelectorAll(el.getAttribute('data-target')); - const elContexts = document.querySelectorAll(el.getAttribute('data-context')); - toggleClass(elTargets, 'disabled', el.value === 'false'); - toggleClass(elContexts, 'disabled', el.value === 'true'); + toggleClass(el.getAttribute('data-target'), 'disabled', el.value === 'false'); + toggleClass(el.getAttribute('data-context'), 'disabled', el.value === 'true'); })); queryElems<HTMLInputElement>(pageContent, '.js-tracker-issue-style', (el) => el.addEventListener('change', () => { diff --git a/web_src/js/modules/fomantic/dropdown.test.ts b/web_src/js/modules/fomantic/dropdown.test.ts index 587e0bca7c..dd3497c8fc 100644 --- a/web_src/js/modules/fomantic/dropdown.test.ts +++ b/web_src/js/modules/fomantic/dropdown.test.ts @@ -23,7 +23,27 @@ test('hideScopedEmptyDividers-simple', () => { `); }); -test('hideScopedEmptyDividers-hidden1', () => { +test('hideScopedEmptyDividers-items-all-filtered', () => { + const container = createElementFromHTML(`<div> +<div class="any"></div> +<div class="divider"></div> +<div class="item filtered">a</div> +<div class="item filtered">b</div> +<div class="divider"></div> +<div class="any"></div> +</div>`); + hideScopedEmptyDividers(container); + expect(container.innerHTML).toEqual(` +<div class="any"></div> +<div class="divider hidden transition"></div> +<div class="item filtered">a</div> +<div class="item filtered">b</div> +<div class="divider"></div> +<div class="any"></div> +`); +}); + +test('hideScopedEmptyDividers-hide-last', () => { const container = createElementFromHTML(`<div> <div class="item">a</div> <div class="divider" data-scope="b"></div> @@ -37,7 +57,7 @@ test('hideScopedEmptyDividers-hidden1', () => { `); }); -test('hideScopedEmptyDividers-hidden2', () => { +test('hideScopedEmptyDividers-scoped-items', () => { const container = createElementFromHTML(`<div> <div class="item" data-scope="">a</div> <div class="divider" data-scope="b"></div> diff --git a/web_src/js/modules/fomantic/dropdown.ts b/web_src/js/modules/fomantic/dropdown.ts index 8736e041df..1b05939cf3 100644 --- a/web_src/js/modules/fomantic/dropdown.ts +++ b/web_src/js/modules/fomantic/dropdown.ts @@ -9,26 +9,36 @@ const fomanticDropdownFn = $.fn.dropdown; // use our own `$().dropdown` function to patch Fomantic's dropdown module export function initAriaDropdownPatch() { if ($.fn.dropdown === ariaDropdownFn) throw new Error('initAriaDropdownPatch could only be called once'); + $.fn.dropdown.settings.onAfterFiltered = onAfterFiltered; $.fn.dropdown = ariaDropdownFn; $.fn.fomanticExt.onResponseKeepSelectedItem = onResponseKeepSelectedItem; (ariaDropdownFn as FomanticInitFunction).settings = fomanticDropdownFn.settings; } // the patched `$.fn.dropdown` function, it passes the arguments to Fomantic's `$.fn.dropdown` function, and: -// * it does the one-time attaching on the first call -// * it delegates the `onLabelCreate` to the patched `onLabelCreate` to add necessary aria attributes +// * it does the one-time element event attaching on the first call +// * it delegates the module internal functions like `onLabelCreate` to the patched functions to add more features. function ariaDropdownFn(this: any, ...args: Parameters<FomanticInitFunction>) { const ret = fomanticDropdownFn.apply(this, args); - // if the `$().dropdown()` call is without arguments, or it has non-string (object) argument, - // it means that this call will reset the dropdown internal settings, then we need to re-delegate the callbacks. - const needDelegate = (!args.length || typeof args[0] !== 'string'); - for (const el of this) { + for (let el of this) { + // dropdown will replace '<select class="ui dropdown"/>' to '<div class="ui dropdown"><select (hidden)></select><div class="menu">...</div></div>' + // so we need to correctly find the closest '.ui.dropdown' element, it is the real fomantic dropdown module. + el = el.closest('.ui.dropdown'); if (!el[ariaPatchKey]) { - attachInit(el); + // the elements don't belong to the dropdown "module" and won't be reset + // so we only need to initialize them once. + attachInitElements(el); } - if (needDelegate) { - delegateOne($(el)); + + // if the `$().dropdown()` is called without arguments, or it has non-string (object) argument, + // it means that such call will reset the dropdown "module" including internal settings, + // then we need to re-delegate the callbacks. + const $dropdown = $(el); + const dropdownModule = $dropdown.data('module-dropdown'); + if (!dropdownModule.giteaDelegated) { + dropdownModule.giteaDelegated = true; + delegateDropdownModule($dropdown); } } return ret; @@ -61,37 +71,17 @@ function updateSelectionLabel(label: HTMLElement) { } } -function processMenuItems($dropdown: any, dropdownCall: any) { - const hideEmptyDividers = dropdownCall('setting', 'hideDividers') === 'empty'; +function onAfterFiltered(this: any) { + const $dropdown = $(this); + const hideEmptyDividers = $dropdown.dropdown('setting', 'hideDividers') === 'empty'; const itemsMenu = $dropdown[0].querySelector('.scrolling.menu') || $dropdown[0].querySelector('.menu'); if (hideEmptyDividers) hideScopedEmptyDividers(itemsMenu); } // delegate the dropdown's template functions and callback functions to add aria attributes. -function delegateOne($dropdown: any) { +function delegateDropdownModule($dropdown: any) { const dropdownCall = fomanticDropdownFn.bind($dropdown); - // If there is a "search input" in the "menu", Fomantic will only "focus the input" but not "toggle the menu" when the "dropdown icon" is clicked. - // Actually, Fomantic UI doesn't support such layout/usage. It needs to patch the "focusSearch" / "blurSearch" functions to make sure it toggles the menu. - const oldFocusSearch = dropdownCall('internal', 'focusSearch'); - const oldBlurSearch = dropdownCall('internal', 'blurSearch'); - // * If the "dropdown icon" is clicked, Fomantic calls "focusSearch", so show the menu - dropdownCall('internal', 'focusSearch', function (this: any) { dropdownCall('show'); oldFocusSearch.call(this) }); - // * If the "dropdown icon" is clicked again when the menu is visible, Fomantic calls "blurSearch", so hide the menu - dropdownCall('internal', 'blurSearch', function (this: any) { oldBlurSearch.call(this); dropdownCall('hide') }); - - const oldFilterItems = dropdownCall('internal', 'filterItems'); - dropdownCall('internal', 'filterItems', function (this: any, ...args: any[]) { - oldFilterItems.call(this, ...args); - processMenuItems($dropdown, dropdownCall); - }); - - const oldShow = dropdownCall('internal', 'show'); - dropdownCall('internal', 'show', function (this: any, ...args: any[]) { - oldShow.call(this, ...args); - processMenuItems($dropdown, dropdownCall); - }); - // the "template" functions are used for dynamic creation (eg: AJAX) const dropdownTemplates = {...dropdownCall('setting', 'templates'), t: performance.now()}; const dropdownTemplatesMenuOld = dropdownTemplates.menu; @@ -163,9 +153,8 @@ function attachStaticElements(dropdown: HTMLElement, focusable: HTMLElement, men } } -function attachInit(dropdown: HTMLElement) { +function attachInitElements(dropdown: HTMLElement) { (dropdown as any)[ariaPatchKey] = {}; - if (dropdown.classList.contains('custom')) return; // Dropdown has 2 different focusing behaviors // * with search input: the input is focused, and it works with aria-activedescendant pointing another sibling element. @@ -305,9 +294,11 @@ export function hideScopedEmptyDividers(container: Element) { const visibleItems: Element[] = []; const curScopeVisibleItems: Element[] = []; let curScope: string = '', lastVisibleScope: string = ''; - const isScopedDivider = (item: Element) => item.matches('.divider') && item.hasAttribute('data-scope'); + const isDivider = (item: Element) => item.classList.contains('divider'); + const isScopedDivider = (item: Element) => isDivider(item) && item.hasAttribute('data-scope'); const hideDivider = (item: Element) => item.classList.add('hidden', 'transition'); // dropdown has its own classes to hide items - + const showDivider = (item: Element) => item.classList.remove('hidden', 'transition'); + const isHidden = (item: Element) => item.classList.contains('hidden') || item.classList.contains('filtered') || item.classList.contains('tw-hidden'); const handleScopeSwitch = (itemScope: string) => { if (curScopeVisibleItems.length === 1 && isScopedDivider(curScopeVisibleItems[0])) { hideDivider(curScopeVisibleItems[0]); @@ -323,13 +314,16 @@ export function hideScopedEmptyDividers(container: Element) { curScopeVisibleItems.length = 0; }; + // reset hidden dividers + queryElems(container, '.divider', showDivider); + // hide the scope dividers if the scope items are empty for (const item of container.children) { const itemScope = item.getAttribute('data-scope') || ''; if (itemScope !== curScope) { handleScopeSwitch(itemScope); } - if (!item.classList.contains('filtered') && !item.classList.contains('tw-hidden')) { + if (!isHidden(item)) { curScopeVisibleItems.push(item as HTMLElement); } } @@ -337,20 +331,20 @@ export function hideScopedEmptyDividers(container: Element) { // hide all leading and trailing dividers while (visibleItems.length) { - if (!visibleItems[0].matches('.divider')) break; + if (!isDivider(visibleItems[0])) break; hideDivider(visibleItems[0]); visibleItems.shift(); } while (visibleItems.length) { - if (!visibleItems[visibleItems.length - 1].matches('.divider')) break; + if (!isDivider(visibleItems[visibleItems.length - 1])) break; hideDivider(visibleItems[visibleItems.length - 1]); visibleItems.pop(); } // hide all duplicate dividers, hide current divider if next sibling is still divider // no need to update "visibleItems" array since this is the last loop - for (const item of visibleItems) { - if (!item.matches('.divider')) continue; - if (item.nextElementSibling?.matches('.divider')) hideDivider(item); + for (let i = 0; i < visibleItems.length - 1; i++) { + if (!visibleItems[i].matches('.divider')) continue; + if (visibleItems[i + 1].matches('.divider')) hideDivider(visibleItems[i]); } } diff --git a/web_src/js/modules/observer.ts b/web_src/js/modules/observer.ts index 06208d0507..3305c2f29d 100644 --- a/web_src/js/modules/observer.ts +++ b/web_src/js/modules/observer.ts @@ -46,9 +46,11 @@ function callGlobalInitFunc(el: HTMLElement) { const func = globalInitFuncs[initFunc]; if (!func) throw new Error(`Global init function "${initFunc}" not found`); + // when an element node is removed and added again, it should not be re-initialized again. type GiteaGlobalInitElement = Partial<HTMLElement> & {_giteaGlobalInited: boolean}; - if ((el as GiteaGlobalInitElement)._giteaGlobalInited) throw new Error(`Global init function "${initFunc}" already executed`); + if ((el as GiteaGlobalInitElement)._giteaGlobalInited) return; (el as GiteaGlobalInitElement)._giteaGlobalInited = true; + func(el); } diff --git a/web_src/js/utils/dom.ts b/web_src/js/utils/dom.ts index 4d15784e6e..6d38ffa8cd 100644 --- a/web_src/js/utils/dom.ts +++ b/web_src/js/utils/dom.ts @@ -25,32 +25,34 @@ function elementsCall(el: ElementArg, func: ElementsCallbackWithArgs, ...args: a } } +export function toggleClass(el: ElementArg, className: string, force?: boolean) { + elementsCall(el, (e: Element) => { + if (force === true) { + e.classList.add(className); + } else if (force === false) { + e.classList.remove(className); + } else if (force === undefined) { + e.classList.toggle(className); + } else { + throw new Error('invalid force argument'); + } + }); +} + /** - * @param el Element + * @param el ElementArg * @param force force=true to show or force=false to hide, undefined to toggle */ -function toggleShown(el: Element, force: boolean) { - if (force === true) { - el.classList.remove('tw-hidden'); - } else if (force === false) { - el.classList.add('tw-hidden'); - } else if (force === undefined) { - el.classList.toggle('tw-hidden'); - } else { - throw new Error('invalid force argument'); - } +export function toggleElem(el: ElementArg, force?: boolean) { + toggleClass(el, 'tw-hidden', !force); } export function showElem(el: ElementArg) { - elementsCall(el, toggleShown, true); + toggleElem(el, true); } export function hideElem(el: ElementArg) { - elementsCall(el, toggleShown, false); -} - -export function toggleElem(el: ElementArg, force?: boolean) { - elementsCall(el, toggleShown, force); + toggleElem(el, false); } export function isElemHidden(el: ElementArg) { diff --git a/web_src/js/webcomponents/absolute-date.test.ts b/web_src/js/webcomponents/absolute-date.test.ts index a3866829a7..bf591358bd 100644 --- a/web_src/js/webcomponents/absolute-date.test.ts +++ b/web_src/js/webcomponents/absolute-date.test.ts @@ -20,7 +20,7 @@ test('toAbsoluteLocaleDate', () => { // test different timezone const oldTZ = process.env.TZ; process.env.TZ = 'America/New_York'; - expect(new Date('2024-03-15').toLocaleString()).toEqual('3/14/2024, 8:00:00 PM'); - expect(toAbsoluteLocaleDate('2024-03-15')).toEqual('3/15/2024, 12:00:00 AM'); + expect(new Date('2024-03-15').toLocaleString('en-US')).toEqual('3/14/2024, 8:00:00 PM'); + expect(toAbsoluteLocaleDate('2024-03-15', 'en-US')).toEqual('3/15/2024, 12:00:00 AM'); process.env.TZ = oldTZ; }); diff --git a/web_src/js/webcomponents/polyfill.test.ts b/web_src/js/webcomponents/polyfill.test.ts new file mode 100644 index 0000000000..4fb4621547 --- /dev/null +++ b/web_src/js/webcomponents/polyfill.test.ts @@ -0,0 +1,7 @@ +import {weakRefClass} from './polyfills.ts'; + +test('polyfillWeakRef', () => { + const WeakRef = weakRefClass(); + const r = new WeakRef(123); + expect(r.deref()).toEqual(123); +}); diff --git a/web_src/js/webcomponents/polyfills.ts b/web_src/js/webcomponents/polyfills.ts index 4a84ee9562..9575324b5a 100644 --- a/web_src/js/webcomponents/polyfills.ts +++ b/web_src/js/webcomponents/polyfills.ts @@ -16,3 +16,19 @@ try { return intlNumberFormat(locales, options); }; } + +export function weakRefClass() { + const weakMap = new WeakMap(); + return class { + constructor(target: any) { + weakMap.set(this, target); + } + deref() { + return weakMap.get(this); + } + }; +} + +if (!window.WeakRef) { + window.WeakRef = weakRefClass() as any; +} |